11import { unmarshalPrivateKey } from '@libp2p/crypto/keys'
22import { logger } from '@libp2p/logger'
3+ import * as cborg from 'cborg'
34import errCode from 'err-code'
45import { Key } from 'interface-datastore/key'
56import { base32upper } from 'multiformats/bases/base32'
@@ -8,9 +9,10 @@ import { identity } from 'multiformats/hashes/identity'
89import NanoDate from 'timestamp-nano'
910import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
1011import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
12+ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
1113import * as ERRORS from './errors.js'
1214import { IpnsEntry } from './pb/ipns.js'
13- import { createCborData , ipnsEntryDataForV1Sig , ipnsEntryDataForV2Sig } from './utils.js'
15+ import { createCborData , ipnsEntryDataForV1Sig , ipnsEntryDataForV2Sig , parseRFC3339 } from './utils.js'
1416import type { PrivateKey } from '@libp2p/interface-keys'
1517import type { PeerId } from '@libp2p/interface-peer-id'
1618
@@ -20,24 +22,49 @@ const ID_MULTIHASH_CODE = identity.code
2022export const namespace = '/ipns/'
2123export const namespaceLength = namespace . length
2224
23- export interface IPNSEntry {
24- value : Uint8Array
25- signature : Uint8Array // signature of the record
26- validityType : IpnsEntry . ValidityType // Type of validation being used
27- validity : Uint8Array // 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- }
25+ export class IPNSRecord {
26+ readonly pb : IpnsEntry
27+ private readonly data : any
28+
29+ constructor ( pb : IpnsEntry ) {
30+ this . pb = pb
31+
32+ if ( pb . data == null ) {
33+ throw errCode ( new Error ( 'Record data is missing' ) , ERRORS . ERR_INVALID_RECORD_DATA )
34+ }
35+
36+ this . data = cborg . decode ( pb . data )
37+ }
3438
35- export interface IPNSEntryData {
36- Value : Uint8Array
37- Validity : Uint8Array
38- ValidityType : IpnsEntry . ValidityType
39- Sequence : bigint
40- TTL : bigint
39+ value ( ) : string {
40+ return uint8ArrayToString ( this . data . Value )
41+ }
42+
43+ validityType ( ) : IpnsEntry . ValidityType {
44+ if ( this . data . ValidityType === 0 ) {
45+ return IpnsEntry . ValidityType . EOL
46+ } else {
47+ throw errCode ( new Error ( 'Unknown validity type' ) , ERRORS . ERR_UNRECOGNIZED_VALIDITY )
48+ }
49+ }
50+
51+ validity ( ) : Date {
52+ const validityType = this . validityType ( )
53+ switch ( validityType ) {
54+ case IpnsEntry . ValidityType . EOL :
55+ return parseRFC3339 ( uint8ArrayToString ( this . data . Validity ) )
56+ default :
57+ throw errCode ( new Error ( 'Unknown validity type' ) , ERRORS . ERR_UNRECOGNIZED_VALIDITY )
58+ }
59+ }
60+
61+ sequence ( ) : bigint {
62+ return BigInt ( this . data . Sequence ?? 0n )
63+ }
64+
65+ ttl ( ) : bigint {
66+ return BigInt ( this . data . TTL ?? 0n )
67+ }
4168}
4269
4370export interface IDKeys {
@@ -47,6 +74,14 @@ export interface IDKeys {
4774 ipnsKey : Key
4875}
4976
77+ export interface CreateOptions {
78+ v1Compatible ?: boolean
79+ }
80+
81+ const defaultCreateOptions : CreateOptions = {
82+ v1Compatible : true
83+ }
84+
5085/**
5186 * Creates a new ipns entry and signs it with the given private key.
5287 * The ipns entry validity should follow the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
@@ -56,15 +91,16 @@ export interface IDKeys {
5691 * @param {Uint8Array } value - value to be stored in the record.
5792 * @param {number | bigint } seq - number representing the current version of the record.
5893 * @param {number } lifetime - lifetime of the record (in milliseconds).
94+ * @param {CreateOptions } options - additional create options.
5995 */
60- export const create = async ( peerId : PeerId , value : Uint8Array , seq : number | bigint , lifetime : number ) : Promise < IPNSEntry > => {
96+ export const create = async ( peerId : PeerId , value : Uint8Array , seq : number | bigint , lifetime : number , options : CreateOptions = defaultCreateOptions ) : Promise < IPNSRecord > => {
6197 // Validity in ISOString with nanoseconds precision and validity type EOL
6298 const expirationDate = new NanoDate ( Date . now ( ) + Number ( lifetime ) )
6399 const validityType = IpnsEntry . ValidityType . EOL
64100 const [ ms , ns ] = lifetime . toString ( ) . split ( '.' )
65101 const lifetimeNs = ( BigInt ( ms ) * BigInt ( 100000 ) ) + BigInt ( ns ?? '0' )
66102
67- return _create ( peerId , value , seq , validityType , expirationDate , lifetimeNs )
103+ return _create ( peerId , value , seq , validityType , expirationDate , lifetimeNs , options )
68104}
69105
70106/**
@@ -75,18 +111,19 @@ export const create = async (peerId: PeerId, value: Uint8Array, seq: number | bi
75111 * @param {Uint8Array } value - value to be stored in the record.
76112 * @param {number | bigint } seq - number representing the current version of the record.
77113 * @param {string } expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
114+ * @param {CreateOptions } options - additional create options.
78115 */
79- export const createWithExpiration = async ( peerId : PeerId , value : Uint8Array , seq : number | bigint , expiration : string ) : Promise < IPNSEntry > => {
116+ export const createWithExpiration = async ( peerId : PeerId , value : Uint8Array , seq : number | bigint , expiration : string , options : CreateOptions = defaultCreateOptions ) : Promise < IPNSRecord > => {
80117 const expirationDate = NanoDate . fromString ( expiration )
81118 const validityType = IpnsEntry . ValidityType . EOL
82119
83120 const ttlMs = expirationDate . toDate ( ) . getTime ( ) - Date . now ( )
84121 const ttlNs = ( BigInt ( ttlMs ) * BigInt ( 100000 ) ) + BigInt ( expirationDate . getNano ( ) )
85122
86- return _create ( peerId , value , seq , validityType , expirationDate , ttlNs )
123+ return _create ( peerId , value , seq , validityType , expirationDate , ttlNs , options )
87124}
88125
89- const _create = async ( peerId : PeerId , value : Uint8Array , seq : number | bigint , validityType : IpnsEntry . ValidityType , expirationDate : NanoDate , ttl : bigint ) : Promise < IPNSEntry > => {
126+ const _create = async ( peerId : PeerId , value : Uint8Array , seq : number | bigint , validityType : IpnsEntry . ValidityType , expirationDate : NanoDate , ttl : bigint , options : CreateOptions = defaultCreateOptions ) : Promise < IPNSRecord > => {
90127 seq = BigInt ( seq )
91128 const isoValidity = uint8ArrayFromString ( expirationDate . toString ( ) )
92129
@@ -95,22 +132,25 @@ const _create = async (peerId: PeerId, value: Uint8Array, seq: number | bigint,
95132 }
96133
97134 const privateKey = await unmarshalPrivateKey ( peerId . privateKey )
98- const signatureV1 = await signLegacyV1 ( privateKey , value , validityType , isoValidity )
99135 const data = createCborData ( value , isoValidity , validityType , seq , ttl )
100136 const sigData = ipnsEntryDataForV2Sig ( data )
101137 const signatureV2 = await privateKey . sign ( sigData )
102138
103- const entry : IPNSEntry = {
104- value,
105- signature : signatureV1 ,
106- validityType,
107- validity : isoValidity ,
108- sequence : seq ,
109- ttl,
139+ const entry : IpnsEntry = {
110140 signatureV2,
111141 data
112142 }
113143
144+ if ( options . v1Compatible === true ) {
145+ const signatureV1 = await signLegacyV1 ( privateKey , value , validityType , isoValidity )
146+ entry . value = value
147+ entry . validity = isoValidity
148+ entry . validityType = validityType
149+ entry . signature = signatureV1
150+ entry . sequence = seq
151+ entry . ttl = ttl
152+ }
153+
114154 // if we cannot derive the public key from the PeerId (e.g. RSA PeerIDs),
115155 // we have to embed it in the IPNS record
116156 if ( peerId . publicKey != null ) {
@@ -122,7 +162,7 @@ const _create = async (peerId: PeerId, value: Uint8Array, seq: number | bigint,
122162 }
123163
124164 log ( 'ipns entry for %b created' , value )
125- return entry
165+ return new IPNSRecord ( entry )
126166}
127167
128168/**
0 commit comments