Skip to content

Commit

Permalink
feat: add record/answer fields to IPNS results (#471)
Browse files Browse the repository at this point in the history
Adds fields to IPNS results:

`record`: present in the return from `ipns.resolve` - contains the resolved IPNS record.

`answer`: present in the return from `ipns.resolveDNSLink` - contains the resolved DNS Answer.
  • Loading branch information
achingbrain authored Mar 15, 2024
1 parent ecf5394 commit b6765fe
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 15 deletions.
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)
}
45 changes: 37 additions & 8 deletions packages/ipns/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ 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,10 +331,33 @@ 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
}

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

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

export interface IPNS {
/**
* Creates an IPNS record signed by the passed PeerId that will resolve to the passed value
Expand All @@ -347,12 +370,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 +439,23 @@ 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)),
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)),
answer: dnslink.answer
}
}

republish (options: RepublishOptions = {}): void {
Expand Down Expand Up @@ -465,7 +494,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
22 changes: 22 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,26 @@ 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')
}

expect(result).to.have.deep.property('answer', answer)
})
})
17 changes: 16 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,19 @@ 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)
})
})

0 comments on commit b6765fe

Please sign in to comment.