Skip to content

Commit 2817e33

Browse files
committed
refactor using interfaces
1 parent ba7e65c commit 2817e33

File tree

9 files changed

+377
-331
lines changed

9 files changed

+377
-331
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![codecov](https://img.shields.io/codecov/c/github/ipfs/js-ipns.svg?style=flat-square)](https://codecov.io/gh/ipfs/js-ipns)
66
[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/js-ipns/js-test-and-release.yml?branch=master\&style=flat-square)](https://github.com/ipfs/js-ipns/actions/workflows/js-test-and-release.yml?query=branch%3Amaster)
77

8-
> IPNS Record definitions.
8+
> IPNS Record definitions
99
1010
## Table of contents <!-- omit in toc -->
1111

@@ -62,7 +62,7 @@ const ipnsRecord = await ipns.create(privateKey, value, sequenceNumber, lifetime
6262
```js
6363
import * as ipns from 'ipns'
6464

65-
await ipns.validate(publicKey, ipnsRecord)
65+
await ipns.validate(publicKey, marshalledData)
6666
// if no error thrown, the record is valid
6767
```
6868

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ipns",
33
"version": "6.0.5",
4-
"description": "ipns record definitions",
4+
"description": "IPNS Record definitions",
55
"author": "Vasco Santos <vasco.santos@moxy.studio>",
66
"license": "Apache-2.0 OR MIT",
77
"homepage": "https://github.com/ipfs/js-ipns#readme",

src/index.ts

Lines changed: 85 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import { unmarshalPrivateKey } from '@libp2p/crypto/keys'
22
import { logger } from '@libp2p/logger'
3-
import * as cborg from 'cborg'
43
import errCode from 'err-code'
54
import { Key } from 'interface-datastore/key'
65
import { base32upper } from 'multiformats/bases/base32'
7-
import { CID } from 'multiformats/cid'
86
import * as Digest from 'multiformats/hashes/digest'
97
import { identity } from 'multiformats/hashes/identity'
108
import NanoDate from 'timestamp-nano'
119
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
1210
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
13-
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
1411
import * as ERRORS from './errors.js'
1512
import { IpnsEntry } from './pb/ipns.js'
16-
import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, parseRFC3339 } from './utils.js'
13+
import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, normalizeValue } from './utils.js'
1714
import type { PrivateKey } from '@libp2p/interface-keys'
1815
import type { PeerId } from '@libp2p/interface-peer-id'
1916

@@ -23,49 +20,35 @@ const ID_MULTIHASH_CODE = identity.code
2320
export const namespace = '/ipns/'
2421
export 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

7154
export 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+
8273
const 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-
}

src/selector.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,12 @@ export const ipnsSelector: SelectFn = (key, data) => {
88
}))
99

1010
entries.sort((a, b) => {
11-
// having a newer signature version is better than an older signature version
12-
if (a.record.pb.signatureV2 != null && b.record.pb.signatureV2 == null) {
13-
return -1
14-
} else if (a.record.pb.signatureV2 == null && b.record.pb.signatureV2 != null) {
15-
return 1
16-
}
11+
// Before we'd sort based on the signature version. Unmarshal now fails if
12+
// a record does not have SignatureV2, so that is no longer needed. V1-only
13+
// records haven't been issues in a long time.
1714

18-
const aSeq = a.record.sequence()
19-
const bSeq = b.record.sequence()
15+
const aSeq = a.record.sequence
16+
const bSeq = b.record.sequence
2017

2118
// choose later sequence number
2219
if (aSeq > bSeq) {
@@ -26,8 +23,8 @@ export const ipnsSelector: SelectFn = (key, data) => {
2623
}
2724

2825
// choose longer lived record if sequence numbers the same
29-
const recordAValidityDate = a.record.validity()
30-
const recordBValidityDate = b.record.validity()
26+
const recordAValidityDate = a.record.validity.toDate()
27+
const recordBValidityDate = b.record.validity.toDate()
3128

3229
if (recordAValidityDate.getTime() > recordBValidityDate.getTime()) {
3330
return -1

0 commit comments

Comments
 (0)