From 6a85abfd299ce896296528b1444cb73fd222704d Mon Sep 17 00:00:00 2001 From: achingbrain Date: Wed, 30 Aug 2023 12:32:37 +0100 Subject: [PATCH 1/4] fix!: only accept CIDs, PeerIds or strings as values This module has accepted `Uint8Array`s as values to store in IPNS records which means it has been mis-used to create records with raw CID bytes. To ensure this doesn't happen in future, accept only CIDs, PeerIds or arbitrary path strings prefixed with `"/"`. --- src/index.ts | 31 ++++++++++++------ src/utils.ts | 57 ++++++++++++++++++++++++--------- test/index.spec.ts | 78 ++++++++++++++++++++++++++++++++++++---------- 3 files changed, 126 insertions(+), 40 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0bb343c..af885b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ import { IpnsEntry } from './pb/ipns.js' import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, normalizeValue } from './utils.js' import type { PrivateKey } from '@libp2p/interface-keys' import type { PeerId } from '@libp2p/interface-peer-id' +import type { CID } from 'multiformats/cid' const log = logger('ipns') const ID_MULTIHASH_CODE = identity.code @@ -79,15 +80,21 @@ const defaultCreateOptions: CreateOptions = { * The IPNS Record validity should follow the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision. * Note: This function does not embed the public key. If you want to do that, use `EmbedPublicKey`. * + * The passed value can be a CID, a PeerID or an arbitrary string path. + * + * * CIDs will be converted to v1 and stored in the record as a string similar to: `/ipfs/${cid}` + * * PeerIDs will create recursive records, eg. the record value will be `/ipns/${peerId}` + * * String paths will be stored in the record as-is, but they must start with `"/"` + * * @param {PeerId} peerId - peer id containing private key for signing the record. - * @param {string | Uint8Array} value - content path to be stored in the record. + * @param {CID | PeerId | string} value - content to be stored in the record. * @param {number | bigint} seq - number representing the current version of the record. * @param {number} lifetime - lifetime of the record (in milliseconds). * @param {CreateOptions} options - additional create options. */ -export async function create (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise -export async function create (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise -export async function create (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise { +export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise +export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise +export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise { // Validity in ISOString with nanoseconds precision and validity type EOL const expirationDate = new NanoDate(Date.now() + Number(lifetime)) const validityType = IpnsEntry.ValidityType.EOL @@ -101,15 +108,21 @@ export async function create (peerId: PeerId, value: string | Uint8Array, seq: n * Same as create(), but instead of generating a new Date, it receives the intended expiration time * WARNING: nano precision is not standard, make sure the value in seconds is 9 orders of magnitude lesser than the one provided. * + * The passed value can be a CID, a PeerID or an arbitrary string path. + * + * * CIDs will be converted to v1 and stored in the record as a string similar to: `/ipfs/${cid}` + * * PeerIDs will create recursive records, eg. the record value will be `/ipns/${peerId}` + * * String paths will be stored in the record as-is, but they must start with `"/"` + * * @param {PeerId} peerId - PeerId containing private key for signing the record. - * @param {string | Uint8Array} value - content path to be stored in the record. + * @param {CID | PeerId | string} value - content to be stored in the record. * @param {number | bigint} seq - number representing the current version of the record. * @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 async function createWithExpiration (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise -export async function createWithExpiration (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, expiration: string, options: CreateV2Options): Promise -export async function createWithExpiration (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise { +export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise +export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise +export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise { const expirationDate = NanoDate.fromString(expiration) const validityType = IpnsEntry.ValidityType.EOL @@ -119,7 +132,7 @@ export async function createWithExpiration (peerId: PeerId, value: string | Uint return _create(peerId, value, seq, validityType, expirationDate, ttlNs, options) } -const _create = async (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, validityType: IpnsEntry.ValidityType, expirationDate: NanoDate, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { +const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, expirationDate: NanoDate, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { seq = BigInt(seq) const isoValidity = uint8ArrayFromString(expirationDate.toString()) const normalizedValue = normalizeValue(value) diff --git a/src/utils.ts b/src/utils.ts index 8999042..6d5a783 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import { unmarshalPublicKey } from '@libp2p/crypto/keys' +import { isPeerId, type PeerId } from '@libp2p/interface-peer-id' import { logger } from '@libp2p/logger' import { peerIdFromBytes, peerIdFromKeys } from '@libp2p/peer-id' import * as cborg from 'cborg' @@ -13,7 +14,6 @@ import * as ERRORS from './errors.js' import { IpnsEntry } from './pb/ipns.js' import type { IPNSRecord, IPNSRecordV2, IPNSRecordData } from './index.js' import type { PublicKey } from '@libp2p/interface-keys' -import type { PeerId } from '@libp2p/interface-peer-id' const log = logger('ipns:utils') const IPNS_PREFIX = uint8ArrayFromString('/ipns/') @@ -164,7 +164,7 @@ export const unmarshal = (buf: Uint8Array): (IPNSRecord | IPNSRecordV2) => { } const data = parseCborData(message.data) - const value = normalizeValue(data.Value ?? new Uint8Array(0)) + const value = normalizeValue(data.Value) let validity try { @@ -259,22 +259,51 @@ export const parseCborData = (buf: Uint8Array): IPNSRecordData => { } /** - * 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}'. + * Normalizes the given record value. It ensures it is a PeerID, a CID or a + * string starting with '/'. PeerIDs become `/ipns/${peerId}`, CIDs become + * `/ipfs/${cidAsV1}`. */ -export const normalizeValue = (value: string | Uint8Array): string => { - const str = typeof value === 'string' ? value : uint8ArrayToString(value) +export const normalizeValue = (value?: CID | PeerId | string | Uint8Array): string => { + if (value != null) { + // if we have a PeerId, turn it into an ipns path + if (isPeerId(value)) { + return `/ipns/${value.toString()}` + } - if (str.startsWith('/')) { - return str - } + // if the value is bytes, stringify it and see if we have a path + if (value instanceof Uint8Array) { + const string = uint8ArrayToString(value) - 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) + if (string.startsWith('/')) { + value = string + } + } + + // if we have a path, check it is a valid path + const string = value.toString().trim() + if (string.startsWith('/') && string.length > 1) { + return string + } + + // if we have a CID, turn it into an ipfs path + const cid = CID.asCID(value) + if (cid != null) { + return `/ipfs/${cid.toV1().toString()}` + } + + // try parsing what we have as CID bytes or a CID string + try { + if (value instanceof Uint8Array) { + return `/ipfs/${CID.decode(value).toV1().toString()}` + } + + return `/ipfs/${CID.parse(string).toV1().toString()}` + } catch { + // fall through + } } + + throw errCode(new Error('Value must be a valid content path starting with /'), ERRORS.ERR_INVALID_VALUE) } const validateCborDataMatchesPbData = (entry: IpnsEntry): void => { diff --git a/test/index.spec.ts b/test/index.spec.ts index 5a8bef9..5e42126 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,17 +1,19 @@ /* eslint-env mocha */ import { randomBytes } from '@libp2p/crypto' -import { generateKeyPair } from '@libp2p/crypto/keys' +import { generateKeyPair, unmarshalPrivateKey } from '@libp2p/crypto/keys' import { peerIdFromKeys, peerIdFromString } from '@libp2p/peer-id' import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { expect } from 'aegir/chai' +import * as cbor from 'cborg' import { base58btc } from 'multiformats/bases/base58' +import { CID } from 'multiformats/cid' import { toString as uint8ArrayToString } from 'uint8arrays' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import * as ERRORS from '../src/errors.js' import * as ipns from '../src/index.js' import { IpnsEntry } from '../src/pb/ipns.js' -import { extractPublicKey, peerIdToRoutingKey, parseCborData, createCborData } from '../src/utils.js' +import { extractPublicKey, peerIdToRoutingKey, parseCborData, createCborData, ipnsRecordDataForV2Sig } from '../src/utils.js' import { ipnsValidator } from '../src/validator.js' import type { PeerId } from '@libp2p/interface-peer-id' @@ -142,37 +144,37 @@ describe('ipns', function () { await ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record)) }) - it('should normalize value when creating an ipns record (string v0 cid)', async () => { - const inputValue = 'QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' - const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' + it('should normalize value when creating an ipns record (arbitrary string path)', async () => { + const inputValue = '/foo/bar/baz' + const expectedValue = '/foo/bar/baz' const record = await ipns.create(peerId, inputValue, 0, 1000000) expect(record.value).to.equal(expectedValue) }) - it('should normalize value when creating an ipns record (string v1 cid)', async () => { - const inputValue = 'bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' - const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' + it('should normalize value when creating a recursive ipns record (peer id)', async () => { + const inputValue = await createEd25519PeerId() + const expectedValue = `/ipns/${inputValue.toString()}` const record = await ipns.create(peerId, inputValue, 0, 1000000) expect(record.value).to.equal(expectedValue) }) - it('should normalize value when creating an ipns record (bytes v0 cid)', async () => { - const inputValue = uint8ArrayFromString('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq') + it('should normalize value when creating an ipns record (v0 cid)', async () => { + const inputValue = CID.parse('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq') const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' const record = await ipns.create(peerId, inputValue, 0, 1000000) expect(record.value).to.equal(expectedValue) }) - it('should normalize value when creating an ipns record (bytes v1 cid)', async () => { - const inputValue = uint8ArrayFromString('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') + it('should normalize value when creating an ipns record (v1 cid)', async () => { + const inputValue = CID.parse('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' const record = await ipns.create(peerId, inputValue, 0, 1000000) expect(record.value).to.equal(expectedValue) }) - it('should normalize value when reading an ipns record (bytes v0 cid)', async () => { - const inputValue = 'QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' - const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' + it('should normalize value when reading an ipns record (string v0 cid path)', async () => { + const inputValue = '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' + const expectedValue = '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' const record = await ipns.create(peerId, inputValue, 0, 1000000) const pb = IpnsEntry.decode(ipns.marshal(record)) @@ -183,8 +185,8 @@ describe('ipns', function () { expect(modifiedRecord.value).to.equal(expectedValue) }) - it('should normalize value when reading an ipns record (bytes v1 cid)', async () => { - const inputValue = 'bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' + it('should normalize value when reading an ipns record (string v1 cid path)', async () => { + const inputValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' const record = await ipns.create(peerId, inputValue, 0, 1000000) @@ -196,6 +198,20 @@ describe('ipns', function () { expect(modifiedRecord.value).to.equal(expectedValue) }) + it('should fail to normalize non-path value', async () => { + const inputValue = 'hello' + + await expect(ipns.create(peerId, inputValue, 0, 1000000)).to.eventually.be.rejected + .with.property('code', ERRORS.ERR_INVALID_VALUE) + }) + + it('should fail to normalize path value that is too short', async () => { + const inputValue = '/' + + await expect(ipns.create(peerId, inputValue, 0, 1000000)).to.eventually.be.rejected + .with.property('code', ERRORS.ERR_INVALID_VALUE) + }) + it('should fail to validate a v1 (deprecated legacy) message', async () => { const sequence = 0 const validity = 1000000 @@ -345,4 +361,32 @@ describe('ipns', function () { bytes: peerId.publicKey }) }) + + it('should unmarshal a record with raw CID bytes', async () => { + // we may encounter these in the wild due to older versions of this module + // but IPNS records should have string path values + + // create a dummy record with an arbitrary string path + const input = await ipns.create(peerId, '/foo', 0n, 10000, { + v1Compatible: false + }) + + // we will store the raw bytes from this CID + const cid = CID.parse('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') + + // override data with raw CID bytes + const data = cbor.decode(input.data) + data.Value = cid.bytes + input.data = cbor.encode(data) + + // re-sign record + const privateKey = await unmarshalPrivateKey(peerId.privateKey ?? new Uint8Array(0)) + const sigData = ipnsRecordDataForV2Sig(input.data) + input.signatureV2 = await privateKey.sign(sigData) + + const buf = ipns.marshal(input) + const record = ipns.unmarshal(buf) + + expect(record).to.have.property('value', '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') + }) }) From 968e537b66a1b0fc18b5e24e21dc9c59a09c2bfc Mon Sep 17 00:00:00 2001 From: achingbrain Date: Mon, 4 Sep 2023 12:43:31 +0100 Subject: [PATCH 2/4] fix: treat a PeerId encoded as CID as a PeerId --- src/utils.ts | 7 +++++++ test/index.spec.ts | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/utils.ts b/src/utils.ts index 6d5a783..54acaae 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,6 +4,7 @@ import { logger } from '@libp2p/logger' import { peerIdFromBytes, peerIdFromKeys } from '@libp2p/peer-id' import * as cborg from 'cborg' import errCode from 'err-code' +import { base58btc } from 'multiformats/bases/base58' import { CID } from 'multiformats/cid' import NanoDate from 'timestamp-nano' import { concat as uint8ArrayConcat } from 'uint8arrays/concat' @@ -17,6 +18,7 @@ import type { PublicKey } from '@libp2p/interface-keys' const log = logger('ipns:utils') const IPNS_PREFIX = uint8ArrayFromString('/ipns/') +const LIBP2P_CID_CODEC = 114 /** * Convert a JavaScript date into an `RFC3339Nano` formatted @@ -288,6 +290,11 @@ export const normalizeValue = (value?: CID | PeerId | string | Uint8Array): stri // if we have a CID, turn it into an ipfs path const cid = CID.asCID(value) if (cid != null) { + // PeerID encoded as a CID + if (cid.code === LIBP2P_CID_CODEC) { + return `/ipns/${base58btc.encode(cid.multihash.bytes).substring(1)}` + } + return `/ipfs/${cid.toV1().toString()}` } diff --git a/test/index.spec.ts b/test/index.spec.ts index 5e42126..ff2a959 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -158,6 +158,13 @@ describe('ipns', function () { expect(record.value).to.equal(expectedValue) }) + it('should normalize value when creating a recursive ipns record (peer id as CID)', async () => { + const inputValue = await createEd25519PeerId() + const expectedValue = `/ipns/${inputValue.toString()}` + const record = await ipns.create(peerId, inputValue.toCID(), 0, 1000000) + expect(record.value).to.equal(expectedValue) + }) + it('should normalize value when creating an ipns record (v0 cid)', async () => { const inputValue = CID.parse('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq') const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' From 76b07998ea1623b6043d188766da8cdbe30401c5 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 8 Sep 2023 16:07:09 +0100 Subject: [PATCH 3/4] fix: encode all /ipns/* keys as base36 encoded CIDv1 libp2p-cid --- src/utils.ts | 6 +++--- test/index.spec.ts | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 54acaae..9f156a1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,7 +4,7 @@ import { logger } from '@libp2p/logger' import { peerIdFromBytes, peerIdFromKeys } from '@libp2p/peer-id' import * as cborg from 'cborg' import errCode from 'err-code' -import { base58btc } from 'multiformats/bases/base58' +import { base36 } from 'multiformats/bases/base36' import { CID } from 'multiformats/cid' import NanoDate from 'timestamp-nano' import { concat as uint8ArrayConcat } from 'uint8arrays/concat' @@ -269,7 +269,7 @@ export const normalizeValue = (value?: CID | PeerId | string | Uint8Array): stri if (value != null) { // if we have a PeerId, turn it into an ipns path if (isPeerId(value)) { - return `/ipns/${value.toString()}` + return `/ipns/${value.toCID().toString(base36)}` } // if the value is bytes, stringify it and see if we have a path @@ -292,7 +292,7 @@ export const normalizeValue = (value?: CID | PeerId | string | Uint8Array): stri if (cid != null) { // PeerID encoded as a CID if (cid.code === LIBP2P_CID_CODEC) { - return `/ipns/${base58btc.encode(cid.multihash.bytes).substring(1)}` + return `/ipns/${cid.toString(base36)}` } return `/ipfs/${cid.toV1().toString()}` diff --git a/test/index.spec.ts b/test/index.spec.ts index ff2a959..c3e0671 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -6,6 +6,7 @@ import { peerIdFromKeys, peerIdFromString } from '@libp2p/peer-id' import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { expect } from 'aegir/chai' import * as cbor from 'cborg' +import { base36 } from 'multiformats/bases/base36' import { base58btc } from 'multiformats/bases/base58' import { CID } from 'multiformats/cid' import { toString as uint8ArrayToString } from 'uint8arrays' @@ -153,14 +154,14 @@ describe('ipns', function () { it('should normalize value when creating a recursive ipns record (peer id)', async () => { const inputValue = await createEd25519PeerId() - const expectedValue = `/ipns/${inputValue.toString()}` + const expectedValue = `/ipns/${inputValue.toCID().toString(base36)}` const record = await ipns.create(peerId, inputValue, 0, 1000000) expect(record.value).to.equal(expectedValue) }) it('should normalize value when creating a recursive ipns record (peer id as CID)', async () => { const inputValue = await createEd25519PeerId() - const expectedValue = `/ipns/${inputValue.toString()}` + const expectedValue = `/ipns/${inputValue.toCID().toString(base36)}` const record = await ipns.create(peerId, inputValue.toCID(), 0, 1000000) expect(record.value).to.equal(expectedValue) }) From 4f832ba4caead337764f9ae927c3da4837b7cffd Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Tue, 12 Sep 2023 17:22:17 +0200 Subject: [PATCH 4/4] docs: update jsdoc about --- src/index.ts | 4 ++-- src/utils.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index af885b7..2cb43ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -83,7 +83,7 @@ const defaultCreateOptions: CreateOptions = { * The passed value can be a CID, a PeerID or an arbitrary string path. * * * CIDs will be converted to v1 and stored in the record as a string similar to: `/ipfs/${cid}` - * * PeerIDs will create recursive records, eg. the record value will be `/ipns/${peerId}` + * * PeerIDs will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}` * * String paths will be stored in the record as-is, but they must start with `"/"` * * @param {PeerId} peerId - peer id containing private key for signing the record. @@ -111,7 +111,7 @@ export async function create (peerId: PeerId, value: CID | PeerId | string, seq: * The passed value can be a CID, a PeerID or an arbitrary string path. * * * CIDs will be converted to v1 and stored in the record as a string similar to: `/ipfs/${cid}` - * * PeerIDs will create recursive records, eg. the record value will be `/ipns/${peerId}` + * * PeerIDs will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}` * * String paths will be stored in the record as-is, but they must start with `"/"` * * @param {PeerId} peerId - PeerId containing private key for signing the record. diff --git a/src/utils.ts b/src/utils.ts index 9f156a1..bea9419 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -262,8 +262,8 @@ export const parseCborData = (buf: Uint8Array): IPNSRecordData => { /** * Normalizes the given record value. It ensures it is a PeerID, a CID or a - * string starting with '/'. PeerIDs become `/ipns/${peerId}`, CIDs become - * `/ipfs/${cidAsV1}`. + * string starting with '/'. PeerIDs become `/ipns/${cidV1Libp2pKey}`, + * CIDs become `/ipfs/${cidAsV1}`. */ export const normalizeValue = (value?: CID | PeerId | string | Uint8Array): string => { if (value != null) {