Skip to content

Commit 4c1d05d

Browse files
committed
feat: normalize value at creation and read time
1 parent 1e0386a commit 4c1d05d

File tree

3 files changed

+107
-6
lines changed

3 files changed

+107
-6
lines changed

src/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const ERR_PEER_ID_FROM_PUBLIC_KEY = 'ERR_PEER_ID_FROM_PUBLIC_KEY'
88
export const ERR_PUBLIC_KEY_FROM_ID = 'ERR_PUBLIC_KEY_FROM_ID'
99
export const ERR_UNDEFINED_PARAMETER = 'ERR_UNDEFINED_PARAMETER'
1010
export const ERR_INVALID_RECORD_DATA = 'ERR_INVALID_RECORD_DATA'
11+
export const ERR_INVALID_VALUE = 'ERR_INVALID_VALUE'
1112
export const ERR_INVALID_EMBEDDED_KEY = 'ERR_INVALID_EMBEDDED_KEY'
1213
export const ERR_MISSING_PRIVATE_KEY = 'ERR_MISSING_PRIVATE_KEY'
1314
export const ERR_RECORD_TOO_LARGE = 'ERR_RECORD_TOO_LARGE'

src/index.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as cborg from 'cborg'
44
import errCode from 'err-code'
55
import { Key } from 'interface-datastore/key'
66
import { base32upper } from 'multiformats/bases/base32'
7+
import { CID } from 'multiformats/cid'
78
import * as Digest from 'multiformats/hashes/digest'
89
import { identity } from 'multiformats/hashes/identity'
910
import NanoDate from 'timestamp-nano'
@@ -24,7 +25,7 @@ export const namespaceLength = namespace.length
2425

2526
export class IPNSRecord {
2627
readonly pb: IpnsEntry
27-
private readonly data: any
28+
readonly data: any
2829

2930
constructor (pb: IpnsEntry) {
3031
this.pb = pb
@@ -37,7 +38,7 @@ export class IPNSRecord {
3738
}
3839

3940
value (): string {
40-
return uint8ArrayToString(this.data.Value)
41+
return normalizeValue(this.data.Value)
4142
}
4243

4344
validityType (): IpnsEntry.ValidityType {
@@ -93,7 +94,7 @@ const defaultCreateOptions: CreateOptions = {
9394
* @param {number} lifetime - lifetime of the record (in milliseconds).
9495
* @param {CreateOptions} options - additional create options.
9596
*/
96-
export const create = async (peerId: PeerId, value: string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
97+
export const create = async (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
9798
// Validity in ISOString with nanoseconds precision and validity type EOL
9899
const expirationDate = new NanoDate(Date.now() + Number(lifetime))
99100
const validityType = IpnsEntry.ValidityType.EOL
@@ -113,7 +114,7 @@ export const create = async (peerId: PeerId, value: string, seq: number | bigint
113114
* @param {string} expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
114115
* @param {CreateOptions} options - additional creation options.
115116
*/
116-
export const createWithExpiration = async (peerId: PeerId, value: string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
117+
export const createWithExpiration = async (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
117118
const expirationDate = NanoDate.fromString(expiration)
118119
const validityType = IpnsEntry.ValidityType.EOL
119120

@@ -123,10 +124,10 @@ export const createWithExpiration = async (peerId: PeerId, value: string, seq: n
123124
return _create(peerId, value, seq, validityType, expirationDate, ttlNs, options)
124125
}
125126

126-
const _create = async (peerId: PeerId, value: string, seq: number | bigint, validityType: IpnsEntry.ValidityType, expirationDate: NanoDate, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
127+
const _create = async (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, validityType: IpnsEntry.ValidityType, expirationDate: NanoDate, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
127128
seq = BigInt(seq)
128129
const isoValidity = uint8ArrayFromString(expirationDate.toString())
129-
const encodedValue = uint8ArrayFromString(value)
130+
const encodedValue = uint8ArrayFromString(normalizeValue(value))
130131

131132
if (peerId.privateKey == null) {
132133
throw errCode(new Error('Missing private key'), ERRORS.ERR_MISSING_PRIVATE_KEY)
@@ -198,3 +199,22 @@ const signLegacyV1 = async (privateKey: PrivateKey, value: Uint8Array, validityT
198199
throw errCode(new Error('record signature creation failed'), ERRORS.ERR_SIGNATURE_CREATION)
199200
}
200201
}
202+
203+
/**
204+
* Normalizes the given record value. It ensures it is a string starting with '/'.
205+
* If the given value is a cid, the returned path will be '/ipfs/{cid}'.
206+
*/
207+
const normalizeValue = (value: string | Uint8Array): string => {
208+
const str = typeof value === 'string' ? value : uint8ArrayToString(value)
209+
210+
if (str.startsWith('/')) {
211+
return str
212+
}
213+
214+
try {
215+
const cid = CID.parse(str)
216+
return '/ipfs/' + cid.toV1().toString()
217+
} catch (_) {
218+
throw errCode(new Error('Value must be a valid content path starting with /'), ERRORS.ERR_INVALID_VALUE)
219+
}
220+
}

test/index.spec.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,86 @@ describe('ipns', function () {
109109
await ipnsValidator(peerIdToRoutingKey(peerId), marshal(record))
110110
})
111111

112+
it('should normalize value when creating an ipns record (string v0 cid)', async () => {
113+
const inputValue = 'QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq'
114+
const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua'
115+
const record = await ipns.create(peerId, inputValue, 0, 1000000)
116+
expect(record.value()).to.equal(expectedValue)
117+
expect(record.pb).to.deep.include({
118+
value: uint8ArrayFromString(expectedValue)
119+
})
120+
})
121+
122+
it('should normalize value when creating an ipns record (string v1 cid)', async () => {
123+
const inputValue = 'bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
124+
const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
125+
const record = await ipns.create(peerId, inputValue, 0, 1000000)
126+
expect(record.value()).to.equal(expectedValue)
127+
expect(record.pb).to.deep.include({
128+
value: uint8ArrayFromString(expectedValue)
129+
})
130+
})
131+
132+
it('should normalize value when creating an ipns record (bytes v0 cid)', async () => {
133+
const inputValue = uint8ArrayFromString('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq')
134+
const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua'
135+
const record = await ipns.create(peerId, inputValue, 0, 1000000)
136+
expect(record.value()).to.equal(expectedValue)
137+
expect(record.pb).to.deep.include({
138+
value: uint8ArrayFromString(expectedValue)
139+
})
140+
})
141+
142+
it('should normalize value when creating an ipns record (bytes v1 cid)', async () => {
143+
const inputValue = uint8ArrayFromString('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu')
144+
const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
145+
const record = await ipns.create(peerId, inputValue, 0, 1000000)
146+
expect(record.value()).to.equal(expectedValue)
147+
expect(record.pb).to.deep.include({
148+
value: uint8ArrayFromString(expectedValue)
149+
})
150+
})
151+
152+
it('should normalize value when reading an ipns record (string v0 cid)', async () => {
153+
const inputValue = 'QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq'
154+
const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua'
155+
const record = await ipns.create(peerId, inputValue, 0, 1000000)
156+
157+
// Force old value type.
158+
record.data.Value = inputValue
159+
expect(record.value()).to.equal(expectedValue)
160+
})
161+
162+
it('should normalize value when reading an ipns record (string v1 cid)', async () => {
163+
const inputValue = 'bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
164+
const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
165+
const record = await ipns.create(peerId, inputValue, 0, 1000000)
166+
167+
// Force old value type.
168+
record.data.Value = inputValue
169+
expect(record.value()).to.equal(expectedValue)
170+
})
171+
172+
it('should normalize value when reading an ipns record (bytes v0 cid)', async () => {
173+
const inputValue = uint8ArrayFromString('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq')
174+
const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua'
175+
const record = await ipns.create(peerId, inputValue, 0, 1000000)
176+
177+
// Force old value type.
178+
record.data.Value = inputValue
179+
expect(record.value()).to.equal(expectedValue)
180+
})
181+
182+
it('should normalize value when reading an ipns record (bytes v1 cid)', async () => {
183+
const inputValue = uint8ArrayFromString('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu')
184+
const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
185+
const record = await ipns.create(peerId, inputValue, 0, 1000000)
186+
187+
// Force old value type.
188+
record.data.Value = inputValue
189+
expect(record.value()).to.equal(expectedValue)
190+
})
191+
112192
it('should fail to validate a v1 (deprecated legacy) message', async () => {
113193
const sequence = 0
114194
const validity = 1000000

0 commit comments

Comments
 (0)