@helia/ipns 10.1.1-b34bc7c7 → 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/metadata.d.ts +14 -1
- package/dist/src/pb/metadata.d.ts.map +1 -1
- package/dist/src/pb/metadata.js +35 -2
- 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 +90 -0
- package/package.json +2 -2
- 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/metadata.proto +10 -0
- package/src/pb/metadata.ts +44 -3
- package/src/routing/index.ts +30 -1
- package/src/utils.ts +19 -0
- package/src/validator.ts +30 -18
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
|
}
|
package/src/ipns/resolver.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { base36 } from 'multiformats/bases/base36'
|
|
|
2
2
|
import { DEFAULT_TTL_NS } from '../constants.ts'
|
|
3
3
|
import { InvalidValueError, RecordNotFoundError, RecordsFailedValidationError } from '../errors.ts'
|
|
4
4
|
import { IPNSEntry } from '../pb/ipns.ts'
|
|
5
|
+
import { isLocalStoreRouting, routerName } from '../routing/index.ts'
|
|
5
6
|
import { ipnsSelector } from '../selector.ts'
|
|
6
7
|
import { multihashToIPNSRoutingKey, normalizeKey, IPNS_STRING_PREFIX, ipnsRecordValueToString, decodeExtensibleData } from '../utils.ts'
|
|
7
8
|
import { ipnsValidator } from '../validator.ts'
|
|
@@ -9,7 +10,7 @@ import type { IPNSResolveOptions, IPNSResolveResult } from '../index.ts'
|
|
|
9
10
|
import type { LocalStore } from '../local-store.ts'
|
|
10
11
|
import type { IPNSRouting } from '../routing/index.ts'
|
|
11
12
|
import type { Routing, Keychain } from '@helia/interface'
|
|
12
|
-
import type { ComponentLogger, Logger } from '@libp2p/interface'
|
|
13
|
+
import type { AbortOptions, ComponentLogger, Logger } from '@libp2p/interface'
|
|
13
14
|
import type { Datastore } from 'interface-datastore'
|
|
14
15
|
import type { MultihashDigest } from 'multiformats/hashes/interface'
|
|
15
16
|
|
|
@@ -135,42 +136,15 @@ export class IPNSResolver {
|
|
|
135
136
|
|
|
136
137
|
this.log('did not have record locally')
|
|
137
138
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
marshaledIPNSRecord = await router.get(routingKey, {
|
|
147
|
-
...options,
|
|
148
|
-
validate: false
|
|
149
|
-
})
|
|
150
|
-
} catch (err: any) {
|
|
151
|
-
this.log.error('error finding IPNS record using router %s - %e', router.toString(), err)
|
|
152
|
-
errors.push(err)
|
|
153
|
-
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
let record: IPNSEntry
|
|
159
|
-
|
|
160
|
-
if (options.validate === false) {
|
|
161
|
-
record = IPNSEntry.decode(marshaledIPNSRecord)
|
|
162
|
-
} else {
|
|
163
|
-
record = await ipnsValidator(routingKey, marshaledIPNSRecord, this.keychain, options)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
records.push(record)
|
|
167
|
-
} catch (err) {
|
|
168
|
-
// we found a record, but the validator rejected it
|
|
169
|
-
foundInvalid++
|
|
170
|
-
this.log.error('error validating IPNS record from router %s - %e', router.toString(), err)
|
|
171
|
-
}
|
|
172
|
-
})
|
|
139
|
+
const { records: networkRecords, foundInvalid } = await findRoutingRecords(
|
|
140
|
+
this.routers,
|
|
141
|
+
routingKey,
|
|
142
|
+
this.keychain,
|
|
143
|
+
this.log,
|
|
144
|
+
options,
|
|
145
|
+
options.validate !== false
|
|
173
146
|
)
|
|
147
|
+
records.push(...networkRecords)
|
|
174
148
|
|
|
175
149
|
if (records.length === 0) {
|
|
176
150
|
if (foundInvalid > 0) {
|
|
@@ -187,3 +161,47 @@ export class IPNSResolver {
|
|
|
187
161
|
return record
|
|
188
162
|
}
|
|
189
163
|
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Query the network routers for the records stored under a routing key, plus a
|
|
167
|
+
* count of records that were fetched but failed validation/decoding. Does not
|
|
168
|
+
* touch the local store; the caller selects the record it wants with
|
|
169
|
+
* `ipnsSelector`. Records are validated unless `validate` is false, in which
|
|
170
|
+
* case they are returned decoded without validation.
|
|
171
|
+
*/
|
|
172
|
+
export async function findRoutingRecords (routers: IPNSRouting[], routingKey: Uint8Array, keychain: Keychain, log: Logger, options: AbortOptions = {}, validate = true): Promise<{ records: IPNSEntry[], foundInvalid: number }> {
|
|
173
|
+
const records: IPNSEntry[] = []
|
|
174
|
+
let foundInvalid = 0
|
|
175
|
+
|
|
176
|
+
await Promise.all(
|
|
177
|
+
routers.map(async (router) => {
|
|
178
|
+
// the local store is not the network
|
|
179
|
+
if (isLocalStoreRouting(router)) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let marshaledRecord: Uint8Array
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
marshaledRecord = await router.get(routingKey, { ...options, validate: false })
|
|
187
|
+
} catch (err: any) {
|
|
188
|
+
log.error('error finding IPNS record using router %s - %e', routerName(router), err)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// when the caller opts out of validation, return the record as decoded
|
|
194
|
+
const record = validate
|
|
195
|
+
? await ipnsValidator(routingKey, marshaledRecord, keychain, options)
|
|
196
|
+
: IPNSEntry.decode(marshaledRecord)
|
|
197
|
+
|
|
198
|
+
records.push(record)
|
|
199
|
+
} catch (err) {
|
|
200
|
+
foundInvalid++
|
|
201
|
+
log.error('error reading IPNS record from router %s - %e', routerName(router), err)
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return { records, foundInvalid }
|
|
207
|
+
}
|
package/src/ipns.ts
CHANGED
|
@@ -7,9 +7,9 @@ import { localStore } from './local-store.ts'
|
|
|
7
7
|
import { heliaIPNSRouting } from './routing/helia.ts'
|
|
8
8
|
import { localStoreIPNSRouting } from './routing/local-store.ts'
|
|
9
9
|
import { ipnsSelector } from './selector.ts'
|
|
10
|
-
import { normalizeKey, normalizeValue } from './utils.ts'
|
|
10
|
+
import { normalizeKey, normalizeKeyName, normalizeValue } from './utils.ts'
|
|
11
11
|
import { ipnsValidator } from './validator.ts'
|
|
12
|
-
import type { IPNSComponents, IPNS as IPNSInterface, IPNSOptions, IPNSPublishResult, PublishOptions, IPNSResolveOptions, IPNSResolveResult } from './index.ts'
|
|
12
|
+
import type { IPNSComponents, IPNS as IPNSInterface, IPNSOptions, IPNSPublishResult, PublishOptions, RepublishOptions, RepublishResult, IPNSResolveOptions, IPNSResolveResult, UnpublishOptions } from './index.ts'
|
|
13
13
|
import type { LocalStore } from './local-store.ts'
|
|
14
14
|
import type { IPNSRouting } from './routing/index.ts'
|
|
15
15
|
import type { PublicKey } from '@helia/interface'
|
|
@@ -42,12 +42,12 @@ export class IPNS implements IPNSInterface, Startable {
|
|
|
42
42
|
routers: this.routers,
|
|
43
43
|
localStore: this.localStore
|
|
44
44
|
})
|
|
45
|
-
this.
|
|
45
|
+
this.resolver = new IPNSResolver(components, {
|
|
46
46
|
...init,
|
|
47
47
|
routers: this.routers,
|
|
48
48
|
localStore: this.localStore
|
|
49
49
|
})
|
|
50
|
-
this.
|
|
50
|
+
this.republisher = new IPNSRepublisher(components, {
|
|
51
51
|
...init,
|
|
52
52
|
routers: this.routers,
|
|
53
53
|
localStore: this.localStore
|
|
@@ -103,15 +103,29 @@ export class IPNS implements IPNSInterface, Startable {
|
|
|
103
103
|
return this.publisher.publish(keyName, normalizeValue(value), options)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
async * resolve (key:
|
|
106
|
+
async * resolve (key: CID<unknown, 0x72> | PublicKey | MultihashDigest | string, options: IPNSResolveOptions = {}): AsyncGenerator<IPNSResolveResult> {
|
|
107
107
|
const { digest } = normalizeKey(key)
|
|
108
108
|
|
|
109
109
|
yield * this.resolver.resolve(digest, options)
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
async unpublish (keyName: string, options?:
|
|
112
|
+
async unpublish (keyName: CID<unknown, 0x72> | PublicKey | MultihashDigest | string, options?: UnpublishOptions): Promise<void> {
|
|
113
|
+
keyName = normalizeKeyName(keyName)
|
|
114
|
+
|
|
113
115
|
return this.publisher.unpublish(keyName, options)
|
|
114
116
|
}
|
|
117
|
+
|
|
118
|
+
async republish (keyName: CID<unknown, 0x72> | PublicKey | MultihashDigest | string, options: RepublishOptions = {}): Promise<RepublishResult> {
|
|
119
|
+
const { digest } = normalizeKey(keyName)
|
|
120
|
+
|
|
121
|
+
return this.republisher.republish(digest, options)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async import (key: CID<unknown, 0x72> | PublicKey | MultihashDigest | string, record: IPNSEntry | Uint8Array, options?: AbortOptions): Promise<void> {
|
|
125
|
+
const { digest } = normalizeKey(key)
|
|
126
|
+
|
|
127
|
+
return this.republisher.import(digest, record, options)
|
|
128
|
+
}
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
function isLibp2p (obj?: any): obj is Libp2p {
|
package/src/local-store.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { CustomProgressEvent } from 'progress-events'
|
|
|
3
3
|
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
|
|
4
4
|
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
5
5
|
import { withArrayBuffer } from 'uint8arrays/with-array-buffer'
|
|
6
|
-
import { IPNSPublishMetadata } from './pb/metadata.ts'
|
|
6
|
+
import { IPNSPublishMetadata, Upkeep } from './pb/metadata.ts'
|
|
7
7
|
import { dhtRoutingKey, DHT_RECORD_PREFIX, ipnsMetadataKey } from './utils.ts'
|
|
8
8
|
import type { DatastoreProgressEvents, IPNSRoutingGetOptions, IPNSRoutingPutOptions } from './routing/index.ts'
|
|
9
9
|
import type { AbortOptions, Logger } from '@libp2p/interface'
|
|
@@ -37,12 +37,67 @@ export interface LocalStore {
|
|
|
37
37
|
get(routingKey: Uint8Array, options?: IPNSRoutingGetOptions): Promise<GetResult>
|
|
38
38
|
has(routingKey: Uint8Array, options?: AbortOptions): Promise<boolean>
|
|
39
39
|
delete(routingKey: Uint8Array, options?: AbortOptions): Promise<void>
|
|
40
|
+
/**
|
|
41
|
+
* Delete only the IPNS metadata for a record, leaving the record itself in
|
|
42
|
+
* place so it is still served but no longer automatically republished
|
|
43
|
+
*/
|
|
44
|
+
deleteMetadata(routingKey: Uint8Array, options?: AbortOptions): Promise<void>
|
|
40
45
|
/**
|
|
41
46
|
* List all IPNS records in the datastore
|
|
42
47
|
*/
|
|
43
48
|
list(options?: ListOptions): AsyncIterable<ListResult>
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Merge a (possibly partial) metadata update into any metadata already stored
|
|
53
|
+
* for the routing key, so fields that are not being changed are preserved.
|
|
54
|
+
*
|
|
55
|
+
* Throws if the existing metadata cannot be read (rather than silently
|
|
56
|
+
* overwriting it and dropping keyName/lifetime), or if the resulting policy
|
|
57
|
+
* needs a keyName it does not have.
|
|
58
|
+
*/
|
|
59
|
+
async function mergeMetadata (datastore: Datastore, routingKey: Uint8Array, incoming: Partial<IPNSPublishMetadata>, options?: AbortOptions): Promise<Partial<IPNSPublishMetadata>> {
|
|
60
|
+
let metadata = incoming
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// merge into any existing metadata so a partial update (e.g. republish
|
|
64
|
+
// setting only `upkeep`) preserves keyName/lifetime instead of wiping them
|
|
65
|
+
const existing = IPNSPublishMetadata.decode(await datastore.get(ipnsMetadataKey(routingKey), options))
|
|
66
|
+
metadata = { ...existing, ...incoming }
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
if (err.name !== 'NotFoundError') {
|
|
69
|
+
// the stored metadata is unreadable, surface it rather than silently
|
|
70
|
+
// overwriting and dropping keyName/lifetime
|
|
71
|
+
throw err
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// the reissue upkeep policy re-signs the record, which needs the key, so
|
|
76
|
+
// refuse to persist that policy without a keyName
|
|
77
|
+
if (metadata.upkeep === Upkeep.reissue && (metadata.keyName == null || metadata.keyName === '')) {
|
|
78
|
+
throw new Error('a keyName is required to reissue a record')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return metadata
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read the stored upkeep metadata for a routing key, if any. A record with no
|
|
86
|
+
* metadata is normal (imported, or unpublished), so that is not an error; only
|
|
87
|
+
* an actual decode/corruption failure is logged.
|
|
88
|
+
*/
|
|
89
|
+
async function readMetadata (datastore: Datastore, routingKey: Uint8Array, log: Logger, options?: AbortOptions): Promise<IPNSPublishMetadata | undefined> {
|
|
90
|
+
try {
|
|
91
|
+
return IPNSPublishMetadata.decode(await datastore.get(ipnsMetadataKey(routingKey), options))
|
|
92
|
+
} catch (err: any) {
|
|
93
|
+
if (err.name !== 'NotFoundError') {
|
|
94
|
+
log.error('error deserializing metadata for %b - %e', routingKey, err)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
46
101
|
/**
|
|
47
102
|
* Read/write IPNS records to the datastore as DHT records.
|
|
48
103
|
*
|
|
@@ -55,18 +110,20 @@ export function localStore (datastore: Datastore, log: Logger): LocalStore {
|
|
|
55
110
|
try {
|
|
56
111
|
const key = dhtRoutingKey(routingKey)
|
|
57
112
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
113
|
+
if (options.overwrite !== true) {
|
|
114
|
+
// don't overwrite existing, identical records as this will affect the
|
|
115
|
+
// TTL
|
|
116
|
+
try {
|
|
117
|
+
const existingBuf = await datastore.get(key)
|
|
118
|
+
const existingRecord = Record.deserialize(existingBuf)
|
|
63
119
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
120
|
+
if (uint8ArrayEquals(existingRecord.value, marshalledRecord)) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
if (err.name !== 'NotFoundError') {
|
|
125
|
+
throw err
|
|
126
|
+
}
|
|
70
127
|
}
|
|
71
128
|
}
|
|
72
129
|
|
|
@@ -78,8 +135,10 @@ export function localStore (datastore: Datastore, log: Logger): LocalStore {
|
|
|
78
135
|
batch.put(key, record.serialize())
|
|
79
136
|
|
|
80
137
|
if (options.metadata != null) {
|
|
81
|
-
//
|
|
82
|
-
|
|
138
|
+
// merge into any existing metadata (preserving keyName/lifetime on a
|
|
139
|
+
// partial update) and validate the result before writing it
|
|
140
|
+
const metadata = await mergeMetadata(datastore, routingKey, options.metadata, options)
|
|
141
|
+
batch.put(ipnsMetadataKey(routingKey), IPNSPublishMetadata.encode(metadata))
|
|
83
142
|
}
|
|
84
143
|
await batch.commit(options)
|
|
85
144
|
} catch (err: any) {
|
|
@@ -117,6 +176,9 @@ export function localStore (datastore: Datastore, log: Logger): LocalStore {
|
|
|
117
176
|
batch.delete(ipnsMetadataKey(routingKey))
|
|
118
177
|
await batch.commit(options)
|
|
119
178
|
},
|
|
179
|
+
async deleteMetadata (routingKey, options): Promise<void> {
|
|
180
|
+
await datastore.delete(ipnsMetadataKey(routingKey), options)
|
|
181
|
+
},
|
|
120
182
|
async * list (options: ListOptions = {}): AsyncIterable<ListResult> {
|
|
121
183
|
try {
|
|
122
184
|
options.onProgress?.(new CustomProgressEvent('ipns:routing:datastore:list'))
|
|
@@ -134,14 +196,7 @@ export function localStore (datastore: Datastore, log: Logger): LocalStore {
|
|
|
134
196
|
const routingKeyBase32 = keyString.substring(DHT_RECORD_PREFIX.length)
|
|
135
197
|
const routingKey = uint8ArrayFromString(routingKeyBase32, 'base32')
|
|
136
198
|
|
|
137
|
-
const
|
|
138
|
-
let metadata: IPNSPublishMetadata | undefined
|
|
139
|
-
try {
|
|
140
|
-
const metadataBuf = await datastore.get(metadataKey, options)
|
|
141
|
-
metadata = IPNSPublishMetadata.decode(metadataBuf)
|
|
142
|
-
} catch (err: any) {
|
|
143
|
-
log.error('Error deserializing metadata for %s - %e', routingKeyBase32, err)
|
|
144
|
-
}
|
|
199
|
+
const metadata = await readMetadata(datastore, routingKey, log, options)
|
|
145
200
|
|
|
146
201
|
yield {
|
|
147
202
|
routingKey,
|
package/src/pb/metadata.proto
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
syntax = "proto3";
|
|
2
2
|
|
|
3
|
+
enum Upkeep {
|
|
4
|
+
reissue = 0; // Re-sign the record with a new validity (key must be in keychain)
|
|
5
|
+
rebroadcast = 1; // Re-publish the record as is (while it is still valid)
|
|
6
|
+
none = 2; // Disables automated republishing for the record
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
message IPNSPublishMetadata {
|
|
4
10
|
// The name of the key that was used to publish the record
|
|
5
11
|
string keyName = 1;
|
|
6
12
|
|
|
7
13
|
// Lifetime is the duration of the signature validity in milliseconds
|
|
14
|
+
// Relevant for Upkeep.reissue
|
|
8
15
|
uint32 lifetime = 2;
|
|
16
|
+
|
|
17
|
+
// Republishing policy
|
|
18
|
+
Upkeep upkeep = 3;
|
|
9
19
|
}
|