@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.
Files changed (81) hide show
  1. package/README.md +3 -7
  2. package/dist/index.min.js +16 -5
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/errors.d.ts +0 -8
  5. package/dist/src/errors.d.ts.map +1 -1
  6. package/dist/src/errors.js +0 -8
  7. package/dist/src/errors.js.map +1 -1
  8. package/dist/src/index.d.ts +58 -20
  9. package/dist/src/index.d.ts.map +1 -1
  10. package/dist/src/index.js +11 -11
  11. package/dist/src/index.js.map +1 -1
  12. package/dist/src/ipns/publisher.d.ts.map +1 -1
  13. package/dist/src/ipns/publisher.js +27 -14
  14. package/dist/src/ipns/publisher.js.map +1 -1
  15. package/dist/src/ipns/republisher.d.ts.map +1 -1
  16. package/dist/src/ipns/republisher.js +22 -9
  17. package/dist/src/ipns/republisher.js.map +1 -1
  18. package/dist/src/ipns/resolver.d.ts.map +1 -1
  19. package/dist/src/ipns/resolver.js +39 -17
  20. package/dist/src/ipns/resolver.js.map +1 -1
  21. package/dist/src/ipns.d.ts.map +1 -1
  22. package/dist/src/ipns.js +8 -8
  23. package/dist/src/ipns.js.map +1 -1
  24. package/dist/src/local-store.d.ts +4 -4
  25. package/dist/src/local-store.d.ts.map +1 -1
  26. package/dist/src/local-store.js +2 -1
  27. package/dist/src/local-store.js.map +1 -1
  28. package/dist/src/pb/ipns.d.ts +29 -29
  29. package/dist/src/pb/ipns.d.ts.map +1 -1
  30. package/dist/src/pb/ipns.js +15 -15
  31. package/dist/src/records.d.ts +27 -142
  32. package/dist/src/records.d.ts.map +1 -1
  33. package/dist/src/records.js +39 -46
  34. package/dist/src/records.js.map +1 -1
  35. package/dist/src/routing/helia.d.ts +5 -5
  36. package/dist/src/routing/helia.d.ts.map +1 -1
  37. package/dist/src/routing/helia.js +3 -3
  38. package/dist/src/routing/helia.js.map +1 -1
  39. package/dist/src/routing/index.d.ts +7 -7
  40. package/dist/src/routing/index.d.ts.map +1 -1
  41. package/dist/src/routing/index.js +2 -2
  42. package/dist/src/routing/index.js.map +1 -1
  43. package/dist/src/routing/local-store.d.ts +1 -1
  44. package/dist/src/routing/local-store.d.ts.map +1 -1
  45. package/dist/src/routing/local-store.js +3 -3
  46. package/dist/src/routing/local-store.js.map +1 -1
  47. package/dist/src/routing/pubsub.d.ts +11 -11
  48. package/dist/src/routing/pubsub.d.ts.map +1 -1
  49. package/dist/src/routing/pubsub.js +11 -12
  50. package/dist/src/routing/pubsub.js.map +1 -1
  51. package/dist/src/selector.d.ts +2 -2
  52. package/dist/src/selector.d.ts.map +1 -1
  53. package/dist/src/selector.js +13 -9
  54. package/dist/src/selector.js.map +1 -1
  55. package/dist/src/utils.d.ts +9 -10
  56. package/dist/src/utils.d.ts.map +1 -1
  57. package/dist/src/utils.js +20 -128
  58. package/dist/src/utils.js.map +1 -1
  59. package/dist/src/validator.d.ts +9 -3
  60. package/dist/src/validator.d.ts.map +1 -1
  61. package/dist/src/validator.js +57 -14
  62. package/dist/src/validator.js.map +1 -1
  63. package/package.json +6 -31
  64. package/src/errors.ts +0 -10
  65. package/src/index.ts +75 -25
  66. package/src/ipns/publisher.ts +27 -14
  67. package/src/ipns/republisher.ts +31 -13
  68. package/src/ipns/resolver.ts +39 -21
  69. package/src/ipns.ts +8 -8
  70. package/src/local-store.ts +8 -7
  71. package/src/pb/ipns.proto +1 -1
  72. package/src/pb/ipns.ts +37 -37
  73. package/src/records.ts +57 -210
  74. package/src/routing/helia.ts +6 -6
  75. package/src/routing/index.ts +7 -7
  76. package/src/routing/local-store.ts +6 -6
  77. package/src/routing/pubsub.ts +24 -25
  78. package/src/selector.ts +14 -11
  79. package/src/utils.ts +25 -147
  80. package/src/validator.ts +73 -16
  81. package/dist/typedoc-urls.json +0 -50
@@ -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, unmarshalIPNSRecord } from '../utils.ts'
16
+ import { IPNS_STRING_PREFIX, multihashToIPNSRoutingKey } from '../utils.ts'
17
17
  import { ipnsValidator } from '../validator.ts'
18
- import type { GetOptions, IPNSRouting, PutOptions } from './index.ts'
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 Message {
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 Subscription {
36
+ export interface PubSubSubscription {
37
37
  topic: string
38
38
  subscribe: boolean
39
39
  }
40
40
 
41
- export interface SubscriptionChangeData {
41
+ export interface PubSubSubscriptionChangeData {
42
42
  peerId: PeerId
43
- subscriptions: Subscription[]
43
+ subscriptions: PubSubSubscription[]
44
44
  }
45
45
 
46
46
  export interface PubSubEvents {
47
- 'subscription-change': CustomEvent<SubscriptionChangeData>
48
- message: CustomEvent<Message>
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 PubSubRouting implements IPNSRouting, Startable {
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: Message, options?: AbortOptions): Promise<void> {
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 unmarshalIPNSRecord(routingKey, marshalledRecord, this.keychain, options)
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 unmarshalIPNSRecord(routingKey, marshaledCurrentRecord, this.keychain, options)
247
+ const currentRecord = await ipnsValidator(routingKey, marshaledCurrentRecord, this.keychain, options)
249
248
 
250
- if (uint8ArrayEquals(marshaledCurrentRecord, record.bytes)) {
249
+ if (uint8ArrayEquals(marshaledCurrentRecord, marshalledRecord)) {
251
250
  log.trace('found identical record for %m', routingKey)
252
- return currentRecord.bytes
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 currentRecord.bytes
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: PutOptions = {}): Promise<void> {
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: GetOptions = {}): Promise<Uint8Array> {
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 pubsub (components: PubsubRoutingComponents, init: PubsubRoutingInit = {}): IPNSRouting {
451
- return new PubSubRouting(components, init)
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 { IpnsEntry } from './pb/ipns.ts'
3
- import type { IPNSRecord } from './records.ts'
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: IPNSRecord[]): number {
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. Unmarshal now fails if
24
- // a record does not have SignatureV2, so that is no longer needed. V1-only
25
- // records haven't been issues in a long time.
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 = a.record.sequence
28
- const bSeq = b.record.sequence
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 (a.record.validityType === IpnsEntry.ValidityType.EOL && b.record.validityType === IpnsEntry.ValidityType.EOL) {
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(a.record.validity).toDate()
40
- const recordBValidityDate = NanoDate.fromString(b.record.validity).toDate()
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 { 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'
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 (ipnsRecord: IPNSRecord, created: Date): boolean {
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 (recordExpiry - now < REPUBLISH_THRESHOLD) {
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: string, validityType: IpnsEntry.ValidityType, validity: Uint8Array): Uint8Array {
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
- valueBytes,
102
+ value,
110
103
  validity,
111
104
  validityTypeBuffer
112
- ], valueBytes.byteLength + validity.byteLength + validityTypeBuffer.byteLength)
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 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 {
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 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
- }
140
+ export function encodeExtensibleData (data?: IPNSRecordData): Uint8Array<ArrayBuffer> {
141
+ return withArrayBuffer(dagCbor.encode(data))
142
+ }
255
143
 
256
- const data = {
257
- Value: uint8ArrayFromString(value),
258
- Validity: validity,
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
- return cborg.encode(data)
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 = IpnsEntry.ValidityType.EOL
272
- } else {
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: IpnsEntry): void {
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 { 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'
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 (record: IPNSRecord, options?: AbortOptions): Promise<void> {
15
- if (record.publicKey == null) {
16
- throw new InvalidEmbeddedPublicKeyError('The record had no public key associated with it')
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 record.publicKey.verify(dataForSignature, record.signatureV2, options)
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 (record.validityType === IpnsEntry.ValidityType.EOL) {
35
- if (NanoDate.fromString(record.validity).toDate().getTime() < Date.now()) {
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 (record.validityType != null) {
39
- throw new UnsupportedValidityError('The validity type is unsupported')
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: IPNSRecord): number {
51
- if (record.validityType !== IpnsEntry.ValidityType.EOL) {
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 (record.validity == null) {
112
+ if (data.Validity == null) {
56
113
  throw new UnsupportedValidityError()
57
114
  }
58
115
 
59
- const validUntil = NanoDate.fromString(record.validity).toDate().getTime()
116
+ const validUntil = NanoDate.fromString(uint8ArrayToString(data.Validity)).toDate().getTime()
60
117
  const now = Date.now()
61
118
 
62
119
  if (validUntil < now) {
@@ -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
- }