11import { unmarshalPrivateKey } from '@libp2p/crypto/keys'
22import { logger } from '@libp2p/logger'
3- import * as cborg from 'cborg'
43import errCode from 'err-code'
54import { Key } from 'interface-datastore/key'
65import { base32upper } from 'multiformats/bases/base32'
7- import { CID } from 'multiformats/cid'
86import * as Digest from 'multiformats/hashes/digest'
97import { identity } from 'multiformats/hashes/identity'
108import NanoDate from 'timestamp-nano'
119import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
1210import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
13- import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
1411import * as ERRORS from './errors.js'
1512import { IpnsEntry } from './pb/ipns.js'
16- import { createCborData , ipnsRecordDataForV1Sig , ipnsRecordDataForV2Sig , parseRFC3339 } from './utils.js'
13+ import { createCborData , ipnsRecordDataForV1Sig , ipnsRecordDataForV2Sig , normalizeValue } from './utils.js'
1714import type { PrivateKey } from '@libp2p/interface-keys'
1815import type { PeerId } from '@libp2p/interface-peer-id'
1916
@@ -23,49 +20,35 @@ const ID_MULTIHASH_CODE = identity.code
2320export const namespace = '/ipns/'
2421export const namespaceLength = namespace . length
2522
26- export class IPNSRecord {
27- readonly pb : IpnsEntry
28- readonly data : any
29-
30- constructor ( pb : IpnsEntry ) {
31- this . pb = pb
32-
33- if ( pb . data == null ) {
34- throw errCode ( new Error ( 'Record data is missing' ) , ERRORS . ERR_INVALID_RECORD_DATA )
35- }
36-
37- this . data = cborg . decode ( pb . data )
38- }
39-
40- value ( ) : string {
41- return normalizeValue ( this . data . Value )
42- }
43-
44- validityType ( ) : IpnsEntry . ValidityType {
45- if ( this . data . ValidityType === 0 ) {
46- return IpnsEntry . ValidityType . EOL
47- } else {
48- throw errCode ( new Error ( 'Unknown validity type' ) , ERRORS . ERR_UNRECOGNIZED_VALIDITY )
49- }
50- }
51-
52- validity ( ) : Date {
53- const validityType = this . validityType ( )
54- switch ( validityType ) {
55- case IpnsEntry . ValidityType . EOL :
56- return parseRFC3339 ( uint8ArrayToString ( this . data . Validity ) )
57- default :
58- throw errCode ( new Error ( 'Unknown validity type' ) , ERRORS . ERR_UNRECOGNIZED_VALIDITY )
59- }
60- }
23+ export interface IPNSRecord {
24+ value : string
25+ signatureV1 : Uint8Array // signature of the record
26+ validityType : IpnsEntry . ValidityType // Type of validation being used
27+ validity : NanoDate // expiration datetime for the record in RFC3339 format
28+ sequence : bigint // number representing the version of the record
29+ ttl ?: bigint // ttl in nanoseconds
30+ pubKey ?: Uint8Array // the public portion of the key that signed this record (only present if it was not embedded in the IPNS key)
31+ signatureV2 : Uint8Array // the v2 signature of the record
32+ data : Uint8Array // extensible data
33+ }
6134
62- sequence ( ) : bigint {
63- return BigInt ( this . data . Sequence ?? 0n )
64- }
35+ export interface IPNSRecordV2 {
36+ value : string
37+ signatureV2 : Uint8Array
38+ validityType : IpnsEntry . ValidityType
39+ validity : NanoDate
40+ sequence : bigint
41+ ttl ?: bigint
42+ pubKey ?: Uint8Array
43+ data : Uint8Array
44+ }
6545
66- ttl ( ) : bigint {
67- return BigInt ( this . data . TTL ?? 0n )
68- }
46+ export interface IPNSRecordData {
47+ Value : Uint8Array
48+ Validity : Uint8Array
49+ ValidityType : IpnsEntry . ValidityType
50+ Sequence : bigint
51+ TTL : bigint
6952}
7053
7154export interface IDKeys {
@@ -79,6 +62,14 @@ export interface CreateOptions {
7962 v1Compatible ?: boolean
8063}
8164
65+ export interface CreateV2OrV1Options {
66+ v1Compatible : true
67+ }
68+
69+ export interface CreateV2Options {
70+ v1Compatible : false
71+ }
72+
8273const defaultCreateOptions : CreateOptions = {
8374 v1Compatible : true
8475}
@@ -89,12 +80,14 @@ const defaultCreateOptions: CreateOptions = {
8980 * Note: This function does not embed the public key. If you want to do that, use `EmbedPublicKey`.
9081 *
9182 * @param {PeerId } peerId - peer id containing private key for signing the record.
92- * @param {string } value - value to be stored in the record.
83+ * @param {string | Uint8Array } value - content path to be stored in the record.
9384 * @param {number | bigint } seq - number representing the current version of the record.
9485 * @param {number } lifetime - lifetime of the record (in milliseconds).
9586 * @param {CreateOptions } options - additional create options.
9687 */
97- export const create = async ( peerId : PeerId , value : string | Uint8Array , seq : number | bigint , lifetime : number , options : CreateOptions = defaultCreateOptions ) : Promise < IPNSRecord > => {
88+ export async function create ( peerId : PeerId , value : string | Uint8Array , seq : number | bigint , lifetime : number , options ?: CreateV2OrV1Options ) : Promise < IPNSRecord >
89+ export async function create ( peerId : PeerId , value : string | Uint8Array , seq : number | bigint , lifetime : number , options : CreateV2Options ) : Promise < IPNSRecordV2 >
90+ export async function create ( peerId : PeerId , value : string | Uint8Array , seq : number | bigint , lifetime : number , options : CreateOptions = defaultCreateOptions ) : Promise < IPNSRecord | IPNSRecordV2 > {
9891 // Validity in ISOString with nanoseconds precision and validity type EOL
9992 const expirationDate = new NanoDate ( Date . now ( ) + Number ( lifetime ) )
10093 const validityType = IpnsEntry . ValidityType . EOL
@@ -109,12 +102,14 @@ export const create = async (peerId: PeerId, value: string | Uint8Array, seq: nu
109102 * WARNING: nano precision is not standard, make sure the value in seconds is 9 orders of magnitude lesser than the one provided.
110103 *
111104 * @param {PeerId } peerId - PeerId containing private key for signing the record.
112- * @param {string } value - value to be stored in the record.
105+ * @param {string | Uint8Array } value - content path to be stored in the record.
113106 * @param {number | bigint } seq - number representing the current version of the record.
114107 * @param {string } expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
115108 * @param {CreateOptions } options - additional creation options.
116109 */
117- export const createWithExpiration = async ( peerId : PeerId , value : string | Uint8Array , seq : number | bigint , expiration : string , options : CreateOptions = defaultCreateOptions ) : Promise < IPNSRecord > => {
110+ export async function createWithExpiration ( peerId : PeerId , value : string | Uint8Array , seq : number | bigint , expiration : string , options ?: CreateV2OrV1Options ) : Promise < IPNSRecord >
111+ export async function createWithExpiration ( peerId : PeerId , value : string | Uint8Array , seq : number | bigint , expiration : string , options : CreateV2Options ) : Promise < IPNSRecordV2 >
112+ export async function createWithExpiration ( peerId : PeerId , value : string | Uint8Array , seq : number | bigint , expiration : string , options : CreateOptions = defaultCreateOptions ) : Promise < IPNSRecord | IPNSRecordV2 > {
118113 const expirationDate = NanoDate . fromString ( expiration )
119114 const validityType = IpnsEntry . ValidityType . EOL
120115
@@ -124,10 +119,11 @@ export const createWithExpiration = async (peerId: PeerId, value: string | Uint8
124119 return _create ( peerId , value , seq , validityType , expirationDate , ttlNs , options )
125120}
126121
127- const _create = async ( peerId : PeerId , value : string | Uint8Array , seq : number | bigint , validityType : IpnsEntry . ValidityType , expirationDate : NanoDate , ttl : bigint , options : CreateOptions = defaultCreateOptions ) : Promise < IPNSRecord > => {
122+ const _create = async ( peerId : PeerId , value : string | Uint8Array , seq : number | bigint , validityType : IpnsEntry . ValidityType , expirationDate : NanoDate , ttl : bigint , options : CreateOptions = defaultCreateOptions ) : Promise < IPNSRecord | IPNSRecordV2 > => {
128123 seq = BigInt ( seq )
129124 const isoValidity = uint8ArrayFromString ( expirationDate . toString ( ) )
130- const encodedValue = uint8ArrayFromString ( normalizeValue ( value ) )
125+ const normalizedValue = normalizeValue ( value )
126+ const encodedValue = uint8ArrayFromString ( normalizedValue )
131127
132128 if ( peerId . privateKey == null ) {
133129 throw errCode ( new Error ( 'Missing private key' ) , ERRORS . ERR_MISSING_PRIVATE_KEY )
@@ -137,34 +133,54 @@ const _create = async (peerId: PeerId, value: string | Uint8Array, seq: number |
137133 const data = createCborData ( encodedValue , isoValidity , validityType , seq , ttl )
138134 const sigData = ipnsRecordDataForV2Sig ( data )
139135 const signatureV2 = await privateKey . sign ( sigData )
140-
141- const pb : IpnsEntry = {
142- signatureV2,
143- data
144- }
145-
146- if ( options . v1Compatible === true ) {
147- const signatureV1 = await signLegacyV1 ( privateKey , encodedValue , validityType , isoValidity )
148- pb . value = encodedValue
149- pb . validity = isoValidity
150- pb . validityType = validityType
151- pb . signatureV1 = signatureV1
152- pb . sequence = seq
153- pb . ttl = ttl
154- }
136+ let pubKey : Uint8Array | undefined
155137
156138 // if we cannot derive the public key from the PeerId (e.g. RSA PeerIDs),
157139 // we have to embed it in the IPNS record
158140 if ( peerId . publicKey != null ) {
159141 const digest = Digest . decode ( peerId . toBytes ( ) )
160142
161143 if ( digest . code !== ID_MULTIHASH_CODE || ! uint8ArrayEquals ( peerId . publicKey , digest . digest ) ) {
162- pb . pubKey = peerId . publicKey
144+ pubKey = peerId . publicKey
163145 }
164146 }
165147
166- log ( 'ipns record for %b created' , value )
167- return new IPNSRecord ( pb )
148+ if ( options . v1Compatible === true ) {
149+ const signatureV1 = await signLegacyV1 ( privateKey , encodedValue , validityType , isoValidity )
150+
151+ const record : IPNSRecord = {
152+ value : normalizedValue ,
153+ signatureV1,
154+ validity : expirationDate ,
155+ validityType,
156+ sequence : seq ,
157+ ttl,
158+ signatureV2,
159+ data
160+ }
161+
162+ if ( pubKey != null ) {
163+ record . pubKey = pubKey
164+ }
165+
166+ return record
167+ } else {
168+ const record : IPNSRecordV2 = {
169+ value : normalizedValue ,
170+ validity : expirationDate ,
171+ validityType,
172+ sequence : seq ,
173+ ttl,
174+ signatureV2,
175+ data
176+ }
177+
178+ if ( pubKey != null ) {
179+ record . pubKey = pubKey
180+ }
181+
182+ return record
183+ }
168184}
169185
170186/**
@@ -199,22 +215,3 @@ const signLegacyV1 = async (privateKey: PrivateKey, value: Uint8Array, validityT
199215 throw errCode ( new Error ( 'record signature creation failed' ) , ERRORS . ERR_SIGNATURE_CREATION )
200216 }
201217}
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- }
0 commit comments