@helia/ipns 8.2.3 → 8.2.4-172345df

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/ipns.ts ADDED
@@ -0,0 +1,400 @@
1
+ import { generateKeyPair } from '@libp2p/crypto/keys'
2
+ import { NotFoundError, NotStartedError, isPeerId, isPublicKey } from '@libp2p/interface'
3
+ import { Queue, repeatingTask } from '@libp2p/utils'
4
+ import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey, unmarshalIPNSRecord } from 'ipns'
5
+ import { ipnsSelector } from 'ipns/selector'
6
+ import { ipnsValidator } from 'ipns/validator'
7
+ import { base36 } from 'multiformats/bases/base36'
8
+ import { base58btc } from 'multiformats/bases/base58'
9
+ import { CID } from 'multiformats/cid'
10
+ import * as Digest from 'multiformats/hashes/digest'
11
+ import { CustomProgressEvent } from 'progress-events'
12
+ import { DEFAULT_LIFETIME_MS, DEFAULT_REPUBLISH_CONCURRENCY, DEFAULT_REPUBLISH_INTERVAL_MS, DEFAULT_TTL_NS } from './constants.ts'
13
+ import { InvalidValueError, RecordsFailedValidationError, UnsupportedMultibasePrefixError, UnsupportedMultihashCodecError } from './errors.js'
14
+ import { localStore } from './local-store.js'
15
+ import { helia } from './routing/helia.js'
16
+ import { localStoreRouting } from './routing/local-store.ts'
17
+ import { isCodec, IDENTITY_CODEC, SHA2_256_CODEC, shouldRepublish, isLibp2pCID } from './utils.js'
18
+ import type { IPNSComponents, IPNS as IPNSInterface, IPNSOptions, IPNSPublishResult, IPNSResolveResult, PublishOptions, ResolveOptions } from './index.js'
19
+ import type { LocalStore } from './local-store.js'
20
+ import type { IPNSRouting } from './routing/index.js'
21
+ import type { AbortOptions, Logger, PeerId, PrivateKey, PublicKey, Startable } from '@libp2p/interface'
22
+ import type { Keychain } from '@libp2p/keychain'
23
+ import type { RepeatingTask } from '@libp2p/utils'
24
+ import type { IPNSRecord } from 'ipns'
25
+ import type { MultibaseDecoder } from 'multiformats/bases/interface'
26
+ import type { MultihashDigest } from 'multiformats/hashes/interface'
27
+
28
+ const bases: Record<string, MultibaseDecoder<string>> = {
29
+ [base36.prefix]: base36,
30
+ [base58btc.prefix]: base58btc
31
+ }
32
+
33
+ export class IPNS implements IPNSInterface, Startable {
34
+ public readonly routers: IPNSRouting[]
35
+ private readonly localStore: LocalStore
36
+ private readonly republishTask: RepeatingTask
37
+ private readonly log: Logger
38
+ private readonly keychain: Keychain
39
+ private started: boolean = false
40
+ private readonly republishConcurrency: number
41
+
42
+ constructor (components: IPNSComponents, init: IPNSOptions = {}) {
43
+ this.log = components.logger.forComponent('helia:ipns')
44
+ this.localStore = localStore(components.datastore, components.logger.forComponent('helia:ipns:local-store'))
45
+ this.keychain = components.libp2p.services.keychain
46
+ this.republishConcurrency = init.republishConcurrency || DEFAULT_REPUBLISH_CONCURRENCY
47
+ this.started = components.libp2p.status === 'started'
48
+
49
+ this.routers = [
50
+ localStoreRouting(this.localStore),
51
+ helia(components.routing),
52
+ ...(init.routers ?? [])
53
+ ]
54
+
55
+ // start republishing on Helia start
56
+ components.events.addEventListener('start', this.start.bind(this))
57
+ // stop republishing on Helia stop
58
+ components.events.addEventListener('stop', this.stop.bind(this))
59
+
60
+ this.republishTask = repeatingTask(this.#republish.bind(this), init.republishInterval ?? DEFAULT_REPUBLISH_INTERVAL_MS, {
61
+ runImmediately: true
62
+ })
63
+
64
+ if (this.started) {
65
+ this.republishTask.start()
66
+ }
67
+ }
68
+
69
+ start (): void {
70
+ if (this.started) {
71
+ return
72
+ }
73
+
74
+ this.started = true
75
+ this.republishTask.start()
76
+ }
77
+
78
+ stop (): void {
79
+ if (!this.started) {
80
+ return
81
+ }
82
+
83
+ this.started = false
84
+ this.republishTask.stop()
85
+ }
86
+
87
+ #throwIfStopped (): void {
88
+ if (!this.started) {
89
+ throw new NotStartedError('Helia is stopped, cannot perform IPNS operations')
90
+ }
91
+ }
92
+
93
+ async publish (keyName: string, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | PeerId | string, options: PublishOptions = {}): Promise<IPNSPublishResult> {
94
+ this.#throwIfStopped()
95
+
96
+ try {
97
+ const privKey = await this.#loadOrCreateKey(keyName)
98
+ let sequenceNumber = 1n
99
+ const routingKey = multihashToIPNSRoutingKey(privKey.publicKey.toMultihash())
100
+
101
+ if (await this.localStore.has(routingKey, options)) {
102
+ // if we have published under this key before, increment the sequence number
103
+ const { record } = await this.localStore.get(routingKey, options)
104
+ const existingRecord = unmarshalIPNSRecord(record)
105
+ sequenceNumber = existingRecord.sequence + 1n
106
+ }
107
+
108
+ if (isPeerId(value)) {
109
+ value = value.toCID()
110
+ }
111
+
112
+ // convert ttl from milliseconds to nanoseconds as createIPNSRecord expects
113
+ const ttlNs = options.ttl != null ? BigInt(options.ttl) * 1_000_000n : DEFAULT_TTL_NS
114
+ const lifetime = options.lifetime ?? DEFAULT_LIFETIME_MS
115
+ const record = await createIPNSRecord(privKey, value, sequenceNumber, lifetime, { ...options, ttlNs })
116
+ const marshaledRecord = marshalIPNSRecord(record)
117
+
118
+ if (options.offline === true) {
119
+ // only store record locally
120
+ await this.localStore.put(routingKey, marshaledRecord, { ...options, metadata: { keyName, lifetime } })
121
+ } else {
122
+ // publish record to routing (including the local store)
123
+ await Promise.all(this.routers.map(async r => {
124
+ await r.put(routingKey, marshaledRecord, { ...options, metadata: { keyName, lifetime } })
125
+ }))
126
+ }
127
+
128
+ return {
129
+ record,
130
+ publicKey: privKey.publicKey
131
+ }
132
+ } catch (err: any) {
133
+ options.onProgress?.(new CustomProgressEvent<Error>('ipns:publish:error', err))
134
+ throw err
135
+ }
136
+ }
137
+
138
+ async resolve (key: CID<unknown, 0x72, 0x00 | 0x12, 1> | PublicKey | MultihashDigest<0x00 | 0x12> | PeerId, options: ResolveOptions = {}): Promise<IPNSResolveResult> {
139
+ this.#throwIfStopped()
140
+ const digest = isPublicKey(key) || isPeerId(key) ? key.toMultihash() : isLibp2pCID(key) ? key.multihash : key
141
+ const routingKey = multihashToIPNSRoutingKey(digest)
142
+ const record = await this.#findIpnsRecord(routingKey, options)
143
+
144
+ return {
145
+ ...(await this.#resolve(record.value, options)),
146
+ record
147
+ }
148
+ }
149
+
150
+ async #loadOrCreateKey (keyName: string): Promise<PrivateKey> {
151
+ try {
152
+ return await this.keychain.exportKey(keyName)
153
+ } catch (err: any) {
154
+ // If no named key found in keychain, generate and import
155
+ const privKey = await generateKeyPair('Ed25519')
156
+ await this.keychain.importKey(keyName, privKey)
157
+ return privKey
158
+ }
159
+ }
160
+
161
+ async #republish (options: AbortOptions = {}): Promise<void> {
162
+ if (!this.started) {
163
+ return
164
+ }
165
+
166
+ this.log('starting ipns republish records loop')
167
+
168
+ const queue = new Queue({
169
+ concurrency: this.republishConcurrency
170
+ })
171
+
172
+ try {
173
+ const recordsToRepublish: Array<{ routingKey: Uint8Array, record: IPNSRecord }> = []
174
+
175
+ // Find all records using the localStore.list method
176
+ for await (const { routingKey, record, metadata, created } of this.localStore.list(options)) {
177
+ if (metadata == null) {
178
+ // Skip if no metadata is found from before we started
179
+ // storing metadata or for records republished without a key
180
+ this.log(`no metadata found for record ${routingKey.toString()}, skipping`)
181
+ continue
182
+ }
183
+ let ipnsRecord: IPNSRecord
184
+ try {
185
+ ipnsRecord = unmarshalIPNSRecord(record)
186
+ } catch (err: any) {
187
+ this.log.error('error unmarshaling record - %e', err)
188
+ continue
189
+ }
190
+
191
+ // Only republish records that are within the DHT or record expiry threshold
192
+ if (!shouldRepublish(ipnsRecord, created)) {
193
+ this.log.trace(`skipping record ${routingKey.toString()}within republish threshold`)
194
+ continue
195
+ }
196
+ const sequenceNumber = ipnsRecord.sequence + 1n
197
+ const ttlNs = ipnsRecord.ttl ?? DEFAULT_TTL_NS
198
+ let privKey: PrivateKey
199
+
200
+ try {
201
+ privKey = await this.keychain.exportKey(metadata.keyName)
202
+ } catch (err: any) {
203
+ this.log.error(`missing key ${metadata.keyName}, skipping republishing record`, err)
204
+ continue
205
+ }
206
+ try {
207
+ const updatedRecord = await createIPNSRecord(privKey, ipnsRecord.value, sequenceNumber, metadata.lifetime, { ...options, ttlNs })
208
+ recordsToRepublish.push({ routingKey, record: updatedRecord })
209
+ } catch (err: any) {
210
+ this.log.error(`error creating updated IPNS record for ${routingKey.toString()}`, err)
211
+ continue
212
+ }
213
+ }
214
+
215
+ this.log(`found ${recordsToRepublish.length} records to republish`)
216
+
217
+ // Republish each record
218
+ for (const { routingKey, record } of recordsToRepublish) {
219
+ // Add job to queue to republish the record to all routers
220
+ queue.add(async () => {
221
+ try {
222
+ const marshaledRecord = marshalIPNSRecord(record)
223
+ await Promise.all(
224
+ this.routers.map(r => r.put(routingKey, marshaledRecord, options))
225
+ )
226
+ } catch (err: any) {
227
+ this.log.error('error republishing record - %e', err)
228
+ }
229
+ }, options)
230
+ }
231
+ } catch (err: any) {
232
+ this.log.error('error during republish - %e', err)
233
+ }
234
+
235
+ await queue.onIdle(options) // Wait for all jobs to complete
236
+ }
237
+
238
+ async unpublish (keyName: string, options?: AbortOptions): Promise<void> {
239
+ const { publicKey } = await this.keychain.exportKey(keyName)
240
+ const digest = publicKey.toMultihash()
241
+ const routingKey = multihashToIPNSRoutingKey(digest)
242
+ await this.localStore.delete(routingKey, options)
243
+ }
244
+
245
+ async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise<{ cid: CID, path: string }> {
246
+ const parts = ipfsPath.split('/')
247
+ try {
248
+ const scheme = parts[1]
249
+
250
+ if (scheme === 'ipns') {
251
+ const str = parts[2]
252
+ const prefix = str.substring(0, 1)
253
+ let buf: Uint8Array | undefined
254
+
255
+ if (prefix === '1' || prefix === 'Q') {
256
+ buf = base58btc.decode(`z${str}`)
257
+ } else if (bases[prefix] != null) {
258
+ buf = bases[prefix].decode(str)
259
+ } else {
260
+ throw new UnsupportedMultibasePrefixError(`Unsupported multibase prefix "${prefix}"`)
261
+ }
262
+
263
+ let digest: MultihashDigest<number>
264
+
265
+ try {
266
+ digest = Digest.decode(buf)
267
+ } catch {
268
+ digest = CID.decode(buf).multihash
269
+ }
270
+
271
+ if (!isCodec(digest, IDENTITY_CODEC) && !isCodec(digest, SHA2_256_CODEC)) {
272
+ throw new UnsupportedMultihashCodecError(`Unsupported multihash codec "${digest.code}"`)
273
+ }
274
+
275
+ const { cid } = await this.resolve(digest, options)
276
+ const path = parts.slice(3).join('/')
277
+ return {
278
+ cid,
279
+ path
280
+ }
281
+ } else if (scheme === 'ipfs') {
282
+ const cid = CID.parse(parts[2])
283
+ const path = parts.slice(3).join('/')
284
+ return {
285
+ cid,
286
+ path
287
+ }
288
+ }
289
+ } catch (err) {
290
+ this.log.error('error parsing ipfs path - %e', err)
291
+ }
292
+
293
+ this.log.error('invalid ipfs path %s', ipfsPath)
294
+ throw new InvalidValueError('Invalid value')
295
+ }
296
+
297
+ async #findIpnsRecord (routingKey: Uint8Array, options: ResolveOptions = {}): Promise<IPNSRecord> {
298
+ const records: Uint8Array[] = []
299
+ const cached = await this.localStore.has(routingKey, options)
300
+
301
+ if (cached) {
302
+ this.log('record is present in the cache')
303
+
304
+ if (options.nocache !== true) {
305
+ try {
306
+ // check the local cache first
307
+ const { record, created } = await this.localStore.get(routingKey, options)
308
+
309
+ this.log('record retrieved from cache')
310
+
311
+ // validate the record
312
+ await ipnsValidator(routingKey, record)
313
+
314
+ this.log('record was valid')
315
+
316
+ // check the TTL
317
+ const ipnsRecord = unmarshalIPNSRecord(record)
318
+
319
+ // IPNS TTL is in nanoseconds, convert to milliseconds, default to one
320
+ // hour
321
+ const ttlMs = Number((ipnsRecord.ttl ?? DEFAULT_TTL_NS) / 1_000_000n)
322
+ const ttlExpires = created.getTime() + ttlMs
323
+
324
+ if (ttlExpires > Date.now()) {
325
+ // the TTL has not yet expired, return the cached record
326
+ this.log('record TTL was valid')
327
+ return ipnsRecord
328
+ }
329
+
330
+ if (options.offline === true) {
331
+ // the TTL has expired but we are skipping the routing search
332
+ this.log('record TTL has been reached but we are resolving offline-only, returning record')
333
+ return ipnsRecord
334
+ }
335
+
336
+ this.log('record TTL has been reached, searching routing for updates')
337
+
338
+ // add the local record to our list of resolved record, and also
339
+ // search the routing for updates - the most up to date record will be
340
+ // returned
341
+ records.push(record)
342
+ } catch (err) {
343
+ this.log('cached record was invalid - %e', err)
344
+ await this.localStore.delete(routingKey, options)
345
+ }
346
+ } else {
347
+ this.log('ignoring local cache due to nocache=true option')
348
+ }
349
+ }
350
+
351
+ if (options.offline === true) {
352
+ throw new NotFoundError('Record was not present in the cache or has expired')
353
+ }
354
+
355
+ this.log('did not have record locally')
356
+
357
+ let foundInvalid = 0
358
+
359
+ await Promise.all(
360
+ this.routers.map(async (router) => {
361
+ let record: Uint8Array
362
+
363
+ try {
364
+ record = await router.get(routingKey, {
365
+ ...options,
366
+ validate: false
367
+ })
368
+ } catch (err: any) {
369
+ this.log.error('error finding IPNS record - %e', err)
370
+
371
+ return
372
+ }
373
+
374
+ try {
375
+ await ipnsValidator(routingKey, record)
376
+
377
+ records.push(record)
378
+ } catch (err) {
379
+ // we found a record, but the validator rejected it
380
+ foundInvalid++
381
+ this.log.error('error finding IPNS record - %e', err)
382
+ }
383
+ })
384
+ )
385
+
386
+ if (records.length === 0) {
387
+ if (foundInvalid > 0) {
388
+ throw new RecordsFailedValidationError(`${foundInvalid > 1 ? `${foundInvalid} records` : 'Record'} found for routing key ${foundInvalid > 1 ? 'were' : 'was'} invalid`)
389
+ }
390
+
391
+ throw new NotFoundError('Could not find record for routing key')
392
+ }
393
+
394
+ const record = records[ipnsSelector(routingKey, records)]
395
+
396
+ await this.localStore.put(routingKey, record, options)
397
+
398
+ return unmarshalIPNSRecord(record)
399
+ }
400
+ }
@@ -0,0 +1,162 @@
1
+ import { Record } from '@libp2p/kad-dht'
2
+ import { CustomProgressEvent } from 'progress-events'
3
+ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
4
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
5
+ import { IPNSPublishMetadata } from './pb/metadata.js'
6
+ import { dhtRoutingKey, DHT_RECORD_PREFIX, ipnsMetadataKey } from './utils.js'
7
+ import type { DatastoreProgressEvents, GetOptions, PutOptions } from './routing/index.js'
8
+ import type { AbortOptions, Logger } from '@libp2p/interface'
9
+ import type { Datastore } from 'interface-datastore'
10
+
11
+ export interface GetResult {
12
+ record: Uint8Array
13
+ created: Date
14
+ }
15
+
16
+ export interface ListResult {
17
+ routingKey: Uint8Array
18
+ record: Uint8Array
19
+ created: Date
20
+ metadata?: IPNSPublishMetadata
21
+ }
22
+
23
+ export interface ListOptions extends AbortOptions {
24
+ onProgress?(evt: DatastoreProgressEvents): void
25
+ }
26
+
27
+ export interface LocalStore {
28
+ /**
29
+ * Put an IPNS record into the datastore
30
+ *
31
+ * @param routingKey - The routing key for the IPNS record
32
+ * @param marshaledRecord - The marshaled IPNS record
33
+ * @param options - options for the put operation including metadata
34
+ */
35
+ put(routingKey: Uint8Array, marshaledRecord: Uint8Array, options?: PutOptions): Promise<void>
36
+ get(routingKey: Uint8Array, options?: GetOptions): Promise<GetResult>
37
+ has(routingKey: Uint8Array, options?: AbortOptions): Promise<boolean>
38
+ delete(routingKey: Uint8Array, options?: AbortOptions): Promise<void>
39
+ /**
40
+ * List all IPNS records in the datastore
41
+ */
42
+ list(options?: ListOptions): AsyncIterable<ListResult>
43
+ }
44
+
45
+ /**
46
+ * Read/write IPNS records to the datastore as DHT records.
47
+ *
48
+ * This lets us publish IPNS records offline then serve them to the network
49
+ * later in response to DHT queries.
50
+ */
51
+ export function localStore (datastore: Datastore, log: Logger): LocalStore {
52
+ return {
53
+ async put (routingKey: Uint8Array, marshalledRecord: Uint8Array, options: PutOptions = {}) {
54
+ try {
55
+ const key = dhtRoutingKey(routingKey)
56
+
57
+ // don't overwrite existing, identical records as this will affect the
58
+ // TTL
59
+ try {
60
+ const existingBuf = await datastore.get(key)
61
+ const existingRecord = Record.deserialize(existingBuf)
62
+
63
+ if (uint8ArrayEquals(existingRecord.value, marshalledRecord)) {
64
+ return
65
+ }
66
+ } catch (err: any) {
67
+ if (err.name !== 'NotFoundError') {
68
+ throw err
69
+ }
70
+ }
71
+
72
+ // Marshal to libp2p record as the DHT does
73
+ const record = new Record(routingKey, marshalledRecord, new Date())
74
+
75
+ options.onProgress?.(new CustomProgressEvent('ipns:routing:datastore:put'))
76
+ const batch = datastore.batch()
77
+ batch.put(key, record.serialize())
78
+
79
+ if (options.metadata != null) {
80
+ // derive the datastore key for the IPNS metadata from the same routing key
81
+ batch.put(ipnsMetadataKey(routingKey), IPNSPublishMetadata.encode(options.metadata))
82
+ }
83
+ await batch.commit(options)
84
+ } catch (err: any) {
85
+ options.onProgress?.(new CustomProgressEvent<Error>('ipns:routing:datastore:error', err))
86
+ throw err
87
+ }
88
+ },
89
+ async get (routingKey: Uint8Array, options: GetOptions = {}): Promise<GetResult> {
90
+ try {
91
+ const key = dhtRoutingKey(routingKey)
92
+
93
+ options.onProgress?.(new CustomProgressEvent('ipns:routing:datastore:get'))
94
+ const buf = await datastore.get(key, options)
95
+
96
+ // Unmarshal libp2p record as the DHT does
97
+ const record = Record.deserialize(buf)
98
+
99
+ return {
100
+ record: record.value,
101
+ created: record.timeReceived
102
+ }
103
+ } catch (err: any) {
104
+ options.onProgress?.(new CustomProgressEvent<Error>('ipns:routing:datastore:error', err))
105
+ throw err
106
+ }
107
+ },
108
+ async has (routingKey: Uint8Array, options: AbortOptions = {}): Promise<boolean> {
109
+ const key = dhtRoutingKey(routingKey)
110
+ return datastore.has(key, options)
111
+ },
112
+ async delete (routingKey, options): Promise<void> {
113
+ const key = dhtRoutingKey(routingKey)
114
+ const batch = datastore.batch()
115
+ batch.delete(key)
116
+ batch.delete(ipnsMetadataKey(routingKey))
117
+ await batch.commit(options)
118
+ },
119
+ async * list (options: ListOptions = {}): AsyncIterable<ListResult> {
120
+ try {
121
+ options.onProgress?.(new CustomProgressEvent('ipns:routing:datastore:list'))
122
+
123
+ // Query all records with the DHT_RECORD_PREFIX
124
+ for await (const { key, value } of datastore.query({
125
+ prefix: DHT_RECORD_PREFIX
126
+ }, options)) {
127
+ try {
128
+ // Deserialize the record
129
+ const libp2pRecord = Record.deserialize(value)
130
+
131
+ // Extract the routing key from the datastore key
132
+ const keyString = key.toString()
133
+ const routingKeyBase32 = keyString.substring(DHT_RECORD_PREFIX.length)
134
+ const routingKey = uint8ArrayFromString(routingKeyBase32, 'base32')
135
+
136
+ const metadataKey = ipnsMetadataKey(routingKey)
137
+ let metadata: IPNSPublishMetadata | undefined
138
+ try {
139
+ const metadataBuf = await datastore.get(metadataKey, options)
140
+ metadata = IPNSPublishMetadata.decode(metadataBuf)
141
+ } catch (err: any) {
142
+ log.error('Error deserializing metadata for %s - %e', routingKeyBase32, err)
143
+ }
144
+
145
+ yield {
146
+ routingKey,
147
+ metadata,
148
+ record: libp2pRecord.value,
149
+ created: libp2pRecord.timeReceived
150
+ }
151
+ } catch (err) {
152
+ // Skip invalid records
153
+ log.error('Error deserializing record - %e', err)
154
+ }
155
+ }
156
+ } catch (err: any) {
157
+ options.onProgress?.(new CustomProgressEvent<Error>('ipns:routing:datastore:error', err))
158
+ throw err
159
+ }
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,9 @@
1
+ syntax = "proto3";
2
+
3
+ message IPNSPublishMetadata {
4
+ // The name of the key that was used to publish the record
5
+ string keyName = 1;
6
+
7
+ // Lifetime is the duration of the signature validity in milliseconds
8
+ uint32 lifetime = 2;
9
+ }
@@ -0,0 +1,74 @@
1
+ import { decodeMessage, encodeMessage, message } from 'protons-runtime'
2
+ import type { Codec, DecodeOptions } from 'protons-runtime'
3
+ import type { Uint8ArrayList } from 'uint8arraylist'
4
+
5
+ export interface IPNSPublishMetadata {
6
+ keyName: string
7
+ lifetime: number
8
+ }
9
+
10
+ export namespace IPNSPublishMetadata {
11
+ let _codec: Codec<IPNSPublishMetadata>
12
+
13
+ export const codec = (): Codec<IPNSPublishMetadata> => {
14
+ if (_codec == null) {
15
+ _codec = message<IPNSPublishMetadata>((obj, w, opts = {}) => {
16
+ if (opts.lengthDelimited !== false) {
17
+ w.fork()
18
+ }
19
+
20
+ if ((obj.keyName != null && obj.keyName !== '')) {
21
+ w.uint32(10)
22
+ w.string(obj.keyName)
23
+ }
24
+
25
+ if ((obj.lifetime != null && obj.lifetime !== 0)) {
26
+ w.uint32(16)
27
+ w.uint32(obj.lifetime)
28
+ }
29
+
30
+ if (opts.lengthDelimited !== false) {
31
+ w.ldelim()
32
+ }
33
+ }, (reader, length, opts = {}) => {
34
+ const obj: any = {
35
+ keyName: '',
36
+ lifetime: 0
37
+ }
38
+
39
+ const end = length == null ? reader.len : reader.pos + length
40
+
41
+ while (reader.pos < end) {
42
+ const tag = reader.uint32()
43
+
44
+ switch (tag >>> 3) {
45
+ case 1: {
46
+ obj.keyName = reader.string()
47
+ break
48
+ }
49
+ case 2: {
50
+ obj.lifetime = reader.uint32()
51
+ break
52
+ }
53
+ default: {
54
+ reader.skipType(tag & 7)
55
+ break
56
+ }
57
+ }
58
+ }
59
+
60
+ return obj
61
+ })
62
+ }
63
+
64
+ return _codec
65
+ }
66
+
67
+ export const encode = (obj: Partial<IPNSPublishMetadata>): Uint8Array => {
68
+ return encodeMessage(obj, IPNSPublishMetadata.codec())
69
+ }
70
+
71
+ export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<IPNSPublishMetadata>): IPNSPublishMetadata => {
72
+ return decodeMessage(buf, IPNSPublishMetadata.codec(), opts)
73
+ }
74
+ }
@@ -1,11 +1,12 @@
1
1
  import type { HeliaRoutingProgressEvents } from './helia.js'
2
- import type { DatastoreProgressEvents } from './local-store.js'
2
+ import type { DatastoreProgressEvents } from '../index.js'
3
3
  import type { PubSubProgressEvents } from './pubsub.js'
4
+ import type { IPNSPublishMetadata } from '../pb/metadata.ts'
4
5
  import type { AbortOptions } from '@libp2p/interface'
5
6
  import type { ProgressOptions } from 'progress-events'
6
7
 
7
8
  export interface PutOptions extends AbortOptions, ProgressOptions {
8
-
9
+ metadata?: IPNSPublishMetadata
9
10
  }
10
11
 
11
12
  export interface GetOptions extends AbortOptions, ProgressOptions {
@@ -33,4 +34,4 @@ export type IPNSRoutingEvents =
33
34
 
34
35
  export { helia } from './helia.js'
35
36
  export { pubsub } from './pubsub.js'
36
- export type { PubsubRoutingComponents } from './pubsub.js'
37
+ export type { PubsubRoutingComponents, PubSub, Message, PublishResult, PubSubEvents } from './pubsub.js'