@helia/ipns 9.2.1-f6cc7640 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +21 -57
  2. package/dist/index.min.js +5 -16
  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 -26
  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 -79
  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/records.d.ts +155 -0
  33. package/dist/src/records.d.ts.map +1 -0
  34. package/dist/src/records.js +88 -0
  35. package/dist/src/records.js.map +1 -0
  36. package/dist/src/routing/pubsub.d.ts +3 -0
  37. package/dist/src/routing/pubsub.d.ts.map +1 -1
  38. package/dist/src/routing/pubsub.js +15 -11
  39. package/dist/src/routing/pubsub.js.map +1 -1
  40. package/dist/src/selector.d.ts +14 -0
  41. package/dist/src/selector.d.ts.map +1 -0
  42. package/dist/src/selector.js +47 -0
  43. package/dist/src/selector.js.map +1 -0
  44. package/dist/src/utils.d.ts +29 -2
  45. package/dist/src/utils.d.ts.map +1 -1
  46. package/dist/src/utils.js +308 -0
  47. package/dist/src/utils.js.map +1 -1
  48. package/dist/src/validator.d.ts +18 -0
  49. package/dist/src/validator.d.ts.map +1 -0
  50. package/dist/src/validator.js +58 -0
  51. package/dist/src/validator.js.map +1 -0
  52. package/dist/typedoc-urls.json +49 -0
  53. package/package.json +16 -15
  54. package/src/errors.ts +40 -25
  55. package/src/index.ts +63 -100
  56. package/src/ipns/publisher.ts +34 -33
  57. package/src/ipns/republisher.ts +24 -13
  58. package/src/ipns/resolver.ts +40 -89
  59. package/src/ipns.ts +44 -7
  60. package/src/pb/ipns.proto +39 -0
  61. package/src/pb/ipns.ts +280 -0
  62. package/src/records.ts +273 -0
  63. package/src/routing/pubsub.ts +17 -11
  64. package/src/selector.ts +55 -0
  65. package/src/utils.ts +371 -2
  66. package/src/validator.ts +67 -0
@@ -1,17 +1,18 @@
1
1
  import { Queue, repeatingTask } from '@libp2p/utils'
2
- import { createIPNSRecord, marshalIPNSRecord, unmarshalIPNSRecord } from 'ipns'
3
2
  import { DEFAULT_REPUBLISH_CONCURRENCY, DEFAULT_REPUBLISH_INTERVAL_MS, DEFAULT_TTL_NS } from '../constants.ts'
3
+ import { createIPNSRecord } from '../records.ts'
4
+ import { marshalIPNSRecord, unmarshalIPNSRecord } from '../utils.ts'
4
5
  import { shouldRepublish } from '../utils.ts'
6
+ import type { IPNSRecord } from '../index.ts'
5
7
  import type { LocalStore } from '../local-store.ts'
6
8
  import type { IPNSRouting } from '../routing/index.ts'
7
- import type { AbortOptions, ComponentLogger, Libp2p, Logger, PrivateKey } from '@libp2p/interface'
8
- import type { Keychain } from '@libp2p/keychain'
9
+ import type { Keychain, PrivateKey } from '@helia/interface'
10
+ import type { AbortOptions, ComponentLogger, Logger } from '@libp2p/interface'
9
11
  import type { RepeatingTask } from '@libp2p/utils'
10
- import type { IPNSRecord } from 'ipns'
11
12
 
12
13
  export interface IPNSRepublisherComponents {
13
14
  logger: ComponentLogger
14
- libp2p: Libp2p<{ keychain: Keychain }>
15
+ keychain: Keychain
15
16
  }
16
17
 
17
18
  export interface IPNSRepublisherInit {
@@ -33,9 +34,9 @@ export class IPNSRepublisher {
33
34
  constructor (components: IPNSRepublisherComponents, init: IPNSRepublisherInit) {
34
35
  this.log = components.logger.forComponent('helia:ipns')
35
36
  this.localStore = init.localStore
36
- this.keychain = components.libp2p.services.keychain
37
+ this.keychain = components.keychain
37
38
  this.republishConcurrency = init.republishConcurrency || DEFAULT_REPUBLISH_CONCURRENCY
38
- this.started = components.libp2p.status === 'started'
39
+ this.started = false
39
40
  this.routers = init.routers ?? []
40
41
 
41
42
  this.republishTask = repeatingTask(this.#republish.bind(this), init.republishInterval ?? DEFAULT_REPUBLISH_INTERVAL_MS, {
@@ -78,18 +79,21 @@ export class IPNSRepublisher {
78
79
 
79
80
  try {
80
81
  const recordsToRepublish: Array<{ routingKey: Uint8Array, record: IPNSRecord }> = []
82
+ let listed = 0
81
83
 
82
84
  // Find all records using the localStore.list method
83
85
  for await (const { routingKey, record, metadata, created } of this.localStore.list(options)) {
86
+ listed++
87
+
84
88
  if (metadata == null) {
85
89
  // Skip if no metadata is found from before we started
86
90
  // storing metadata or for records republished without a key
87
- this.log(`no metadata found for record ${routingKey.toString()}, skipping`)
91
+ this.log('no metadata found for record %b, skipping', routingKey)
88
92
  continue
89
93
  }
90
94
  let ipnsRecord: IPNSRecord
91
95
  try {
92
- ipnsRecord = unmarshalIPNSRecord(record)
96
+ ipnsRecord = await unmarshalIPNSRecord(routingKey, record, this.keychain, options)
93
97
  } catch (err: any) {
94
98
  this.log.error('error unmarshaling record - %e', err)
95
99
  continue
@@ -97,7 +101,7 @@ export class IPNSRepublisher {
97
101
 
98
102
  // Only republish records that are within the DHT or record expiry threshold
99
103
  if (!shouldRepublish(ipnsRecord, created)) {
100
- this.log.trace(`skipping record ${routingKey.toString()}within republish threshold`)
104
+ this.log.trace('skipping record %b within republish threshold', routingKey)
101
105
  continue
102
106
  }
103
107
  const sequenceNumber = ipnsRecord.sequence + 1n
@@ -110,16 +114,23 @@ export class IPNSRepublisher {
110
114
  this.log.error('missing key %s, skipping republishing record - %e', metadata.keyName, err)
111
115
  continue
112
116
  }
117
+
113
118
  try {
114
- const updatedRecord = await createIPNSRecord(privKey, ipnsRecord.value, sequenceNumber, metadata.lifetime, { ...options, ttlNs })
115
- recordsToRepublish.push({ routingKey, record: updatedRecord })
119
+ const updatedRecord = await createIPNSRecord(privKey, ipnsRecord.value, sequenceNumber, metadata.lifetime, {
120
+ ...options,
121
+ ttlNs
122
+ })
123
+ recordsToRepublish.push({
124
+ routingKey,
125
+ record: updatedRecord
126
+ })
116
127
  } catch (err: any) {
117
128
  this.log.error('error creating updated IPNS record for %s - %e', routingKey, err)
118
129
  continue
119
130
  }
120
131
  }
121
132
 
122
- this.log(`found ${recordsToRepublish.length} records to republish`)
133
+ this.log(`found ${recordsToRepublish.length}/${listed} records to republish`)
123
134
 
124
135
  // Republish each record
125
136
  for (const { routingKey, record } of recordsToRepublish) {
@@ -1,33 +1,21 @@
1
- import { isPeerId, isPublicKey } from '@libp2p/interface'
2
- import { multihashToIPNSRoutingKey, unmarshalIPNSRecord } from 'ipns'
3
- import { ipnsSelector } from 'ipns/selector'
4
- import { ipnsValidator } from 'ipns/validator'
5
- import { base36 } from 'multiformats/bases/base36'
6
- import { base58btc } from 'multiformats/bases/base58'
7
- import { CID } from 'multiformats/cid'
8
- import * as Digest from 'multiformats/hashes/digest'
9
1
  import { DEFAULT_TTL_NS } from '../constants.ts'
10
- import { InvalidValueError, RecordNotFoundError, RecordsFailedValidationError, UnsupportedMultibasePrefixError, UnsupportedMultihashCodecError } from '../errors.ts'
11
- import { isCodec, IDENTITY_CODEC, SHA2_256_CODEC, isLibp2pCID } from '../utils.ts'
12
- import type { IPNSResolveResult, ResolveOptions, ResolveResult } from '../index.ts'
2
+ import { RecordNotFoundError, RecordsFailedValidationError } from '../errors.ts'
3
+ import { ipnsSelector } from '../selector.ts'
4
+ import { multihashToIPNSRoutingKey, unmarshalIPNSRecord, normalizeKey, IPNS_STRING_PREFIX } from '../utils.ts'
5
+ import { ipnsValidator } from '../validator.ts'
6
+ import type { IPNSRecord, ResolveOptions, ResolveResult } from '../index.ts'
13
7
  import type { LocalStore } from '../local-store.ts'
14
8
  import type { IPNSRouting } from '../routing/index.ts'
15
- import type { Routing } from '@helia/interface'
16
- import type { ComponentLogger, Logger, PeerId, PublicKey } from '@libp2p/interface'
9
+ import type { Routing, Keychain } from '@helia/interface'
10
+ import type { ComponentLogger, Logger } from '@libp2p/interface'
17
11
  import type { Datastore } from 'interface-datastore'
18
- import type { IPNSRecord } from 'ipns'
19
- import type { MultibaseDecoder } from 'multiformats/bases/interface'
20
12
  import type { MultihashDigest } from 'multiformats/hashes/interface'
21
13
 
22
- const bases: Record<string, MultibaseDecoder<string>> = {
23
- [base36.prefix]: base36,
24
- [base58btc.prefix]: base58btc
25
- }
26
-
27
14
  export interface IPNSResolverComponents {
28
15
  datastore: Datastore
29
16
  routing: Routing
30
17
  logger: ComponentLogger
18
+ keychain: Keychain
31
19
  }
32
20
 
33
21
  export interface IPNResolverInit {
@@ -39,81 +27,37 @@ export class IPNSResolver {
39
27
  public readonly routers: IPNSRouting[]
40
28
  private readonly localStore: LocalStore
41
29
  private readonly log: Logger
30
+ private keychain: Keychain
42
31
 
43
32
  constructor (components: IPNSResolverComponents, init: IPNResolverInit) {
44
33
  this.log = components.logger.forComponent('helia:ipns')
45
34
  this.localStore = init.localStore
46
35
  this.routers = init.routers
36
+ this.keychain = components.keychain
47
37
  }
48
38
 
49
- async resolve (key: CID<unknown, 0x72, 0x00 | 0x12, 1> | PublicKey | MultihashDigest<0x00 | 0x12> | PeerId, options: ResolveOptions = {}): Promise<IPNSResolveResult> {
50
- const digest = isPublicKey(key) || isPeerId(key) ? key.toMultihash() : isLibp2pCID(key) ? key.multihash : key
51
- // @ts-expect-error @libp2p/peer-id needs new multiformats
52
- const routingKey = multihashToIPNSRoutingKey(digest)
53
- const record = await this.#findIpnsRecord(routingKey, options)
54
-
55
- return {
56
- ...(await this.#resolve(record.value, options)),
57
- record
58
- }
59
- }
39
+ async * resolve (key: MultihashDigest, options: ResolveOptions = {}): AsyncGenerator<ResolveResult> {
40
+ let { digest } = normalizeKey(key)
60
41
 
61
- async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise<ResolveResult> {
62
- const parts = ipfsPath.split('/')
63
- try {
64
- const scheme = parts[1]
65
-
66
- if (scheme === 'ipns') {
67
- const str = parts[2]
68
- const prefix = str.substring(0, 1)
69
- let buf: Uint8Array | undefined
70
-
71
- if (prefix === '1' || prefix === 'Q') {
72
- buf = base58btc.decode(`z${str}`)
73
- } else if (bases[prefix] != null) {
74
- buf = bases[prefix].decode(str)
75
- } else {
76
- throw new UnsupportedMultibasePrefixError(`Unsupported multibase prefix "${prefix}"`)
77
- }
42
+ while (true) {
43
+ const routingKey = multihashToIPNSRoutingKey(digest)
44
+ const record = await this.#findIpnsRecord(routingKey, options)
78
45
 
79
- let digest: MultihashDigest<number>
80
-
81
- try {
82
- digest = Digest.decode(buf)
83
- } catch {
84
- digest = CID.decode(buf).multihash
85
- }
86
-
87
- if (!isCodec(digest, IDENTITY_CODEC) && !isCodec(digest, SHA2_256_CODEC)) {
88
- throw new UnsupportedMultihashCodecError(`Unsupported multihash codec "${digest.code}"`)
89
- }
90
-
91
- const { cid } = await this.resolve(digest, options)
92
- const path = parts.slice(3).join('/')
93
-
94
- return {
95
- cid,
96
- path: path === '' ? undefined : path
97
- }
98
- } else if (scheme === 'ipfs') {
99
- const cid = CID.parse(parts[2])
100
- const path = parts.slice(3).join('/')
46
+ yield {
47
+ record
48
+ }
101
49
 
102
- return {
103
- cid,
104
- path: path === '' ? undefined : path
105
- }
50
+ if (!record.value.startsWith(IPNS_STRING_PREFIX)) {
51
+ // not a recursive record
52
+ break
106
53
  }
107
- } catch (err) {
108
- this.log.error('error parsing ipfs path - %e', err)
109
- }
110
54
 
111
- this.log.error('invalid ipfs path %s', ipfsPath)
112
- throw new InvalidValueError('Invalid value')
55
+ ({ digest } = normalizeKey(record.value))
56
+ }
113
57
  }
114
58
 
115
59
  async #findIpnsRecord (routingKey: Uint8Array, options: ResolveOptions = {}): Promise<IPNSRecord> {
116
- const records: Uint8Array[] = []
60
+ const records: IPNSRecord[] = []
117
61
  const cached = await this.localStore.has(routingKey, options)
118
62
 
119
63
  if (cached) {
@@ -122,17 +66,19 @@ export class IPNSResolver {
122
66
  if (options.nocache !== true) {
123
67
  try {
124
68
  // check the local cache first
125
- const { record, created } = await this.localStore.get(routingKey, options)
69
+ const { record: marshaledIPNSRecord, created } = await this.localStore.get(routingKey, options)
126
70
 
127
71
  this.log('record retrieved from cache')
128
72
 
73
+ // unmarshal the record
74
+ const ipnsRecord = await unmarshalIPNSRecord(routingKey, marshaledIPNSRecord, this.keychain, options)
75
+
129
76
  // validate the record
130
- await ipnsValidator(routingKey, record)
77
+ await ipnsValidator(ipnsRecord, options)
131
78
 
132
79
  this.log('record was valid')
133
80
 
134
81
  // check the TTL
135
- const ipnsRecord = unmarshalIPNSRecord(record)
136
82
 
137
83
  // IPNS TTL is in nanoseconds, convert to milliseconds, default to one
138
84
  // hour
@@ -156,7 +102,7 @@ export class IPNSResolver {
156
102
  // add the local record to our list of resolved record, and also
157
103
  // search the routing for updates - the most up to date record will be
158
104
  // returned
159
- records.push(record)
105
+ records.push(ipnsRecord)
160
106
  } catch (err) {
161
107
  this.log('cached record was invalid - %e', err)
162
108
  await this.localStore.delete(routingKey, options)
@@ -177,10 +123,10 @@ export class IPNSResolver {
177
123
 
178
124
  await Promise.all(
179
125
  this.routers.map(async (router) => {
180
- let record: Uint8Array
126
+ let marshaledIPNSRecord: Uint8Array
181
127
 
182
128
  try {
183
- record = await router.get(routingKey, {
129
+ marshaledIPNSRecord = await router.get(routingKey, {
184
130
  ...options,
185
131
  validate: false
186
132
  })
@@ -192,7 +138,12 @@ export class IPNSResolver {
192
138
  }
193
139
 
194
140
  try {
195
- await ipnsValidator(routingKey, record)
141
+ // unmarshal ensures that (1) SignatureV2 and Data are present, (2) that ValidityType
142
+ // and Validity are of valid types and have a value, (3) that CBOR data matches protobuf
143
+ // if it's a V1+V2 record
144
+ const record = await unmarshalIPNSRecord(routingKey, marshaledIPNSRecord, this.keychain, options)
145
+
146
+ await ipnsValidator(record, options)
196
147
 
197
148
  records.push(record)
198
149
  } catch (err) {
@@ -213,8 +164,8 @@ export class IPNSResolver {
213
164
 
214
165
  const record = records[ipnsSelector(routingKey, records)]
215
166
 
216
- await this.localStore.put(routingKey, record, options)
167
+ await this.localStore.put(routingKey, record.bytes, options)
217
168
 
218
- return unmarshalIPNSRecord(record)
169
+ return record
219
170
  }
220
171
  }
package/src/ipns.ts CHANGED
@@ -5,10 +5,15 @@ import { IPNSResolver } from './ipns/resolver.ts'
5
5
  import { localStore } from './local-store.ts'
6
6
  import { helia } from './routing/helia.ts'
7
7
  import { localStoreRouting } from './routing/local-store.ts'
8
- import type { IPNSComponents, IPNS as IPNSInterface, IPNSOptions, IPNSPublishResult, IPNSResolveResult, PublishOptions, ResolveOptions } from './index.ts'
8
+ import { ipnsSelector } from './selector.ts'
9
+ import { normalizeKey, normalizeValue, unmarshalIPNSRecord } from './utils.ts'
10
+ import { ipnsValidator } from './validator.ts'
11
+ import type { IPNSComponents, IPNS as IPNSInterface, IPNSOptions, PublishResult, PublishOptions, ResolveOptions, ResolveResult } from './index.ts'
9
12
  import type { LocalStore } from './local-store.ts'
10
13
  import type { IPNSRouting } from './routing/index.ts'
11
- import type { AbortOptions, PeerId, PublicKey, Startable } from '@libp2p/interface'
14
+ import type { PublicKey } from '@helia/interface'
15
+ import type { AbortOptions, Libp2p, Startable } from '@libp2p/interface'
16
+ import type { ValidateFn, SelectFn } from '@libp2p/kad-dht'
12
17
  import type { MultihashDigest } from 'multiformats/hashes/interface'
13
18
 
14
19
  export class IPNS implements IPNSInterface, Startable {
@@ -17,11 +22,13 @@ export class IPNS implements IPNSInterface, Startable {
17
22
  private readonly republisher: IPNSRepublisher
18
23
  private readonly resolver: IPNSResolver
19
24
  private readonly localStore: LocalStore
25
+ private readonly components: IPNSComponents
20
26
  private started: boolean
21
27
 
22
28
  constructor (components: IPNSComponents, init: IPNSOptions = {}) {
23
29
  this.localStore = localStore(components.datastore, components.logger.forComponent('helia:ipns:local-store'))
24
- this.started = components.libp2p.status === 'started'
30
+ this.components = components
31
+ this.started = false
25
32
 
26
33
  this.routers = [
27
34
  localStoreRouting(this.localStore),
@@ -53,6 +60,26 @@ export class IPNS implements IPNSInterface, Startable {
53
60
  if (this.started) {
54
61
  this.republisher.start()
55
62
  }
63
+
64
+ for (const component of Object.values(this.components)) {
65
+ if (isLibp2p(component)) {
66
+ for (const service of Object.values(component.services)) {
67
+ if (isKadDHT(service)) {
68
+ // @ ts-expect-error https://github.com/libp2p/js-libp2p/pull/3506
69
+ service.selectors.ipns = async (key: Uint8Array, values: Uint8Array[]): Promise<number> => {
70
+ const records = await Promise.all(values.map(buf => unmarshalIPNSRecord(key, buf, this.components.keychain)))
71
+
72
+ return ipnsSelector(key, records)
73
+ }
74
+
75
+ service.validators.ipns = async (key: Uint8Array, value: Uint8Array): Promise<void> => {
76
+ const record = await unmarshalIPNSRecord(key, value, this.components.keychain)
77
+ await ipnsValidator(record)
78
+ }
79
+ }
80
+ }
81
+ }
82
+ }
56
83
  }
57
84
 
58
85
  start (): void {
@@ -73,15 +100,25 @@ export class IPNS implements IPNSInterface, Startable {
73
100
  this.republisher.stop()
74
101
  }
75
102
 
76
- async publish (keyName: string, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | PeerId | string, options: PublishOptions = {}): Promise<IPNSPublishResult> {
77
- return this.publisher.publish(keyName, value, options)
103
+ async publish (keyName: string, value: PublicKey | CID | MultihashDigest | string, options: PublishOptions = {}): Promise<PublishResult> {
104
+ return this.publisher.publish(keyName, normalizeValue(value), options)
78
105
  }
79
106
 
80
- async resolve (key: CID<unknown, 0x72, 0x00 | 0x12, 1> | PublicKey | MultihashDigest<0x00 | 0x12> | PeerId, options: ResolveOptions = {}): Promise<IPNSResolveResult> {
81
- return this.resolver.resolve(key, options)
107
+ async * resolve (key: PublicKey | CID<unknown, 0x72> | MultihashDigest | string, options: ResolveOptions = {}): AsyncGenerator<ResolveResult> {
108
+ const { digest } = normalizeKey(key)
109
+
110
+ yield * this.resolver.resolve(digest, options)
82
111
  }
83
112
 
84
113
  async unpublish (keyName: string, options?: AbortOptions): Promise<void> {
85
114
  return this.publisher.unpublish(keyName, options)
86
115
  }
87
116
  }
117
+
118
+ function isLibp2p (obj?: any): obj is Libp2p {
119
+ return obj?.services != null
120
+ }
121
+
122
+ function isKadDHT (obj?: any): obj is { validators: Record<string, ValidateFn>, selectors: Record<string, SelectFn> } {
123
+ return obj?.validators != null && obj?.selectors != null
124
+ }
@@ -0,0 +1,39 @@
1
+ // https://github.com/ipfs/boxo/blob/main/ipns/pb/record.proto
2
+
3
+ syntax = "proto3";
4
+
5
+ message IpnsEntry {
6
+ enum ValidityType {
7
+ // setting an EOL says "this record is valid until..."
8
+ EOL = 0;
9
+ }
10
+
11
+ // legacy V1 copy of data[Value]
12
+ optional bytes value = 1;
13
+
14
+ // legacy V1 field, verify 'signatureV2' instead
15
+ optional bytes signatureV1 = 2;
16
+
17
+ // legacy V1 copies of data[ValidityType] and data[Validity]
18
+ optional ValidityType validityType = 3;
19
+ optional bytes validity = 4;
20
+
21
+ // legacy V1 copy of data[Sequence]
22
+ optional uint64 sequence = 5;
23
+
24
+ // legacy V1 copy copy of data[TTL]
25
+ optional uint64 ttl = 6;
26
+
27
+ // Optional Public Key to be used for signature verification.
28
+ // Used for big keys such as old RSA keys. Including the public key as part of
29
+ // the record itself makes it verifiable in offline mode, without any additional lookup.
30
+ // For newer Ed25519 keys, the public key is small enough that it can be embedded in the
31
+ // IPNS Name itself, making this field unnecessary.
32
+ optional bytes publicKey = 7;
33
+
34
+ // (mandatory V2) signature of the IPNS record
35
+ optional bytes signatureV2 = 8;
36
+
37
+ // (mandatory V2) extensible record data in DAG-CBOR format
38
+ optional bytes data = 9;
39
+ }