Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
7d1e77d
add refresh property to metadata
tabcat Oct 24, 2025
32f5d61
conditionally refresh localStore record
tabcat Oct 24, 2025
4c2e7b1
PutOptions.metadata is Partial
tabcat Oct 24, 2025
c6d5d0a
#republish method supports refresh
tabcat Oct 25, 2025
28b48fd
use overwrite options instead of metadata.refresh
tabcat Oct 25, 2025
5fe9d7d
add refresh and unrefresh to republisher
tabcat Oct 25, 2025
a67063f
add RefreshOptions.repeat
tabcat Oct 25, 2025
fa80d54
add @default jsdoc tage to Refresh.force
tabcat Oct 25, 2025
b90b877
fix bool tag
tabcat Oct 26, 2025
1e0602e
log unable to refresh record as error
tabcat Oct 26, 2025
9286a63
move refresh records processing outside iterator
tabcat Oct 26, 2025
486373c
test refresh feature in #republish tests
tabcat Oct 26, 2025
c7f1345
wrap refresh logic in try/catch
tabcat Oct 27, 2025
f2f6ea5
refactor(ipns)!: nocache does not read cache
tabcat Oct 28, 2025
85b912f
cleanup record comments in refresh
tabcat Oct 28, 2025
ec19f54
published record via nocache
tabcat Oct 28, 2025
7108a72
shorten error message in refresh
tabcat Oct 29, 2025
3ab3052
fix putOptions.metadata
tabcat Oct 29, 2025
3142df1
add refresh tests
tabcat Oct 29, 2025
7822605
Merge branch 'main' into feat/refresh-record
tabcat Oct 29, 2025
1a5f2bc
lint --fix
tabcat Oct 29, 2025
13ace87
manual linter fixes
tabcat Oct 29, 2025
d4788ce
fix unrefresh test
tabcat Oct 29, 2025
85c9e7c
undo pointless change
tabcat Oct 30, 2025
e1a7017
spy on localStore not datastore
tabcat Oct 30, 2025
c572206
rename refresh to republish and remove unrefresh
tabcat Oct 31, 2025
7108a53
update example
tabcat Oct 31, 2025
82df957
fix resolve test
tabcat Oct 31, 2025
c4aa5fd
fix docs
tabcat Oct 31, 2025
0f493a5
fix localStore spy
tabcat Oct 31, 2025
488469d
move shouldRefresh check before recordsToRefresh.push
tabcat Oct 31, 2025
9bebca3
recordsToRefresh -> keysToRepublish
tabcat Oct 31, 2025
77ea1c9
fix linter error
tabcat Oct 31, 2025
d25aa37
remove additional hour for tolerance
tabcat Nov 4, 2025
754f515
fix lint error
tabcat Nov 4, 2025
25b513c
replace refresh with upkeep in protobuf
tabcat Feb 2, 2026
d983e48
add upkeep to Publish/RepublishOptions
tabcat Feb 2, 2026
6057cc0
clean up comment
tabcat Feb 2, 2026
cd4c899
skip looking for published records if force = true
tabcat Feb 2, 2026
775d0b3
publish and republish use upkeep policy
tabcat Feb 2, 2026
a4d5cf8
republish offline
tabcat Feb 2, 2026
e2b1ff6
test republish disabled records and fix tests
tabcat Feb 2, 2026
732b74b
lint --fix
tabcat Feb 2, 2026
ab66537
test: remove redudant test
tabcat Feb 2, 2026
3a28a2b
republishing offline skips public resolution
tabcat Feb 2, 2026
8168562
test: offline republish
tabcat Feb 2, 2026
b704448
clearer comment
tabcat Feb 2, 2026
0aa5118
style and fix publish metadata
tabcat Feb 3, 2026
f6950bf
chore!: remove unused metadata interface
tabcat Feb 3, 2026
025e34d
just check if already published
tabcat Feb 3, 2026
3571286
Merge branch 'main' into feat/refresh-record
tabcat Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions packages/ipns/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,18 +193,12 @@ const delegatedClient = delegatedRoutingV1HttpApiClient({
})
const record = await delegatedClient.getIPNS(parsedCid)

const routingKey = multihashToIPNSRoutingKey(parsedCid.multihash)
const marshaledRecord = marshalIPNSRecord(record)

// validate that they key corresponds to the record
await ipnsValidator(routingKey, marshaledRecord)

// publish record to routing
await Promise.all(
name.routers.map(async r => {
await r.put(routingKey, marshaledRecord)
})
)
// publish the latest existing record to routing
// use `options.force` if the record is already published
const { record: latestRecord } = await name.republish(parsedCid, { record })

// stop republishing a key
await name.unpublish(parsedCid)
```

# Install
Expand Down
86 changes: 68 additions & 18 deletions packages/ipns/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
* value.
*
* ```TypeScript
* import { createHelia } from 'helia'
* import { createHelia } from 'helia'export interface IPNSRecordMetadata {
keyName: string
lifetime: number
upkeep: Upkeep
}
* import { ipns } from '@helia/ipns'
* import { unixfs } from '@helia/unixfs'
* import { generateKeyPair } from '@libp2p/crypto/keys'
Expand Down Expand Up @@ -164,18 +168,12 @@
* })
* const record = await delegatedClient.getIPNS(parsedCid)
*
* const routingKey = multihashToIPNSRoutingKey(parsedCid.multihash)
* const marshaledRecord = marshalIPNSRecord(record)
*
* // validate that they key corresponds to the record
* await ipnsValidator(routingKey, marshaledRecord)
* // publish the latest existing record to routing
* // use `options.force` if the record is already published
* const { record: latestRecord } = await name.republish(parsedCid, { record })
*
* // publish record to routing
* await Promise.all(
* name.routers.map(async r => {
* await r.put(routingKey, marshaledRecord)
* })
* )
* // stop republishing a key
* await name.unpublish(parsedCid)
* ```
*/

Expand Down Expand Up @@ -206,6 +204,11 @@ export type ResolveProgressEvents =
ProgressEvent<'ipns:resolve:success', IPNSRecord> |
ProgressEvent<'ipns:resolve:error', Error>

export type RepublishProgressEvents =
ProgressEvent<'ipns:republish:start'> |
ProgressEvent<'ipns:republish:success', IPNSRecord> |
ProgressEvent<'ipns:republish:error', Error>

export type DatastoreProgressEvents =
ProgressEvent<'ipns:routing:datastore:put'> |
ProgressEvent<'ipns:routing:datastore:get'> |
Expand All @@ -219,7 +222,7 @@ export interface PublishOptions extends AbortOptions, ProgressOptions<PublishPro
lifetime?: number

/**
* Only publish to a local datastore (default: false)
* Initially only publish to a local datastore (default: false)
*/
offline?: boolean

Expand All @@ -233,11 +236,15 @@ export interface PublishOptions extends AbortOptions, ProgressOptions<PublishPro
* The TTL of the record in ms (default: 5 minutes)
*/
ttl?: number
}

export interface IPNSRecordMetadata {
keyName: string
lifetime: number
/**
* Automated record upkeep policy. (default: "republish")
*
* - `republish`: create a new record with a refreshed TTL
* - `refresh`: publish the existing record until it expires
* - `none`: disable automated publishing
*/
upkeep?: 'republish' | 'refresh' | 'none'
}

export interface ResolveOptions extends AbortOptions, ProgressOptions<ResolveProgressEvents | IPNSRoutingProgressEvents> {
Expand All @@ -256,6 +263,33 @@ export interface ResolveOptions extends AbortOptions, ProgressOptions<ResolvePro
nocache?: boolean
}

export interface RepublishOptions extends AbortOptions, ProgressOptions<RepublishProgressEvents | IPNSRoutingProgressEvents> {
/**
* A candidate IPNS record to use if no newer records are found
*/
record?: IPNSRecord

/**
* Initially only republish to a local datastore (default: false)
*/
offline?: boolean

/**
* Force the record to be republished even if already resolvable
*
* @default false
*/
force?: boolean

/**
* Automated record upkeep policy. (default: "refresh")
*
* - `refresh`: republish the existing record until it expires
* - `none`: disable automated publishing
*/
upkeep?: 'refresh' | 'none'
}

export interface ResolveResult {
/**
* The CID that was resolved
Expand Down Expand Up @@ -287,6 +321,13 @@ export interface IPNSPublishResult {
publicKey: PublicKey
}

export interface IPNSRepublishResult {
/**
* The published record
*/
record: IPNSRecord
}

export interface IPNSResolver {
/**
* Accepts a libp2p public key, a CID with the libp2p-key codec and either the
Expand Down Expand Up @@ -352,7 +393,16 @@ export interface IPNS {
* Note that the record may still be resolved by other peers until it expires
* or is no longer valid.
*/
unpublish(keyName: string, options?: AbortOptions): Promise<void>
unpublish(keyName: string | CID<unknown, 0x72, 0x00 | 0x12, 1> | PublicKey | MultihashDigest<0x00 | 0x12> | PeerId, options?: AbortOptions): Promise<void>

/**
* Republish the latest known existing record to all routers
*
* This will automatically be done regularly unless `options.repeat` is false
*
* Use `unpublish` to stop republishing a key
*/
republish(key: CID<unknown, 0x72, 0x00 | 0x12, 1> | PublicKey | MultihashDigest<0x00 | 0x12> | PeerId, options?: RepublishOptions): Promise<IPNSRepublishResult>
}

export type { IPNSRouting } from './routing/index.js'
Expand Down
13 changes: 9 additions & 4 deletions packages/ipns/src/ipns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { IPNSResolver } from './ipns/resolver.ts'
import { localStore } from './local-store.js'
import { helia } from './routing/helia.js'
import { localStoreRouting } from './routing/local-store.ts'
import type { IPNSComponents, IPNS as IPNSInterface, IPNSOptions, IPNSPublishResult, IPNSResolveResult, PublishOptions, ResolveOptions } from './index.js'
import type { IPNSComponents, IPNS as IPNSInterface, IPNSOptions, IPNSPublishResult, IPNSRepublishResult, IPNSResolveResult, PublishOptions, RepublishOptions, ResolveOptions } from './index.js'
import type { LocalStore } from './local-store.js'
import type { IPNSRouting } from './routing/index.js'
import type { AbortOptions, PeerId, PublicKey, Startable } from '@libp2p/interface'
Expand Down Expand Up @@ -34,13 +34,14 @@ export class IPNS implements IPNSInterface, Startable {
routers: this.routers,
localStore: this.localStore
})
this.republisher = new IPNSRepublisher(components, {
this.resolver = new IPNSResolver(components, {
...init,
routers: this.routers,
localStore: this.localStore
})
this.resolver = new IPNSResolver(components, {
this.republisher = new IPNSRepublisher(components, {
...init,
resolver: this.resolver,
routers: this.routers,
localStore: this.localStore
})
Expand Down Expand Up @@ -81,7 +82,11 @@ export class IPNS implements IPNSInterface, Startable {
return this.resolver.resolve(key, options)
}

async unpublish (keyName: string, options?: AbortOptions): Promise<void> {
async unpublish (keyName: string | CID<unknown, 0x72, 0x00 | 0x12, 1> | PublicKey | MultihashDigest<0x00 | 0x12> | PeerId, options?: AbortOptions): Promise<void> {
return this.publisher.unpublish(keyName, options)
}

async republish (key: CID<unknown, 0x72, 0x00 | 0x12, 1> | PublicKey | MultihashDigest<0x00 | 0x12> | PeerId, options: RepublishOptions = {}): Promise<IPNSRepublishResult> {
return this.republisher.republish(key, options)
}
}
18 changes: 12 additions & 6 deletions packages/ipns/src/ipns/publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey, unmarsh
import { CID } from 'multiformats/cid'
import { CustomProgressEvent } from 'progress-events'
import { DEFAULT_LIFETIME_MS, DEFAULT_TTL_NS } from '../constants.ts'
import { Upkeep } from '../pb/metadata.ts'
import { keyToMultihash } from '../utils.ts'
import type { IPNSPublishResult, PublishOptions } from '../index.js'
import type { LocalStore } from '../local-store.js'
import type { IPNSRouting } from '../routing/index.js'
Expand Down Expand Up @@ -57,13 +59,14 @@ export class IPNSPublisher {
const record = await createIPNSRecord(privKey, value, sequenceNumber, lifetime, { ...options, ttlNs })
const marshaledRecord = marshalIPNSRecord(record)

const metadata = { keyName, lifetime, upkeep: Upkeep[options.upkeep ?? 'republish'] }
if (options.offline === true) {
// only store record locally
await this.localStore.put(routingKey, marshaledRecord, { ...options, metadata: { keyName, lifetime } })
await this.localStore.put(routingKey, marshaledRecord, { ...options, metadata })
} else {
// publish record to routing (including the local store)
await Promise.all(this.routers.map(async r => {
await r.put(routingKey, marshaledRecord, { ...options, metadata: { keyName, lifetime } })
await r.put(routingKey, marshaledRecord, { ...options, metadata })
}))
}

Expand All @@ -88,10 +91,13 @@ export class IPNSPublisher {
}
}

async unpublish (keyName: string, options?: AbortOptions): Promise<void> {
const { publicKey } = await this.keychain.exportKey(keyName)
const digest = publicKey.toMultihash()
const routingKey = multihashToIPNSRoutingKey(digest)
async unpublish (keyName: string | CID<unknown, 0x72, 0x00 | 0x12, 1> | PublicKey | MultihashDigest<0x00 | 0x12> | PeerId, options?: AbortOptions): Promise<void> {
if (typeof keyName === 'string') {
const { publicKey } = await this.keychain.exportKey(keyName)
keyName = publicKey.toMultihash()
}

const routingKey = multihashToIPNSRoutingKey(keyToMultihash(keyName))
await this.localStore.delete(routingKey, options)
}
}
Loading
Loading