@helia/ipns 8.2.4 → 9.0.0-9c67dbd4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +31 -135
  2. package/dist/index.min.js +10 -11
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/constants.d.ts +17 -0
  5. package/dist/src/constants.d.ts.map +1 -0
  6. package/dist/src/constants.js +19 -0
  7. package/dist/src/constants.js.map +1 -0
  8. package/dist/src/errors.d.ts +0 -4
  9. package/dist/src/errors.d.ts.map +1 -1
  10. package/dist/src/errors.js +0 -7
  11. package/dist/src/errors.js.map +1 -1
  12. package/dist/src/index.d.ts +109 -201
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/index.js +34 -417
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/ipns.d.ts +22 -0
  17. package/dist/src/ipns.d.ts.map +1 -0
  18. package/dist/src/ipns.js +339 -0
  19. package/dist/src/ipns.js.map +1 -0
  20. package/dist/src/local-store.d.ts +42 -0
  21. package/dist/src/local-store.d.ts.map +1 -0
  22. package/dist/src/local-store.js +119 -0
  23. package/dist/src/local-store.js.map +1 -0
  24. package/dist/src/pb/metadata.d.ts +12 -0
  25. package/dist/src/pb/metadata.d.ts.map +1 -0
  26. package/dist/src/pb/metadata.js +57 -0
  27. package/dist/src/pb/metadata.js.map +1 -0
  28. package/dist/src/routing/index.d.ts +4 -2
  29. package/dist/src/routing/index.d.ts.map +1 -1
  30. package/dist/src/routing/index.js.map +1 -1
  31. package/dist/src/routing/local-store.d.ts +4 -19
  32. package/dist/src/routing/local-store.d.ts.map +1 -1
  33. package/dist/src/routing/local-store.js +7 -62
  34. package/dist/src/routing/local-store.js.map +1 -1
  35. package/dist/src/routing/pubsub.d.ts +21 -1
  36. package/dist/src/routing/pubsub.d.ts.map +1 -1
  37. package/dist/src/routing/pubsub.js +2 -2
  38. package/dist/src/routing/pubsub.js.map +1 -1
  39. package/dist/src/utils.d.ts +24 -0
  40. package/dist/src/utils.d.ts.map +1 -1
  41. package/dist/src/utils.js +56 -0
  42. package/dist/src/utils.js.map +1 -1
  43. package/package.json +21 -23
  44. package/src/constants.ts +24 -0
  45. package/src/errors.ts +0 -9
  46. package/src/index.ts +116 -545
  47. package/src/ipns.ts +400 -0
  48. package/src/local-store.ts +162 -0
  49. package/src/pb/metadata.proto +9 -0
  50. package/src/pb/metadata.ts +74 -0
  51. package/src/routing/index.ts +4 -3
  52. package/src/routing/local-store.ts +9 -87
  53. package/src/routing/pubsub.ts +28 -4
  54. package/src/utils.ts +70 -0
  55. package/dist/src/dnslink.d.ts +0 -9
  56. package/dist/src/dnslink.d.ts.map +0 -1
  57. package/dist/src/dnslink.js +0 -138
  58. package/dist/src/dnslink.js.map +0 -1
  59. package/dist/typedoc-urls.json +0 -48
  60. package/src/dnslink.ts +0 -163
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @packageDocumentation
3
3
  *
4
- * IPNS operations using a Helia node
4
+ * [IPNS](https://docs.ipfs.tech/concepts/ipns/) operations using a Helia node
5
5
  *
6
6
  * @example Getting started
7
7
  *
@@ -11,23 +11,19 @@
11
11
  * import { createHelia } from 'helia'
12
12
  * import { ipns } from '@helia/ipns'
13
13
  * import { unixfs } from '@helia/unixfs'
14
- * import { generateKeyPair } from '@libp2p/crypto/keys'
15
14
  *
16
15
  * const helia = await createHelia()
17
16
  * const name = ipns(helia)
18
17
  *
19
- * // create a keypair to publish an IPNS name
20
- * const privateKey = await generateKeyPair('Ed25519')
21
- *
22
18
  * // store some data to publish
23
19
  * const fs = unixfs(helia)
24
20
  * const cid = await fs.addBytes(Uint8Array.from([0, 1, 2, 3, 4]))
25
21
  *
26
22
  * // publish the name
27
- * await name.publish(privateKey, cid)
23
+ * const { publicKey } = await name.publish('key-1', cid)
28
24
  *
29
25
  * // resolve the name
30
- * const result = await name.resolve(privateKey.publicKey)
26
+ * const result = await name.resolve(publicKey)
31
27
  *
32
28
  * console.info(result.cid, result.path)
33
29
  * ```
@@ -46,24 +42,18 @@
46
42
  * const helia = await createHelia()
47
43
  * const name = ipns(helia)
48
44
  *
49
- * // create a keypair to publish an IPNS name
50
- * const privateKey = await generateKeyPair('Ed25519')
51
- *
52
45
  * // store some data to publish
53
46
  * const fs = unixfs(helia)
54
47
  * const cid = await fs.addBytes(Uint8Array.from([0, 1, 2, 3, 4]))
55
48
  *
56
49
  * // publish the name
57
- * await name.publish(privateKey, cid)
58
- *
59
- * // create another keypair to re-publish the original record
60
- * const recursivePrivateKey = await generateKeyPair('Ed25519')
50
+ * const { publicKey } = await name.publish('key-1', cid)
61
51
  *
62
52
  * // publish the recursive name
63
- * await name.publish(recursivePrivateKey, privateKey.publicKey)
53
+ * const { publicKey: recursivePublicKey } = await name.publish('key-2', publicKey)
64
54
  *
65
55
  * // resolve the name recursively - it resolves until a CID is found
66
- * const result = await name.resolve(recursivePrivateKey.publicKey)
56
+ * const result = await name.resolve(recursivePublicKey)
67
57
  * console.info(result.cid.toString() === cid.toString()) // true
68
58
  * ```
69
59
  *
@@ -80,9 +70,6 @@
80
70
  * const helia = await createHelia()
81
71
  * const name = ipns(helia)
82
72
  *
83
- * // create a keypair to publish an IPNS name
84
- * const privateKey = await generateKeyPair('Ed25519')
85
- *
86
73
  * // store some data to publish
87
74
  * const fs = unixfs(helia)
88
75
  * const fileCid = await fs.addBytes(Uint8Array.from([0, 1, 2, 3, 4]))
@@ -92,10 +79,10 @@
92
79
  * const finalDirCid = await fs.cp(fileCid, dirCid, '/foo.txt')
93
80
  *
94
81
  * // publish the name
95
- * await name.publish(privateKey, `/ipfs/${finalDirCid}/foo.txt`)
82
+ * const { publicKey } = await name.publish('key-1', `/ipfs/${finalDirCid}/foo.txt`)
96
83
  *
97
84
  * // resolve the name
98
- * const result = await name.resolve(privateKey.publicKey)
85
+ * const result = await name.resolve(publicKey)
99
86
  *
100
87
  * console.info(result.cid, result.path) // QmFoo.. 'foo.txt'
101
88
  * ```
@@ -121,13 +108,14 @@
121
108
  * import { ipns } from '@helia/ipns'
122
109
  * import { pubsub } from '@helia/ipns/routing'
123
110
  * import { unixfs } from '@helia/unixfs'
124
- * import { gossipsub } from '@chainsafe/libp2p-gossipsub'
111
+ * import { floodsub } from '@libp2p/floodsub'
125
112
  * import { generateKeyPair } from '@libp2p/crypto/keys'
126
- * import type { Libp2p, PubSub } from '@libp2p/interface'
113
+ * import type { PubSub } from '@helia/ipns/routing'
114
+ * import type { Libp2p } from '@libp2p/interface'
127
115
  * import type { DefaultLibp2pServices } from 'helia'
128
116
  *
129
117
  * const libp2pOptions = libp2pDefaults()
130
- * libp2pOptions.services.pubsub = gossipsub()
118
+ * libp2pOptions.services.pubsub = floodsub()
131
119
  *
132
120
  * const helia = await createHelia<Libp2p<DefaultLibp2pServices & { pubsub: PubSub }>>({
133
121
  * libp2p: libp2pOptions
@@ -138,133 +126,30 @@
138
126
  * ]
139
127
  * })
140
128
  *
141
- * // create a keypair to publish an IPNS name
142
- * const privateKey = await generateKeyPair('Ed25519')
143
129
  *
144
130
  * // store some data to publish
145
131
  * const fs = unixfs(helia)
146
132
  * const cid = await fs.addBytes(Uint8Array.from([0, 1, 2, 3, 4]))
147
133
  *
148
134
  * // publish the name
149
- * await name.publish(privateKey, cid)
135
+ * const { publicKey } = await name.publish('key-1', cid)
150
136
  *
151
137
  * // resolve the name
152
- * const result = await name.resolve(privateKey.publicKey)
153
- * ```
154
- *
155
- * @example Using custom DNS over HTTPS resolvers
156
- *
157
- * To use custom resolvers, configure Helia's `dns` option:
158
- *
159
- * ```TypeScript
160
- * import { createHelia } from 'helia'
161
- * import { ipns } from '@helia/ipns'
162
- * import { dns } from '@multiformats/dns'
163
- * import { dnsOverHttps } from '@multiformats/dns/resolvers'
164
- * import { helia } from '@helia/ipns/routing'
165
- *
166
- * const node = await createHelia({
167
- * dns: dns({
168
- * resolvers: {
169
- * '.': dnsOverHttps('https://private-dns-server.me/dns-query')
170
- * }
171
- * })
172
- * })
173
- * const name = ipns(node, {
174
- * routers: [
175
- * helia(node.routing)
176
- * ]
177
- * })
178
- *
179
- * const result = name.resolveDNSLink('some-domain-with-dnslink-entry.com')
180
- * ```
181
- *
182
- * @example Resolving a domain with a dnslink entry
183
- *
184
- * Calling `resolveDNSLink` with the `@helia/ipns` instance:
185
- *
186
- * ```TypeScript
187
- * // resolve a CID from a TXT record in a DNS zone file, using the default
188
- * // resolver for the current platform eg:
189
- * // > dig _dnslink.ipfs.io TXT
190
- * // ;; ANSWER SECTION:
191
- * // _dnslink.ipfs.io. 60 IN TXT "dnslink=/ipns/website.ipfs.io"
192
- * // > dig _dnslink.website.ipfs.io TXT
193
- * // ;; ANSWER SECTION:
194
- * // _dnslink.website.ipfs.io. 60 IN TXT "dnslink=/ipfs/QmWebsite"
195
- *
196
- * import { createHelia } from 'helia'
197
- * import { ipns } from '@helia/ipns'
198
- *
199
- * const node = await createHelia()
200
- * const name = ipns(node)
201
- *
202
- * const { answer } = await name.resolveDNSLink('ipfs.io')
203
- *
204
- * console.info(answer)
205
- * // { data: '/ipfs/QmWebsite' }
206
- * ```
207
- *
208
- * @example Using DNS-Over-HTTPS
209
- *
210
- * This example uses the Mozilla provided RFC 1035 DNS over HTTPS service. This
211
- * uses binary DNS records so requires extra dependencies to process the
212
- * response which can increase browser bundle sizes.
213
- *
214
- * If this is a concern, use the DNS-JSON-Over-HTTPS resolver instead.
215
- *
216
- * ```TypeScript
217
- * import { createHelia } from 'helia'
218
- * import { ipns } from '@helia/ipns'
219
- * import { dns } from '@multiformats/dns'
220
- * import { dnsOverHttps } from '@multiformats/dns/resolvers'
221
- *
222
- * const node = await createHelia({
223
- * dns: dns({
224
- * resolvers: {
225
- * '.': dnsOverHttps('https://mozilla.cloudflare-dns.com/dns-query')
226
- * }
227
- * })
228
- * })
229
- * const name = ipns(node)
230
- *
231
- * const result = await name.resolveDNSLink('ipfs.io')
232
- * ```
233
- *
234
- * @example Using DNS-JSON-Over-HTTPS
235
- *
236
- * DNS-JSON-Over-HTTPS resolvers use the RFC 8427 `application/dns-json` and can
237
- * result in a smaller browser bundle due to the response being plain JSON.
238
- *
239
- * ```TypeScript
240
- * import { createHelia } from 'helia'
241
- * import { ipns } from '@helia/ipns'
242
- * import { dns } from '@multiformats/dns'
243
- * import { dnsJsonOverHttps } from '@multiformats/dns/resolvers'
244
- *
245
- * const node = await createHelia({
246
- * dns: dns({
247
- * resolvers: {
248
- * '.': dnsJsonOverHttps('https://mozilla.cloudflare-dns.com/dns-query')
249
- * }
250
- * })
251
- * })
252
- * const name = ipns(node)
253
- *
254
- * const result = await name.resolveDNSLink('ipfs.io')
138
+ * const result = await name.resolve(publicKey)
255
139
  * ```
256
140
  *
257
141
  * @example Republishing an existing IPNS record
258
142
  *
259
- * The `republishRecord` method allows you to republish an existing IPNS record without
260
- * needing the private key. This is useful for relay nodes or when you want to extend
261
- * the availability of a record that was created elsewhere.
143
+ * It is sometimes useful to be able to republish an existing IPNS record
144
+ * without needing the private key. This allows you to extend the availability
145
+ * of a record that was created elsewhere.
262
146
  *
263
147
  * ```TypeScript
264
148
  * import { createHelia } from 'helia'
265
- * import { ipns } from '@helia/ipns'
149
+ * import { ipns, ipnsValidator } from '@helia/ipns'
266
150
  * import { createDelegatedRoutingV1HttpApiClient } from '@helia/delegated-routing-v1-http-api-client'
267
151
  * import { CID } from 'multiformats/cid'
152
+ * import { multihashToIPNSRoutingKey, marshalIPNSRecord } from 'ipns'
268
153
  *
269
154
  * const helia = await createHelia()
270
155
  * const name = ipns(helia)
@@ -274,47 +159,33 @@
274
159
  * const delegatedClient = createDelegatedRoutingV1HttpApiClient('https://delegated-ipfs.dev')
275
160
  * const record = await delegatedClient.getIPNS(parsedCid)
276
161
  *
277
- * await name.republishRecord(ipnsName, record)
162
+ * const routingKey = multihashToIPNSRoutingKey(parsedCid.multihash)
163
+ * const marshaledRecord = marshalIPNSRecord(record)
164
+ *
165
+ * // validate that they key corresponds to the record
166
+ * await ipnsValidator(routingKey, marshaledRecord)
167
+ *
168
+ * // publish record to routing
169
+ * await Promise.all(
170
+ * name.routers.map(async r => {
171
+ * await r.put(routingKey, marshaledRecord)
172
+ * })
173
+ * )
278
174
  * ```
279
175
  */
280
176
 
281
- import { NotFoundError, isPublicKey } from '@libp2p/interface'
282
- import { logger } from '@libp2p/logger'
283
- import { peerIdFromString } from '@libp2p/peer-id'
284
- import { createIPNSRecord, extractPublicKeyFromIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey, unmarshalIPNSRecord } from 'ipns'
285
- import { ipnsSelector } from 'ipns/selector'
286
177
  import { ipnsValidator } from 'ipns/validator'
287
- import { base36 } from 'multiformats/bases/base36'
288
- import { base58btc } from 'multiformats/bases/base58'
289
178
  import { CID } from 'multiformats/cid'
290
- import * as Digest from 'multiformats/hashes/digest'
291
- import { CustomProgressEvent } from 'progress-events'
292
- import { resolveDNSLink } from './dnslink.js'
293
- import { InvalidValueError, RecordsFailedValidationError, UnsupportedMultibasePrefixError, UnsupportedMultihashCodecError } from './errors.js'
294
- import { helia } from './routing/helia.js'
295
- import { localStore } from './routing/local-store.js'
296
- import { isCodec, IDENTITY_CODEC, SHA2_256_CODEC, IPNS_STRING_PREFIX } from './utils.js'
179
+ import { IPNS as IPNSClass } from './ipns.js'
297
180
  import type { IPNSRouting, IPNSRoutingEvents } from './routing/index.js'
298
- import type { LocalStore } from './routing/local-store.js'
299
- import type { Routing } from '@helia/interface'
300
- import type { AbortOptions, ComponentLogger, Logger, PrivateKey, PublicKey } from '@libp2p/interface'
301
- import type { Answer, DNS, ResolveDnsProgressEvents } from '@multiformats/dns'
181
+ import type { Routing, HeliaEvents } from '@helia/interface'
182
+ import type { AbortOptions, ComponentLogger, Libp2p, PeerId, PublicKey, TypedEventEmitter } from '@libp2p/interface'
183
+ import type { Keychain } from '@libp2p/keychain'
302
184
  import type { Datastore } from 'interface-datastore'
303
185
  import type { IPNSRecord } from 'ipns'
304
- import type { MultibaseDecoder } from 'multiformats/bases/interface'
305
186
  import type { MultihashDigest } from 'multiformats/hashes/interface'
306
187
  import type { ProgressEvent, ProgressOptions } from 'progress-events'
307
188
 
308
- const log = logger('helia:ipns')
309
-
310
- const MINUTE = 60 * 1000
311
- const HOUR = 60 * MINUTE
312
-
313
- const DEFAULT_LIFETIME_MS = 48 * HOUR
314
- const DEFAULT_REPUBLISH_INTERVAL_MS = 23 * HOUR
315
-
316
- const DEFAULT_TTL_NS = BigInt(MINUTE) * 5_000_000n // 5 minutes
317
-
318
189
  export type PublishProgressEvents =
319
190
  ProgressEvent<'ipns:publish:start'> |
320
191
  ProgressEvent<'ipns:publish:success', IPNSRecord> |
@@ -325,15 +196,11 @@ export type ResolveProgressEvents =
325
196
  ProgressEvent<'ipns:resolve:success', IPNSRecord> |
326
197
  ProgressEvent<'ipns:resolve:error', Error>
327
198
 
328
- export type RepublishProgressEvents =
329
- ProgressEvent<'ipns:republish:start', unknown> |
330
- ProgressEvent<'ipns:republish:success', IPNSRecord> |
331
- ProgressEvent<'ipns:republish:error', { key?: MultihashDigest<0x00 | 0x12>, record: IPNSRecord, err: Error }>
332
-
333
- export type ResolveDNSLinkProgressEvents =
334
- ResolveProgressEvents |
335
- IPNSRoutingEvents |
336
- ResolveDnsProgressEvents
199
+ export type DatastoreProgressEvents =
200
+ ProgressEvent<'ipns:routing:datastore:put'> |
201
+ ProgressEvent<'ipns:routing:datastore:get'> |
202
+ ProgressEvent<'ipns:routing:datastore:list'> |
203
+ ProgressEvent<'ipns:routing:datastore:error', Error>
337
204
 
338
205
  export interface PublishOptions extends AbortOptions, ProgressOptions<PublishProgressEvents | IPNSRoutingEvents> {
339
206
  /**
@@ -358,23 +225,12 @@ export interface PublishOptions extends AbortOptions, ProgressOptions<PublishPro
358
225
  ttl?: number
359
226
  }
360
227
 
361
- export interface ResolveOptions extends AbortOptions, ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents> {
362
- /**
363
- * Do not query the network for the IPNS record
364
- *
365
- * @default false
366
- */
367
- offline?: boolean
368
-
369
- /**
370
- * Do not use cached IPNS Record entries
371
- *
372
- * @default false
373
- */
374
- nocache?: boolean
228
+ export interface IPNSRecordMetadata {
229
+ keyName: string
230
+ lifetime: number
375
231
  }
376
232
 
377
- export interface ResolveDNSLinkOptions extends AbortOptions, ProgressOptions<ResolveDNSLinkProgressEvents> {
233
+ export interface ResolveOptions extends AbortOptions, ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents> {
378
234
  /**
379
235
  * Do not query the network for the IPNS record
380
236
  *
@@ -383,35 +239,11 @@ export interface ResolveDNSLinkOptions extends AbortOptions, ProgressOptions<Res
383
239
  offline?: boolean
384
240
 
385
241
  /**
386
- * Do not use cached DNS entries
242
+ * Do not use cached IPNS Record entries
387
243
  *
388
244
  * @default false
389
245
  */
390
246
  nocache?: boolean
391
-
392
- /**
393
- * When resolving DNSLink records that resolve to other DNSLink records, limit
394
- * how many times we will recursively resolve them.
395
- *
396
- * @default 32
397
- */
398
- maxRecursiveDepth?: number
399
- }
400
-
401
- export interface RepublishOptions extends AbortOptions, ProgressOptions<RepublishProgressEvents | IPNSRoutingEvents> {
402
- /**
403
- * The republish interval in ms (default: 23hrs)
404
- */
405
- interval?: number
406
- }
407
-
408
- export interface RepublishRecordOptions extends AbortOptions, ProgressOptions<RepublishProgressEvents | IPNSRoutingEvents> {
409
- /**
410
- * Only publish to a local datastore
411
- *
412
- * @default false
413
- */
414
- offline?: boolean
415
247
  }
416
248
 
417
249
  export interface ResolveResult {
@@ -435,44 +267,73 @@ export interface IPNSResolveResult extends ResolveResult {
435
267
  record: IPNSRecord
436
268
  }
437
269
 
438
- export interface DNSLinkResolveResult extends ResolveResult {
270
+ export interface IPNSPublishResult {
439
271
  /**
440
- * The resolved record
272
+ * The published record
441
273
  */
442
- answer: Answer
443
- }
274
+ record: IPNSRecord
444
275
 
445
- export interface IPNS {
446
276
  /**
447
- * Creates an IPNS record signed by the passed PeerId that will resolve to the passed value
448
- *
449
- * If the value is a PeerId, a recursive IPNS record will be created.
277
+ * The public key that was used to publish the record
450
278
  */
451
- publish(key: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, options?: PublishOptions): Promise<IPNSRecord>
279
+ publicKey: PublicKey
280
+ }
452
281
 
282
+ export interface IPNS {
453
283
  /**
454
- * Accepts a public key formatted as a libp2p PeerID and resolves the IPNS record
455
- * corresponding to that public key until a value is found
284
+ * Configured routing subsystems used to publish/resolve IPNS names
456
285
  */
457
- resolve(key: PublicKey | MultihashDigest<0x00 | 0x12>, options?: ResolveOptions): Promise<IPNSResolveResult>
286
+ routers: IPNSRouting[]
458
287
 
459
288
  /**
460
- * Resolve a CID from a dns-link style IPNS record
289
+ * Creates and publishes an IPNS record that will resolve the passed value
290
+ * signed by a key stored in the libp2p keychain under the passed key name.
291
+ *
292
+ * It is possible to create a recursive IPNS record by passing:
293
+ *
294
+ * - A PeerId,
295
+ * - A PublicKey
296
+ * - A CID with the libp2p-key codec and Identity or SHA256 hash algorithms
297
+ * - A Multihash with the Identity or SHA256 hash algorithms
298
+ * - A string IPNS key (e.g. `/ipns/Qmfoo`)
299
+ *
300
+ * @example
301
+ *
302
+ * ```TypeScript
303
+ * import { createHelia } from 'helia'
304
+ * import { ipns } from '@helia/ipns'
305
+ *
306
+ * const helia = await createHelia()
307
+ * const name = ipns(helia)
308
+ *
309
+ * const result = await name.publish('my-key-name', cid, {
310
+ * signal: AbortSignal.timeout(5_000)
311
+ * })
312
+ *
313
+ * console.info(result) // { answer: ... }
314
+ * ```
461
315
  */
462
- resolveDNSLink(domain: string, options?: ResolveDNSLinkOptions): Promise<DNSLinkResolveResult>
316
+ publish(keyName: string, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | PeerId | string, options?: PublishOptions): Promise<IPNSPublishResult>
463
317
 
464
318
  /**
465
- * Periodically republish all IPNS records found in the datastore
319
+ * Accepts a libp2p public key, a CID with the libp2p-key codec and either the
320
+ * identity hash (for Ed25519 and secp256k1 public keys) or a SHA256 hash (for
321
+ * RSA public keys), or the multihash of a libp2p-key encoded CID, or a
322
+ * Ed25519, secp256k1 or RSA PeerId and recursively resolves the IPNS record
323
+ * corresponding to that key until a value is found.
466
324
  */
467
- republish(options?: RepublishOptions): void
325
+ resolve(key: CID<unknown, 0x72, 0x00 | 0x12, 1> | PublicKey | MultihashDigest<0x00 | 0x12> | PeerId, options?: ResolveOptions): Promise<IPNSResolveResult>
468
326
 
469
327
  /**
470
- * Republish an existing IPNS record without the private key.
328
+ * Stop republishing of an IPNS record
329
+ *
330
+ * This will delete the last signed IPNS record from the datastore, but the
331
+ * key will remain in the keychain.
471
332
  *
472
- * Before republishing the record will be validated to ensure it has a valid signature and lifetime(validity) in the future.
473
- * The key is a multihash of the public key or a string representation of the PeerID (either base58btc encoded multihash or base36 encoded CID)
333
+ * Note that the record may still be resolved by other peers until it expires
334
+ * or is no longer valid.
474
335
  */
475
- republishRecord(key: MultihashDigest<0x00 | 0x12> | string, record: IPNSRecord, options?: RepublishRecordOptions): Promise<void>
336
+ unpublish(keyName: string, options?: AbortOptions): Promise<void>
476
337
  }
477
338
 
478
339
  export type { IPNSRouting } from './routing/index.js'
@@ -482,325 +343,35 @@ export type { IPNSRecord } from 'ipns'
482
343
  export interface IPNSComponents {
483
344
  datastore: Datastore
484
345
  routing: Routing
485
- dns: DNS
486
346
  logger: ComponentLogger
487
- }
488
-
489
- const bases: Record<string, MultibaseDecoder<string>> = {
490
- [base36.prefix]: base36,
491
- [base58btc.prefix]: base58btc
492
- }
493
-
494
- class DefaultIPNS implements IPNS {
495
- private readonly routers: IPNSRouting[]
496
- private readonly localStore: LocalStore
497
- private timeout?: ReturnType<typeof setTimeout>
498
- private readonly dns: DNS
499
- private readonly log: Logger
500
-
501
- constructor (components: IPNSComponents, routers: IPNSRouting[] = []) {
502
- this.routers = [
503
- helia(components.routing),
504
- ...routers
505
- ]
506
- this.localStore = localStore(components.datastore)
507
- this.dns = components.dns
508
- this.log = components.logger.forComponent('helia:ipns')
509
- }
510
-
511
- async publish (key: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, options: PublishOptions = {}): Promise<IPNSRecord> {
512
- try {
513
- let sequenceNumber = 1n
514
- const routingKey = multihashToIPNSRoutingKey(key.publicKey.toMultihash())
515
-
516
- if (await this.localStore.has(routingKey, options)) {
517
- // if we have published under this key before, increment the sequence number
518
- const { record } = await this.localStore.get(routingKey, options)
519
- const existingRecord = unmarshalIPNSRecord(record)
520
- sequenceNumber = existingRecord.sequence + 1n
521
- }
522
-
523
- // convert ttl from milliseconds to nanoseconds as createIPNSRecord expects
524
- const ttlNs = options.ttl != null ? BigInt(options.ttl) * 1_000_000n : DEFAULT_TTL_NS
525
- const record = await createIPNSRecord(key, value, sequenceNumber, options.lifetime ?? DEFAULT_LIFETIME_MS, { ...options, ttlNs })
526
- const marshaledRecord = marshalIPNSRecord(record)
527
-
528
- await this.localStore.put(routingKey, marshaledRecord, options)
529
-
530
- if (options.offline !== true) {
531
- // publish record to routing
532
- await Promise.all(this.routers.map(async r => { await r.put(routingKey, marshaledRecord, options) }))
533
- }
534
-
535
- return record
536
- } catch (err: any) {
537
- options.onProgress?.(new CustomProgressEvent<Error>('ipns:publish:error', err))
538
- throw err
539
- }
540
- }
541
-
542
- async resolve (key: PublicKey | MultihashDigest<0x00 | 0x12>, options: ResolveOptions = {}): Promise<IPNSResolveResult> {
543
- const digest = isPublicKey(key) ? key.toMultihash() : key
544
- const routingKey = multihashToIPNSRoutingKey(digest)
545
- const record = await this.#findIpnsRecord(routingKey, options)
546
-
547
- return {
548
- ...(await this.#resolve(record.value, options)),
549
- record
550
- }
551
- }
552
-
553
- async resolveDNSLink (domain: string, options: ResolveDNSLinkOptions = {}): Promise<DNSLinkResolveResult> {
554
- const dnslink = await resolveDNSLink(domain, this.dns, this.log, options)
555
-
556
- return {
557
- ...(await this.#resolve(dnslink.value, options)),
558
- answer: dnslink.answer
559
- }
560
- }
561
-
562
- republish (options: RepublishOptions = {}): void {
563
- if (this.timeout != null) {
564
- throw new Error('Republish is already running')
565
- }
566
-
567
- options.signal?.addEventListener('abort', () => {
568
- clearTimeout(this.timeout)
569
- })
570
-
571
- async function republish (): Promise<void> {
572
- const startTime = Date.now()
573
-
574
- options.onProgress?.(new CustomProgressEvent('ipns:republish:start'))
575
-
576
- const finishType = Date.now()
577
- const timeTaken = finishType - startTime
578
- let nextInterval = DEFAULT_REPUBLISH_INTERVAL_MS - timeTaken
579
-
580
- if (nextInterval < 0) {
581
- nextInterval = options.interval ?? DEFAULT_REPUBLISH_INTERVAL_MS
582
- }
583
-
584
- setTimeout(() => {
585
- republish().catch(err => {
586
- log.error('error republishing', err)
587
- })
588
- }, nextInterval)
589
- }
590
-
591
- this.timeout = setTimeout(() => {
592
- republish().catch(err => {
593
- log.error('error republishing', err)
594
- })
595
- }, options.interval ?? DEFAULT_REPUBLISH_INTERVAL_MS)
596
- }
597
-
598
- async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise<{ cid: CID, path: string }> {
599
- const parts = ipfsPath.split('/')
600
- try {
601
- const scheme = parts[1]
602
-
603
- if (scheme === 'ipns') {
604
- const str = parts[2]
605
- const prefix = str.substring(0, 1)
606
- let buf: Uint8Array | undefined
607
-
608
- if (prefix === '1' || prefix === 'Q') {
609
- buf = base58btc.decode(`z${str}`)
610
- } else if (bases[prefix] != null) {
611
- buf = bases[prefix].decode(str)
612
- } else {
613
- throw new UnsupportedMultibasePrefixError(`Unsupported multibase prefix "${prefix}"`)
614
- }
615
-
616
- let digest: MultihashDigest<number>
617
-
618
- try {
619
- digest = Digest.decode(buf)
620
- } catch {
621
- digest = CID.decode(buf).multihash
622
- }
623
-
624
- if (!isCodec(digest, IDENTITY_CODEC) && !isCodec(digest, SHA2_256_CODEC)) {
625
- throw new UnsupportedMultihashCodecError(`Unsupported multihash codec "${digest.code}"`)
626
- }
627
-
628
- const { cid } = await this.resolve(digest, options)
629
- const path = parts.slice(3).join('/')
630
- return {
631
- cid,
632
- path
633
- }
634
- } else if (scheme === 'ipfs') {
635
- const cid = CID.parse(parts[2])
636
- const path = parts.slice(3).join('/')
637
- return {
638
- cid,
639
- path
640
- }
641
- }
642
- } catch (err) {
643
- log.error('error parsing ipfs path', err)
644
- }
645
-
646
- log.error('invalid ipfs path %s', ipfsPath)
647
- throw new InvalidValueError('Invalid value')
648
- }
649
-
650
- async #findIpnsRecord (routingKey: Uint8Array, options: ResolveOptions = {}): Promise<IPNSRecord> {
651
- const records: Uint8Array[] = []
652
- const cached = await this.localStore.has(routingKey, options)
653
-
654
- if (cached) {
655
- log('record is present in the cache')
656
-
657
- if (options.nocache !== true) {
658
- try {
659
- // check the local cache first
660
- const { record, created } = await this.localStore.get(routingKey, options)
661
-
662
- this.log('record retrieved from cache')
663
-
664
- // validate the record
665
- await ipnsValidator(routingKey, record)
666
-
667
- this.log('record was valid')
668
-
669
- // check the TTL
670
- const ipnsRecord = unmarshalIPNSRecord(record)
671
-
672
- // IPNS TTL is in nanoseconds, convert to milliseconds, default to one
673
- // hour
674
- const ttlMs = Number((ipnsRecord.ttl ?? DEFAULT_TTL_NS) / 1_000_000n)
675
- const ttlExpires = created.getTime() + ttlMs
676
-
677
- if (ttlExpires > Date.now()) {
678
- // the TTL has not yet expired, return the cached record
679
- this.log('record TTL was valid')
680
- return ipnsRecord
681
- }
682
-
683
- if (options.offline === true) {
684
- // the TTL has expired but we are skipping the routing search
685
- this.log('record TTL has been reached but we are resolving offline-only, returning record')
686
- return ipnsRecord
687
- }
688
-
689
- this.log('record TTL has been reached, searching routing for updates')
690
-
691
- // add the local record to our list of resolved record, and also
692
- // search the routing for updates - the most up to date record will be
693
- // returned
694
- records.push(record)
695
- } catch (err) {
696
- this.log('cached record was invalid', err)
697
- await this.localStore.delete(routingKey, options)
698
- }
699
- } else {
700
- log('ignoring local cache due to nocache=true option')
701
- }
702
- }
703
-
704
- if (options.offline === true) {
705
- throw new NotFoundError('Record was not present in the cache or has expired')
706
- }
707
-
708
- log('did not have record locally')
709
-
710
- let foundInvalid = 0
711
-
712
- await Promise.all(
713
- this.routers.map(async (router) => {
714
- let record: Uint8Array
715
-
716
- try {
717
- record = await router.get(routingKey, {
718
- ...options,
719
- validate: false
720
- })
721
- } catch (err: any) {
722
- log.error('error finding IPNS record', err)
723
-
724
- return
725
- }
726
-
727
- try {
728
- await ipnsValidator(routingKey, record)
729
-
730
- records.push(record)
731
- } catch (err) {
732
- // we found a record, but the validator rejected it
733
- foundInvalid++
734
- log.error('error finding IPNS record', err)
735
- }
736
- })
737
- )
738
-
739
- if (records.length === 0) {
740
- if (foundInvalid > 0) {
741
- throw new RecordsFailedValidationError(`${foundInvalid > 1 ? `${foundInvalid} records` : 'Record'} found for routing key ${foundInvalid > 1 ? 'were' : 'was'} invalid`)
742
- }
743
-
744
- throw new NotFoundError('Could not find record for routing key')
745
- }
746
-
747
- const record = records[ipnsSelector(routingKey, records)]
748
-
749
- await this.localStore.put(routingKey, record, options)
750
-
751
- return unmarshalIPNSRecord(record)
752
- }
753
-
754
- async republishRecord (key: MultihashDigest<0x00 | 0x12> | string, record: IPNSRecord, options: RepublishRecordOptions = {}): Promise<void> {
755
- let mh: MultihashDigest<0x00 | 0x12> | undefined
756
- try {
757
- mh = extractPublicKeyFromIPNSRecord(record)?.toMultihash() // embedded public key take precedence, if present
758
- if (mh == null) {
759
- // if no public key is embedded in the record, use the key that was passed in
760
- if (typeof key === 'string') {
761
- if (key.startsWith(IPNS_STRING_PREFIX)) {
762
- // remove the /ipns/ prefix from the key
763
- key = key.slice(IPNS_STRING_PREFIX.length)
764
- }
765
- // Convert string key to MultihashDigest
766
- try {
767
- mh = peerIdFromString(key).toMultihash()
768
- } catch (err: any) {
769
- throw new Error(`Invalid string key: ${err.message}`)
770
- }
771
- } else {
772
- mh = key
773
- }
774
- }
775
-
776
- if (mh == null) {
777
- throw new Error('No public key multihash found to determine the routing key')
778
- }
779
-
780
- const routingKey = multihashToIPNSRoutingKey(mh)
781
- const marshaledRecord = marshalIPNSRecord(record)
782
-
783
- await ipnsValidator(routingKey, marshaledRecord) // validate that they key corresponds to the record
784
-
785
- await this.localStore.put(routingKey, marshaledRecord, options) // add to local store
786
-
787
- if (options.offline !== true) {
788
- // publish record to routing
789
- await Promise.all(this.routers.map(async r => { await r.put(routingKey, marshaledRecord, options) }))
790
- }
791
- } catch (err: any) {
792
- options.onProgress?.(new CustomProgressEvent('ipns:republish:error', { key: mh, record, err }))
793
- throw err
794
- }
795
- }
347
+ libp2p: Libp2p<{ keychain: Keychain }>
348
+ events: TypedEventEmitter<HeliaEvents> // Helia event bus
796
349
  }
797
350
 
798
351
  export interface IPNSOptions {
352
+ /**
353
+ * Different routing systems for IPNS publishing/resolving
354
+ */
799
355
  routers?: IPNSRouting[]
356
+
357
+ /**
358
+ * How often to check if published records have expired and need republishing
359
+ * in ms
360
+ *
361
+ * @default 3_600_000
362
+ */
363
+ republishInterval?: number
364
+
365
+ /**
366
+ * How many IPNS records to republish at once
367
+ *
368
+ * @default 5
369
+ */
370
+ republishConcurrency?: number
800
371
  }
801
372
 
802
- export function ipns (components: IPNSComponents, { routers = [] }: IPNSOptions = {}): IPNS {
803
- return new DefaultIPNS(components, routers)
373
+ export function ipns (components: IPNSComponents, options: IPNSOptions = {}): IPNS {
374
+ return new IPNSClass(components, options)
804
375
  }
805
376
 
806
377
  export { ipnsValidator, type IPNSRoutingEvents }