@helia/ipns 7.2.3-c9c644c → 7.2.3-ec8bf66

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/errors.ts ADDED
@@ -0,0 +1,53 @@
1
+ export class DNSLinkNotFoundError extends Error {
2
+ static name = 'DNSLinkNotFoundError'
3
+
4
+ constructor (message = 'DNSLink not found') {
5
+ super(message)
6
+ this.name = 'DNSLinkNotFoundError'
7
+ }
8
+ }
9
+
10
+ export class RecordsFailedValidationError extends Error {
11
+ static name = 'RecordsFailedValidationError'
12
+
13
+ constructor (message = 'Records failed validation') {
14
+ super(message)
15
+ this.name = 'RecordsFailedValidationError'
16
+ }
17
+ }
18
+
19
+ export class UnsupportedMultibasePrefixError extends Error {
20
+ static name = 'UnsupportedMultibasePrefixError'
21
+
22
+ constructor (message = 'Unsupported multibase prefix') {
23
+ super(message)
24
+ this.name = 'UnsupportedMultibasePrefixError'
25
+ }
26
+ }
27
+
28
+ export class UnsupportedMultihashCodecError extends Error {
29
+ static name = 'UnsupportedMultihashCodecError'
30
+
31
+ constructor (message = 'Unsupported multihash codec') {
32
+ super(message)
33
+ this.name = 'UnsupportedMultihashCodecError'
34
+ }
35
+ }
36
+
37
+ export class InvalidValueError extends Error {
38
+ static name = 'InvalidValueError'
39
+
40
+ constructor (message = 'Invalid value') {
41
+ super(message)
42
+ this.name = 'InvalidValueError'
43
+ }
44
+ }
45
+
46
+ export class InvalidTopicError extends Error {
47
+ static name = 'InvalidTopicError'
48
+
49
+ constructor (message = 'Invalid topic') {
50
+ super(message)
51
+ this.name = 'InvalidTopicError'
52
+ }
53
+ }
package/src/index.ts CHANGED
@@ -227,23 +227,28 @@
227
227
  * ```
228
228
  */
229
229
 
230
- import { CodeError } from '@libp2p/interface'
230
+ import { NotFoundError, isPublicKey } from '@libp2p/interface'
231
231
  import { logger } from '@libp2p/logger'
232
- import { peerIdFromString } from '@libp2p/peer-id'
233
- import { create, marshal, peerIdToRoutingKey, unmarshal } from 'ipns'
232
+ import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey, unmarshalIPNSRecord, type IPNSRecord } from 'ipns'
234
233
  import { ipnsSelector } from 'ipns/selector'
235
234
  import { ipnsValidator } from 'ipns/validator'
235
+ import { base36 } from 'multiformats/bases/base36'
236
+ import { base58btc } from 'multiformats/bases/base58'
236
237
  import { CID } from 'multiformats/cid'
238
+ import * as Digest from 'multiformats/hashes/digest'
237
239
  import { CustomProgressEvent } from 'progress-events'
238
240
  import { resolveDNSLink } from './dnslink.js'
241
+ import { InvalidValueError, RecordsFailedValidationError, UnsupportedMultibasePrefixError, UnsupportedMultihashCodecError } from './errors.js'
239
242
  import { helia } from './routing/helia.js'
240
243
  import { localStore, type LocalStore } from './routing/local-store.js'
244
+ import { isCodec, IDENTITY_CODEC, SHA2_256_CODEC } from './utils.js'
241
245
  import type { IPNSRouting, IPNSRoutingEvents } from './routing/index.js'
242
246
  import type { Routing } from '@helia/interface'
243
- import type { AbortOptions, ComponentLogger, Logger, PeerId } from '@libp2p/interface'
247
+ import type { AbortOptions, ComponentLogger, Logger, PrivateKey, PublicKey } from '@libp2p/interface'
244
248
  import type { Answer, DNS, ResolveDnsProgressEvents } from '@multiformats/dns'
245
249
  import type { Datastore } from 'interface-datastore'
246
- import type { IPNSRecord } from 'ipns'
250
+ import type { MultibaseDecoder } from 'multiformats/bases/interface'
251
+ import type { MultihashDigest } from 'multiformats/hashes/interface'
247
252
  import type { ProgressEvent, ProgressOptions } from 'progress-events'
248
253
 
249
254
  const log = logger('helia:ipns')
@@ -375,13 +380,13 @@ export interface IPNS {
375
380
  *
376
381
  * If the value is a PeerId, a recursive IPNS record will be created.
377
382
  */
378
- publish(key: PeerId, value: CID | PeerId | string, options?: PublishOptions): Promise<IPNSRecord>
383
+ publish(key: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, options?: PublishOptions): Promise<IPNSRecord>
379
384
 
380
385
  /**
381
386
  * Accepts a public key formatted as a libp2p PeerID and resolves the IPNS record
382
387
  * corresponding to that public key until a value is found
383
388
  */
384
- resolve(key: PeerId, options?: ResolveOptions): Promise<IPNSResolveResult>
389
+ resolve(key: PublicKey | MultihashDigest<0x00 | 0x12>, options?: ResolveOptions): Promise<IPNSResolveResult>
385
390
 
386
391
  /**
387
392
  * Resolve a CID from a dns-link style IPNS record
@@ -403,6 +408,11 @@ export interface IPNSComponents {
403
408
  logger: ComponentLogger
404
409
  }
405
410
 
411
+ const bases: Record<string, MultibaseDecoder<string>> = {
412
+ [base36.prefix]: base36,
413
+ [base58btc.prefix]: base58btc
414
+ }
415
+
406
416
  class DefaultIPNS implements IPNS {
407
417
  private readonly routers: IPNSRouting[]
408
418
  private readonly localStore: LocalStore
@@ -420,21 +430,21 @@ class DefaultIPNS implements IPNS {
420
430
  this.log = components.logger.forComponent('helia:ipns')
421
431
  }
422
432
 
423
- async publish (key: PeerId, value: CID | PeerId | string, options: PublishOptions = {}): Promise<IPNSRecord> {
433
+ async publish (key: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, options: PublishOptions = {}): Promise<IPNSRecord> {
424
434
  try {
425
435
  let sequenceNumber = 1n
426
- const routingKey = peerIdToRoutingKey(key)
436
+ const routingKey = multihashToIPNSRoutingKey(key.publicKey.toMultihash())
427
437
 
428
438
  if (await this.localStore.has(routingKey, options)) {
429
439
  // if we have published under this key before, increment the sequence number
430
440
  const { record } = await this.localStore.get(routingKey, options)
431
- const existingRecord = unmarshal(record)
441
+ const existingRecord = unmarshalIPNSRecord(record)
432
442
  sequenceNumber = existingRecord.sequence + 1n
433
443
  }
434
444
 
435
445
  // create record
436
- const record = await create(key, value, sequenceNumber, options.lifetime ?? DEFAULT_LIFETIME_MS, options)
437
- const marshaledRecord = marshal(record)
446
+ const record = await createIPNSRecord(key, value, sequenceNumber, options.lifetime ?? DEFAULT_LIFETIME_MS, options)
447
+ const marshaledRecord = marshalIPNSRecord(record)
438
448
 
439
449
  await this.localStore.put(routingKey, marshaledRecord, options)
440
450
 
@@ -450,8 +460,9 @@ class DefaultIPNS implements IPNS {
450
460
  }
451
461
  }
452
462
 
453
- async resolve (key: PeerId, options: ResolveOptions = {}): Promise<IPNSResolveResult> {
454
- const routingKey = peerIdToRoutingKey(key)
463
+ async resolve (key: PublicKey | MultihashDigest<0x00 | 0x12>, options: ResolveOptions = {}): Promise<IPNSResolveResult> {
464
+ const digest = isPublicKey(key) ? key.toMultihash() : key
465
+ const routingKey = multihashToIPNSRoutingKey(digest)
455
466
  const record = await this.#findIpnsRecord(routingKey, options)
456
467
 
457
468
  return {
@@ -511,7 +522,31 @@ class DefaultIPNS implements IPNS {
511
522
  const scheme = parts[1]
512
523
 
513
524
  if (scheme === 'ipns') {
514
- const { cid } = await this.resolve(peerIdFromString(parts[2]), options)
525
+ const str = parts[2]
526
+ const prefix = str.substring(0, 1)
527
+ let buf: Uint8Array | undefined
528
+
529
+ if (prefix === '1' || prefix === 'Q') {
530
+ buf = base58btc.decode(`z${str}`)
531
+ } else if (bases[prefix] != null) {
532
+ buf = bases[prefix].decode(str)
533
+ } else {
534
+ throw new UnsupportedMultibasePrefixError(`Unsupported multibase prefix "${prefix}"`)
535
+ }
536
+
537
+ let digest: MultihashDigest<number>
538
+
539
+ try {
540
+ digest = Digest.decode(buf)
541
+ } catch {
542
+ digest = CID.decode(buf).multihash
543
+ }
544
+
545
+ if (!isCodec(digest, IDENTITY_CODEC) && !isCodec(digest, SHA2_256_CODEC)) {
546
+ throw new UnsupportedMultihashCodecError(`Unsupported multihash codec "${digest.code}"`)
547
+ }
548
+
549
+ const { cid } = await this.resolve(digest, options)
515
550
  const path = parts.slice(3).join('/')
516
551
  return {
517
552
  cid,
@@ -530,7 +565,7 @@ class DefaultIPNS implements IPNS {
530
565
  }
531
566
 
532
567
  log.error('invalid ipfs path %s', ipfsPath)
533
- throw new Error('Invalid value')
568
+ throw new InvalidValueError('Invalid value')
534
569
  }
535
570
 
536
571
  async #findIpnsRecord (routingKey: Uint8Array, options: ResolveOptions = {}): Promise<IPNSRecord> {
@@ -553,7 +588,7 @@ class DefaultIPNS implements IPNS {
553
588
  this.log('record was valid')
554
589
 
555
590
  // check the TTL
556
- const ipnsRecord = unmarshal(record)
591
+ const ipnsRecord = unmarshalIPNSRecord(record)
557
592
 
558
593
  // IPNS TTL is in nanoseconds, convert to milliseconds, default to one
559
594
  // hour
@@ -588,7 +623,7 @@ class DefaultIPNS implements IPNS {
588
623
  }
589
624
 
590
625
  if (options.offline === true) {
591
- throw new CodeError('Record was not present in the cache or has expired', 'ERR_NOT_FOUND')
626
+ throw new NotFoundError('Record was not present in the cache or has expired')
592
627
  }
593
628
 
594
629
  log('did not have record locally')
@@ -624,17 +659,17 @@ class DefaultIPNS implements IPNS {
624
659
 
625
660
  if (records.length === 0) {
626
661
  if (foundInvalid > 0) {
627
- throw new CodeError(`${foundInvalid > 1 ? `${foundInvalid} records` : 'Record'} found for routing key ${foundInvalid > 1 ? 'were' : 'was'} invalid`, 'ERR_RECORDS_FAILED_VALIDATION')
662
+ throw new RecordsFailedValidationError(`${foundInvalid > 1 ? `${foundInvalid} records` : 'Record'} found for routing key ${foundInvalid > 1 ? 'were' : 'was'} invalid`)
628
663
  }
629
664
 
630
- throw new CodeError('Could not find record for routing key', 'ERR_NOT_FOUND')
665
+ throw new NotFoundError('Could not find record for routing key')
631
666
  }
632
667
 
633
668
  const record = records[ipnsSelector(routingKey, records)]
634
669
 
635
670
  await this.localStore.put(routingKey, record, options)
636
671
 
637
- return unmarshal(record)
672
+ return unmarshalIPNSRecord(record)
638
673
  }
639
674
  }
640
675
 
@@ -48,7 +48,7 @@ export function localStore (datastore: Datastore): LocalStore {
48
48
  return
49
49
  }
50
50
  } catch (err: any) {
51
- if (err.code !== 'ERR_NOT_FOUND') {
51
+ if (err.name !== 'NotFoundError') {
52
52
  throw err
53
53
  }
54
54
  }
@@ -1,16 +1,18 @@
1
- import { CodeError } from '@libp2p/interface'
1
+ import { isPublicKey } from '@libp2p/interface'
2
2
  import { logger } from '@libp2p/logger'
3
- import { peerIdToRoutingKey } from 'ipns'
3
+ import { multihashToIPNSRoutingKey } from 'ipns'
4
4
  import { ipnsSelector } from 'ipns/selector'
5
5
  import { ipnsValidator } from 'ipns/validator'
6
6
  import { CustomProgressEvent, type ProgressEvent } from 'progress-events'
7
7
  import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
8
8
  import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
9
9
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
10
+ import { InvalidTopicError } from '../errors.js'
10
11
  import { localStore, type LocalStore } from './local-store.js'
11
12
  import type { GetOptions, IPNSRouting, PutOptions } from './index.js'
12
- import type { PeerId, Message, PublishResult, PubSub } from '@libp2p/interface'
13
+ import type { PeerId, Message, PublishResult, PubSub, PublicKey } from '@libp2p/interface'
13
14
  import type { Datastore } from 'interface-datastore'
15
+ import type { MultihashDigest } from 'multiformats/hashes/interface'
14
16
 
15
17
  const log = logger('helia:ipns:routing:pubsub')
16
18
 
@@ -147,8 +149,9 @@ class PubSubRouting implements IPNSRouting {
147
149
  /**
148
150
  * Cancel pubsub subscriptions related to ipns
149
151
  */
150
- cancel (key: PeerId): void {
151
- const routingKey = peerIdToRoutingKey(key)
152
+ cancel (key: PublicKey | MultihashDigest<0x00 | 0x12>): void {
153
+ const digest = isPublicKey(key) ? key.toMultihash() : key
154
+ const routingKey = multihashToIPNSRoutingKey(digest)
152
155
  const topic = keyToTopic(routingKey)
153
156
 
154
157
  // Not found topic
@@ -177,7 +180,7 @@ function keyToTopic (key: Uint8Array): string {
177
180
  */
178
181
  function topicToKey (topic: string): Uint8Array {
179
182
  if (topic.substring(0, PUBSUB_NAMESPACE.length) !== PUBSUB_NAMESPACE) {
180
- throw new CodeError('topic received is not from a record', 'ERR_TOPIC_IS_NOT_FROM_RECORD_NAMESPACE')
183
+ throw new InvalidTopicError('Topic received is not from a record')
181
184
  }
182
185
 
183
186
  const key = topic.substring(PUBSUB_NAMESPACE.length)
package/src/utils.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { MultihashDigest } from 'multiformats/hashes/interface'
2
+
3
+ export const IDENTITY_CODEC = 0x0
4
+ export const SHA2_256_CODEC = 0x12
5
+
6
+ export function isCodec <T extends number> (digest: MultihashDigest, codec: T): digest is MultihashDigest<T> {
7
+ return digest.code === codec
8
+ }