Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add record/answer fields to IPNS results #471

Merged
merged 1 commit into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@

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)
})
})
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)
})
})
Loading