@helia/ipns 9.2.1-eb1908b3 → 9.2.1-ed6c3b79
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 +21 -57
- package/dist/index.min.js +5 -16
- package/dist/index.min.js.map +4 -4
- package/dist/src/errors.d.ts +33 -5
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +33 -20
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +62 -99
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +21 -60
- package/dist/src/index.js.map +1 -1
- package/dist/src/ipns/publisher.d.ts +5 -9
- package/dist/src/ipns/publisher.d.ts.map +1 -1
- package/dist/src/ipns/publisher.js +30 -22
- package/dist/src/ipns/publisher.js.map +1 -1
- package/dist/src/ipns/republisher.d.ts +3 -5
- package/dist/src/ipns/republisher.d.ts.map +1 -1
- package/dist/src/ipns/republisher.js +18 -9
- package/dist/src/ipns/republisher.js.map +1 -1
- package/dist/src/ipns/resolver.d.ts +6 -5
- package/dist/src/ipns/resolver.d.ts.map +1 -1
- package/dist/src/ipns/resolver.js +32 -78
- package/dist/src/ipns/resolver.js.map +1 -1
- package/dist/src/ipns.d.ts +6 -4
- package/dist/src/ipns.d.ts.map +1 -1
- package/dist/src/ipns.js +33 -4
- package/dist/src/ipns.js.map +1 -1
- package/dist/src/pb/ipns.d.ts +62 -0
- package/dist/src/pb/ipns.d.ts.map +1 -0
- package/dist/src/pb/ipns.js +203 -0
- package/dist/src/pb/ipns.js.map +1 -0
- package/dist/src/pb/metadata.d.ts +1 -1
- package/dist/src/pb/metadata.d.ts.map +1 -1
- package/dist/src/records.d.ts +155 -0
- package/dist/src/records.d.ts.map +1 -0
- package/dist/src/records.js +88 -0
- package/dist/src/records.js.map +1 -0
- package/dist/src/routing/pubsub.d.ts +3 -0
- package/dist/src/routing/pubsub.d.ts.map +1 -1
- package/dist/src/routing/pubsub.js +15 -10
- package/dist/src/routing/pubsub.js.map +1 -1
- package/dist/src/selector.d.ts +14 -0
- package/dist/src/selector.d.ts.map +1 -0
- package/dist/src/selector.js +47 -0
- package/dist/src/selector.js.map +1 -0
- package/dist/src/utils.d.ts +29 -2
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +308 -0
- package/dist/src/utils.js.map +1 -1
- package/dist/src/validator.d.ts +18 -0
- package/dist/src/validator.d.ts.map +1 -0
- package/dist/src/validator.js +58 -0
- package/dist/src/validator.js.map +1 -0
- package/package.json +24 -23
- package/src/errors.ts +40 -25
- package/src/index.ts +63 -100
- package/src/ipns/publisher.ts +34 -29
- package/src/ipns/republisher.ts +24 -13
- package/src/ipns/resolver.ts +40 -88
- package/src/ipns.ts +44 -7
- package/src/pb/ipns.proto +39 -0
- package/src/pb/ipns.ts +280 -0
- package/src/pb/metadata.ts +1 -1
- package/src/records.ts +273 -0
- package/src/routing/pubsub.ts +17 -10
- package/src/selector.ts +55 -0
- package/src/utils.ts +371 -2
- package/src/validator.ts +67 -0
package/src/utils.ts
CHANGED
|
@@ -1,15 +1,34 @@
|
|
|
1
|
+
import { isPublicKey } from '@helia/interface'
|
|
1
2
|
import { InvalidParametersError } from '@libp2p/interface'
|
|
3
|
+
import * as cborg from 'cborg'
|
|
2
4
|
import { Key } from 'interface-datastore'
|
|
5
|
+
import { base36 } from 'multiformats/bases/base36'
|
|
6
|
+
import { base58btc } from 'multiformats/bases/base58'
|
|
7
|
+
import { CID } from 'multiformats/cid'
|
|
8
|
+
import * as Digest from 'multiformats/hashes/digest'
|
|
9
|
+
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
|
|
10
|
+
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
|
|
11
|
+
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
3
12
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
4
13
|
import { DHT_EXPIRY_MS, REPUBLISH_THRESHOLD } from './constants.ts'
|
|
5
|
-
import
|
|
6
|
-
import
|
|
14
|
+
import { InvalidEmbeddedPublicKeyError, InvalidRecordDataError, InvalidValueError, RecordTooLargeError, SignatureVerificationError, UnsupportedValidityError } from './errors.ts'
|
|
15
|
+
import { IpnsEntry } from './pb/ipns.ts'
|
|
16
|
+
import type { IPNSRecord, IPNSRecordV2, IPNSRecordData } from './records.ts'
|
|
17
|
+
import type { PublicKey, Keychain } from '@helia/interface'
|
|
18
|
+
import type { AbortOptions } from '@libp2p/interface'
|
|
19
|
+
import type { MultibaseDecoder } from 'multiformats/cid'
|
|
7
20
|
import type { MultihashDigest } from 'multiformats/hashes/interface'
|
|
8
21
|
|
|
9
22
|
export const LIBP2P_KEY_CODEC = 0x72
|
|
10
23
|
export const IDENTITY_CODEC = 0x0
|
|
11
24
|
export const SHA2_256_CODEC = 0x12
|
|
12
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Limit valid IPNS record sizes to 10kb
|
|
28
|
+
*/
|
|
29
|
+
const MAX_RECORD_SIZE = 1024 * 10
|
|
30
|
+
|
|
31
|
+
const IPNS_PREFIX = uint8ArrayFromString('/ipns/')
|
|
13
32
|
export const IPNS_STRING_PREFIX = '/ipns/'
|
|
14
33
|
|
|
15
34
|
export function isCodec <T extends number> (digest: MultihashDigest, codec: T): digest is MultihashDigest<T> {
|
|
@@ -78,3 +97,353 @@ export function isLibp2pCID (obj?: any): obj is CID<unknown, 0x72, 0x00 | 0x12,
|
|
|
78
97
|
|
|
79
98
|
return true
|
|
80
99
|
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Utility for creating the record data for being signed
|
|
103
|
+
*/
|
|
104
|
+
export function ipnsRecordDataForV1Sig (value: string, validityType: IpnsEntry.ValidityType, validity: Uint8Array): Uint8Array {
|
|
105
|
+
const validityTypeBuffer = uint8ArrayFromString(validityType)
|
|
106
|
+
const valueBytes = uint8ArrayFromString(value)
|
|
107
|
+
|
|
108
|
+
return uint8ArrayConcat([
|
|
109
|
+
valueBytes,
|
|
110
|
+
validity,
|
|
111
|
+
validityTypeBuffer
|
|
112
|
+
], valueBytes.byteLength + validity.byteLength + validityTypeBuffer.byteLength)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Utility for creating the record data for being signed
|
|
117
|
+
*/
|
|
118
|
+
export function ipnsRecordDataForV2Sig (data: Uint8Array): Uint8Array {
|
|
119
|
+
const entryData = uint8ArrayFromString('ipns-signature:')
|
|
120
|
+
|
|
121
|
+
return uint8ArrayConcat([entryData, data])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function marshalIPNSRecord (obj: IPNSRecord | IPNSRecordV2): Uint8Array {
|
|
125
|
+
let publicKey: Uint8Array | undefined = obj.publicKey?.toProtobuf()
|
|
126
|
+
|
|
127
|
+
// do not embed public keys whose multihash is an identity hash as these can
|
|
128
|
+
// be derived from the routing key
|
|
129
|
+
if (obj.publicKey?.toMultihash().code === 0x00) {
|
|
130
|
+
publicKey = undefined
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if ('signatureV1' in obj) {
|
|
134
|
+
return IpnsEntry.encode({
|
|
135
|
+
value: uint8ArrayFromString(obj.value),
|
|
136
|
+
signatureV1: obj.signatureV1,
|
|
137
|
+
validityType: obj.validityType,
|
|
138
|
+
validity: uint8ArrayFromString(obj.validity),
|
|
139
|
+
sequence: obj.sequence,
|
|
140
|
+
ttl: obj.ttl,
|
|
141
|
+
publicKey,
|
|
142
|
+
signatureV2: obj.signatureV2,
|
|
143
|
+
data: obj.data
|
|
144
|
+
})
|
|
145
|
+
} else {
|
|
146
|
+
return IpnsEntry.encode({
|
|
147
|
+
publicKey,
|
|
148
|
+
signatureV2: obj.signatureV2,
|
|
149
|
+
data: obj.data
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function valueToString (value: Uint8Array): string {
|
|
155
|
+
// handle legacy case where record value is raw CID bytes
|
|
156
|
+
try {
|
|
157
|
+
const cid = CID.decode(value)
|
|
158
|
+
return `/ipfs/${cid}`
|
|
159
|
+
} catch {
|
|
160
|
+
// ignore error
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return uint8ArrayToString(value)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function unmarshalIPNSRecord (routingKey: Uint8Array, marshalledRecord: Uint8Array, keychain: Keychain, options?: AbortOptions): Promise<IPNSRecord> {
|
|
167
|
+
if (marshalledRecord.byteLength > MAX_RECORD_SIZE) {
|
|
168
|
+
throw new RecordTooLargeError('The record is too large')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const message = IpnsEntry.decode(marshalledRecord)
|
|
172
|
+
|
|
173
|
+
// Check if we have the data field. If we don't, we fail. We've been producing
|
|
174
|
+
// V1+V2 records for quite a while and we don't support V1-only records during
|
|
175
|
+
// validation any more
|
|
176
|
+
if (message.signatureV2 == null || message.data == null) {
|
|
177
|
+
throw new SignatureVerificationError('Missing data or signatureV2')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const data = parseCborData(message.data)
|
|
181
|
+
const validity = uint8ArrayToString(data.Validity)
|
|
182
|
+
|
|
183
|
+
let publicKey: PublicKey | undefined
|
|
184
|
+
|
|
185
|
+
// try to extract public key from routing key
|
|
186
|
+
const routingMultihash = multihashFromIPNSRoutingKey(routingKey)
|
|
187
|
+
|
|
188
|
+
// identity hash
|
|
189
|
+
if (isCodec(routingMultihash, 0x0)) {
|
|
190
|
+
publicKey = await keychain.loadPublicKeyFromProtobuf(routingMultihash.digest, options)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// otherwise try to load key from message
|
|
194
|
+
if (publicKey == null && message.publicKey != null) {
|
|
195
|
+
publicKey = await keychain.loadPublicKeyFromProtobuf(message.publicKey, options)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (publicKey == null) {
|
|
199
|
+
throw new InvalidEmbeddedPublicKeyError('Could not extract public key from IPNS record or routing key')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (message.value != null && message.signatureV1 != null) {
|
|
203
|
+
// V1+V2
|
|
204
|
+
validateCborDataMatchesPbData(message)
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
value: valueToString(data.Value),
|
|
208
|
+
validityType: IpnsEntry.ValidityType.EOL,
|
|
209
|
+
validity,
|
|
210
|
+
sequence: data.Sequence,
|
|
211
|
+
ttl: data.TTL,
|
|
212
|
+
publicKey,
|
|
213
|
+
signatureV1: message.signatureV1,
|
|
214
|
+
signatureV2: message.signatureV2,
|
|
215
|
+
data: message.data,
|
|
216
|
+
bytes: marshalledRecord
|
|
217
|
+
}
|
|
218
|
+
} else if (message.signatureV2 != null) {
|
|
219
|
+
// V2-only
|
|
220
|
+
return {
|
|
221
|
+
value: valueToString(data.Value),
|
|
222
|
+
validityType: IpnsEntry.ValidityType.EOL,
|
|
223
|
+
validity,
|
|
224
|
+
sequence: data.Sequence,
|
|
225
|
+
ttl: data.TTL,
|
|
226
|
+
publicKey,
|
|
227
|
+
signatureV2: message.signatureV2,
|
|
228
|
+
data: message.data,
|
|
229
|
+
bytes: marshalledRecord
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
throw new Error('invalid record: does not include signatureV1 or signatureV2')
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function multihashToIPNSRoutingKey (digest: MultihashDigest): Uint8Array {
|
|
237
|
+
return uint8ArrayConcat([
|
|
238
|
+
IPNS_PREFIX,
|
|
239
|
+
digest.bytes
|
|
240
|
+
])
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function multihashFromIPNSRoutingKey (key: Uint8Array): MultihashDigest {
|
|
244
|
+
return Digest.decode(key.slice(IPNS_PREFIX.length))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function createCborData (value: string, validityType: IpnsEntry.ValidityType, validity: Uint8Array, sequence: bigint, ttl: bigint): Uint8Array {
|
|
248
|
+
let ValidityType
|
|
249
|
+
|
|
250
|
+
if (validityType === IpnsEntry.ValidityType.EOL) {
|
|
251
|
+
ValidityType = 0
|
|
252
|
+
} else {
|
|
253
|
+
throw new UnsupportedValidityError('The validity type is unsupported')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const data = {
|
|
257
|
+
Value: uint8ArrayFromString(value),
|
|
258
|
+
Validity: validity,
|
|
259
|
+
ValidityType,
|
|
260
|
+
Sequence: sequence,
|
|
261
|
+
TTL: ttl
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return cborg.encode(data)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function parseCborData (buf: Uint8Array): IPNSRecordData {
|
|
268
|
+
const data = cborg.decode(buf)
|
|
269
|
+
|
|
270
|
+
if (data.ValidityType === 0) {
|
|
271
|
+
data.ValidityType = IpnsEntry.ValidityType.EOL
|
|
272
|
+
} else {
|
|
273
|
+
throw new UnsupportedValidityError('The validity type is unsupported')
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (Number.isInteger(data.Sequence)) {
|
|
277
|
+
// sequence must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range
|
|
278
|
+
data.Sequence = BigInt(data.Sequence)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (Number.isInteger(data.TTL)) {
|
|
282
|
+
// ttl must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range
|
|
283
|
+
data.TTL = BigInt(data.TTL)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return data
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Normalizes the given record value. It ensures it is a PeerID, a CID or a
|
|
291
|
+
* string starting with '/'. PeerIDs become `/ipns/${cidV1Libp2pKey}`,
|
|
292
|
+
* CIDs become `/ipfs/${cidAsV1}`.
|
|
293
|
+
*/
|
|
294
|
+
export function normalizeValue (value?: PublicKey | CID | MultihashDigest | string): string {
|
|
295
|
+
if (value != null) {
|
|
296
|
+
if (isPublicKey(value)) {
|
|
297
|
+
return `/ipns/${value.toCID().toV1().toString(base36)}`
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const cid = asCID(value)
|
|
301
|
+
|
|
302
|
+
// if we have a CID, turn it into an ipfs path
|
|
303
|
+
if (cid != null) {
|
|
304
|
+
// PeerID encoded as a CID
|
|
305
|
+
if (cid.code === LIBP2P_KEY_CODEC) {
|
|
306
|
+
return `/ipns/${cid.toV1().toString(base36)}`
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return `/ipfs/${cid.toV1()}`
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (hasBytes(value)) {
|
|
313
|
+
return `/ipns/${base36.encode(value.bytes)}`
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// if we have a path, check it is a valid path
|
|
317
|
+
const string = value.toString().trim()
|
|
318
|
+
|
|
319
|
+
if (string.startsWith('/ipfs/')) {
|
|
320
|
+
const [, name, ...rest] = string.split('/')
|
|
321
|
+
.filter(component => component.trim() !== '')
|
|
322
|
+
|
|
323
|
+
return `/ipfs/${CID.parse(name).toV1()}${rest.length > 0 ? `/${rest.join('/')}` : ''}`
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (string.startsWith('/') && string.length > 1) {
|
|
327
|
+
return string
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
throw new InvalidValueError('Value must be a valid content path starting with /')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function isMultihashDigest (obj: any): obj is MultihashDigest {
|
|
335
|
+
return typeof obj.code === 'number' && obj.digest instanceof Uint8Array && typeof obj.size === 'number' && obj.bytes instanceof Uint8Array
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function normalizeKey (key?: PublicKey | CID<unknown, 0x72> | MultihashDigest | string): { digest: MultihashDigest, path: string } {
|
|
339
|
+
if (key != null) {
|
|
340
|
+
if (isPublicKey(key)) {
|
|
341
|
+
return {
|
|
342
|
+
digest: key.toMultihash(),
|
|
343
|
+
path: '/'
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const cid = asCID(key)
|
|
348
|
+
|
|
349
|
+
// if we have a CID, turn it into an ipfs path
|
|
350
|
+
if (cid != null) {
|
|
351
|
+
// PeerID encoded as a CID
|
|
352
|
+
if (cid.code !== LIBP2P_KEY_CODEC) {
|
|
353
|
+
throw new InvalidValueError('CIDs must have the `libp2p-key` codec')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
digest: cid.multihash,
|
|
358
|
+
path: '/'
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (isMultihashDigest(key)) {
|
|
363
|
+
return {
|
|
364
|
+
digest: key,
|
|
365
|
+
path: '/'
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
key = key.toString()
|
|
370
|
+
|
|
371
|
+
if (key.startsWith('/ipns/')) {
|
|
372
|
+
let [,, name, ...rest] = key.split('/')
|
|
373
|
+
let codec: MultibaseDecoder<any> = base36
|
|
374
|
+
|
|
375
|
+
// base58btc encoded public key hash or protobuf in identity hash
|
|
376
|
+
if (name.startsWith('1') || name.startsWith('Q')) {
|
|
377
|
+
name = `z${name}`
|
|
378
|
+
codec = base58btc
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const buf = codec.decode(name)
|
|
382
|
+
let digest: MultihashDigest
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
digest = CID.decode(buf).multihash
|
|
386
|
+
} catch {
|
|
387
|
+
digest = Digest.decode(buf)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
digest,
|
|
392
|
+
path: `/${rest.join('/')}`
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
throw new InvalidValueError('Value must be a valid IPNS path starting with /')
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function validateCborDataMatchesPbData (entry: IpnsEntry): void {
|
|
401
|
+
if (entry.data == null) {
|
|
402
|
+
throw new InvalidRecordDataError('Record data is missing')
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const data = parseCborData(entry.data)
|
|
406
|
+
|
|
407
|
+
if (!uint8ArrayEquals(data.Value, entry.value ?? new Uint8Array(0))) {
|
|
408
|
+
throw new SignatureVerificationError('Field "value" did not match between protobuf and CBOR')
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!uint8ArrayEquals(data.Validity, entry.validity ?? new Uint8Array(0))) {
|
|
412
|
+
throw new SignatureVerificationError('Field "validity" did not match between protobuf and CBOR')
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (data.ValidityType !== entry.validityType) {
|
|
416
|
+
throw new SignatureVerificationError('Field "validityType" did not match between protobuf and CBOR')
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (data.Sequence !== entry.sequence) {
|
|
420
|
+
throw new SignatureVerificationError('Field "sequence" did not match between protobuf and CBOR')
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (data.TTL !== entry.ttl) {
|
|
424
|
+
throw new SignatureVerificationError('Field "ttl" did not match between protobuf and CBOR')
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function hasBytes (obj?: any): obj is { bytes: Uint8Array } {
|
|
429
|
+
return obj.bytes instanceof Uint8Array
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function hasToCID (obj?: any): obj is { toCID(): CID } {
|
|
433
|
+
return typeof obj?.toCID === 'function'
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function asCID (obj?: any): CID | null {
|
|
437
|
+
if (hasToCID(obj)) {
|
|
438
|
+
return obj.toCID()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// try parsing as a CID string
|
|
442
|
+
try {
|
|
443
|
+
return CID.parse(obj)
|
|
444
|
+
} catch {
|
|
445
|
+
// fall through
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return CID.asCID(obj)
|
|
449
|
+
}
|
package/src/validator.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import NanoDate from 'timestamp-nano'
|
|
2
|
+
import { InvalidEmbeddedPublicKeyError, RecordExpiredError, SignatureVerificationError, UnsupportedValidityError } from './errors.ts'
|
|
3
|
+
import { IpnsEntry } from './pb/ipns.ts'
|
|
4
|
+
import { ipnsRecordDataForV2Sig } from './utils.ts'
|
|
5
|
+
import type { IPNSRecord } from './index.ts'
|
|
6
|
+
import type { AbortOptions } from '@libp2p/interface'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate the given IPNS record against the given routing key.
|
|
10
|
+
*
|
|
11
|
+
* @see https://specs.ipfs.tech/ipns/ipns-record/#routing-record for the binary
|
|
12
|
+
* format of the routing key
|
|
13
|
+
*/
|
|
14
|
+
export async function ipnsValidator (record: IPNSRecord, options?: AbortOptions): Promise<void> {
|
|
15
|
+
if (record.publicKey == null) {
|
|
16
|
+
throw new InvalidEmbeddedPublicKeyError('The record had no public key associated with it')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate Signature V2
|
|
20
|
+
let isValid
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const dataForSignature = ipnsRecordDataForV2Sig(record.data)
|
|
24
|
+
isValid = await record.publicKey.verify(dataForSignature, record.signatureV2, options)
|
|
25
|
+
} catch {
|
|
26
|
+
isValid = false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!isValid) {
|
|
30
|
+
throw new SignatureVerificationError('Record signature verification failed')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Validate according to the validity type
|
|
34
|
+
if (record.validityType === IpnsEntry.ValidityType.EOL) {
|
|
35
|
+
if (NanoDate.fromString(record.validity).toDate().getTime() < Date.now()) {
|
|
36
|
+
throw new RecordExpiredError('record has expired')
|
|
37
|
+
}
|
|
38
|
+
} else if (record.validityType != null) {
|
|
39
|
+
throw new UnsupportedValidityError('The validity type is unsupported')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the number of milliseconds until the record expires.
|
|
45
|
+
* If the record is already expired, returns 0.
|
|
46
|
+
*
|
|
47
|
+
* @param record - The IPNS record to validate.
|
|
48
|
+
* @returns The number of milliseconds until the record expires, or 0 if the record is already expired.
|
|
49
|
+
*/
|
|
50
|
+
export function validFor (record: IPNSRecord): number {
|
|
51
|
+
if (record.validityType !== IpnsEntry.ValidityType.EOL) {
|
|
52
|
+
throw new UnsupportedValidityError()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (record.validity == null) {
|
|
56
|
+
throw new UnsupportedValidityError()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const validUntil = NanoDate.fromString(record.validity).toDate().getTime()
|
|
60
|
+
const now = Date.now()
|
|
61
|
+
|
|
62
|
+
if (validUntil < now) {
|
|
63
|
+
return 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return validUntil - now
|
|
67
|
+
}
|