Skip to content

Commit 5b1c273

Browse files
authored
feat: add validFor function to return the validity interval (#388)
Add `validFor` function to which returns the number of milliseconds until the record expires. --------- Co-authored-by: Daniel N <[email protected]>
1 parent 32e4511 commit 5b1c273

File tree

2 files changed

+66
-3
lines changed

2 files changed

+66
-3
lines changed

src/validator.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
55
import { InvalidEmbeddedPublicKeyError, RecordExpiredError, RecordTooLargeError, SignatureVerificationError, UnsupportedValidityError } from './errors.js'
66
import { IpnsEntry } from './pb/ipns.js'
77
import { extractPublicKeyFromIPNSRecord, ipnsRecordDataForV2Sig, isCodec, multihashFromIPNSRoutingKey, multihashToIPNSRoutingKey, unmarshalIPNSRecord } from './utils.js'
8+
import type { IPNSRecord } from './index.js'
89
import type { PublicKey } from '@libp2p/interface'
910

1011
const log = logger('ipns:validator')
@@ -18,7 +19,7 @@ const MAX_RECORD_SIZE = 1024 * 10
1819
* Validates the given IPNS Record against the given public key. We need a "raw"
1920
* record in order to be able to access to all of its fields.
2021
*/
21-
export const validate = async (publicKey: PublicKey, marshalledRecord: Uint8Array): Promise<void> => {
22+
export async function validate (publicKey: PublicKey, marshalledRecord: Uint8Array): Promise<void> {
2223
// unmarshal ensures that (1) SignatureV2 and Data are present, (2) that ValidityType
2324
// and Validity are of valid types and have a value, (3) that CBOR data matches protobuf
2425
// if it's a V1+V2 record.
@@ -92,3 +93,29 @@ export async function ipnsValidator (routingKey: Uint8Array, marshalledRecord: U
9293
// Record validation
9394
await validate(recordPubKey, marshalledRecord)
9495
}
96+
97+
/**
98+
* Returns the number of milliseconds until the record expires.
99+
* If the record is already expired, returns 0.
100+
*
101+
* @param record - The IPNS record to validate.
102+
* @returns The number of milliseconds until the record expires, or 0 if the record is already expired.
103+
*/
104+
export function validFor (record: IPNSRecord): number {
105+
if (record.validityType !== IpnsEntry.ValidityType.EOL) {
106+
throw new UnsupportedValidityError()
107+
}
108+
109+
if (record.validity == null) {
110+
throw new UnsupportedValidityError()
111+
}
112+
113+
const validUntil = NanoDate.fromString(record.validity).toDate().getTime()
114+
const now = Date.now()
115+
116+
if (validUntil < now) {
117+
return 0
118+
}
119+
120+
return validUntil - now
121+
}

test/validator.spec.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { randomBytes } from '@libp2p/crypto'
44
import { generateKeyPair, publicKeyToProtobuf } from '@libp2p/crypto/keys'
55
import { expect } from 'aegir/chai'
66
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
7-
import { InvalidEmbeddedPublicKeyError, RecordTooLargeError, SignatureVerificationError } from '../src/errors.js'
7+
import { InvalidEmbeddedPublicKeyError, RecordTooLargeError, SignatureVerificationError, UnsupportedValidityError } from '../src/errors.js'
88
import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey } from '../src/index.js'
9-
import { ipnsValidator } from '../src/validator.js'
9+
import { ipnsValidator, validFor } from '../src/validator.js'
1010
import type { PrivateKey } from '@libp2p/interface'
1111

1212
describe('validator', function () {
@@ -91,4 +91,40 @@ describe('validator', function () {
9191
await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected()
9292
.with.property('name', RecordTooLargeError.name)
9393
})
94+
95+
describe('validFor', () => {
96+
it('should return the number of milliseconds until the record expires', async () => {
97+
const record = await createIPNSRecord(privateKey1, contentPath, 0, 1000000)
98+
const result = validFor(record)
99+
expect(result).to.be.greaterThan(0)
100+
})
101+
102+
it('should return 0 for expired records', async () => {
103+
const record = await createIPNSRecord(privateKey1, contentPath, 0, 0)
104+
105+
expect(validFor(record)).to.equal(0)
106+
})
107+
108+
it('should throw UnsupportedValidityError for non-EOL validity types', async () => {
109+
const record = await createIPNSRecord(privateKey1, contentPath, 0, 1000000)
110+
record.validityType = 5 as any
111+
112+
expect(() => validFor(record)).to.throw(UnsupportedValidityError)
113+
})
114+
115+
it('should throw UnsupportedValidityError for null validity', async () => {
116+
const record = await createIPNSRecord(privateKey1, contentPath, 0, 1000000)
117+
record.validityType = null as any
118+
119+
expect(() => validFor(record)).to.throw(UnsupportedValidityError)
120+
})
121+
122+
it('should return correct milliseconds until expiration', async () => {
123+
const record = await createIPNSRecord(privateKey1, contentPath, 0, 5000)
124+
125+
const result = validFor(record)
126+
127+
expect(result).to.be.within(4900, 5000)
128+
})
129+
})
94130
})

0 commit comments

Comments
 (0)