@helia/ipns 10.0.1 → 10.0.2-ef258e7e
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 +3 -7
- package/dist/index.min.js +16 -5
- package/dist/index.min.js.map +4 -4
- package/dist/src/errors.d.ts +0 -8
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +0 -8
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +58 -20
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +11 -11
- package/dist/src/index.js.map +1 -1
- package/dist/src/ipns/publisher.d.ts.map +1 -1
- package/dist/src/ipns/publisher.js +27 -14
- package/dist/src/ipns/publisher.js.map +1 -1
- package/dist/src/ipns/republisher.d.ts.map +1 -1
- package/dist/src/ipns/republisher.js +22 -9
- package/dist/src/ipns/republisher.js.map +1 -1
- package/dist/src/ipns/resolver.d.ts.map +1 -1
- package/dist/src/ipns/resolver.js +39 -17
- package/dist/src/ipns/resolver.js.map +1 -1
- package/dist/src/ipns.d.ts.map +1 -1
- package/dist/src/ipns.js +8 -8
- package/dist/src/ipns.js.map +1 -1
- package/dist/src/local-store.d.ts +4 -4
- package/dist/src/local-store.d.ts.map +1 -1
- package/dist/src/local-store.js +2 -1
- package/dist/src/local-store.js.map +1 -1
- package/dist/src/pb/ipns.d.ts +29 -29
- package/dist/src/pb/ipns.d.ts.map +1 -1
- package/dist/src/pb/ipns.js +15 -15
- package/dist/src/records.d.ts +27 -142
- package/dist/src/records.d.ts.map +1 -1
- package/dist/src/records.js +39 -46
- package/dist/src/records.js.map +1 -1
- package/dist/src/routing/helia.d.ts +5 -5
- package/dist/src/routing/helia.d.ts.map +1 -1
- package/dist/src/routing/helia.js +3 -3
- package/dist/src/routing/helia.js.map +1 -1
- package/dist/src/routing/index.d.ts +7 -7
- package/dist/src/routing/index.d.ts.map +1 -1
- package/dist/src/routing/index.js +2 -2
- package/dist/src/routing/index.js.map +1 -1
- package/dist/src/routing/local-store.d.ts +1 -1
- package/dist/src/routing/local-store.d.ts.map +1 -1
- package/dist/src/routing/local-store.js +3 -3
- package/dist/src/routing/local-store.js.map +1 -1
- package/dist/src/routing/pubsub.d.ts +11 -11
- package/dist/src/routing/pubsub.d.ts.map +1 -1
- package/dist/src/routing/pubsub.js +11 -12
- package/dist/src/routing/pubsub.js.map +1 -1
- package/dist/src/selector.d.ts +2 -2
- package/dist/src/selector.d.ts.map +1 -1
- package/dist/src/selector.js +13 -9
- package/dist/src/selector.js.map +1 -1
- package/dist/src/utils.d.ts +9 -10
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +20 -128
- package/dist/src/utils.js.map +1 -1
- package/dist/src/validator.d.ts +9 -3
- package/dist/src/validator.d.ts.map +1 -1
- package/dist/src/validator.js +57 -14
- package/dist/src/validator.js.map +1 -1
- package/package.json +6 -31
- package/src/errors.ts +0 -10
- package/src/index.ts +75 -25
- package/src/ipns/publisher.ts +27 -14
- package/src/ipns/republisher.ts +31 -13
- package/src/ipns/resolver.ts +39 -21
- package/src/ipns.ts +8 -8
- package/src/local-store.ts +8 -7
- package/src/pb/ipns.proto +1 -1
- package/src/pb/ipns.ts +37 -37
- package/src/records.ts +57 -210
- package/src/routing/helia.ts +6 -6
- package/src/routing/index.ts +7 -7
- package/src/routing/local-store.ts +6 -6
- package/src/routing/pubsub.ts +24 -25
- package/src/selector.ts +14 -11
- package/src/utils.ts +25 -147
- package/src/validator.ts +73 -16
- package/dist/typedoc-urls.json +0 -50
package/src/routing/pubsub.ts
CHANGED
|
@@ -9,13 +9,13 @@ import { raceSignal } from 'race-signal'
|
|
|
9
9
|
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
|
|
10
10
|
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
11
11
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
12
|
+
import { withArrayBuffer } from 'uint8arrays/with-array-buffer'
|
|
12
13
|
import { InvalidTopicError } from '../errors.ts'
|
|
13
14
|
import { localStore } from '../local-store.ts'
|
|
14
|
-
import { multihashToIPNSRoutingKey } from '../records.ts'
|
|
15
15
|
import { ipnsSelector } from '../selector.ts'
|
|
16
|
-
import { IPNS_STRING_PREFIX,
|
|
16
|
+
import { IPNS_STRING_PREFIX, multihashToIPNSRoutingKey } from '../utils.ts'
|
|
17
17
|
import { ipnsValidator } from '../validator.ts'
|
|
18
|
-
import type {
|
|
18
|
+
import type { IPNSRoutingGetOptions, IPNSRouting, IPNSRoutingPutOptions } from './index.ts'
|
|
19
19
|
import type { LocalStore } from '../local-store.ts'
|
|
20
20
|
import type { Keychain } from '@helia/interface'
|
|
21
21
|
import type { Fetch } from '@libp2p/fetch'
|
|
@@ -26,26 +26,26 @@ import type { ProgressEvent } from 'progress-events'
|
|
|
26
26
|
|
|
27
27
|
const log = logger('helia:ipns:routing:pubsub')
|
|
28
28
|
|
|
29
|
-
export interface
|
|
29
|
+
export interface PubSubMessage {
|
|
30
30
|
type: 'signed' | 'unsigned'
|
|
31
31
|
from: PeerId
|
|
32
32
|
topic: string
|
|
33
33
|
data: Uint8Array
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export interface
|
|
36
|
+
export interface PubSubSubscription {
|
|
37
37
|
topic: string
|
|
38
38
|
subscribe: boolean
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export interface
|
|
41
|
+
export interface PubSubSubscriptionChangeData {
|
|
42
42
|
peerId: PeerId
|
|
43
|
-
subscriptions:
|
|
43
|
+
subscriptions: PubSubSubscription[]
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export interface PubSubEvents {
|
|
47
|
-
'subscription-change': CustomEvent<
|
|
48
|
-
message: CustomEvent<
|
|
47
|
+
'subscription-change': CustomEvent<PubSubSubscriptionChangeData>
|
|
48
|
+
message: CustomEvent<PubSubMessage>
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export interface PublishResult {
|
|
@@ -96,7 +96,7 @@ export type PubSubProgressEvents =
|
|
|
96
96
|
ProgressEvent<'ipns:pubsub:subscribe', { topic: string }> |
|
|
97
97
|
ProgressEvent<'ipns:pubsub:error', Error>
|
|
98
98
|
|
|
99
|
-
export class
|
|
99
|
+
export class PubSubIPNSRouting implements IPNSRouting, Startable {
|
|
100
100
|
private readonly subscriptions: Set<string>
|
|
101
101
|
private readonly localStore: LocalStore
|
|
102
102
|
private readonly libp2p: Pick<Libp2p<{ pubsub: PubSub, fetch?: Fetch }>, 'peerId' | 'register' | 'unregister' | 'services'>
|
|
@@ -196,7 +196,7 @@ export class PubSubRouting implements IPNSRouting, Startable {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
async #processPubSubMessage (message:
|
|
199
|
+
async #processPubSubMessage (message: PubSubMessage, options?: AbortOptions): Promise<void> {
|
|
200
200
|
log('message received for topic', message.topic)
|
|
201
201
|
|
|
202
202
|
if (message.type !== 'signed') {
|
|
@@ -212,7 +212,7 @@ export class PubSubRouting implements IPNSRouting, Startable {
|
|
|
212
212
|
await this.#handleRecord(message.topic, topicToKey(message.topic), message.data, false, options)
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
async #fetchFromPeer (topic: string, routingKey: Uint8Array, peerId: PeerId, options?: AbortOptions): Promise<Uint8Array
|
|
215
|
+
async #fetchFromPeer (topic: string, routingKey: Uint8Array, peerId: PeerId, options?: AbortOptions): Promise<Uint8Array<ArrayBuffer>> {
|
|
216
216
|
const marshalledRecord = await this.fetchQueue.add(async ({ signal }) => {
|
|
217
217
|
log('fetching ipns record for %m from peer %s', routingKey, peerId)
|
|
218
218
|
|
|
@@ -238,18 +238,17 @@ export class PubSubRouting implements IPNSRouting, Startable {
|
|
|
238
238
|
return this.#handleRecord(topic, routingKey, marshalledRecord, true, options)
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
async #handleRecord (topic: string, routingKey: Uint8Array, marshalledRecord: Uint8Array, publish: boolean, options?: AbortOptions): Promise<Uint8Array
|
|
242
|
-
const record = await
|
|
243
|
-
await ipnsValidator(record, options)
|
|
241
|
+
async #handleRecord (topic: string, routingKey: Uint8Array, marshalledRecord: Uint8Array, publish: boolean, options?: AbortOptions): Promise<Uint8Array<ArrayBuffer>> {
|
|
242
|
+
const record = await ipnsValidator(routingKey, marshalledRecord, this.keychain, options)
|
|
244
243
|
this.shutdownController.signal.throwIfAborted()
|
|
245
244
|
|
|
246
245
|
if (await this.localStore.has(routingKey)) {
|
|
247
246
|
const { record: marshaledCurrentRecord } = await this.localStore.get(routingKey, options)
|
|
248
|
-
const currentRecord = await
|
|
247
|
+
const currentRecord = await ipnsValidator(routingKey, marshaledCurrentRecord, this.keychain, options)
|
|
249
248
|
|
|
250
|
-
if (uint8ArrayEquals(marshaledCurrentRecord,
|
|
249
|
+
if (uint8ArrayEquals(marshaledCurrentRecord, marshalledRecord)) {
|
|
251
250
|
log.trace('found identical record for %m', routingKey)
|
|
252
|
-
return
|
|
251
|
+
return marshaledCurrentRecord
|
|
253
252
|
}
|
|
254
253
|
|
|
255
254
|
const records = [currentRecord, record]
|
|
@@ -257,7 +256,7 @@ export class PubSubRouting implements IPNSRouting, Startable {
|
|
|
257
256
|
|
|
258
257
|
if (index === 0) {
|
|
259
258
|
log.trace('found old record for %m', routingKey)
|
|
260
|
-
return
|
|
259
|
+
return marshaledCurrentRecord
|
|
261
260
|
}
|
|
262
261
|
}
|
|
263
262
|
|
|
@@ -276,13 +275,13 @@ export class PubSubRouting implements IPNSRouting, Startable {
|
|
|
276
275
|
}
|
|
277
276
|
}
|
|
278
277
|
|
|
279
|
-
return marshalledRecord
|
|
278
|
+
return withArrayBuffer(marshalledRecord)
|
|
280
279
|
}
|
|
281
280
|
|
|
282
281
|
/**
|
|
283
282
|
* Put a value to the pubsub datastore indexed by the received key properly encoded
|
|
284
283
|
*/
|
|
285
|
-
async put (routingKey: Uint8Array, marshaledRecord: Uint8Array, options:
|
|
284
|
+
async put (routingKey: Uint8Array, marshaledRecord: Uint8Array, options: IPNSRoutingPutOptions = {}): Promise<void> {
|
|
286
285
|
try {
|
|
287
286
|
const topic = keyToTopic(routingKey)
|
|
288
287
|
|
|
@@ -303,7 +302,7 @@ export class PubSubRouting implements IPNSRouting, Startable {
|
|
|
303
302
|
* Also, the identifier topic is subscribed to and the pubsub datastore records will be
|
|
304
303
|
* updated once new publishes occur
|
|
305
304
|
*/
|
|
306
|
-
async get (routingKey: Uint8Array, options:
|
|
305
|
+
async get (routingKey: Uint8Array, options: IPNSRoutingGetOptions = {}): Promise<Uint8Array<ArrayBuffer>> {
|
|
307
306
|
const topic = keyToTopic(routingKey)
|
|
308
307
|
|
|
309
308
|
try {
|
|
@@ -324,7 +323,7 @@ export class PubSubRouting implements IPNSRouting, Startable {
|
|
|
324
323
|
await raceSignal(delay(this.fetchDelay), this.shutdownController.signal)
|
|
325
324
|
|
|
326
325
|
const fetchController = new AbortController()
|
|
327
|
-
const promises: Array<Promise<Uint8Array
|
|
326
|
+
const promises: Array<Promise<Uint8Array<ArrayBuffer>>> = []
|
|
328
327
|
|
|
329
328
|
for (const peerId of this.libp2p.services.pubsub.getSubscribers(topic)) {
|
|
330
329
|
const signal = anySignal([
|
|
@@ -447,6 +446,6 @@ function topicToKey (topic: string): Uint8Array {
|
|
|
447
446
|
* updated records, so the first call to `.get` should be expected
|
|
448
447
|
* to fail!
|
|
449
448
|
*/
|
|
450
|
-
export function
|
|
451
|
-
return new
|
|
449
|
+
export function pubSubIPNSRouting (components: PubsubRoutingComponents, init: PubsubRoutingInit = {}): IPNSRouting {
|
|
450
|
+
return new PubSubIPNSRouting(components, init)
|
|
452
451
|
}
|
package/src/selector.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import NanoDate from 'timestamp-nano'
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
3
|
+
import { IPNSEntry } from './pb/ipns.ts'
|
|
4
|
+
import { decodeExtensibleData } from './utils.ts'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Selects the latest valid IPNS record from an array of marshalled IPNS records.
|
|
@@ -13,19 +14,21 @@ import type { IPNSRecord } from './records.ts'
|
|
|
13
14
|
* @param data - Array of marshalled IPNS records to select from
|
|
14
15
|
* @returns The index of the most valid record from the input array
|
|
15
16
|
*/
|
|
16
|
-
export function ipnsSelector (key: Uint8Array, data:
|
|
17
|
+
export function ipnsSelector (key: Uint8Array, data: IPNSEntry[]): number {
|
|
17
18
|
const entries = data.map((record, index) => ({
|
|
18
19
|
record,
|
|
19
20
|
index
|
|
20
21
|
}))
|
|
21
22
|
|
|
22
23
|
entries.sort((a, b) => {
|
|
23
|
-
// Before we'd sort based on the signature version.
|
|
24
|
-
//
|
|
25
|
-
// records
|
|
24
|
+
// Before we'd sort based on the signature version. Validation fails if a
|
|
25
|
+
// record does not have SignatureV2, so that is no longer needed. V1-only
|
|
26
|
+
// records will have all expired by this point.
|
|
27
|
+
const aData = decodeExtensibleData(a.record.data)
|
|
28
|
+
const bData = decodeExtensibleData(b.record.data)
|
|
26
29
|
|
|
27
|
-
const aSeq =
|
|
28
|
-
const bSeq =
|
|
30
|
+
const aSeq = aData.Sequence
|
|
31
|
+
const bSeq = bData.Sequence
|
|
29
32
|
|
|
30
33
|
// choose later sequence number
|
|
31
34
|
if (aSeq > bSeq) {
|
|
@@ -34,10 +37,10 @@ export function ipnsSelector (key: Uint8Array, data: IPNSRecord[]): number {
|
|
|
34
37
|
return 1
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
if (
|
|
40
|
+
if (aData.ValidityType === IPNSEntry.ValidityType.EOL && bData.ValidityType === IPNSEntry.ValidityType.EOL) {
|
|
38
41
|
// choose longer lived record if sequence numbers the same
|
|
39
|
-
const recordAValidityDate = NanoDate.fromString(
|
|
40
|
-
const recordBValidityDate = NanoDate.fromString(
|
|
42
|
+
const recordAValidityDate = NanoDate.fromString(uint8ArrayToString(aData.Validity)).toDate()
|
|
43
|
+
const recordBValidityDate = NanoDate.fromString(uint8ArrayToString(bData.Validity)).toDate()
|
|
41
44
|
|
|
42
45
|
if (recordAValidityDate.getTime() > recordBValidityDate.getTime()) {
|
|
43
46
|
return -1
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isPublicKey } from '@helia/interface'
|
|
2
|
+
import * as dagCbor from '@ipld/dag-cbor'
|
|
2
3
|
import { InvalidParametersError } from '@libp2p/interface'
|
|
3
|
-
import * as cborg from 'cborg'
|
|
4
4
|
import { Key } from 'interface-datastore'
|
|
5
5
|
import { base36 } from 'multiformats/bases/base36'
|
|
6
6
|
import { base58btc } from 'multiformats/bases/base58'
|
|
@@ -10,12 +10,12 @@ import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
|
|
|
10
10
|
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
|
|
11
11
|
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
|
|
12
12
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
13
|
+
import { withArrayBuffer } from 'uint8arrays/with-array-buffer'
|
|
13
14
|
import { DHT_EXPIRY_MS, REPUBLISH_THRESHOLD } from './constants.ts'
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import type {
|
|
17
|
-
import type { PublicKey
|
|
18
|
-
import type { AbortOptions } from '@libp2p/interface'
|
|
15
|
+
import { InvalidRecordDataError, InvalidValueError, SignatureVerificationError, UnsupportedValidityError } from './errors.ts'
|
|
16
|
+
import { IPNSEntry } from './pb/ipns.ts'
|
|
17
|
+
import type { IPNSRecordData } from './index.ts'
|
|
18
|
+
import type { PublicKey } from '@helia/interface'
|
|
19
19
|
import type { MultibaseDecoder } from 'multiformats/cid'
|
|
20
20
|
import type { MultihashDigest } from 'multiformats/hashes/interface'
|
|
21
21
|
|
|
@@ -23,11 +23,6 @@ export const LIBP2P_KEY_CODEC = 0x72
|
|
|
23
23
|
export const IDENTITY_CODEC = 0x0
|
|
24
24
|
export const SHA2_256_CODEC = 0x12
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
* Limit valid IPNS record sizes to 10kb
|
|
28
|
-
*/
|
|
29
|
-
const MAX_RECORD_SIZE = 1024 * 10
|
|
30
|
-
|
|
31
26
|
const IPNS_PREFIX = uint8ArrayFromString('/ipns/')
|
|
32
27
|
export const IPNS_STRING_PREFIX = '/ipns/'
|
|
33
28
|
|
|
@@ -60,10 +55,9 @@ export function ipnsMetadataKey (key: Uint8Array): Key {
|
|
|
60
55
|
return new Key(IPNS_METADATA_PREFIX + uint8ArrayToString(key, 'base32'), false)
|
|
61
56
|
}
|
|
62
57
|
|
|
63
|
-
export function shouldRepublish (
|
|
58
|
+
export function shouldRepublish (created: Date, expiry: Date): boolean {
|
|
64
59
|
const now = Date.now()
|
|
65
60
|
const dhtExpiry = created.getTime() + DHT_EXPIRY_MS
|
|
66
|
-
const recordExpiry = new Date(ipnsRecord.validity).getTime()
|
|
67
61
|
|
|
68
62
|
// If the DHT expiry is within the threshold, republish it
|
|
69
63
|
if (dhtExpiry - now < REPUBLISH_THRESHOLD) {
|
|
@@ -71,7 +65,7 @@ export function shouldRepublish (ipnsRecord: IPNSRecord, created: Date): boolean
|
|
|
71
65
|
}
|
|
72
66
|
|
|
73
67
|
// If the record expiry (based on validity/lifetime) is within the threshold, republish it
|
|
74
|
-
if (
|
|
68
|
+
if (expiry.getTime() - now < REPUBLISH_THRESHOLD) {
|
|
75
69
|
return true
|
|
76
70
|
}
|
|
77
71
|
|
|
@@ -101,15 +95,14 @@ export function isLibp2pCID (obj?: any): obj is CID<unknown, 0x72, 0x00 | 0x12,
|
|
|
101
95
|
/**
|
|
102
96
|
* Utility for creating the record data for being signed
|
|
103
97
|
*/
|
|
104
|
-
export function ipnsRecordDataForV1Sig (value:
|
|
98
|
+
export function ipnsRecordDataForV1Sig (value: Uint8Array, validityType: IPNSEntry.ValidityType, validity: Uint8Array): Uint8Array {
|
|
105
99
|
const validityTypeBuffer = uint8ArrayFromString(validityType)
|
|
106
|
-
const valueBytes = uint8ArrayFromString(value)
|
|
107
100
|
|
|
108
101
|
return uint8ArrayConcat([
|
|
109
|
-
|
|
102
|
+
value,
|
|
110
103
|
validity,
|
|
111
104
|
validityTypeBuffer
|
|
112
|
-
],
|
|
105
|
+
], value.byteLength + validity.byteLength + validityTypeBuffer.byteLength)
|
|
113
106
|
}
|
|
114
107
|
|
|
115
108
|
/**
|
|
@@ -121,37 +114,7 @@ export function ipnsRecordDataForV2Sig (data: Uint8Array): Uint8Array {
|
|
|
121
114
|
return uint8ArrayConcat([entryData, data])
|
|
122
115
|
}
|
|
123
116
|
|
|
124
|
-
export function
|
|
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 {
|
|
117
|
+
export function ipnsRecordValueToString (value: Uint8Array): string {
|
|
155
118
|
// handle legacy case where record value is raw CID bytes
|
|
156
119
|
try {
|
|
157
120
|
const cid = CID.decode(value)
|
|
@@ -163,76 +126,6 @@ function valueToString (value: Uint8Array): string {
|
|
|
163
126
|
return uint8ArrayToString(value)
|
|
164
127
|
}
|
|
165
128
|
|
|
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
129
|
export function multihashToIPNSRoutingKey (digest: MultihashDigest): Uint8Array {
|
|
237
130
|
return uint8ArrayConcat([
|
|
238
131
|
IPNS_PREFIX,
|
|
@@ -244,32 +137,23 @@ export function multihashFromIPNSRoutingKey (key: Uint8Array): MultihashDigest {
|
|
|
244
137
|
return Digest.decode(key.slice(IPNS_PREFIX.length))
|
|
245
138
|
}
|
|
246
139
|
|
|
247
|
-
export function
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (validityType === IpnsEntry.ValidityType.EOL) {
|
|
251
|
-
ValidityType = 0
|
|
252
|
-
} else {
|
|
253
|
-
throw new UnsupportedValidityError('The validity type is unsupported')
|
|
254
|
-
}
|
|
140
|
+
export function encodeExtensibleData (data?: IPNSRecordData): Uint8Array<ArrayBuffer> {
|
|
141
|
+
return withArrayBuffer(dagCbor.encode(data))
|
|
142
|
+
}
|
|
255
143
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
ValidityType,
|
|
260
|
-
Sequence: sequence,
|
|
261
|
-
TTL: ttl
|
|
144
|
+
export function decodeExtensibleData (buf?: Uint8Array): IPNSRecordData {
|
|
145
|
+
if (buf == null) {
|
|
146
|
+
throw new InvalidRecordDataError('Record data is missing')
|
|
262
147
|
}
|
|
263
148
|
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export function parseCborData (buf: Uint8Array): IPNSRecordData {
|
|
268
|
-
const data = cborg.decode(buf)
|
|
149
|
+
const data = dagCbor.decode<IPNSRecordData>(buf)
|
|
269
150
|
|
|
151
|
+
// @ts-expect-error TODO: remove typescript enums
|
|
270
152
|
if (data.ValidityType === 0) {
|
|
271
|
-
data.ValidityType =
|
|
272
|
-
}
|
|
153
|
+
data.ValidityType = IPNSEntry.ValidityType.EOL
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (data.ValidityType !== IPNSEntry.ValidityType.EOL) {
|
|
273
157
|
throw new UnsupportedValidityError('The validity type is unsupported')
|
|
274
158
|
}
|
|
275
159
|
|
|
@@ -397,13 +281,7 @@ export function normalizeKey (key?: PublicKey | CID | MultihashDigest | string):
|
|
|
397
281
|
throw new InvalidValueError('Value must be a valid IPNS path starting with /')
|
|
398
282
|
}
|
|
399
283
|
|
|
400
|
-
function validateCborDataMatchesPbData (entry:
|
|
401
|
-
if (entry.data == null) {
|
|
402
|
-
throw new InvalidRecordDataError('Record data is missing')
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const data = parseCborData(entry.data)
|
|
406
|
-
|
|
284
|
+
export function validateCborDataMatchesPbData (entry: IPNSEntry, data: IPNSRecordData): void {
|
|
407
285
|
if (!uint8ArrayEquals(data.Value, entry.value ?? new Uint8Array(0))) {
|
|
408
286
|
throw new SignatureVerificationError('Field "value" did not match between protobuf and CBOR')
|
|
409
287
|
}
|
package/src/validator.ts
CHANGED
|
@@ -1,27 +1,75 @@
|
|
|
1
1
|
import NanoDate from 'timestamp-nano'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
2
|
+
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
3
|
+
import { InvalidEmbeddedPublicKeyError, RecordExpiredError, RecordTooLargeError, SignatureVerificationError, UnsupportedValidityError } from './errors.ts'
|
|
4
|
+
import { IPNSEntry } from './pb/ipns.ts'
|
|
5
|
+
import { decodeExtensibleData, ipnsRecordDataForV2Sig, isCodec, multihashFromIPNSRoutingKey, validateCborDataMatchesPbData } from './utils.ts'
|
|
6
|
+
import type { PublicKey } from '@ipshipyard/crypto'
|
|
7
|
+
import type { Keychain } from '@ipshipyard/keychain'
|
|
6
8
|
import type { AbortOptions } from '@libp2p/interface'
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Limit valid IPNS record sizes to 10kb
|
|
12
|
+
*/
|
|
13
|
+
const MAX_RECORD_SIZE = 1024 * 10
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
16
|
* Validate the given IPNS record against the given routing key.
|
|
10
17
|
*
|
|
18
|
+
* Ensure that
|
|
19
|
+
* - SignatureV2 and Data are present
|
|
20
|
+
* - ValidityType and Validity are of valid types and have a value
|
|
21
|
+
* - CBOR data matches protobuf if it's a V1+V2 record
|
|
22
|
+
*
|
|
11
23
|
* @see https://specs.ipfs.tech/ipns/ipns-record/#routing-record for the binary
|
|
12
24
|
* format of the routing key
|
|
13
25
|
*/
|
|
14
|
-
export async function ipnsValidator (
|
|
15
|
-
if (
|
|
16
|
-
throw new
|
|
26
|
+
export async function ipnsValidator (routingKey: Uint8Array, marshalledRecord: Uint8Array, keychain: Keychain, options?: AbortOptions): Promise<IPNSEntry> {
|
|
27
|
+
if (marshalledRecord.byteLength > MAX_RECORD_SIZE) {
|
|
28
|
+
throw new RecordTooLargeError('The record is too large')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const record = IPNSEntry.decode(marshalledRecord)
|
|
32
|
+
|
|
33
|
+
// Check if we have the data field. If we don't, we fail. We've been producing
|
|
34
|
+
// V1+V2 records for quite a while and we don't support V1-only records during
|
|
35
|
+
// validation any more
|
|
36
|
+
if (record.signatureV2 == null) {
|
|
37
|
+
throw new SignatureVerificationError('Missing signatureV2')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const data = decodeExtensibleData(record.data)
|
|
41
|
+
const validity = uint8ArrayToString(data.Validity)
|
|
42
|
+
|
|
43
|
+
let publicKey: PublicKey | undefined
|
|
44
|
+
|
|
45
|
+
// try to extract public key from routing key
|
|
46
|
+
const routingMultihash = multihashFromIPNSRoutingKey(routingKey)
|
|
47
|
+
|
|
48
|
+
// identity hash
|
|
49
|
+
if (isCodec(routingMultihash, 0x0)) {
|
|
50
|
+
publicKey = await keychain.loadPublicKeyFromProtobuf(routingMultihash.digest, options)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// otherwise try to load key from message
|
|
54
|
+
if (publicKey == null && record.publicKey != null) {
|
|
55
|
+
publicKey = await keychain.loadPublicKeyFromProtobuf(record.publicKey, options)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (publicKey == null) {
|
|
59
|
+
throw new InvalidEmbeddedPublicKeyError('Could not extract public key from IPNS record or routing key')
|
|
17
60
|
}
|
|
18
61
|
|
|
19
62
|
// Validate Signature V2
|
|
20
63
|
let isValid
|
|
21
64
|
|
|
22
65
|
try {
|
|
66
|
+
if (record.data == null) {
|
|
67
|
+
// n.b. decodeExtensibleData would have thrown if record data was missing
|
|
68
|
+
throw new Error('Missing data')
|
|
69
|
+
}
|
|
70
|
+
|
|
23
71
|
const dataForSignature = ipnsRecordDataForV2Sig(record.data)
|
|
24
|
-
isValid = await
|
|
72
|
+
isValid = await publicKey.verify(dataForSignature, record.signatureV2, options)
|
|
25
73
|
} catch {
|
|
26
74
|
isValid = false
|
|
27
75
|
}
|
|
@@ -31,13 +79,20 @@ export async function ipnsValidator (record: IPNSRecord, options?: AbortOptions)
|
|
|
31
79
|
}
|
|
32
80
|
|
|
33
81
|
// Validate according to the validity type
|
|
34
|
-
if (
|
|
35
|
-
if (NanoDate.fromString(
|
|
82
|
+
if (data.ValidityType === IPNSEntry.ValidityType.EOL) {
|
|
83
|
+
if (NanoDate.fromString(validity).toDate().getTime() < Date.now()) {
|
|
36
84
|
throw new RecordExpiredError('record has expired')
|
|
37
85
|
}
|
|
38
|
-
} else if (
|
|
39
|
-
throw new UnsupportedValidityError(
|
|
86
|
+
} else if (data.ValidityType != null) {
|
|
87
|
+
throw new UnsupportedValidityError(`The validity type ${data.ValidityType} is unsupported`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (record.value != null && record.signatureV1 != null) {
|
|
91
|
+
// V1+V2
|
|
92
|
+
validateCborDataMatchesPbData(record, data)
|
|
40
93
|
}
|
|
94
|
+
|
|
95
|
+
return record
|
|
41
96
|
}
|
|
42
97
|
|
|
43
98
|
/**
|
|
@@ -47,16 +102,18 @@ export async function ipnsValidator (record: IPNSRecord, options?: AbortOptions)
|
|
|
47
102
|
* @param record - The IPNS record to validate.
|
|
48
103
|
* @returns The number of milliseconds until the record expires, or 0 if the record is already expired.
|
|
49
104
|
*/
|
|
50
|
-
export function validFor (record:
|
|
51
|
-
|
|
105
|
+
export function validFor (record: IPNSEntry): number {
|
|
106
|
+
const data = decodeExtensibleData(record.data)
|
|
107
|
+
|
|
108
|
+
if (data.ValidityType !== IPNSEntry.ValidityType.EOL) {
|
|
52
109
|
throw new UnsupportedValidityError()
|
|
53
110
|
}
|
|
54
111
|
|
|
55
|
-
if (
|
|
112
|
+
if (data.Validity == null) {
|
|
56
113
|
throw new UnsupportedValidityError()
|
|
57
114
|
}
|
|
58
115
|
|
|
59
|
-
const validUntil = NanoDate.fromString(
|
|
116
|
+
const validUntil = NanoDate.fromString(uint8ArrayToString(data.Validity)).toDate().getTime()
|
|
60
117
|
const now = Date.now()
|
|
61
118
|
|
|
62
119
|
if (validUntil < now) {
|
package/dist/typedoc-urls.json
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"IPNS": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNS.html",
|
|
3
|
-
".:IPNS": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNS.html",
|
|
4
|
-
"IPNSComponents": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSComponents.html",
|
|
5
|
-
".:IPNSComponents": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSComponents.html",
|
|
6
|
-
"IPNSOptions": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSOptions.html",
|
|
7
|
-
".:IPNSOptions": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSOptions.html",
|
|
8
|
-
"IPNSPublishResult": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSPublishResult.html",
|
|
9
|
-
".:IPNSPublishResult": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSPublishResult.html",
|
|
10
|
-
"IPNSRecordMetadata": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSRecordMetadata.html",
|
|
11
|
-
".:IPNSRecordMetadata": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSRecordMetadata.html",
|
|
12
|
-
"IPNSResolveOptions": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSResolveOptions.html",
|
|
13
|
-
".:IPNSResolveOptions": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSResolveOptions.html",
|
|
14
|
-
"IPNSResolver": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSResolver.html",
|
|
15
|
-
".:IPNSResolver": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSResolver.html",
|
|
16
|
-
"IPNSResolveResult": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSResolveResult.html",
|
|
17
|
-
".:IPNSResolveResult": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSResolveResult.html",
|
|
18
|
-
"IPNSResolverOptions": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSResolverOptions.html",
|
|
19
|
-
".:IPNSResolverOptions": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.IPNSResolverOptions.html",
|
|
20
|
-
"PublishOptions": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.PublishOptions.html",
|
|
21
|
-
".:PublishOptions": "https://ipfs.github.io/helia/interfaces/_helia_ipns.index.PublishOptions.html",
|
|
22
|
-
"DatastoreProgressEvents": "https://ipfs.github.io/helia/types/_helia_ipns.index.DatastoreProgressEvents.html",
|
|
23
|
-
".:DatastoreProgressEvents": "https://ipfs.github.io/helia/types/_helia_ipns.index.DatastoreProgressEvents.html",
|
|
24
|
-
"IPNSRecord": "https://ipfs.github.io/helia/types/_helia_ipns.index.IPNSRecord.html",
|
|
25
|
-
"PublishProgressEvents": "https://ipfs.github.io/helia/types/_helia_ipns.index.PublishProgressEvents.html",
|
|
26
|
-
".:PublishProgressEvents": "https://ipfs.github.io/helia/types/_helia_ipns.index.PublishProgressEvents.html",
|
|
27
|
-
"ResolveProgressEvents": "https://ipfs.github.io/helia/types/_helia_ipns.index.ResolveProgressEvents.html",
|
|
28
|
-
".:ResolveProgressEvents": "https://ipfs.github.io/helia/types/_helia_ipns.index.ResolveProgressEvents.html",
|
|
29
|
-
"ipns": "https://ipfs.github.io/helia/functions/_helia_ipns.index.ipns.html",
|
|
30
|
-
".:ipns": "https://ipfs.github.io/helia/functions/_helia_ipns.index.ipns.html",
|
|
31
|
-
"ipnsResolver": "https://ipfs.github.io/helia/functions/_helia_ipns.index.ipnsResolver.html",
|
|
32
|
-
".:ipnsResolver": "https://ipfs.github.io/helia/functions/_helia_ipns.index.ipnsResolver.html",
|
|
33
|
-
"GetOptions": "https://ipfs.github.io/helia/interfaces/_helia_ipns.routing.GetOptions.html",
|
|
34
|
-
"./routing:GetOptions": "https://ipfs.github.io/helia/interfaces/_helia_ipns.routing.GetOptions.html",
|
|
35
|
-
"IPNSRouting": "https://ipfs.github.io/helia/interfaces/_helia_ipns.routing.IPNSRouting.html",
|
|
36
|
-
"./routing:IPNSRouting": "https://ipfs.github.io/helia/interfaces/_helia_ipns.routing.IPNSRouting.html",
|
|
37
|
-
"Message": "https://ipfs.github.io/helia/interfaces/_helia_ipns.routing.Message.html",
|
|
38
|
-
"PublishResult": "https://ipfs.github.io/helia/interfaces/_helia_ipns.routing.PublishResult.html",
|
|
39
|
-
"PubSub": "https://ipfs.github.io/helia/interfaces/_helia_ipns.routing.PubSub.html",
|
|
40
|
-
"PubSubEvents": "https://ipfs.github.io/helia/interfaces/_helia_ipns.routing.PubSubEvents.html",
|
|
41
|
-
"PubsubRoutingComponents": "https://ipfs.github.io/helia/interfaces/_helia_ipns.routing.PubsubRoutingComponents.html",
|
|
42
|
-
"PutOptions": "https://ipfs.github.io/helia/interfaces/_helia_ipns.routing.PutOptions.html",
|
|
43
|
-
"./routing:PutOptions": "https://ipfs.github.io/helia/interfaces/_helia_ipns.routing.PutOptions.html",
|
|
44
|
-
"HeliaRoutingProgressEvents": "https://ipfs.github.io/helia/types/_helia_ipns.routing.HeliaRoutingProgressEvents.html",
|
|
45
|
-
"IPNSRoutingProgressEvents": "https://ipfs.github.io/helia/types/_helia_ipns.routing.IPNSRoutingProgressEvents.html",
|
|
46
|
-
"./routing:IPNSRoutingProgressEvents": "https://ipfs.github.io/helia/types/_helia_ipns.routing.IPNSRoutingProgressEvents.html",
|
|
47
|
-
"PubSubProgressEvents": "https://ipfs.github.io/helia/types/_helia_ipns.routing.PubSubProgressEvents.html",
|
|
48
|
-
"helia": "https://ipfs.github.io/helia/functions/_helia_ipns.routing.helia.html",
|
|
49
|
-
"pubsub": "https://ipfs.github.io/helia/functions/_helia_ipns.routing.pubsub.html"
|
|
50
|
-
}
|