Skip to content

Commit 975eaca

Browse files
committed
fix!: update to libp2p@2.x.x deps
- Updates method names so they can be invidually imported but still be legible - Operates on PrivateKey/PublicKeys instead of PeerIds to integrate with the libp2p@2.x.x keychain will less friction BREAKING CHANGE: uses libp2p@2.x.x deps, operates on PrivateKey/PublicKeys instead of PeerIds
1 parent 3ec7233 commit 975eaca

12 files changed

+391
-347
lines changed

README.md

-15
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
- [Validate record](#validate-record)
1717
- [Embed public key to record](#embed-public-key-to-record)
1818
- [Extract public key from record](#extract-public-key-from-record)
19-
- [Datastore key](#datastore-key)
2019
- [Marshal data with proto buffer](#marshal-data-with-proto-buffer)
2120
- [Unmarshal data from proto buffer](#unmarshal-data-from-proto-buffer)
2221
- [Validator](#validator)
@@ -82,20 +81,6 @@ import * as ipns from 'ipns'
8281
const publicKey = await ipns.extractPublicKey(peerId, ipnsRecord)
8382
```
8483

85-
### Datastore key
86-
87-
```js
88-
import * as ipns from 'ipns'
89-
90-
ipns.getLocalKey(peerId)
91-
```
92-
93-
Returns a key to be used for storing the IPNS record locally, that is:
94-
95-
```
96-
/ipns/${base32(<HASH>)}
97-
```
98-
9984
### Marshal data with proto buffer
10085

10186
```js

package.json

+11-13
Original file line numberDiff line numberDiff line change
@@ -166,23 +166,21 @@
166166
"docs:no-publish": "NODE_OPTIONS=--max_old_space_size=8192 aegir docs --publish false"
167167
},
168168
"dependencies": {
169-
"@libp2p/crypto": "^4.0.0",
170-
"@libp2p/interface": "^1.1.0",
171-
"@libp2p/logger": "^4.0.3",
172-
"@libp2p/peer-id": "^4.0.3",
173-
"cborg": "^4.0.1",
174-
"err-code": "^3.0.1",
175-
"interface-datastore": "^8.1.0",
176-
"multiformats": "^13.0.0",
177-
"protons-runtime": "^5.2.1",
178-
"timestamp-nano": "^1.0.0",
169+
"@libp2p/crypto": "^5.0.0",
170+
"@libp2p/interface": "^2.0.0",
171+
"@libp2p/logger": "^5.0.0",
172+
"cborg": "^4.2.3",
173+
"interface-datastore": "^8.3.0",
174+
"multiformats": "^13.2.2",
175+
"protons-runtime": "^5.5.0",
176+
"timestamp-nano": "^1.0.1",
179177
"uint8arraylist": "^2.4.8",
180-
"uint8arrays": "^5.0.1"
178+
"uint8arrays": "^5.1.0"
181179
},
182180
"devDependencies": {
183-
"@libp2p/peer-id-factory": "^4.0.2",
181+
"@libp2p/peer-id": "^5.0.0",
184182
"aegir": "^44.1.1",
185-
"protons": "^7.3.3"
183+
"protons": "^7.6.0"
186184
},
187185
"sideEffects": false
188186
}

src/errors.ts

+71-13
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,71 @@
1-
export const ERR_IPNS_EXPIRED_RECORD = 'ERR_IPNS_EXPIRED_RECORD'
2-
export const ERR_UNRECOGNIZED_VALIDITY = 'ERR_UNRECOGNIZED_VALIDITY'
3-
export const ERR_SIGNATURE_CREATION = 'ERR_SIGNATURE_CREATION'
4-
export const ERR_SIGNATURE_VERIFICATION = 'ERR_SIGNATURE_VERIFICATION'
5-
export const ERR_UNRECOGNIZED_FORMAT = 'ERR_UNRECOGNIZED_FORMAT'
6-
export const ERR_PEER_ID_FROM_PUBLIC_KEY = 'ERR_PEER_ID_FROM_PUBLIC_KEY'
7-
export const ERR_PUBLIC_KEY_FROM_ID = 'ERR_PUBLIC_KEY_FROM_ID'
8-
export const ERR_UNDEFINED_PARAMETER = 'ERR_UNDEFINED_PARAMETER'
9-
export const ERR_INVALID_RECORD_DATA = 'ERR_INVALID_RECORD_DATA'
10-
export const ERR_INVALID_VALUE = 'ERR_INVALID_VALUE'
11-
export const ERR_INVALID_EMBEDDED_KEY = 'ERR_INVALID_EMBEDDED_KEY'
12-
export const ERR_MISSING_PRIVATE_KEY = 'ERR_MISSING_PRIVATE_KEY'
13-
export const ERR_RECORD_TOO_LARGE = 'ERR_RECORD_TOO_LARGE'
1+
export class SignatureCreationError extends Error {
2+
static name = 'SignatureCreationError'
3+
4+
constructor (message = 'Record signature creation failed') {
5+
super(message)
6+
this.name = 'SignatureCreationError'
7+
}
8+
}
9+
10+
export class SignatureVerificationError extends Error {
11+
static name = 'SignatureVerificationError'
12+
13+
constructor (message = 'Record signature verification failed') {
14+
super(message)
15+
this.name = 'SignatureVerificationError'
16+
}
17+
}
18+
19+
export class RecordExpiredError extends Error {
20+
static name = 'RecordExpiredError'
21+
22+
constructor (message = 'Record has expired') {
23+
super(message)
24+
this.name = 'RecordExpiredError'
25+
}
26+
}
27+
28+
export class UnsupportedValidityError extends Error {
29+
static name = 'UnsupportedValidityError'
30+
31+
constructor (message = 'The validity type is unsupported') {
32+
super(message)
33+
this.name = 'UnsupportedValidityError'
34+
}
35+
}
36+
37+
export class RecordTooLargeError extends Error {
38+
static name = 'RecordTooLargeError'
39+
40+
constructor (message = 'The record is too large') {
41+
super(message)
42+
this.name = 'RecordTooLargeError'
43+
}
44+
}
45+
46+
export class InvalidValueError extends Error {
47+
static name = 'InvalidValueError'
48+
49+
constructor (message = 'Value must be a valid content path starting with /') {
50+
super(message)
51+
this.name = 'InvalidValueError'
52+
}
53+
}
54+
55+
export class InvalidRecordDataError extends Error {
56+
static name = 'InvalidRecordDataError'
57+
58+
constructor (message = 'Invalid record data') {
59+
super(message)
60+
this.name = 'InvalidRecordDataError'
61+
}
62+
}
63+
64+
export class InvalidEmbeddedPublicKeyError extends Error {
65+
static name = 'InvalidEmbeddedPublicKeyError'
66+
67+
constructor (message = 'Invalid embedded public key') {
68+
super(message)
69+
this.name = 'InvalidEmbeddedPublicKeyError'
70+
}
71+
}

src/index.ts

+29-56
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
1-
import { unmarshalPrivateKey } from '@libp2p/crypto/keys'
1+
import { publicKeyToProtobuf } from '@libp2p/crypto/keys'
22
import { logger } from '@libp2p/logger'
3-
import errCode from 'err-code'
4-
import { Key } from 'interface-datastore/key'
5-
import { base32upper } from 'multiformats/bases/base32'
6-
import * as Digest from 'multiformats/hashes/digest'
7-
import { identity } from 'multiformats/hashes/identity'
3+
import { type Key } from 'interface-datastore/key'
84
import NanoDate from 'timestamp-nano'
9-
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
105
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
11-
import * as ERRORS from './errors.js'
6+
import { SignatureCreationError } from './errors.js'
127
import { IpnsEntry } from './pb/ipns.js'
138
import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, normalizeValue } from './utils.js'
14-
import type { PrivateKey, PeerId } from '@libp2p/interface'
9+
import type { PrivateKey, PublicKey } from '@libp2p/interface'
1510
import type { CID } from 'multiformats/cid'
1611

1712
const log = logger('ipns')
18-
const ID_MULTIHASH_CODE = identity.code
1913
const DEFAULT_TTL_NS = 60 * 60 * 1e+9 // 1 Hour or 3600 Seconds
2014

2115
export const namespace = '/ipns/'
@@ -157,22 +151,22 @@ const defaultCreateOptions: CreateOptions = {
157151
* * PeerIDs will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}`
158152
* * String paths will be stored in the record as-is, but they must start with `"/"`
159153
*
160-
* @param {PeerId} peerId - peer id containing private key for signing the record.
161-
* @param {CID | PeerId | string} value - content to be stored in the record.
154+
* @param {PrivateKey} privateKey - the private key for signing the record.
155+
* @param {CID | PublicKey | string} value - content to be stored in the record.
162156
* @param {number | bigint} seq - number representing the current version of the record.
163157
* @param {number} lifetime - lifetime of the record (in milliseconds).
164158
* @param {CreateOptions} options - additional create options.
165159
*/
166-
export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
167-
export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise<IPNSRecordV2>
168-
export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise<IPNSRecordV1V2>
169-
export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
160+
export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
161+
export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise<IPNSRecordV2>
162+
export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise<IPNSRecordV1V2>
163+
export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
170164
// Validity in ISOString with nanoseconds precision and validity type EOL
171165
const expirationDate = new NanoDate(Date.now() + Number(lifetime))
172166
const validityType = IpnsEntry.ValidityType.EOL
173167
const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS)
174168

175-
return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options)
169+
return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options)
176170
}
177171

178172
/**
@@ -185,47 +179,37 @@ export async function create (peerId: PeerId, value: CID | PeerId | string, seq:
185179
* * PeerIDs will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}`
186180
* * String paths will be stored in the record as-is, but they must start with `"/"`
187181
*
188-
* @param {PeerId} peerId - PeerId containing private key for signing the record.
189-
* @param {CID | PeerId | string} value - content to be stored in the record.
182+
* @param {PrivateKey} privateKey - the private key for signing the record.
183+
* @param {CID | PublicKey | string} value - content to be stored in the record.
190184
* @param {number | bigint} seq - number representing the current version of the record.
191185
* @param {string} expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
192186
* @param {CreateOptions} options - additional creation options.
193187
*/
194-
export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
195-
export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise<IPNSRecordV2>
196-
export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise<IPNSRecordV1V2>
197-
export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
188+
export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
189+
export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise<IPNSRecordV2>
190+
export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise<IPNSRecordV1V2>
191+
export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
198192
const expirationDate = NanoDate.fromString(expiration)
199193
const validityType = IpnsEntry.ValidityType.EOL
200194
const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS)
201195

202-
return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options)
196+
return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options)
203197
}
204198

205-
const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
199+
const _create = async (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
206200
seq = BigInt(seq)
207201
const isoValidity = uint8ArrayFromString(validity)
208202
const normalizedValue = normalizeValue(value)
209203
const encodedValue = uint8ArrayFromString(normalizedValue)
210-
211-
if (peerId.privateKey == null) {
212-
throw errCode(new Error('Missing private key'), ERRORS.ERR_MISSING_PRIVATE_KEY)
213-
}
214-
215-
const privateKey = await unmarshalPrivateKey(peerId.privateKey)
216204
const data = createCborData(encodedValue, validityType, isoValidity, seq, ttl)
217205
const sigData = ipnsRecordDataForV2Sig(data)
218206
const signatureV2 = await privateKey.sign(sigData)
219207
let pubKey: Uint8Array | undefined
220208

221209
// if we cannot derive the public key from the PeerId (e.g. RSA PeerIDs),
222210
// we have to embed it in the IPNS record
223-
if (peerId.publicKey != null) {
224-
const digest = Digest.decode(peerId.toBytes())
225-
226-
if (digest.code !== ID_MULTIHASH_CODE || !uint8ArrayEquals(peerId.publicKey, digest.digest)) {
227-
pubKey = peerId.publicKey
228-
}
211+
if (privateKey.type === 'RSA') {
212+
pubKey = publicKeyToProtobuf(privateKey.publicKey)
229213
}
230214

231215
if (options.v1Compatible === true) {
@@ -266,24 +250,13 @@ const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number
266250
}
267251
}
268252

269-
/**
270-
* rawStdEncoding with RFC4648
271-
*/
272-
const rawStdEncoding = (key: Uint8Array): string => base32upper.encode(key).slice(1)
273-
274-
/**
275-
* Get key for storing the record locally.
276-
* Format: /ipns/${base32(<HASH>)}
277-
*
278-
* @param {Uint8Array} key - peer identifier object.
279-
*/
280-
export const getLocalKey = (key: Uint8Array): Key => new Key(`/ipns/${rawStdEncoding(key)}`)
281-
282-
export { unmarshal } from './utils.js'
283-
export { marshal } from './utils.js'
284-
export { peerIdToRoutingKey } from './utils.js'
285-
export { peerIdFromRoutingKey } from './utils.js'
286-
export { extractPublicKey } from './utils.js'
253+
export { unmarshalIPNSRecord } from './utils.js'
254+
export { marshalIPNSRecord } from './utils.js'
255+
export { multihashToIPNSRoutingKey } from './utils.js'
256+
export { multihashFromIPNSRoutingKey } from './utils.js'
257+
export { publicKeyToIPNSRoutingKey } from './utils.js'
258+
export { publicKeyFromIPNSRoutingKey } from './utils.js'
259+
export { extractPublicKeyFromIPNSRecord } from './utils.js'
287260

288261
/**
289262
* Sign ipns record data using the legacy V1 signature scheme
@@ -295,6 +268,6 @@ const signLegacyV1 = async (privateKey: PrivateKey, value: Uint8Array, validityT
295268
return await privateKey.sign(dataForSignature)
296269
} catch (error: any) {
297270
log.error('record signature creation failed', error)
298-
throw errCode(new Error('record signature creation failed'), ERRORS.ERR_SIGNATURE_CREATION)
271+
throw new SignatureCreationError('Record signature creation failed')
299272
}
300273
}

src/pb/ipns.ts

+24-15
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
55
/* eslint-disable @typescript-eslint/no-empty-interface */
66

7-
import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime'
8-
import type { Codec } from 'protons-runtime'
7+
import { type Codec, decodeMessage, type DecodeOptions, encodeMessage, enumeration, message } from 'protons-runtime'
98
import type { Uint8ArrayList } from 'uint8arraylist'
109

1110
export interface IpnsEntry {
@@ -92,7 +91,7 @@ export namespace IpnsEntry {
9291
if (opts.lengthDelimited !== false) {
9392
w.ldelim()
9493
}
95-
}, (reader, length) => {
94+
}, (reader, length, opts = {}) => {
9695
const obj: any = {}
9796

9897
const end = length == null ? reader.len : reader.pos + length
@@ -101,36 +100,46 @@ export namespace IpnsEntry {
101100
const tag = reader.uint32()
102101

103102
switch (tag >>> 3) {
104-
case 1:
103+
case 1: {
105104
obj.value = reader.bytes()
106105
break
107-
case 2:
106+
}
107+
case 2: {
108108
obj.signatureV1 = reader.bytes()
109109
break
110-
case 3:
110+
}
111+
case 3: {
111112
obj.validityType = IpnsEntry.ValidityType.codec().decode(reader)
112113
break
113-
case 4:
114+
}
115+
case 4: {
114116
obj.validity = reader.bytes()
115117
break
116-
case 5:
118+
}
119+
case 5: {
117120
obj.sequence = reader.uint64()
118121
break
119-
case 6:
122+
}
123+
case 6: {
120124
obj.ttl = reader.uint64()
121125
break
122-
case 7:
126+
}
127+
case 7: {
123128
obj.pubKey = reader.bytes()
124129
break
125-
case 8:
130+
}
131+
case 8: {
126132
obj.signatureV2 = reader.bytes()
127133
break
128-
case 9:
134+
}
135+
case 9: {
129136
obj.data = reader.bytes()
130137
break
131-
default:
138+
}
139+
default: {
132140
reader.skipType(tag & 7)
133141
break
142+
}
134143
}
135144
}
136145

@@ -145,7 +154,7 @@ export namespace IpnsEntry {
145154
return encodeMessage(obj, IpnsEntry.codec())
146155
}
147156

148-
export const decode = (buf: Uint8Array | Uint8ArrayList): IpnsEntry => {
149-
return decodeMessage(buf, IpnsEntry.codec())
157+
export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<IpnsEntry>): IpnsEntry => {
158+
return decodeMessage(buf, IpnsEntry.codec(), opts)
150159
}
151160
}

0 commit comments

Comments
 (0)