Skip to content

Commit

Permalink
feat: add expires and record/answer fields to IPNS results
Browse files Browse the repository at this point in the history
Adds fields to IPNS results:

- `expires`: this is a `Date` after which the record will be evicted from the local cache

Resolving IPNS records has a `record` field that contains the resolved
IPNS record.

Resolving DNSLink records has an `answer` field that contains the
resolved DNS Answer.
  • Loading branch information
achingbrain committed Mar 14, 2024
1 parent ecf5394 commit ce6e22b
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 15 deletions.
1 change: 1 addition & 0 deletions packages/ipns/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
"ipns": "^9.0.0",
"multiformats": "^13.1.0",
"progress-events": "^1.0.0",
"timestamp-nano": "^1.0.1",
"uint8arrays": "^5.0.2"
},
"devDependencies": {
Expand Down
23 changes: 17 additions & 6 deletions packages/ipns/src/dnslink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import { peerIdFromString } from '@libp2p/peer-id'
import { RecordType } from '@multiformats/dns'
import { CID } from 'multiformats/cid'
import type { ResolveDNSLinkOptions } from './index.js'
import type { DNS } from '@multiformats/dns'
import type { Answer, DNS } from '@multiformats/dns'

const MAX_RECURSIVE_DEPTH = 32

async function recursiveResolveDnslink (domain: string, depth: number, dns: DNS, log: Logger, options: ResolveDNSLinkOptions = {}): Promise<string> {
export interface DNSLinkResult {
answer: Answer
value: string
}

async function recursiveResolveDnslink (domain: string, depth: number, dns: DNS, log: Logger, options: ResolveDNSLinkOptions = {}): Promise<DNSLinkResult> {
if (depth === 0) {
throw new Error('recursion limit exceeded')
}
Expand Down Expand Up @@ -52,14 +57,20 @@ async function recursiveResolveDnslink (domain: string, depth: number, dns: DNS,
const cid = CID.parse(domainOrCID)

// if the result is a CID, we've reached the end of the recursion
return `/ipfs/${cid}${rest.length > 0 ? `/${rest.join('/')}` : ''}`
return {
value: `/ipfs/${cid}${rest.length > 0 ? `/${rest.join('/')}` : ''}`,
answer
}
} catch {}
} else if (protocol === 'ipns') {
try {
const peerId = peerIdFromString(domainOrCID)

// if the result is a PeerId, we've reached the end of the recursion
return `/ipns/${peerId}${rest.length > 0 ? `/${rest.join('/')}` : ''}`
return {
value: `/ipns/${peerId}${rest.length > 0 ? `/${rest.join('/')}` : ''}`,
answer
}
} catch {}

// if the result was another IPNS domain, try to follow it
Expand Down Expand Up @@ -103,7 +114,7 @@ async function recursiveResolveDnslink (domain: string, depth: number, dns: DNS,
throw new CodeError(`No DNSLink records found for domain: ${domain}`, 'ERR_DNSLINK_NOT_FOUND')
}

async function recursiveResolveDomain (domain: string, depth: number, dns: DNS, log: Logger, options: ResolveDNSLinkOptions = {}): Promise<string> {
async function recursiveResolveDomain (domain: string, depth: number, dns: DNS, log: Logger, options: ResolveDNSLinkOptions = {}): Promise<DNSLinkResult> {
if (depth === 0) {
throw new Error('recursion limit exceeded')
}
Expand Down Expand Up @@ -137,6 +148,6 @@ async function recursiveResolveDomain (domain: string, depth: number, dns: DNS,
}
}

export async function resolveDNSLink (domain: string, dns: DNS, log: Logger, options: ResolveDNSLinkOptions = {}): Promise<string> {
export async function resolveDNSLink (domain: string, dns: DNS, log: Logger, options: ResolveDNSLinkOptions = {}): Promise<DNSLinkResult> {
return recursiveResolveDomain(domain, options.maxRecursiveDepth ?? MAX_RECURSIVE_DEPTH, dns, log, options)
}
54 changes: 46 additions & 8 deletions packages/ipns/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,14 @@ import { ipnsSelector } from 'ipns/selector'
import { ipnsValidator } from 'ipns/validator'
import { CID } from 'multiformats/cid'
import { CustomProgressEvent } from 'progress-events'
import NanoDate from 'timestamp-nano'
import { resolveDNSLink } from './dnslink.js'
import { helia } from './routing/helia.js'
import { localStore, type LocalStore } from './routing/local-store.js'
import type { IPNSRouting, IPNSRoutingEvents } from './routing/index.js'
import type { Routing } from '@helia/interface'
import type { AbortOptions, ComponentLogger, Logger, PeerId } from '@libp2p/interface'
import type { DNS, ResolveDnsProgressEvents } from '@multiformats/dns'
import type { Answer, DNS, ResolveDnsProgressEvents } from '@multiformats/dns'
import type { Datastore } from 'interface-datastore'
import type { IPNSRecord } from 'ipns'
import type { ProgressEvent, ProgressOptions } from 'progress-events'
Expand Down Expand Up @@ -331,8 +332,37 @@ export interface RepublishOptions extends AbortOptions, ProgressOptions<Republis
}

export interface ResolveResult {
/**
* The CID that was resolved
*/
cid: CID

/**
* Any path component that was part of the resolved record
*
* @default ""
*/
path: string

/**
* When this record expires - requesting the same key before this time will
* cause the result to be loaded from a local cache
*/
expires: Date
}

export interface IPNSResolveResult extends ResolveResult {
/**
* The resolved record
*/
record: IPNSRecord
}

export interface DNSLinkResolveResult extends ResolveResult {
/**
* The resolved record
*/
answer: Answer
}

export interface IPNS {
Expand All @@ -347,12 +377,12 @@ export interface IPNS {
* Accepts a public key formatted as a libp2p PeerID and resolves the IPNS record
* corresponding to that public key until a value is found
*/
resolve(key: PeerId, options?: ResolveOptions): Promise<ResolveResult>
resolve(key: PeerId, options?: ResolveOptions): Promise<IPNSResolveResult>

/**
* Resolve a CID from a dns-link style IPNS record
*/
resolveDNSLink(domain: string, options?: ResolveDNSLinkOptions): Promise<ResolveResult>
resolveDNSLink(domain: string, options?: ResolveDNSLinkOptions): Promise<DNSLinkResolveResult>

/**
* Periodically republish all IPNS records found in the datastore
Expand Down Expand Up @@ -416,17 +446,25 @@ class DefaultIPNS implements IPNS {
}
}

async resolve (key: PeerId, options: ResolveOptions = {}): Promise<ResolveResult> {
async resolve (key: PeerId, options: ResolveOptions = {}): Promise<IPNSResolveResult> {
const routingKey = peerIdToRoutingKey(key)
const record = await this.#findIpnsRecord(routingKey, options)

return this.#resolve(record.value, options)
return {
...(await this.#resolve(record.value, options)),
expires: NanoDate.fromString(record.validity).toDate(),
record
}
}

async resolveDNSLink (domain: string, options: ResolveDNSLinkOptions = {}): Promise<ResolveResult> {
async resolveDNSLink (domain: string, options: ResolveDNSLinkOptions = {}): Promise<DNSLinkResolveResult> {
const dnslink = await resolveDNSLink(domain, this.dns, this.log, options)

return this.#resolve(dnslink, options)
return {
...(await this.#resolve(dnslink.value, options)),
expires: new Date(Date.now() + (dnslink.answer.TTL * 1000)),
answer: dnslink.answer
}
}

republish (options: RepublishOptions = {}): void {
Expand Down Expand Up @@ -465,7 +503,7 @@ class DefaultIPNS implements IPNS {
}, options.interval ?? DEFAULT_REPUBLISH_INTERVAL_MS)
}

async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise<ResolveResult> {
async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise<{ cid: CID, path: string }> {
const parts = ipfsPath.split('/')
try {
const scheme = parts[1]
Expand Down
23 changes: 23 additions & 0 deletions packages/ipns/test/resolve-dnslink.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,27 @@ describe('resolveDNSLink', () => {

expect(result.cid.toString()).to.equal(cid.toV1().toString())
})

it('should include DNS Answer in result', async () => {
const cid = CID.parse('bafybeifcaqowoyito3qvsmbwbiugsu4umlxn4ehu223hvtubbfvwyuxjoe')
const key = await createEd25519PeerId()
const answer = {
name: '_dnslink.foobar.baz.',
TTL: 60,
type: RecordType.TXT,
data: 'dnslink=/ipfs/bafybeifcaqowoyito3qvsmbwbiugsu4umlxn4ehu223hvtubbfvwyuxjoe'
}
dns.query.withArgs('_dnslink.foobar.baz').resolves(dnsResponse([answer]))

await name.publish(key, cid)

const result = await name.resolveDNSLink('foobar.baz', { nocache: true })

if (result == null) {
throw new Error('Did not resolve entry')
}

Check warning on line 242 in packages/ipns/test/resolve-dnslink.spec.ts

View check run for this annotation

Codecov / codecov/patch

packages/ipns/test/resolve-dnslink.spec.ts#L241-L242

Added lines #L241 - L242 were not covered by tests

expect(result).to.have.deep.property('answer', answer)
expect(result).to.have.deep.property('expires').that.is.a('date')
})
})
18 changes: 17 additions & 1 deletion packages/ipns/test/resolve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import { expect } from 'aegir/chai'
import { MemoryDatastore } from 'datastore-core'
import { type Datastore, Key } from 'interface-datastore'
import { create, marshal, peerIdToRoutingKey } from 'ipns'
import { create, marshal, peerIdToRoutingKey, unmarshal } from 'ipns'
import { CID } from 'multiformats/cid'
import Sinon from 'sinon'
import { type StubbedInstance, stubInterface } from 'sinon-ts'
Expand Down Expand Up @@ -165,4 +165,20 @@ describe('resolve', () => {
// should have cached the updated record
expect(record.value).to.equalBytes(marshalledRecordB)
})

it('should include IPNS record in result', async () => {
const key = await createEd25519PeerId()
await name.publish(key, cid)

const customRoutingKey = peerIdToRoutingKey(key)
const dhtKey = new Key('/dht/record/' + uint8ArrayToString(customRoutingKey, 'base32'), false)
const buf = await datastore.get(dhtKey)
const dhtRecord = Record.deserialize(buf)
const record = unmarshal(dhtRecord.value)

const result = await name.resolve(key)

expect(result).to.have.deep.property('record', record)
expect(result).to.have.deep.property('expires').that.is.a('date')
})
})

0 comments on commit ce6e22b

Please sign in to comment.