@helia/ipns 10.1.1 → 10.2.0
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/README.md +40 -0
- package/dist/index.min.js +5 -5
- package/dist/index.min.js.map +4 -4
- package/dist/src/errors.d.ts +11 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +13 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +136 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +41 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/ipns/publisher.d.ts +3 -3
- package/dist/src/ipns/publisher.d.ts.map +1 -1
- package/dist/src/ipns/publisher.js +17 -18
- package/dist/src/ipns/publisher.js.map +1 -1
- package/dist/src/ipns/republisher.d.ts +6 -1
- package/dist/src/ipns/republisher.d.ts.map +1 -1
- package/dist/src/ipns/republisher.js +157 -24
- package/dist/src/ipns/republisher.js.map +1 -1
- package/dist/src/ipns/resolver.d.ts +13 -1
- package/dist/src/ipns/resolver.d.ts.map +1 -1
- package/dist/src/ipns/resolver.js +40 -31
- package/dist/src/ipns/resolver.js.map +1 -1
- package/dist/src/ipns.d.ts +6 -3
- package/dist/src/ipns.d.ts.map +1 -1
- package/dist/src/ipns.js +12 -3
- package/dist/src/ipns.js.map +1 -1
- package/dist/src/local-store.d.ts +5 -0
- package/dist/src/local-store.d.ts.map +1 -1
- package/dist/src/local-store.js +68 -23
- package/dist/src/local-store.js.map +1 -1
- package/dist/src/pb/ipns.js +1 -1
- package/dist/src/pb/metadata.d.ts +14 -1
- package/dist/src/pb/metadata.d.ts.map +1 -1
- package/dist/src/pb/metadata.js +36 -3
- package/dist/src/pb/metadata.js.map +1 -1
- package/dist/src/routing/index.d.ts +13 -1
- package/dist/src/routing/index.d.ts.map +1 -1
- package/dist/src/routing/index.js +17 -0
- package/dist/src/routing/index.js.map +1 -1
- package/dist/src/utils.d.ts +5 -0
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +18 -0
- package/dist/src/utils.js.map +1 -1
- package/dist/src/validator.d.ts +6 -0
- package/dist/src/validator.d.ts.map +1 -1
- package/dist/src/validator.js +24 -14
- package/dist/src/validator.js.map +1 -1
- package/dist/typedoc-urls.json +23 -1
- package/package.json +3 -3
- package/src/errors.ts +18 -0
- package/src/index.ts +145 -7
- package/src/ipns/publisher.ts +22 -20
- package/src/ipns/republisher.ts +182 -26
- package/src/ipns/resolver.ts +54 -36
- package/src/ipns.ts +20 -6
- package/src/local-store.ts +77 -22
- package/src/pb/ipns.ts +1 -1
- package/src/pb/metadata.proto +10 -0
- package/src/pb/metadata.ts +45 -4
- package/src/routing/index.ts +30 -1
- package/src/utils.ts +19 -0
- package/src/validator.ts +30 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helia/ipns",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.2.0",
|
|
4
4
|
"description": "An implementation of IPNS for Helia",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
6
|
"homepage": "https://github.com/ipfs/helia/tree/main/packages/ipns#readme",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"interface-datastore": "^10.0.1",
|
|
67
67
|
"multiformats": "^14.0.0",
|
|
68
68
|
"progress-events": "^1.1.0",
|
|
69
|
-
"protons-runtime": "^7.1.
|
|
69
|
+
"protons-runtime": "^7.1.1",
|
|
70
70
|
"race-signal": "^2.0.0",
|
|
71
71
|
"timestamp-nano": "^1.0.1",
|
|
72
72
|
"uint8arraylist": "^3.0.2",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"datastore-core": "^12.0.1",
|
|
79
79
|
"it-drain": "^3.0.12",
|
|
80
80
|
"it-last": "^3.0.11",
|
|
81
|
-
"protons": "^9.1.
|
|
81
|
+
"protons": "^9.1.1",
|
|
82
82
|
"sinon": "^22.0.0",
|
|
83
83
|
"sinon-ts": "^2.0.0"
|
|
84
84
|
},
|
package/src/errors.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { IPNSEntry } from './pb/ipns.ts'
|
|
2
|
+
|
|
1
3
|
export class RecordsFailedValidationError extends Error {
|
|
2
4
|
static name = 'RecordsFailedValidationError'
|
|
3
5
|
name = 'RecordsFailedValidationError'
|
|
@@ -18,6 +20,22 @@ export class RecordNotFoundError extends Error {
|
|
|
18
20
|
name = 'RecordNotFoundError'
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
export class RecordObsoleteError extends Error {
|
|
24
|
+
static name = 'RecordObsoleteError'
|
|
25
|
+
name = 'RecordObsoleteError'
|
|
26
|
+
/**
|
|
27
|
+
* The more suitable record that made the one being imported or republished
|
|
28
|
+
* obsolete.
|
|
29
|
+
*/
|
|
30
|
+
readonly record: IPNSEntry
|
|
31
|
+
|
|
32
|
+
constructor (message: string, record: IPNSEntry) {
|
|
33
|
+
super(message)
|
|
34
|
+
|
|
35
|
+
this.record = record
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
export class SignatureCreationError extends Error {
|
|
22
40
|
static name = 'SignatureCreationError'
|
|
23
41
|
name = 'SignatureCreationError'
|
package/src/index.ts
CHANGED
|
@@ -135,6 +135,46 @@
|
|
|
135
135
|
* console.info(result.record.value)
|
|
136
136
|
* }
|
|
137
137
|
* ```
|
|
138
|
+
*
|
|
139
|
+
* @example Republishing an existing IPNS record
|
|
140
|
+
*
|
|
141
|
+
* It is sometimes useful to be able to republish an existing IPNS record
|
|
142
|
+
* without needing the private key. This allows you to extend the availability
|
|
143
|
+
* of a record that was created elsewhere.
|
|
144
|
+
*
|
|
145
|
+
* There should be only one republisher per IPNS key. Multiple machines
|
|
146
|
+
* republishing the same key flood the routers with redundant writes.
|
|
147
|
+
*
|
|
148
|
+
* ```TypeScript
|
|
149
|
+
* import { createHelia } from 'helia'
|
|
150
|
+
* import { ipns } from '@helia/ipns'
|
|
151
|
+
* import { delegatedRoutingV1HttpApiClient } from '@helia/delegated-routing-v1-http-api-client'
|
|
152
|
+
* import { defaultLogger } from 'birnam'
|
|
153
|
+
* import { CID } from 'multiformats/cid'
|
|
154
|
+
*
|
|
155
|
+
* const helia = await createHelia()
|
|
156
|
+
* const name = ipns(helia)
|
|
157
|
+
*
|
|
158
|
+
* const ipnsName = 'k51qzi5uqu5dktsyfv7xz8h631pri4ct7osmb43nibxiojpttxzoft6hdyyzg4'
|
|
159
|
+
* const parsedCid: CID<unknown, 114, 0 | 18, 1> = CID.parse(ipnsName)
|
|
160
|
+
* const delegatedClient = delegatedRoutingV1HttpApiClient({
|
|
161
|
+
* url: 'https://delegated-ipfs.dev'
|
|
162
|
+
* })({
|
|
163
|
+
* logger: defaultLogger()
|
|
164
|
+
* })
|
|
165
|
+
* const record = await delegatedClient.getIPNS(parsedCid)
|
|
166
|
+
*
|
|
167
|
+
* // import the record into the local store (validates and stores it locally,
|
|
168
|
+
* // but does not publish it to the routers)
|
|
169
|
+
* await name.import(parsedCid, record)
|
|
170
|
+
*
|
|
171
|
+
* // start republishing it; throws RecordObsoleteError if the routers
|
|
172
|
+
* // already have a newer record for this key
|
|
173
|
+
* const { record: latestRecord } = await name.republish(parsedCid)
|
|
174
|
+
*
|
|
175
|
+
* // stop republishing a key
|
|
176
|
+
* await name.unpublish(parsedCid)
|
|
177
|
+
* ```
|
|
138
178
|
*/
|
|
139
179
|
|
|
140
180
|
import { CID } from 'multiformats/cid'
|
|
@@ -155,6 +195,7 @@ import type { ProgressEvent, ProgressOptions } from 'progress-events'
|
|
|
155
195
|
|
|
156
196
|
export * from './routing/index.ts'
|
|
157
197
|
export * from './pb/ipns.ts'
|
|
198
|
+
export * from './errors.ts'
|
|
158
199
|
|
|
159
200
|
export {
|
|
160
201
|
multihashFromIPNSRoutingKey,
|
|
@@ -184,6 +225,14 @@ export type DatastoreProgressEvents =
|
|
|
184
225
|
ProgressEvent<'ipns:routing:datastore:list'> |
|
|
185
226
|
ProgressEvent<'ipns:routing:datastore:error', Error>
|
|
186
227
|
|
|
228
|
+
/**
|
|
229
|
+
* The automated upkeep policy for a stored IPNS record. `republish()` accepts
|
|
230
|
+
* every policy except `reissue`, which needs the private key to re-sign.
|
|
231
|
+
*
|
|
232
|
+
* These strings must match the `Upkeep` enum values in `pb/metadata.proto`.
|
|
233
|
+
*/
|
|
234
|
+
export type UpkeepPolicy = 'reissue' | 'rebroadcast' | 'none'
|
|
235
|
+
|
|
187
236
|
export interface PublishOptions extends AbortOptions, ProgressOptions<PublishProgressEvents | IPNSRoutingProgressEvents> {
|
|
188
237
|
/**
|
|
189
238
|
* Time duration of the signature validity in ms - after this many ms have
|
|
@@ -219,6 +268,17 @@ export interface PublishOptions extends AbortOptions, ProgressOptions<PublishPro
|
|
|
219
268
|
*/
|
|
220
269
|
ttl?: number
|
|
221
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Automated record upkeep policy.
|
|
273
|
+
*
|
|
274
|
+
* - `reissue`: re-sign the record with a new validity
|
|
275
|
+
* - `rebroadcast`: re-publish the existing record until it expires
|
|
276
|
+
* - `none`: disable automated publishing
|
|
277
|
+
*
|
|
278
|
+
* @default 'reissue'
|
|
279
|
+
*/
|
|
280
|
+
upkeep?: UpkeepPolicy
|
|
281
|
+
|
|
222
282
|
/**
|
|
223
283
|
* Extensible data that will be added to the IPNS record data and signed to
|
|
224
284
|
* verify it's integrity.
|
|
@@ -273,6 +333,43 @@ export interface IPNSResolveOptions extends AbortOptions, ProgressOptions<Resolv
|
|
|
273
333
|
validate?: boolean
|
|
274
334
|
}
|
|
275
335
|
|
|
336
|
+
export interface RepublishOptions extends AbortOptions, ProgressOptions<IPNSRoutingProgressEvents> {
|
|
337
|
+
/**
|
|
338
|
+
* Skip resolution of latest record before republishing.
|
|
339
|
+
*
|
|
340
|
+
* It's important to resolve the latest record before republishing to routers
|
|
341
|
+
*
|
|
342
|
+
* Resolution should only be skipped when confident the latest record is
|
|
343
|
+
* already known.
|
|
344
|
+
*
|
|
345
|
+
* @default false
|
|
346
|
+
*/
|
|
347
|
+
skipResolution?: boolean
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Automated record upkeep policy.
|
|
351
|
+
*
|
|
352
|
+
* Defaults to `rebroadcast` since `republish()` cannot sign new records
|
|
353
|
+
* without the private key.
|
|
354
|
+
*
|
|
355
|
+
* - `rebroadcast`: re-publish the existing record until it expires
|
|
356
|
+
* - `none`: disable automated publishing
|
|
357
|
+
*
|
|
358
|
+
* @default 'rebroadcast'
|
|
359
|
+
*/
|
|
360
|
+
upkeep?: Exclude<UpkeepPolicy, 'reissue'>
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export interface UnpublishOptions extends AbortOptions {
|
|
364
|
+
/**
|
|
365
|
+
* Also delete the record from the local store, so the node stops serving it
|
|
366
|
+
* immediately instead of keeping it until it expires.
|
|
367
|
+
*
|
|
368
|
+
* @default false
|
|
369
|
+
*/
|
|
370
|
+
removeRecord?: boolean
|
|
371
|
+
}
|
|
372
|
+
|
|
276
373
|
export interface IPNSResolveResult {
|
|
277
374
|
/**
|
|
278
375
|
* The resolved record
|
|
@@ -302,6 +399,18 @@ export interface IPNSPublishResult {
|
|
|
302
399
|
publicKey: PublicKey
|
|
303
400
|
}
|
|
304
401
|
|
|
402
|
+
export interface RepublishResult {
|
|
403
|
+
/**
|
|
404
|
+
* The published record
|
|
405
|
+
*/
|
|
406
|
+
record: IPNSEntry
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The public key that the record is published under
|
|
410
|
+
*/
|
|
411
|
+
publicKey: PublicKey
|
|
412
|
+
}
|
|
413
|
+
|
|
305
414
|
export interface IPNSResolver {
|
|
306
415
|
/**
|
|
307
416
|
* Accepts a CID with the libp2p-key codec and either the identity hash (for
|
|
@@ -345,7 +454,7 @@ export interface IPNS {
|
|
|
345
454
|
* signal: AbortSignal.timeout(5_000)
|
|
346
455
|
* })
|
|
347
456
|
*
|
|
348
|
-
* console.info(result) // {
|
|
457
|
+
* console.info(result) // { record, name, publicKey }
|
|
349
458
|
* ```
|
|
350
459
|
*/
|
|
351
460
|
publish(keyName: string, value: CID | PublicKey | MultihashDigest | string, options?: PublishOptions): Promise<IPNSPublishResult>
|
|
@@ -357,17 +466,46 @@ export interface IPNS {
|
|
|
357
466
|
* (e.g. the value can be parsed as a string that does not start with
|
|
358
467
|
* `/ipns/`).
|
|
359
468
|
*/
|
|
360
|
-
resolve(name: CID | PublicKey | MultihashDigest | string, options?: IPNSResolveOptions): AsyncGenerator<IPNSResolveResult>
|
|
469
|
+
resolve(name: CID<unknown, 0x72> | PublicKey | MultihashDigest | string, options?: IPNSResolveOptions): AsyncGenerator<IPNSResolveResult>
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Stop automatically republishing an IPNS record
|
|
473
|
+
*
|
|
474
|
+
* By default this removes only the republishing metadata, keeping the record
|
|
475
|
+
* in the datastore so it is still served until it expires. Pass
|
|
476
|
+
* `removeRecord: true` to also delete the record so the node stops serving it.
|
|
477
|
+
* If a key name is passed, the key remains in the keychain.
|
|
478
|
+
*/
|
|
479
|
+
unpublish(key: CID<unknown, 0x72> | PublicKey | MultihashDigest | string, options?: UnpublishOptions): Promise<void>
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Republish the record already in the local store, setting its upkeep policy.
|
|
483
|
+
*
|
|
484
|
+
* Sets the record's upkeep policy to `options.upkeep` (default `rebroadcast`)
|
|
485
|
+
* so the background republisher keeps it available on the routers.
|
|
486
|
+
*
|
|
487
|
+
* @throws {NotFoundError} when there is no local record to republish
|
|
488
|
+
* @throws {RecordObsoleteError} when the routers already have a newer record
|
|
489
|
+
*/
|
|
490
|
+
republish(key: CID<unknown, 0x72> | PublicKey | MultihashDigest | string, options?: RepublishOptions): Promise<RepublishResult>
|
|
361
491
|
|
|
362
492
|
/**
|
|
363
|
-
*
|
|
493
|
+
* Import an existing IPNS record into the local store without publishing it.
|
|
494
|
+
*
|
|
495
|
+
* The record is validated and stored so this node serves it on a GET, but it
|
|
496
|
+
* is not broadcast to the routers. A newly imported record (for a key with no
|
|
497
|
+
* existing upkeep policy) is not automatically republished; call `republish`
|
|
498
|
+
* with an upkeep policy to start keeping it alive.
|
|
364
499
|
*
|
|
365
|
-
*
|
|
500
|
+
* Overwrites an older record already stored under this key, or rejects with
|
|
501
|
+
* `RecordObsoleteError` if the stored record is newer. Importing never changes
|
|
502
|
+
* the key's upkeep metadata, so a key that already has an upkeep policy keeps
|
|
503
|
+
* it and continues to be republished automatically.
|
|
366
504
|
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
505
|
+
* @throws {RecordExpiredError} (or another validation error) when the record fails validation
|
|
506
|
+
* @throws {RecordObsoleteError} when the stored record is more suitable than the one being imported
|
|
369
507
|
*/
|
|
370
|
-
|
|
508
|
+
import(key: CID<unknown, 0x72> | PublicKey | MultihashDigest | string, record: IPNSEntry | Uint8Array, options?: AbortOptions): Promise<void>
|
|
371
509
|
}
|
|
372
510
|
|
|
373
511
|
export interface IPNSComponents {
|
package/src/ipns/publisher.ts
CHANGED
|
@@ -2,9 +2,10 @@ import { base36 } from 'multiformats/bases/base36'
|
|
|
2
2
|
import { CustomProgressEvent } from 'progress-events'
|
|
3
3
|
import { DEFAULT_LIFETIME_MS } from '../constants.ts'
|
|
4
4
|
import { IPNSEntry } from '../pb/ipns.ts'
|
|
5
|
+
import { Upkeep } from '../pb/metadata.ts'
|
|
5
6
|
import { createIPNSRecord } from '../records.ts'
|
|
6
7
|
import { decodeExtensibleData, multihashToIPNSRoutingKey } from '../utils.ts'
|
|
7
|
-
import type { IPNSPublishResult, PublishOptions } from '../index.ts'
|
|
8
|
+
import type { IPNSPublishResult, PublishOptions, UnpublishOptions } from '../index.ts'
|
|
8
9
|
import type { LocalStore } from '../local-store.ts'
|
|
9
10
|
import type { IPNSRouting } from '../routing/index.ts'
|
|
10
11
|
import type { Keychain, PrivateKey, PublicKey } from '@helia/interface'
|
|
@@ -58,25 +59,14 @@ export class IPNSPublisher {
|
|
|
58
59
|
})
|
|
59
60
|
const marshaledRecord = IPNSEntry.encode(record)
|
|
60
61
|
|
|
62
|
+
const metadata = { keyName, lifetime, upkeep: Upkeep[options.upkeep ?? 'reissue'] }
|
|
61
63
|
if (options.offline === true) {
|
|
62
64
|
// only store record locally
|
|
63
|
-
await this.localStore.put(routingKey, marshaledRecord, {
|
|
64
|
-
...options,
|
|
65
|
-
metadata: {
|
|
66
|
-
keyName,
|
|
67
|
-
lifetime
|
|
68
|
-
}
|
|
69
|
-
})
|
|
65
|
+
await this.localStore.put(routingKey, marshaledRecord, { ...options, metadata })
|
|
70
66
|
} else {
|
|
71
67
|
// publish record to routing (including the local store)
|
|
72
68
|
await Promise.all(this.routers.map(async r => {
|
|
73
|
-
await r.put(routingKey, marshaledRecord, {
|
|
74
|
-
...options,
|
|
75
|
-
metadata: {
|
|
76
|
-
keyName,
|
|
77
|
-
lifetime
|
|
78
|
-
}
|
|
79
|
-
})
|
|
69
|
+
await r.put(routingKey, marshaledRecord, { ...options, metadata })
|
|
80
70
|
}))
|
|
81
71
|
}
|
|
82
72
|
|
|
@@ -108,10 +98,22 @@ export class IPNSPublisher {
|
|
|
108
98
|
}
|
|
109
99
|
}
|
|
110
100
|
|
|
111
|
-
async unpublish (keyName: string, options?:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
async unpublish (keyName: string | MultihashDigest, options?: UnpublishOptions): Promise<void> {
|
|
102
|
+
if (typeof keyName === 'string') {
|
|
103
|
+
const key = await this.keychain.exportKey(keyName, options)
|
|
104
|
+
keyName = key.publicKey.toMultihash()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const routingKey = multihashToIPNSRoutingKey(keyName)
|
|
108
|
+
|
|
109
|
+
if (options?.removeRecord === true) {
|
|
110
|
+
// delete the record and its metadata so this node stops serving it
|
|
111
|
+
await this.localStore.delete(routingKey, options)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// remove only the metadata so the record is still served but no longer
|
|
116
|
+
// automatically republished; it expires naturally at its validity
|
|
117
|
+
await this.localStore.deleteMetadata(routingKey, options)
|
|
116
118
|
}
|
|
117
119
|
}
|
package/src/ipns/republisher.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
+
import { NotFoundError } from '@libp2p/interface'
|
|
1
2
|
import { Queue, repeatingTask } from '@libp2p/utils'
|
|
3
|
+
import NanoDate from 'timestamp-nano'
|
|
2
4
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
3
5
|
import { DEFAULT_REPUBLISH_CONCURRENCY, DEFAULT_REPUBLISH_INTERVAL_MS, DEFAULT_TTL_NS } from '../constants.ts'
|
|
6
|
+
import { RecordObsoleteError } from '../errors.ts'
|
|
4
7
|
import { IPNSEntry } from '../pb/ipns.ts'
|
|
8
|
+
import { Upkeep } from '../pb/metadata.ts'
|
|
5
9
|
import { createIPNSRecord } from '../records.ts'
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
10
|
+
import { isLocalStoreRouting, routerName } from '../routing/index.ts'
|
|
11
|
+
import { ipnsSelector } from '../selector.ts'
|
|
12
|
+
import { decodeExtensibleData, multihashToIPNSRoutingKey, shouldRepublish } from '../utils.ts'
|
|
13
|
+
import { extractPublicKey, ipnsValidator } from '../validator.ts'
|
|
14
|
+
import { findRoutingRecords } from './resolver.ts'
|
|
15
|
+
import type { IPNSRecordData, RepublishOptions, RepublishResult } from '../index.ts'
|
|
8
16
|
import type { LocalStore } from '../local-store.ts'
|
|
9
|
-
import type { IPNSRouting } from '../routing/index.ts'
|
|
17
|
+
import type { IPNSRouting, IPNSRoutingPutOptions } from '../routing/index.ts'
|
|
10
18
|
import type { Keychain, PrivateKey } from '@helia/interface'
|
|
11
19
|
import type { AbortOptions, ComponentLogger, Logger } from '@libp2p/interface'
|
|
12
20
|
import type { RepeatingTask } from '@libp2p/utils'
|
|
21
|
+
import type { MultihashDigest } from 'multiformats'
|
|
13
22
|
|
|
14
23
|
export interface IPNSRepublisherComponents {
|
|
15
24
|
logger: ComponentLogger
|
|
@@ -79,7 +88,8 @@ export class IPNSRepublisher {
|
|
|
79
88
|
})
|
|
80
89
|
|
|
81
90
|
try {
|
|
82
|
-
const
|
|
91
|
+
const recordsToReissue: Array<{ routingKey: Uint8Array, record: IPNSEntry }> = []
|
|
92
|
+
const recordsToRebroadcast: Array<{ routingKey: Uint8Array, record: IPNSEntry }> = []
|
|
83
93
|
let listed = 0
|
|
84
94
|
|
|
85
95
|
// Find all records using the localStore.list method
|
|
@@ -87,23 +97,31 @@ export class IPNSRepublisher {
|
|
|
87
97
|
listed++
|
|
88
98
|
|
|
89
99
|
if (metadata == null) {
|
|
90
|
-
//
|
|
91
|
-
//
|
|
100
|
+
// no metadata: an imported record not yet given an upkeep policy, or a
|
|
101
|
+
// record stored before we tracked metadata
|
|
92
102
|
this.log('no metadata found for record %b, skipping', routingKey)
|
|
93
103
|
continue
|
|
94
104
|
}
|
|
95
105
|
|
|
106
|
+
if (metadata.upkeep === Upkeep.none) {
|
|
107
|
+
// Skip republishing, disabled for this record
|
|
108
|
+
this.log('republishing is disabled for record %b, skipping', routingKey)
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
96
112
|
let ipnsRecord: IPNSEntry
|
|
97
113
|
|
|
98
114
|
try {
|
|
99
115
|
ipnsRecord = IPNSEntry.decode(record)
|
|
100
116
|
} catch (err) {
|
|
101
|
-
|
|
117
|
+
// the record was valid when stored, so a decode failure now means
|
|
118
|
+
// on-disk corruption or a format change worth surfacing
|
|
119
|
+
this.log.error('skipping stored record %b, could not decode it - %e', routingKey, err)
|
|
102
120
|
continue
|
|
103
121
|
}
|
|
104
122
|
|
|
105
123
|
if (ipnsRecord.data == null) {
|
|
106
|
-
this.log.
|
|
124
|
+
this.log.error('skipping stored record %b, its data is missing', routingKey)
|
|
107
125
|
continue
|
|
108
126
|
}
|
|
109
127
|
|
|
@@ -111,17 +129,40 @@ export class IPNSRepublisher {
|
|
|
111
129
|
|
|
112
130
|
try {
|
|
113
131
|
data = decodeExtensibleData(ipnsRecord.data)
|
|
114
|
-
} catch {
|
|
115
|
-
this.log.
|
|
132
|
+
} catch (err) {
|
|
133
|
+
this.log.error('skipping stored record %b, could not decode its data - %e', routingKey, err)
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let recordExpiry: Date
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
recordExpiry = NanoDate.fromString(uint8ArrayToString(data.Validity)).toDate()
|
|
141
|
+
} catch (err) {
|
|
142
|
+
this.log.error('skipping stored record %b, could not parse its validity - %e', routingKey, err)
|
|
116
143
|
continue
|
|
117
144
|
}
|
|
118
145
|
|
|
119
146
|
// Only republish records that are within the DHT or record expiry threshold
|
|
120
|
-
if (!shouldRepublish(created,
|
|
147
|
+
if (!shouldRepublish(created, recordExpiry)) {
|
|
121
148
|
this.log.trace('skipping record %b within republish threshold', routingKey)
|
|
122
149
|
continue
|
|
123
150
|
}
|
|
124
151
|
|
|
152
|
+
if (metadata.upkeep === Upkeep.rebroadcast) {
|
|
153
|
+
// only re-broadcast records that are still valid: an expired record
|
|
154
|
+
// cannot be revived without re-signing, which the rebroadcast policy
|
|
155
|
+
// has no key for
|
|
156
|
+
if (recordExpiry.getTime() <= Date.now()) {
|
|
157
|
+
this.log.trace('skipping expired record %b for rebroadcast', routingKey)
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
recordsToRebroadcast.push({ routingKey, record: ipnsRecord })
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Upkeep.reissue: re-sign the record with a new validity
|
|
125
166
|
const sequenceNumber = data.Sequence + 1n
|
|
126
167
|
const ttlNs = data.TTL ?? DEFAULT_TTL_NS
|
|
127
168
|
let privKey: PrivateKey
|
|
@@ -138,36 +179,151 @@ export class IPNSRepublisher {
|
|
|
138
179
|
...options,
|
|
139
180
|
ttlNs
|
|
140
181
|
})
|
|
141
|
-
|
|
182
|
+
|
|
183
|
+
recordsToReissue.push({
|
|
142
184
|
routingKey,
|
|
143
185
|
record: updatedRecord
|
|
144
186
|
})
|
|
145
187
|
} catch (err: any) {
|
|
146
|
-
this.log.error('error creating updated IPNS record for %
|
|
188
|
+
this.log.error('error creating updated IPNS record for %b - %e', routingKey, err)
|
|
147
189
|
continue
|
|
148
190
|
}
|
|
149
191
|
}
|
|
150
192
|
|
|
151
|
-
this.log(`
|
|
193
|
+
this.log(`reissuing ${recordsToReissue.length} and rebroadcasting ${recordsToRebroadcast.length} of ${listed} records`)
|
|
194
|
+
|
|
195
|
+
// reissue: re-publish the re-signed record as-is. the jobs run
|
|
196
|
+
// fire-and-forget, so catch any failure (including a local-store write
|
|
197
|
+
// that #publishToRouters surfaces) rather than leaving an unhandled rejection
|
|
198
|
+
for (const { routingKey, record } of recordsToReissue) {
|
|
199
|
+
queue.add(async () => this.#publishToRouters(routingKey, IPNSEntry.encode(record), options), options)
|
|
200
|
+
.catch((err: any) => { this.log.error('failed to reissue record %b - %e', routingKey, err) })
|
|
201
|
+
}
|
|
152
202
|
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
203
|
+
// rebroadcast: resolve the latest record from the network and re-broadcast
|
|
204
|
+
// whichever of the local and network records is most suitable, so we adopt
|
|
205
|
+
// a newer record rather than pushing a stale local copy
|
|
206
|
+
for (const { routingKey, record } of recordsToRebroadcast) {
|
|
156
207
|
queue.add(async () => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
} catch (err: any) {
|
|
163
|
-
this.log.error('error republishing record - %e', err)
|
|
164
|
-
}
|
|
208
|
+
const { records } = await findRoutingRecords(this.routers, routingKey, this.keychain, this.log, options)
|
|
209
|
+
const candidates = [record, ...records]
|
|
210
|
+
const best = candidates[ipnsSelector(routingKey, candidates)]
|
|
211
|
+
|
|
212
|
+
await this.#publishToRouters(routingKey, IPNSEntry.encode(best), { ...options, overwrite: true })
|
|
165
213
|
}, options)
|
|
214
|
+
.catch((err: any) => { this.log.error('failed to rebroadcast record %b - %e', routingKey, err) })
|
|
166
215
|
}
|
|
167
216
|
} catch (err: any) {
|
|
168
217
|
this.log.error('error during republish - %e', err)
|
|
169
218
|
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Persist the record to the local store first, throwing if that fails: it
|
|
223
|
+
* holds the record and its upkeep policy, so that failure must surface rather
|
|
224
|
+
* than be swallowed. Then broadcast to the network routers best-effort,
|
|
225
|
+
* logging any that fail.
|
|
226
|
+
*/
|
|
227
|
+
async #publishToRouters (routingKey: Uint8Array, marshaledRecord: Uint8Array, putOptions: IPNSRoutingPutOptions): Promise<void> {
|
|
228
|
+
const localStoreRouter = this.routers.find(isLocalStoreRouting)
|
|
229
|
+
|
|
230
|
+
if (localStoreRouter != null) {
|
|
231
|
+
await localStoreRouter.put(routingKey, marshaledRecord, putOptions)
|
|
232
|
+
this.log('published record %b to %s', routingKey, routerName(localStoreRouter))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await Promise.all(
|
|
236
|
+
this.routers
|
|
237
|
+
.filter(router => router !== localStoreRouter)
|
|
238
|
+
.map(async (router) => {
|
|
239
|
+
try {
|
|
240
|
+
await router.put(routingKey, marshaledRecord, putOptions)
|
|
241
|
+
this.log('published record %b to %s', routingKey, routerName(router))
|
|
242
|
+
} catch (err) {
|
|
243
|
+
this.log.error('failed to publish record %b to %s - %e', routingKey, routerName(router), err)
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async republish (key: MultihashDigest, options: RepublishOptions = {}): Promise<RepublishResult> {
|
|
250
|
+
const routingKey = multihashToIPNSRoutingKey(key)
|
|
251
|
+
|
|
252
|
+
// read the record we hold for this key directly, it was validated when it
|
|
253
|
+
// was imported or published, and reading it through resolve() would recurse
|
|
254
|
+
// and rewrite the local store
|
|
255
|
+
let record: Uint8Array<ArrayBuffer>
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
({ record } = await this.localStore.get(routingKey, options))
|
|
259
|
+
} catch (err: any) {
|
|
260
|
+
if (err.name !== 'NotFoundError') {
|
|
261
|
+
throw err
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
throw new NotFoundError('No local record found to republish - import it first')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// re-validate: a record that was valid when stored can have expired since
|
|
268
|
+
const localRecord = await ipnsValidator(routingKey, record, this.keychain, options)
|
|
269
|
+
|
|
270
|
+
// unless we are told not to look, check whether the routing already has a
|
|
271
|
+
// more suitable record; if so, back off
|
|
272
|
+
if (options.skipResolution !== true) {
|
|
273
|
+
const { records } = await findRoutingRecords(this.routers, routingKey, this.keychain, this.log, options)
|
|
274
|
+
const candidates = [localRecord, ...records]
|
|
275
|
+
const best = ipnsSelector(routingKey, candidates)
|
|
276
|
+
|
|
277
|
+
if (best !== 0) {
|
|
278
|
+
throw new RecordObsoleteError('A newer record is already published for this key', candidates[best])
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const metadata = {
|
|
283
|
+
upkeep: Upkeep[options.upkeep ?? 'rebroadcast']
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// a publish that fails on every router does not throw; use onProgress to see
|
|
287
|
+
// which routers worked and watch the logs
|
|
288
|
+
await this.#publishToRouters(routingKey, record, {
|
|
289
|
+
...options,
|
|
290
|
+
overwrite: true,
|
|
291
|
+
metadata
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
record: localRecord,
|
|
296
|
+
publicKey: await extractPublicKey(routingKey, localRecord, this.keychain, options)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async import (key: MultihashDigest, record: IPNSEntry | Uint8Array, options?: AbortOptions): Promise<void> {
|
|
301
|
+
const routingKey = multihashToIPNSRoutingKey(key)
|
|
302
|
+
const entry = record instanceof Uint8Array ? IPNSEntry.decode(record) : record
|
|
303
|
+
const marshaledRecord = IPNSEntry.encode(entry)
|
|
304
|
+
|
|
305
|
+
// validate the record, throws if it is invalid (bad signature, expired, etc)
|
|
306
|
+
await ipnsValidator(routingKey, marshaledRecord, this.keychain, options)
|
|
307
|
+
|
|
308
|
+
// do not downgrade a record we already hold
|
|
309
|
+
try {
|
|
310
|
+
const { record: existingBytes } = await this.localStore.get(routingKey, options)
|
|
311
|
+
const existing = IPNSEntry.decode(existingBytes)
|
|
312
|
+
|
|
313
|
+
// if the record we already have wins the selector, the incoming one is obsolete
|
|
314
|
+
if (ipnsSelector(routingKey, [entry, existing]) !== 0) {
|
|
315
|
+
throw new RecordObsoleteError('A newer record is already stored for this key', existing)
|
|
316
|
+
}
|
|
317
|
+
} catch (err: any) {
|
|
318
|
+
// holding no record for this key is the normal case, everything else
|
|
319
|
+
// (the RecordObsoleteError above included) is a real failure
|
|
320
|
+
if (err.name !== 'NotFoundError') {
|
|
321
|
+
throw err
|
|
322
|
+
}
|
|
323
|
+
}
|
|
170
324
|
|
|
171
|
-
|
|
325
|
+
// store the record locally only, without metadata, so it is served but not
|
|
326
|
+
// automatically republished until republish() gives it an upkeep policy
|
|
327
|
+
await this.localStore.put(routingKey, marshaledRecord, options)
|
|
172
328
|
}
|
|
173
329
|
}
|