Skip to content

Commit

Permalink
feat: normalize value at creation and read time
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Aug 25, 2023
1 parent b6566f7 commit ba7e65c
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const ERR_PEER_ID_FROM_PUBLIC_KEY = 'ERR_PEER_ID_FROM_PUBLIC_KEY'
export const ERR_PUBLIC_KEY_FROM_ID = 'ERR_PUBLIC_KEY_FROM_ID'
export const ERR_UNDEFINED_PARAMETER = 'ERR_UNDEFINED_PARAMETER'
export const ERR_INVALID_RECORD_DATA = 'ERR_INVALID_RECORD_DATA'
export const ERR_INVALID_VALUE = 'ERR_INVALID_VALUE'
export const ERR_INVALID_EMBEDDED_KEY = 'ERR_INVALID_EMBEDDED_KEY'
export const ERR_MISSING_PRIVATE_KEY = 'ERR_MISSING_PRIVATE_KEY'
export const ERR_RECORD_TOO_LARGE = 'ERR_RECORD_TOO_LARGE'
32 changes: 26 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as cborg from 'cborg'
import errCode from 'err-code'
import { Key } from 'interface-datastore/key'
import { base32upper } from 'multiformats/bases/base32'
import { CID } from 'multiformats/cid'
import * as Digest from 'multiformats/hashes/digest'
import { identity } from 'multiformats/hashes/identity'
import NanoDate from 'timestamp-nano'
Expand All @@ -24,7 +25,7 @@ export const namespaceLength = namespace.length

export class IPNSRecord {
readonly pb: IpnsEntry
private readonly data: any
readonly data: any

constructor (pb: IpnsEntry) {
this.pb = pb
Expand All @@ -37,7 +38,7 @@ export class IPNSRecord {
}

value (): string {
return uint8ArrayToString(this.data.Value)
return normalizeValue(this.data.Value)
}

validityType (): IpnsEntry.ValidityType {
Expand Down Expand Up @@ -93,7 +94,7 @@ const defaultCreateOptions: CreateOptions = {
* @param {number} lifetime - lifetime of the record (in milliseconds).
* @param {CreateOptions} options - additional create options.
*/
export const create = async (peerId: PeerId, value: string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
export const create = async (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
// Validity in ISOString with nanoseconds precision and validity type EOL
const expirationDate = new NanoDate(Date.now() + Number(lifetime))
const validityType = IpnsEntry.ValidityType.EOL
Expand All @@ -113,7 +114,7 @@ export const create = async (peerId: PeerId, value: string, seq: number | bigint
* @param {string} expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
* @param {CreateOptions} options - additional creation options.
*/
export const createWithExpiration = async (peerId: PeerId, value: string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
export const createWithExpiration = async (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
const expirationDate = NanoDate.fromString(expiration)
const validityType = IpnsEntry.ValidityType.EOL

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

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

if (peerId.privateKey == null) {
throw errCode(new Error('Missing private key'), ERRORS.ERR_MISSING_PRIVATE_KEY)
Expand Down Expand Up @@ -198,3 +199,22 @@ const signLegacyV1 = async (privateKey: PrivateKey, value: Uint8Array, validityT
throw errCode(new Error('record signature creation failed'), ERRORS.ERR_SIGNATURE_CREATION)
}
}

/**
* Normalizes the given record value. It ensures it is a string starting with '/'.
* If the given value is a cid, the returned path will be '/ipfs/{cid}'.
*/
const normalizeValue = (value: string | Uint8Array): string => {
const str = typeof value === 'string' ? value : uint8ArrayToString(value)

if (str.startsWith('/')) {
return str
}

try {
const cid = CID.parse(str)
return '/ipfs/' + cid.toV1().toString()
} catch (_) {
throw errCode(new Error('Value must be a valid content path starting with /'), ERRORS.ERR_INVALID_VALUE)
}
}
80 changes: 80 additions & 0 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,86 @@ describe('ipns', function () {
await ipnsValidator(peerIdToRoutingKey(peerId), marshal(record))
})

it('should normalize value when creating an ipns record (string v0 cid)', async () => {
const inputValue = 'QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq'
const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua'
const record = await ipns.create(peerId, inputValue, 0, 1000000)
expect(record.value()).to.equal(expectedValue)
expect(record.pb).to.deep.include({
value: uint8ArrayFromString(expectedValue)
})
})

it('should normalize value when creating an ipns record (string v1 cid)', async () => {
const inputValue = 'bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
const record = await ipns.create(peerId, inputValue, 0, 1000000)
expect(record.value()).to.equal(expectedValue)
expect(record.pb).to.deep.include({
value: uint8ArrayFromString(expectedValue)
})
})

it('should normalize value when creating an ipns record (bytes v0 cid)', async () => {
const inputValue = uint8ArrayFromString('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq')
const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua'
const record = await ipns.create(peerId, inputValue, 0, 1000000)
expect(record.value()).to.equal(expectedValue)
expect(record.pb).to.deep.include({
value: uint8ArrayFromString(expectedValue)
})
})

it('should normalize value when creating an ipns record (bytes v1 cid)', async () => {
const inputValue = uint8ArrayFromString('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu')
const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
const record = await ipns.create(peerId, inputValue, 0, 1000000)
expect(record.value()).to.equal(expectedValue)
expect(record.pb).to.deep.include({
value: uint8ArrayFromString(expectedValue)
})
})

it('should normalize value when reading an ipns record (string v0 cid)', async () => {
const inputValue = 'QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq'
const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua'
const record = await ipns.create(peerId, inputValue, 0, 1000000)

// Force old value type.
record.data.Value = inputValue
expect(record.value()).to.equal(expectedValue)
})

it('should normalize value when reading an ipns record (string v1 cid)', async () => {
const inputValue = 'bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
const record = await ipns.create(peerId, inputValue, 0, 1000000)

// Force old value type.
record.data.Value = inputValue
expect(record.value()).to.equal(expectedValue)
})

it('should normalize value when reading an ipns record (bytes v0 cid)', async () => {
const inputValue = uint8ArrayFromString('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq')
const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua'
const record = await ipns.create(peerId, inputValue, 0, 1000000)

// Force old value type.
record.data.Value = inputValue
expect(record.value()).to.equal(expectedValue)
})

it('should normalize value when reading an ipns record (bytes v1 cid)', async () => {
const inputValue = uint8ArrayFromString('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu')
const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
const record = await ipns.create(peerId, inputValue, 0, 1000000)

// Force old value type.
record.data.Value = inputValue
expect(record.value()).to.equal(expectedValue)
})

it('should fail to validate a v1 (deprecated legacy) message', async () => {
const sequence = 0
const validity = 1000000
Expand Down

0 comments on commit ba7e65c

Please sign in to comment.