@helia/ipns 7.0.0-ecf5394 → 7.1.0-1561e4a

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.
package/src/index.ts CHANGED
@@ -241,7 +241,7 @@ import { localStore, type LocalStore } from './routing/local-store.js'
241
241
  import type { IPNSRouting, IPNSRoutingEvents } from './routing/index.js'
242
242
  import type { Routing } from '@helia/interface'
243
243
  import type { AbortOptions, ComponentLogger, Logger, PeerId } from '@libp2p/interface'
244
- import type { DNS, ResolveDnsProgressEvents } from '@multiformats/dns'
244
+ import type { Answer, DNS, ResolveDnsProgressEvents } from '@multiformats/dns'
245
245
  import type { Datastore } from 'interface-datastore'
246
246
  import type { IPNSRecord } from 'ipns'
247
247
  import type { ProgressEvent, ProgressOptions } from 'progress-events'
@@ -254,6 +254,8 @@ const HOUR = 60 * MINUTE
254
254
  const DEFAULT_LIFETIME_MS = 24 * HOUR
255
255
  const DEFAULT_REPUBLISH_INTERVAL_MS = 23 * HOUR
256
256
 
257
+ const DEFAULT_TTL_NS = BigInt(HOUR) * 1_000_000n
258
+
257
259
  export type PublishProgressEvents =
258
260
  ProgressEvent<'ipns:publish:start'> |
259
261
  ProgressEvent<'ipns:publish:success', IPNSRecord> |
@@ -294,9 +296,18 @@ export interface PublishOptions extends AbortOptions, ProgressOptions<PublishPro
294
296
 
295
297
  export interface ResolveOptions extends AbortOptions, ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents> {
296
298
  /**
297
- * Do not query the network for the IPNS record (default: false)
299
+ * Do not query the network for the IPNS record
300
+ *
301
+ * @default false
298
302
  */
299
303
  offline?: boolean
304
+
305
+ /**
306
+ * Do not use cached IPNS Record entries
307
+ *
308
+ * @default false
309
+ */
310
+ nocache?: boolean
300
311
  }
301
312
 
302
313
  export interface ResolveDNSLinkOptions extends AbortOptions, ProgressOptions<ResolveDNSLinkProgressEvents> {
@@ -331,10 +342,33 @@ export interface RepublishOptions extends AbortOptions, ProgressOptions<Republis
331
342
  }
332
343
 
333
344
  export interface ResolveResult {
345
+ /**
346
+ * The CID that was resolved
347
+ */
334
348
  cid: CID
349
+
350
+ /**
351
+ * Any path component that was part of the resolved record
352
+ *
353
+ * @default ""
354
+ */
335
355
  path: string
336
356
  }
337
357
 
358
+ export interface IPNSResolveResult extends ResolveResult {
359
+ /**
360
+ * The resolved record
361
+ */
362
+ record: IPNSRecord
363
+ }
364
+
365
+ export interface DNSLinkResolveResult extends ResolveResult {
366
+ /**
367
+ * The resolved record
368
+ */
369
+ answer: Answer
370
+ }
371
+
338
372
  export interface IPNS {
339
373
  /**
340
374
  * Creates an IPNS record signed by the passed PeerId that will resolve to the passed value
@@ -347,12 +381,12 @@ export interface IPNS {
347
381
  * Accepts a public key formatted as a libp2p PeerID and resolves the IPNS record
348
382
  * corresponding to that public key until a value is found
349
383
  */
350
- resolve(key: PeerId, options?: ResolveOptions): Promise<ResolveResult>
384
+ resolve(key: PeerId, options?: ResolveOptions): Promise<IPNSResolveResult>
351
385
 
352
386
  /**
353
387
  * Resolve a CID from a dns-link style IPNS record
354
388
  */
355
- resolveDNSLink(domain: string, options?: ResolveDNSLinkOptions): Promise<ResolveResult>
389
+ resolveDNSLink(domain: string, options?: ResolveDNSLinkOptions): Promise<DNSLinkResolveResult>
356
390
 
357
391
  /**
358
392
  * Periodically republish all IPNS records found in the datastore
@@ -393,8 +427,8 @@ class DefaultIPNS implements IPNS {
393
427
 
394
428
  if (await this.localStore.has(routingKey, options)) {
395
429
  // if we have published under this key before, increment the sequence number
396
- const buf = await this.localStore.get(routingKey, options)
397
- const existingRecord = unmarshal(buf)
430
+ const { record } = await this.localStore.get(routingKey, options)
431
+ const existingRecord = unmarshal(record)
398
432
  sequenceNumber = existingRecord.sequence + 1n
399
433
  }
400
434
 
@@ -416,17 +450,23 @@ class DefaultIPNS implements IPNS {
416
450
  }
417
451
  }
418
452
 
419
- async resolve (key: PeerId, options: ResolveOptions = {}): Promise<ResolveResult> {
453
+ async resolve (key: PeerId, options: ResolveOptions = {}): Promise<IPNSResolveResult> {
420
454
  const routingKey = peerIdToRoutingKey(key)
421
455
  const record = await this.#findIpnsRecord(routingKey, options)
422
456
 
423
- return this.#resolve(record.value, options)
457
+ return {
458
+ ...(await this.#resolve(record.value, options)),
459
+ record
460
+ }
424
461
  }
425
462
 
426
- async resolveDNSLink (domain: string, options: ResolveDNSLinkOptions = {}): Promise<ResolveResult> {
463
+ async resolveDNSLink (domain: string, options: ResolveDNSLinkOptions = {}): Promise<DNSLinkResolveResult> {
427
464
  const dnslink = await resolveDNSLink(domain, this.dns, this.log, options)
428
465
 
429
- return this.#resolve(dnslink, options)
466
+ return {
467
+ ...(await this.#resolve(dnslink.value, options)),
468
+ answer: dnslink.answer
469
+ }
430
470
  }
431
471
 
432
472
  republish (options: RepublishOptions = {}): void {
@@ -465,7 +505,7 @@ class DefaultIPNS implements IPNS {
465
505
  }, options.interval ?? DEFAULT_REPUBLISH_INTERVAL_MS)
466
506
  }
467
507
 
468
- async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise<ResolveResult> {
508
+ async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise<{ cid: CID, path: string }> {
469
509
  const parts = ipfsPath.split('/')
470
510
  try {
471
511
  const scheme = parts[1]
@@ -494,22 +534,69 @@ class DefaultIPNS implements IPNS {
494
534
  }
495
535
 
496
536
  async #findIpnsRecord (routingKey: Uint8Array, options: ResolveOptions = {}): Promise<IPNSRecord> {
497
- let routers = [
498
- this.localStore,
499
- ...this.routers
500
- ]
537
+ const records: Uint8Array[] = []
538
+ const cached = await this.localStore.has(routingKey, options)
539
+
540
+ if (cached) {
541
+ log('record is present in the cache')
542
+
543
+ if (options.nocache !== true) {
544
+ try {
545
+ // check the local cache first
546
+ const { record, created } = await this.localStore.get(routingKey, options)
547
+
548
+ this.log('record retrieved from cache')
549
+
550
+ // validate the record
551
+ await ipnsValidator(routingKey, record)
552
+
553
+ this.log('record was valid')
554
+
555
+ // check the TTL
556
+ const ipnsRecord = unmarshal(record)
557
+
558
+ // IPNS TTL is in nanoseconds, convert to milliseconds, default to one
559
+ // hour
560
+ const ttlMs = Number((ipnsRecord.ttl ?? DEFAULT_TTL_NS) / 1_000_000n)
561
+ const ttlExpires = created.getTime() + ttlMs
562
+
563
+ if (ttlExpires > Date.now()) {
564
+ // the TTL has not yet expired, return the cached record
565
+ this.log('record TTL was valid')
566
+ return ipnsRecord
567
+ }
568
+
569
+ if (options.offline === true) {
570
+ // the TTL has expired but we are skipping the routing search
571
+ this.log('record TTL has been reached but we are resolving offline-only, returning record')
572
+ return ipnsRecord
573
+ }
574
+
575
+ this.log('record TTL has been reached, searching routing for updates')
576
+
577
+ // add the local record to our list of resolved record, and also
578
+ // search the routing for updates - the most up to date record will be
579
+ // returned
580
+ records.push(record)
581
+ } catch (err) {
582
+ this.log('cached record was invalid', err)
583
+ await this.localStore.delete(routingKey, options)
584
+ }
585
+ } else {
586
+ log('ignoring local cache due to nocache=true option')
587
+ }
588
+ }
501
589
 
502
590
  if (options.offline === true) {
503
- routers = [
504
- this.localStore
505
- ]
591
+ throw new CodeError('Record was not present in the cache or has expired', 'ERR_NOT_FOUND')
506
592
  }
507
593
 
508
- const records: Uint8Array[] = []
594
+ log('did not have record locally')
595
+
509
596
  let foundInvalid = 0
510
597
 
511
598
  await Promise.all(
512
- routers.map(async (router) => {
599
+ this.routers.map(async (router) => {
513
600
  let record: Uint8Array
514
601
 
515
602
  try {
@@ -518,11 +605,7 @@ class DefaultIPNS implements IPNS {
518
605
  validate: false
519
606
  })
520
607
  } catch (err: any) {
521
- if (router === this.localStore && err.code === 'ERR_NOT_FOUND') {
522
- log('did not have record locally')
523
- } else {
524
- log.error('error finding IPNS record', err)
525
- }
608
+ log.error('error finding IPNS record', err)
526
609
 
527
610
  return
528
611
  }
@@ -1,8 +1,9 @@
1
1
  import { Record } from '@libp2p/kad-dht'
2
2
  import { type Datastore, Key } from 'interface-datastore'
3
3
  import { CustomProgressEvent, type ProgressEvent } from 'progress-events'
4
+ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
4
5
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
5
- import type { GetOptions, IPNSRouting, PutOptions } from '../routing'
6
+ import type { GetOptions, PutOptions } from '../routing'
6
7
  import type { AbortOptions } from '@libp2p/interface'
7
8
 
8
9
  function dhtRoutingKey (key: Uint8Array): Key {
@@ -14,8 +15,16 @@ export type DatastoreProgressEvents =
14
15
  ProgressEvent<'ipns:routing:datastore:get'> |
15
16
  ProgressEvent<'ipns:routing:datastore:error', Error>
16
17
 
17
- export interface LocalStore extends IPNSRouting {
18
+ export interface GetResult {
19
+ record: Uint8Array
20
+ created: Date
21
+ }
22
+
23
+ export interface LocalStore {
24
+ put(routingKey: Uint8Array, marshaledRecord: Uint8Array, options?: PutOptions): Promise<void>
25
+ get(routingKey: Uint8Array, options?: GetOptions): Promise<GetResult>
18
26
  has(routingKey: Uint8Array, options?: AbortOptions): Promise<boolean>
27
+ delete(routingKey: Uint8Array, options?: AbortOptions): Promise<void>
19
28
  }
20
29
 
21
30
  /**
@@ -29,6 +38,21 @@ export function localStore (datastore: Datastore): LocalStore {
29
38
  try {
30
39
  const key = dhtRoutingKey(routingKey)
31
40
 
41
+ // don't overwrite existing, identical records as this will affect the
42
+ // TTL
43
+ try {
44
+ const existingBuf = await datastore.get(key)
45
+ const existingRecord = Record.deserialize(existingBuf)
46
+
47
+ if (uint8ArrayEquals(existingRecord.value, marshalledRecord)) {
48
+ return
49
+ }
50
+ } catch (err: any) {
51
+ if (err.code !== 'ERR_NOT_FOUND') {
52
+ throw err
53
+ }
54
+ }
55
+
32
56
  // Marshal to libp2p record as the DHT does
33
57
  const record = new Record(routingKey, marshalledRecord, new Date())
34
58
 
@@ -39,7 +63,7 @@ export function localStore (datastore: Datastore): LocalStore {
39
63
  throw err
40
64
  }
41
65
  },
42
- async get (routingKey: Uint8Array, options: GetOptions = {}): Promise<Uint8Array> {
66
+ async get (routingKey: Uint8Array, options: GetOptions = {}): Promise<GetResult> {
43
67
  try {
44
68
  const key = dhtRoutingKey(routingKey)
45
69
 
@@ -49,7 +73,10 @@ export function localStore (datastore: Datastore): LocalStore {
49
73
  // Unmarshal libp2p record as the DHT does
50
74
  const record = Record.deserialize(buf)
51
75
 
52
- return record.value
76
+ return {
77
+ record: record.value,
78
+ created: record.timeReceived
79
+ }
53
80
  } catch (err: any) {
54
81
  options.onProgress?.(new CustomProgressEvent<Error>('ipns:routing:datastore:error', err))
55
82
  throw err
@@ -58,6 +85,10 @@ export function localStore (datastore: Datastore): LocalStore {
58
85
  async has (routingKey: Uint8Array, options: AbortOptions = {}): Promise<boolean> {
59
86
  const key = dhtRoutingKey(routingKey)
60
87
  return datastore.has(key, options)
88
+ },
89
+ async delete (routingKey, options): Promise<void> {
90
+ const key = dhtRoutingKey(routingKey)
91
+ return datastore.delete(key, options)
61
92
  }
62
93
  }
63
94
  }
@@ -72,7 +72,7 @@ class PubSubRouting implements IPNSRouting {
72
72
  await ipnsValidator(routingKey, message.data)
73
73
 
74
74
  if (await this.localStore.has(routingKey)) {
75
- const currentRecord = await this.localStore.get(routingKey)
75
+ const { record: currentRecord } = await this.localStore.get(routingKey)
76
76
 
77
77
  if (uint8ArrayEquals(currentRecord, message.data)) {
78
78
  log('not storing record as we already have it')
@@ -128,7 +128,9 @@ class PubSubRouting implements IPNSRouting {
128
128
  }
129
129
 
130
130
  // chain through to local store
131
- return await this.localStore.get(routingKey, options)
131
+ const { record } = await this.localStore.get(routingKey, options)
132
+
133
+ return record
132
134
  } catch (err: any) {
133
135
  options.onProgress?.(new CustomProgressEvent<Error>('ipns:pubsub:error', err))
134
136
  throw err