diff --git a/packages/ipns/src/dnslink.ts b/packages/ipns/src/dnslink.ts index 24d71a0a..d0985cbc 100644 --- a/packages/ipns/src/dnslink.ts +++ b/packages/ipns/src/dnslink.ts @@ -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 { +export interface DNSLinkResult { + answer: Answer + value: string +} + +async function recursiveResolveDnslink (domain: string, depth: number, dns: DNS, log: Logger, options: ResolveDNSLinkOptions = {}): Promise { if (depth === 0) { throw new Error('recursion limit exceeded') } @@ -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 @@ -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 { +async function recursiveResolveDomain (domain: string, depth: number, dns: DNS, log: Logger, options: ResolveDNSLinkOptions = {}): Promise { if (depth === 0) { throw new Error('recursion limit exceeded') } @@ -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 { +export async function resolveDNSLink (domain: string, dns: DNS, log: Logger, options: ResolveDNSLinkOptions = {}): Promise { return recursiveResolveDomain(domain, options.maxRecursiveDepth ?? MAX_RECURSIVE_DEPTH, dns, log, options) } diff --git a/packages/ipns/src/index.ts b/packages/ipns/src/index.ts index 72f9013a..eb2554df 100644 --- a/packages/ipns/src/index.ts +++ b/packages/ipns/src/index.ts @@ -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' @@ -331,10 +331,33 @@ export interface RepublishOptions extends AbortOptions, ProgressOptions + resolve(key: PeerId, options?: ResolveOptions): Promise /** * Resolve a CID from a dns-link style IPNS record */ - resolveDNSLink(domain: string, options?: ResolveDNSLinkOptions): Promise + resolveDNSLink(domain: string, options?: ResolveDNSLinkOptions): Promise /** * Periodically republish all IPNS records found in the datastore @@ -416,17 +439,23 @@ class DefaultIPNS implements IPNS { } } - async resolve (key: PeerId, options: ResolveOptions = {}): Promise { + async resolve (key: PeerId, options: ResolveOptions = {}): Promise { 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 { + async resolveDNSLink (domain: string, options: ResolveDNSLinkOptions = {}): Promise { 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 { @@ -465,7 +494,7 @@ class DefaultIPNS implements IPNS { }, options.interval ?? DEFAULT_REPUBLISH_INTERVAL_MS) } - async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise { + async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise<{ cid: CID, path: string }> { const parts = ipfsPath.split('/') try { const scheme = parts[1] diff --git a/packages/ipns/test/resolve-dnslink.spec.ts b/packages/ipns/test/resolve-dnslink.spec.ts index 40c3ceb4..b95f5231 100644 --- a/packages/ipns/test/resolve-dnslink.spec.ts +++ b/packages/ipns/test/resolve-dnslink.spec.ts @@ -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) + }) }) diff --git a/packages/ipns/test/resolve.spec.ts b/packages/ipns/test/resolve.spec.ts index 1e8b0fd8..3aaaba1b 100644 --- a/packages/ipns/test/resolve.spec.ts +++ b/packages/ipns/test/resolve.spec.ts @@ -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' @@ -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) + }) })