@helia/ipns 9.2.0 → 9.2.1-1361bfa5

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 (69) hide show
  1. package/README.md +21 -57
  2. package/dist/index.min.js +9 -23
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/errors.d.ts +33 -5
  5. package/dist/src/errors.d.ts.map +1 -1
  6. package/dist/src/errors.js +33 -20
  7. package/dist/src/errors.js.map +1 -1
  8. package/dist/src/index.d.ts +62 -99
  9. package/dist/src/index.d.ts.map +1 -1
  10. package/dist/src/index.js +21 -60
  11. package/dist/src/index.js.map +1 -1
  12. package/dist/src/ipns/publisher.d.ts +5 -9
  13. package/dist/src/ipns/publisher.d.ts.map +1 -1
  14. package/dist/src/ipns/publisher.js +30 -22
  15. package/dist/src/ipns/publisher.js.map +1 -1
  16. package/dist/src/ipns/republisher.d.ts +3 -5
  17. package/dist/src/ipns/republisher.d.ts.map +1 -1
  18. package/dist/src/ipns/republisher.js +18 -9
  19. package/dist/src/ipns/republisher.js.map +1 -1
  20. package/dist/src/ipns/resolver.d.ts +6 -5
  21. package/dist/src/ipns/resolver.d.ts.map +1 -1
  22. package/dist/src/ipns/resolver.js +32 -78
  23. package/dist/src/ipns/resolver.js.map +1 -1
  24. package/dist/src/ipns.d.ts +6 -4
  25. package/dist/src/ipns.d.ts.map +1 -1
  26. package/dist/src/ipns.js +33 -4
  27. package/dist/src/ipns.js.map +1 -1
  28. package/dist/src/pb/ipns.d.ts +62 -0
  29. package/dist/src/pb/ipns.d.ts.map +1 -0
  30. package/dist/src/pb/ipns.js +203 -0
  31. package/dist/src/pb/ipns.js.map +1 -0
  32. package/dist/src/pb/metadata.d.ts +1 -1
  33. package/dist/src/pb/metadata.d.ts.map +1 -1
  34. package/dist/src/records.d.ts +155 -0
  35. package/dist/src/records.d.ts.map +1 -0
  36. package/dist/src/records.js +88 -0
  37. package/dist/src/records.js.map +1 -0
  38. package/dist/src/routing/pubsub.d.ts +3 -0
  39. package/dist/src/routing/pubsub.d.ts.map +1 -1
  40. package/dist/src/routing/pubsub.js +15 -10
  41. package/dist/src/routing/pubsub.js.map +1 -1
  42. package/dist/src/selector.d.ts +14 -0
  43. package/dist/src/selector.d.ts.map +1 -0
  44. package/dist/src/selector.js +47 -0
  45. package/dist/src/selector.js.map +1 -0
  46. package/dist/src/utils.d.ts +29 -2
  47. package/dist/src/utils.d.ts.map +1 -1
  48. package/dist/src/utils.js +308 -0
  49. package/dist/src/utils.js.map +1 -1
  50. package/dist/src/validator.d.ts +18 -0
  51. package/dist/src/validator.d.ts.map +1 -0
  52. package/dist/src/validator.js +58 -0
  53. package/dist/src/validator.js.map +1 -0
  54. package/package.json +32 -28
  55. package/src/errors.ts +40 -25
  56. package/src/index.ts +63 -100
  57. package/src/ipns/publisher.ts +34 -29
  58. package/src/ipns/republisher.ts +24 -13
  59. package/src/ipns/resolver.ts +40 -88
  60. package/src/ipns.ts +44 -7
  61. package/src/pb/ipns.proto +39 -0
  62. package/src/pb/ipns.ts +280 -0
  63. package/src/pb/metadata.ts +1 -1
  64. package/src/records.ts +273 -0
  65. package/src/routing/pubsub.ts +17 -10
  66. package/src/selector.ts +55 -0
  67. package/src/utils.ts +371 -2
  68. package/src/validator.ts +67 -0
  69. package/dist/typedoc-urls.json +0 -51
package/src/pb/ipns.ts ADDED
@@ -0,0 +1,280 @@
1
+ import { decodeMessage, encodeMessage, enumeration, message, streamMessage } from 'protons-runtime'
2
+ import type { Codec, DecodeOptions } from 'protons-runtime'
3
+ import type { Uint8ArrayList } from 'uint8arraylist'
4
+
5
+ export interface IpnsEntry {
6
+ value?: Uint8Array
7
+ signatureV1?: Uint8Array
8
+ validityType?: IpnsEntry.ValidityType
9
+ validity?: Uint8Array
10
+ sequence?: bigint
11
+ ttl?: bigint
12
+ publicKey?: Uint8Array
13
+ signatureV2?: Uint8Array
14
+ data?: Uint8Array
15
+ }
16
+
17
+ export namespace IpnsEntry {
18
+ export enum ValidityType {
19
+ EOL = 'EOL'
20
+ }
21
+
22
+ enum __ValidityTypeValues {
23
+ EOL = 0
24
+ }
25
+
26
+ export namespace ValidityType {
27
+ export const codec = (): Codec<ValidityType> => {
28
+ return enumeration<ValidityType>(__ValidityTypeValues)
29
+ }
30
+ }
31
+
32
+ let _codec: Codec<IpnsEntry>
33
+
34
+ export const codec = (): Codec<IpnsEntry> => {
35
+ if (_codec == null) {
36
+ _codec = message<IpnsEntry>((obj, w, opts = {}) => {
37
+ if (opts.lengthDelimited !== false) {
38
+ w.fork()
39
+ }
40
+
41
+ if (obj.value != null) {
42
+ w.uint32(10)
43
+ w.bytes(obj.value)
44
+ }
45
+
46
+ if (obj.signatureV1 != null) {
47
+ w.uint32(18)
48
+ w.bytes(obj.signatureV1)
49
+ }
50
+
51
+ if (obj.validityType != null) {
52
+ w.uint32(24)
53
+ IpnsEntry.ValidityType.codec().encode(obj.validityType, w)
54
+ }
55
+
56
+ if (obj.validity != null) {
57
+ w.uint32(34)
58
+ w.bytes(obj.validity)
59
+ }
60
+
61
+ if (obj.sequence != null) {
62
+ w.uint32(40)
63
+ w.uint64(obj.sequence)
64
+ }
65
+
66
+ if (obj.ttl != null) {
67
+ w.uint32(48)
68
+ w.uint64(obj.ttl)
69
+ }
70
+
71
+ if (obj.publicKey != null) {
72
+ w.uint32(58)
73
+ w.bytes(obj.publicKey)
74
+ }
75
+
76
+ if (obj.signatureV2 != null) {
77
+ w.uint32(66)
78
+ w.bytes(obj.signatureV2)
79
+ }
80
+
81
+ if (obj.data != null) {
82
+ w.uint32(74)
83
+ w.bytes(obj.data)
84
+ }
85
+
86
+ if (opts.lengthDelimited !== false) {
87
+ w.ldelim()
88
+ }
89
+ }, (reader, length, opts = {}) => {
90
+ const obj: any = {}
91
+
92
+ const end = length == null ? reader.len : reader.pos + length
93
+
94
+ while (reader.pos < end) {
95
+ const tag = reader.uint32()
96
+
97
+ switch (tag >>> 3) {
98
+ case 1: {
99
+ obj.value = reader.bytes()
100
+ break
101
+ }
102
+ case 2: {
103
+ obj.signatureV1 = reader.bytes()
104
+ break
105
+ }
106
+ case 3: {
107
+ obj.validityType = IpnsEntry.ValidityType.codec().decode(reader)
108
+ break
109
+ }
110
+ case 4: {
111
+ obj.validity = reader.bytes()
112
+ break
113
+ }
114
+ case 5: {
115
+ obj.sequence = reader.uint64()
116
+ break
117
+ }
118
+ case 6: {
119
+ obj.ttl = reader.uint64()
120
+ break
121
+ }
122
+ case 7: {
123
+ obj.publicKey = reader.bytes()
124
+ break
125
+ }
126
+ case 8: {
127
+ obj.signatureV2 = reader.bytes()
128
+ break
129
+ }
130
+ case 9: {
131
+ obj.data = reader.bytes()
132
+ break
133
+ }
134
+ default: {
135
+ reader.skipType(tag & 7)
136
+ break
137
+ }
138
+ }
139
+ }
140
+
141
+ return obj
142
+ }, function * (reader, length, prefix, opts = {}) {
143
+ const end = length == null ? reader.len : reader.pos + length
144
+
145
+ while (reader.pos < end) {
146
+ const tag = reader.uint32()
147
+
148
+ switch (tag >>> 3) {
149
+ case 1: {
150
+ yield {
151
+ field: `${prefix}.value`,
152
+ value: reader.bytes()
153
+ }
154
+ break
155
+ }
156
+ case 2: {
157
+ yield {
158
+ field: `${prefix}.signatureV1`,
159
+ value: reader.bytes()
160
+ }
161
+ break
162
+ }
163
+ case 3: {
164
+ yield {
165
+ field: `${prefix}.validityType`,
166
+ value: IpnsEntry.ValidityType.codec().decode(reader)
167
+ }
168
+ break
169
+ }
170
+ case 4: {
171
+ yield {
172
+ field: `${prefix}.validity`,
173
+ value: reader.bytes()
174
+ }
175
+ break
176
+ }
177
+ case 5: {
178
+ yield {
179
+ field: `${prefix}.sequence`,
180
+ value: reader.uint64()
181
+ }
182
+ break
183
+ }
184
+ case 6: {
185
+ yield {
186
+ field: `${prefix}.ttl`,
187
+ value: reader.uint64()
188
+ }
189
+ break
190
+ }
191
+ case 7: {
192
+ yield {
193
+ field: `${prefix}.publicKey`,
194
+ value: reader.bytes()
195
+ }
196
+ break
197
+ }
198
+ case 8: {
199
+ yield {
200
+ field: `${prefix}.signatureV2`,
201
+ value: reader.bytes()
202
+ }
203
+ break
204
+ }
205
+ case 9: {
206
+ yield {
207
+ field: `${prefix}.data`,
208
+ value: reader.bytes()
209
+ }
210
+ break
211
+ }
212
+ default: {
213
+ reader.skipType(tag & 7)
214
+ break
215
+ }
216
+ }
217
+ }
218
+ })
219
+ }
220
+
221
+ return _codec
222
+ }
223
+
224
+ export interface IpnsEntryValueFieldEvent {
225
+ field: '$.value'
226
+ value: Uint8Array
227
+ }
228
+
229
+ export interface IpnsEntrySignatureV1FieldEvent {
230
+ field: '$.signatureV1'
231
+ value: Uint8Array
232
+ }
233
+
234
+ export interface IpnsEntryValidityTypeFieldEvent {
235
+ field: '$.validityType'
236
+ value: IpnsEntry.ValidityType
237
+ }
238
+
239
+ export interface IpnsEntryValidityFieldEvent {
240
+ field: '$.validity'
241
+ value: Uint8Array
242
+ }
243
+
244
+ export interface IpnsEntrySequenceFieldEvent {
245
+ field: '$.sequence'
246
+ value: bigint
247
+ }
248
+
249
+ export interface IpnsEntryTtlFieldEvent {
250
+ field: '$.ttl'
251
+ value: bigint
252
+ }
253
+
254
+ export interface IpnsEntryPublicKeyFieldEvent {
255
+ field: '$.publicKey'
256
+ value: Uint8Array
257
+ }
258
+
259
+ export interface IpnsEntrySignatureV2FieldEvent {
260
+ field: '$.signatureV2'
261
+ value: Uint8Array
262
+ }
263
+
264
+ export interface IpnsEntryDataFieldEvent {
265
+ field: '$.data'
266
+ value: Uint8Array
267
+ }
268
+
269
+ export function encode (obj: Partial<IpnsEntry>): Uint8Array<ArrayBuffer> {
270
+ return encodeMessage(obj, IpnsEntry.codec())
271
+ }
272
+
273
+ export function decode (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<IpnsEntry>): IpnsEntry {
274
+ return decodeMessage(buf, IpnsEntry.codec(), opts)
275
+ }
276
+
277
+ export function stream (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<IpnsEntry>): Generator<IpnsEntryValueFieldEvent | IpnsEntrySignatureV1FieldEvent | IpnsEntryValidityTypeFieldEvent | IpnsEntryValidityFieldEvent | IpnsEntrySequenceFieldEvent | IpnsEntryTtlFieldEvent | IpnsEntryPublicKeyFieldEvent | IpnsEntrySignatureV2FieldEvent | IpnsEntryDataFieldEvent> {
278
+ return streamMessage(buf, IpnsEntry.codec(), opts)
279
+ }
280
+ }
@@ -101,7 +101,7 @@ export namespace IPNSPublishMetadata {
101
101
  value: number
102
102
  }
103
103
 
104
- export function encode (obj: Partial<IPNSPublishMetadata>): Uint8Array {
104
+ export function encode (obj: Partial<IPNSPublishMetadata>): Uint8Array<ArrayBuffer> {
105
105
  return encodeMessage(obj, IPNSPublishMetadata.codec())
106
106
  }
107
107
 
package/src/records.ts ADDED
@@ -0,0 +1,273 @@
1
+ import { logger } from '@libp2p/logger'
2
+ import NanoDate from 'timestamp-nano'
3
+ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
4
+ import { SignatureCreationError } from './errors.ts'
5
+ import { IpnsEntry } from './pb/ipns.ts'
6
+ import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, marshalIPNSRecord } from './utils.ts'
7
+ import type { PrivateKey, PublicKey } from '@helia/interface'
8
+ import type { AbortOptions } from 'abort-error'
9
+ import type { Key } from 'interface-datastore/key'
10
+
11
+ const log = logger('ipns')
12
+ const DEFAULT_TTL_NS = 5 * 60 * 1e+9 // 5 Minutes or 300 Seconds, as suggested by https://specs.ipfs.tech/ipns/ipns-record/#ttl-uint64
13
+
14
+ export const namespace = '/ipns/'
15
+ export const namespaceLength = namespace.length
16
+
17
+ export interface IPNSRecordV1V2 {
18
+ /**
19
+ * value of the record
20
+ */
21
+ value: string
22
+
23
+ /**
24
+ * signature of the record
25
+ */
26
+ signatureV1: Uint8Array
27
+
28
+ /**
29
+ * Type of validation being used
30
+ */
31
+ validityType: IpnsEntry.ValidityType
32
+
33
+ /**
34
+ * expiration datetime for the record in RFC3339 format
35
+ */
36
+ validity: string
37
+
38
+ /**
39
+ * number representing the version of the record
40
+ */
41
+ sequence: bigint
42
+
43
+ /**
44
+ * ttl in nanoseconds
45
+ */
46
+ ttl?: bigint
47
+
48
+ /**
49
+ * the public portion of the key that signed this record
50
+ */
51
+ publicKey: PublicKey
52
+
53
+ /**
54
+ * the v2 signature of the record
55
+ */
56
+ signatureV2: Uint8Array
57
+
58
+ /**
59
+ * extensible data
60
+ */
61
+ data: Uint8Array
62
+
63
+ /**
64
+ * The marshalled record
65
+ */
66
+ bytes: Uint8Array
67
+ }
68
+
69
+ export interface IPNSRecordV2 {
70
+ /**
71
+ * value of the record
72
+ */
73
+ value: string
74
+
75
+ /**
76
+ * the v2 signature of the record
77
+ */
78
+ signatureV2: Uint8Array
79
+
80
+ /**
81
+ * Type of validation being used
82
+ */
83
+ validityType: IpnsEntry.ValidityType
84
+
85
+ /**
86
+ * If the validity type is EOL, this is the expiration datetime for the record
87
+ * in RFC3339 format
88
+ */
89
+ validity: string
90
+
91
+ /**
92
+ * number representing the version of the record
93
+ */
94
+ sequence: bigint
95
+
96
+ /**
97
+ * ttl in nanoseconds
98
+ */
99
+ ttl?: bigint
100
+
101
+ /**
102
+ * the public portion of the key that signed this record
103
+ */
104
+ publicKey: PublicKey
105
+
106
+ /**
107
+ * extensible data
108
+ */
109
+ data: Uint8Array
110
+
111
+ /**
112
+ * The marshalled record
113
+ */
114
+ bytes: Uint8Array
115
+ }
116
+
117
+ export type IPNSRecord = IPNSRecordV1V2 | IPNSRecordV2
118
+
119
+ export interface IPNSRecordData {
120
+ Value: Uint8Array
121
+ Validity: Uint8Array
122
+ ValidityType: IpnsEntry.ValidityType
123
+ Sequence: bigint
124
+ TTL: bigint
125
+ }
126
+
127
+ export interface IDKeys {
128
+ routingPubKey: Key
129
+ pkKey: Key
130
+ routingKey: Key
131
+ ipnsKey: Key
132
+ }
133
+
134
+ export interface CreateOptions extends AbortOptions {
135
+ ttlNs?: number | bigint
136
+ v1Compatible?: boolean
137
+ }
138
+
139
+ export interface CreateV2OrV1Options extends AbortOptions {
140
+ v1Compatible: true
141
+ }
142
+
143
+ export interface CreateV2Options extends AbortOptions {
144
+ v1Compatible: false
145
+ }
146
+
147
+ const defaultCreateOptions: CreateOptions = {
148
+ v1Compatible: true,
149
+ ttlNs: DEFAULT_TTL_NS
150
+ }
151
+
152
+ /**
153
+ * Creates a new IPNS record and signs it with the given private key.
154
+ * The IPNS Record validity should follow the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
155
+ * Note: This function does not embed the public key. If you want to do that, use `EmbedPublicKey`.
156
+ *
157
+ * The passed value can be a CID, a PublicKey or an arbitrary string path e.g. `/ipfs/...` or `/ipns/...`.
158
+ *
159
+ * CIDs will be converted to v1 and stored in the record as a string similar to: `/ipfs/${cid}`
160
+ * PublicKeys will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}`
161
+ * String paths will be stored in the record as-is, but they must start with `"/"`
162
+ *
163
+ * @param {PrivateKey} privateKey - the private key for signing the record.
164
+ * @param {CID | PublicKey | string} value - content to be stored in the record.
165
+ * @param {number | bigint} seq - number representing the current version of the record.
166
+ * @param {number} lifetime - lifetime of the record (in milliseconds).
167
+ * @param {CreateOptions} options - additional create options.
168
+ */
169
+ export async function createIPNSRecord (privateKey: PrivateKey, value: string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
170
+ export async function createIPNSRecord (privateKey: PrivateKey, value: string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise<IPNSRecordV2>
171
+ export async function createIPNSRecord (privateKey: PrivateKey, value: string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise<IPNSRecordV1V2>
172
+ export async function createIPNSRecord (privateKey: PrivateKey, value: string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
173
+ // Validity in ISOString with nanoseconds precision and validity type EOL
174
+ const expirationDate = new NanoDate(Date.now() + Number(lifetime))
175
+ const validityType = IpnsEntry.ValidityType.EOL
176
+ const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS)
177
+
178
+ return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options)
179
+ }
180
+
181
+ /**
182
+ * Same as create(), but instead of generating a new Date, it receives the intended expiration time
183
+ * WARNING: nano precision is not standard, make sure the value in seconds is 9 orders of magnitude lesser than the one provided.
184
+ *
185
+ * The passed value can be a CID, a PublicKey or an arbitrary string path e.g. `/ipfs/...` or `/ipns/...`.
186
+ *
187
+ * CIDs will be converted to v1 and stored in the record as a string similar to: `/ipfs/${cid}`
188
+ * PublicKeys will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}`
189
+ * String paths will be stored in the record as-is, but they must start with `"/"`
190
+ *
191
+ * @param {PrivateKey} privateKey - the private key for signing the record.
192
+ * @param {CID | PublicKey | string} value - content to be stored in the record.
193
+ * @param {number | bigint} seq - number representing the current version of the record.
194
+ * @param {string} expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
195
+ * @param {CreateOptions} options - additional creation options.
196
+ */
197
+ export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
198
+ export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise<IPNSRecordV2>
199
+ export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: string, seq: number | bigint, expiration: string, options: CreateOptions): Promise<IPNSRecordV1V2>
200
+ export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
201
+ const expirationDate = NanoDate.fromString(expiration)
202
+ const validityType = IpnsEntry.ValidityType.EOL
203
+ const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS)
204
+
205
+ return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options)
206
+ }
207
+
208
+ const _create = async (privateKey: PrivateKey, value: string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
209
+ seq = BigInt(seq)
210
+ const isoValidity = uint8ArrayFromString(validity)
211
+ const data = createCborData(value, validityType, isoValidity, seq, ttl)
212
+ const sigData = ipnsRecordDataForV2Sig(data)
213
+ const signatureV2 = await privateKey.sign(sigData, options)
214
+ const publicKey = shouldEmbedPublicKey(privateKey.publicKey) ? privateKey.publicKey : undefined
215
+ let record: any
216
+
217
+ if (options.v1Compatible === true) {
218
+ const signatureV1 = await signLegacyV1(privateKey, value, validityType, isoValidity)
219
+
220
+ record = {
221
+ value,
222
+ signatureV1,
223
+ validity,
224
+ validityType,
225
+ sequence: seq,
226
+ ttl,
227
+ signatureV2,
228
+ data,
229
+ publicKey
230
+ }
231
+ } else {
232
+ record = {
233
+ value,
234
+ validity,
235
+ validityType,
236
+ sequence: seq,
237
+ ttl,
238
+ signatureV2,
239
+ data,
240
+ publicKey
241
+ }
242
+ }
243
+
244
+ record.bytes = marshalIPNSRecord(record)
245
+
246
+ return record
247
+ }
248
+
249
+ export { unmarshalIPNSRecord } from './utils.ts'
250
+ export { marshalIPNSRecord } from './utils.ts'
251
+ export { multihashToIPNSRoutingKey } from './utils.ts'
252
+ export { multihashFromIPNSRoutingKey } from './utils.ts'
253
+
254
+ /**
255
+ * Sign ipns record data using the legacy V1 signature scheme
256
+ */
257
+ const signLegacyV1 = async (privateKey: PrivateKey, value: string, validityType: IpnsEntry.ValidityType, validity: Uint8Array, options?: AbortOptions): Promise<Uint8Array> => {
258
+ try {
259
+ const dataForSignature = ipnsRecordDataForV1Sig(value, validityType, validity)
260
+
261
+ return await privateKey.sign(dataForSignature, options)
262
+ } catch (error: any) {
263
+ log.error('record signature creation failed', error)
264
+ throw new SignatureCreationError('Record signature creation failed')
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Returns true if the public key multihash is not an identity hash
270
+ */
271
+ function shouldEmbedPublicKey (key: PublicKey): boolean {
272
+ return key.toMultihash().code !== 0
273
+ }
@@ -4,9 +4,6 @@ import { PeerSet } from '@libp2p/peer-collections'
4
4
  import { Queue } from '@libp2p/utils'
5
5
  import { anySignal } from 'any-signal'
6
6
  import delay from 'delay'
7
- import { multihashToIPNSRoutingKey } from 'ipns'
8
- import { ipnsSelector } from 'ipns/selector'
9
- import { ipnsValidator } from 'ipns/validator'
10
7
  import { CustomProgressEvent } from 'progress-events'
11
8
  import { raceSignal } from 'race-signal'
12
9
  import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
@@ -14,9 +11,13 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
14
11
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
15
12
  import { InvalidTopicError } from '../errors.ts'
16
13
  import { localStore } from '../local-store.ts'
17
- import { IPNS_STRING_PREFIX } from '../utils.ts'
14
+ import { multihashToIPNSRoutingKey } from '../records.ts'
15
+ import { ipnsSelector } from '../selector.ts'
16
+ import { IPNS_STRING_PREFIX, unmarshalIPNSRecord } from '../utils.ts'
17
+ import { ipnsValidator } from '../validator.ts'
18
18
  import type { GetOptions, IPNSRouting, PutOptions } from './index.ts'
19
19
  import type { LocalStore } from '../local-store.ts'
20
+ import type { Keychain } from '@helia/interface'
20
21
  import type { Fetch } from '@libp2p/fetch'
21
22
  import type { PeerId, PublicKey, TypedEventTarget, ComponentLogger, Startable, AbortOptions, Metrics, Libp2p } from '@libp2p/interface'
22
23
  import type { Datastore } from 'interface-datastore'
@@ -62,6 +63,7 @@ export interface PubSub extends TypedEventTarget<PubSubEvents> {
62
63
  export interface PubsubRoutingComponents {
63
64
  datastore: Datastore
64
65
  logger: ComponentLogger
66
+ keychain: Keychain
65
67
  metrics?: Metrics
66
68
  libp2p: Pick<Libp2p<{ pubsub: PubSub, fetch?: Fetch }>, 'peerId' | 'register' | 'unregister' | 'services'>
67
69
  }
@@ -105,6 +107,7 @@ export class PubSubRouting implements IPNSRouting, Startable {
105
107
  private readonly fetchPeers: PeerSet
106
108
  private shutdownController: AbortController
107
109
  private fetchTopologyId?: string
110
+ private keychain: Keychain
108
111
 
109
112
  constructor (components: PubsubRoutingComponents, init: PubsubRoutingInit = {}) {
110
113
  this.subscriptions = new Set()
@@ -115,6 +118,7 @@ export class PubSubRouting implements IPNSRouting, Startable {
115
118
  this.libp2p = components.libp2p
116
119
  this.fetchConcurrency = init.fetchConcurrency ?? 8
117
120
  this.fetchDelay = init.fetchDelay ?? 0
121
+ this.keychain = components.keychain
118
122
 
119
123
  // default libp2p-fetch timeout is 10 seconds - we should have an existing
120
124
  // connection to the peer so this can be shortened
@@ -235,23 +239,25 @@ export class PubSubRouting implements IPNSRouting, Startable {
235
239
  }
236
240
 
237
241
  async #handleRecord (topic: string, routingKey: Uint8Array, marshalledRecord: Uint8Array, publish: boolean, options?: AbortOptions): Promise<Uint8Array> {
238
- await ipnsValidator(routingKey, marshalledRecord)
242
+ const record = await unmarshalIPNSRecord(routingKey, marshalledRecord, this.keychain, options)
243
+ await ipnsValidator(record, options)
239
244
  this.shutdownController.signal.throwIfAborted()
240
245
 
241
246
  if (await this.localStore.has(routingKey)) {
242
- const { record: currentRecord } = await this.localStore.get(routingKey, options)
247
+ const { record: marshaledCurrentRecord } = await this.localStore.get(routingKey, options)
248
+ const currentRecord = await unmarshalIPNSRecord(routingKey, marshaledCurrentRecord, this.keychain, options)
243
249
 
244
- if (uint8ArrayEquals(currentRecord, marshalledRecord)) {
250
+ if (uint8ArrayEquals(marshaledCurrentRecord, record.bytes)) {
245
251
  log.trace('found identical record for %m', routingKey)
246
- return currentRecord
252
+ return currentRecord.bytes
247
253
  }
248
254
 
249
- const records = [currentRecord, marshalledRecord]
255
+ const records = [currentRecord, record]
250
256
  const index = ipnsSelector(routingKey, records)
251
257
 
252
258
  if (index === 0) {
253
259
  log.trace('found old record for %m', routingKey)
254
- return currentRecord
260
+ return currentRecord.bytes
255
261
  }
256
262
  }
257
263
 
@@ -364,6 +370,7 @@ export class PubSubRouting implements IPNSRouting, Startable {
364
370
  */
365
371
  cancel (key: PublicKey | MultihashDigest<0x00 | 0x12>): void {
366
372
  const digest = isPublicKey(key) ? key.toMultihash() : key
373
+ // @ ts-expect-error @libp2p/crypto needs new multiformats
367
374
  const routingKey = multihashToIPNSRoutingKey(digest)
368
375
  const topic = keyToTopic(routingKey)
369
376
 
@@ -0,0 +1,55 @@
1
+ import NanoDate from 'timestamp-nano'
2
+ import { IpnsEntry } from './pb/ipns.ts'
3
+ import type { IPNSRecord } from './records.ts'
4
+
5
+ /**
6
+ * Selects the latest valid IPNS record from an array of marshalled IPNS records.
7
+ *
8
+ * Records are sorted by:
9
+ * 1. Sequence number (higher takes precedence)
10
+ * 2. Validity time for EOL records with same sequence number (longer lived record takes precedence)
11
+ *
12
+ * @param key - The routing key for the IPNS record
13
+ * @param data - Array of marshalled IPNS records to select from
14
+ * @returns The index of the most valid record from the input array
15
+ */
16
+ export function ipnsSelector (key: Uint8Array, data: IPNSRecord[]): number {
17
+ const entries = data.map((record, index) => ({
18
+ record,
19
+ index
20
+ }))
21
+
22
+ 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.
26
+
27
+ const aSeq = a.record.sequence
28
+ const bSeq = b.record.sequence
29
+
30
+ // choose later sequence number
31
+ if (aSeq > bSeq) {
32
+ return -1
33
+ } else if (aSeq < bSeq) {
34
+ return 1
35
+ }
36
+
37
+ if (a.record.validityType === IpnsEntry.ValidityType.EOL && b.record.validityType === IpnsEntry.ValidityType.EOL) {
38
+ // 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()
41
+
42
+ if (recordAValidityDate.getTime() > recordBValidityDate.getTime()) {
43
+ return -1
44
+ }
45
+
46
+ if (recordAValidityDate.getTime() < recordBValidityDate.getTime()) {
47
+ return 1
48
+ }
49
+ }
50
+
51
+ return 0
52
+ })
53
+
54
+ return entries[0].index
55
+ }