From f4d79795c2feda74dd5457385137bea64f41ee4c Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:34:25 -0800 Subject: [PATCH 1/4] fix: @helia/ipns doesn't enforce no-path see https://github.com/ipfs/helia/issues/402 --- packages/ipns/src/index.ts | 8 +++++--- packages/ipns/test/resolve-dns.spec.ts | 11 +++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/ipns/src/index.ts b/packages/ipns/src/index.ts index adcad2ee..7888d61f 100644 --- a/packages/ipns/src/index.ts +++ b/packages/ipns/src/index.ts @@ -397,16 +397,18 @@ class DefaultIPNS implements IPNS { } async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise { + // TODO: https://github.com/ipfs/helia/issues/402 const parts = ipfsPath.split('/') - - if (parts.length === 3) { + try { const scheme = parts[1] if (scheme === 'ipns') { - return this.resolve(peerIdFromString(parts[2]), options) + return await this.resolve(peerIdFromString(parts[2]), options) } else if (scheme === 'ipfs') { return CID.parse(parts[2]) } + } catch (err) { + log.error('error parsing ipfs path', err) } log.error('invalid ipfs path %s', ipfsPath) diff --git a/packages/ipns/test/resolve-dns.spec.ts b/packages/ipns/test/resolve-dns.spec.ts index 2ee10f16..315c3bbb 100644 --- a/packages/ipns/test/resolve-dns.spec.ts +++ b/packages/ipns/test/resolve-dns.spec.ts @@ -41,4 +41,15 @@ describe('resolveDns', () => { expect(stubbedResolver2.calledWith('foobar.baz')).to.be.true() expect(result.toString()).to.equal('bafkreibm6jg3ux5qumhcn2b3flc3tyu6dmlb4xa7u5bf44yegnrjhc4yeq') }) + + it('should support trailing slash in returned dnslink value', async () => { + // see https://github.com/ipfs/helia/issues/402 + const stubbedResolver1 = stub().returns('dnslink=/ipfs/bafybeifcaqowoyito3qvsmbwbiugsu4umlxn4ehu223hvtubbfvwyuxjoe/') + + const name = ipns({ datastore, routing: heliaRouting }, { routers: [customRouting], resolvers: [stubbedResolver1] }) + const result = await name.resolveDns('foobar.baz', { nocache: true }) + expect(stubbedResolver1.called).to.be.true() + expect(stubbedResolver1.calledWith('foobar.baz')).to.be.true() + expect(result.toString()).to.equal('bafybeifcaqowoyito3qvsmbwbiugsu4umlxn4ehu223hvtubbfvwyuxjoe', 'doesn\'t support trailing slashes') + }) }) From db9f6c96fe42e529afa8974342bdda9f04e70c50 Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:03:39 -0800 Subject: [PATCH 2/4] feat!: ipns resolve supports paths (#402) BREAKING CHANGE: ipns resolve now supports paths, so the return type is now `{ path: string, cid: CID }` instead of just `CID` \#### Before \```typescript const cidFromPeerId = await ipns.resolve(peerId) const cidFromDnsLink = await ipns.resolve(domainName) \``` \#### After \```typescript const { cid, path } = await ipns.resolve(peerId) const { cid, path } = await ipns.resolve(domainName) \``` --- packages/ipns/src/index.ts | 40 +++++++++++++++++--------- packages/ipns/test/resolve-dns.spec.ts | 40 ++++++++++++++++++++++++-- packages/ipns/test/resolve.spec.ts | 25 ++++++++++++---- 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/packages/ipns/src/index.ts b/packages/ipns/src/index.ts index 7888d61f..ad45bd10 100644 --- a/packages/ipns/src/index.ts +++ b/packages/ipns/src/index.ts @@ -77,7 +77,7 @@ * await name.publish(peerId, cid) * * // resolve the name - * const cid = name.resolve(peerId) + * const {cid, path} = name.resolve(peerId) * ``` * * @example Using custom DNS over HTTPS resolvers @@ -97,7 +97,7 @@ * ] * }) * - * const cid = name.resolveDns('some-domain-with-dnslink-entry.com') + * const {cid, path} = name.resolveDns('some-domain-with-dnslink-entry.com') * ``` * * @example Resolving a domain with a dnslink entry @@ -114,7 +114,7 @@ * // ;; ANSWER SECTION: * // _dnslink.website.ipfs.io. 60 IN TXT "dnslink=/ipfs/QmWebsite" * - * const cid = name.resolveDns('ipfs.io') + * const {cid, path} = name.resolveDns('ipfs.io') * * console.info(cid) * // QmWebsite @@ -132,7 +132,7 @@ * // use DNS-Over-HTTPS * import { dnsOverHttps } from '@helia/ipns/dns-resolvers' * - * const cid = name.resolveDns('ipfs.io', { + * const {cid, path} = name.resolveDns('ipfs.io', { * resolvers: [ * dnsOverHttps('https://mozilla.cloudflare-dns.com/dns-query') * ] @@ -148,7 +148,7 @@ * // use DNS-JSON-Over-HTTPS * import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers' * - * const cid = name.resolveDns('ipfs.io', { + * const {cid, path} = name.resolveDns('ipfs.io', { * resolvers: [ * dnsJsonOverHttps('https://mozilla.cloudflare-dns.com/dns-query') * ] @@ -266,6 +266,11 @@ export interface RepublishOptions extends AbortOptions, ProgressOptions + resolve(key: PeerId, options?: ResolveOptions): Promise /** * Resolve a CID from a dns-link style IPNS record */ - resolveDns(domain: string, options?: ResolveDNSOptions): Promise + resolveDns(domain: string, options?: ResolveDNSOptions): Promise /** * Periodically republish all IPNS records found in the datastore @@ -343,14 +348,14 @@ 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) } - async resolveDns (domain: string, options: ResolveDNSOptions = {}): Promise { + async resolveDns (domain: string, options: ResolveDNSOptions = {}): Promise { const resolvers = options.resolvers ?? this.defaultResolvers const dnslink = await Promise.any( @@ -396,16 +401,25 @@ class DefaultIPNS implements IPNS { }, options.interval ?? DEFAULT_REPUBLISH_INTERVAL_MS) } - async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise { - // TODO: https://github.com/ipfs/helia/issues/402 + async #resolve (ipfsPath: string, options: ResolveOptions = {}): Promise { const parts = ipfsPath.split('/') try { const scheme = parts[1] if (scheme === 'ipns') { - return await this.resolve(peerIdFromString(parts[2]), options) + const { cid } = await this.resolve(peerIdFromString(parts[2]), options) + const path = parts.slice(3).join('/') + return { + cid, + path + } } else if (scheme === 'ipfs') { - return CID.parse(parts[2]) + const cid = CID.parse(parts[2]) + const path = parts.slice(3).join('/') + return { + cid, + path + } } } catch (err) { log.error('error parsing ipfs path', err) diff --git a/packages/ipns/test/resolve-dns.spec.ts b/packages/ipns/test/resolve-dns.spec.ts index 315c3bbb..7adc0dbc 100644 --- a/packages/ipns/test/resolve-dns.spec.ts +++ b/packages/ipns/test/resolve-dns.spec.ts @@ -1,8 +1,10 @@ /* eslint-env mocha */ +import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { expect } from 'aegir/chai' import { MemoryDatastore } from 'datastore-core' import { type Datastore } from 'interface-datastore' +import { CID } from 'multiformats/cid' import { stub } from 'sinon' import { type StubbedInstance, stubInterface } from 'sinon-ts' import { type IPNSRouting, ipns } from '../src/index.js' @@ -27,7 +29,7 @@ describe('resolveDns', () => { const result = await name.resolveDns('foobar.baz', { nocache: true, offline: true }) expect(stubbedResolver1.called).to.be.true() expect(stubbedResolver1.calledWith('foobar.baz')).to.be.true() - expect(result.toString()).to.equal('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn') + expect(result.cid.toString()).to.equal('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn') }) it('should allow overriding of resolvers passed in constructor', async () => { @@ -39,7 +41,7 @@ describe('resolveDns', () => { expect(stubbedResolver1.called).to.be.false() expect(stubbedResolver2.called).to.be.true() expect(stubbedResolver2.calledWith('foobar.baz')).to.be.true() - expect(result.toString()).to.equal('bafkreibm6jg3ux5qumhcn2b3flc3tyu6dmlb4xa7u5bf44yegnrjhc4yeq') + expect(result.cid.toString()).to.equal('bafkreibm6jg3ux5qumhcn2b3flc3tyu6dmlb4xa7u5bf44yegnrjhc4yeq') }) it('should support trailing slash in returned dnslink value', async () => { @@ -50,6 +52,38 @@ describe('resolveDns', () => { const result = await name.resolveDns('foobar.baz', { nocache: true }) expect(stubbedResolver1.called).to.be.true() expect(stubbedResolver1.calledWith('foobar.baz')).to.be.true() - expect(result.toString()).to.equal('bafybeifcaqowoyito3qvsmbwbiugsu4umlxn4ehu223hvtubbfvwyuxjoe', 'doesn\'t support trailing slashes') + expect(result.cid.toString()).to.equal('bafybeifcaqowoyito3qvsmbwbiugsu4umlxn4ehu223hvtubbfvwyuxjoe', 'doesn\'t support trailing slashes') + }) + + it('should support paths in returned dnslink value', async () => { + // see https://github.com/ipfs/helia/issues/402 + const stubbedResolver1 = stub().returns('dnslink=/ipfs/bafybeifcaqowoyito3qvsmbwbiugsu4umlxn4ehu223hvtubbfvwyuxjoe/foobar/path/123') + + const name = ipns({ datastore, routing: heliaRouting }, { routers: [customRouting], resolvers: [stubbedResolver1] }) + const result = await name.resolveDns('foobar.baz', { nocache: true }) + expect(stubbedResolver1.called).to.be.true() + expect(stubbedResolver1.calledWith('foobar.baz')).to.be.true() + expect(result.cid.toString()).to.equal('bafybeifcaqowoyito3qvsmbwbiugsu4umlxn4ehu223hvtubbfvwyuxjoe', 'doesn\'t support trailing slashes') + expect(result.path).to.equal('foobar/path/123') + }) + + it('should resolve recursive dnslink -> /', async () => { + const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn') + const key = await createEd25519PeerId() + const stubbedResolver1 = stub().returns(`dnslink=/ipns/${key.toString()}/foobar/path/123`) + const name = ipns({ datastore, routing: heliaRouting }, { routers: [customRouting], resolvers: [stubbedResolver1] }) + + await name.publish(key, cid) + + const result = await name.resolveDns('foobar.baz', { nocache: true }) + + if (result == null) { + throw new Error('Did not resolve entry') + } + + expect(stubbedResolver1.called).to.be.true() + expect(stubbedResolver1.calledWith('foobar.baz')).to.be.true() + expect(result.cid.toString()).to.equal(cid.toV1().toString()) + expect(result.path).to.equal('foobar/path/123') }) }) diff --git a/packages/ipns/test/resolve.spec.ts b/packages/ipns/test/resolve.spec.ts index 386b377b..001ad8ef 100644 --- a/packages/ipns/test/resolve.spec.ts +++ b/packages/ipns/test/resolve.spec.ts @@ -41,7 +41,7 @@ describe('resolve', () => { throw new Error('Did not resolve entry') } - expect(resolvedValue.toString()).to.equal(cid.toV1().toString()) + expect(resolvedValue.cid.toString()).to.equal(cid.toV1().toString()) expect(heliaRouting.get.called).to.be.true() expect(customRouting.get.called).to.be.true() @@ -65,7 +65,7 @@ describe('resolve', () => { throw new Error('Did not resolve entry') } - expect(resolvedValue.toString()).to.equal(cid.toV1().toString()) + expect(resolvedValue.cid.toString()).to.equal(cid.toV1().toString()) }) it('should resolve a recursive record', async () => { @@ -80,7 +80,22 @@ describe('resolve', () => { throw new Error('Did not resolve entry') } - expect(resolvedValue.toString()).to.equal(cid.toV1().toString()) + expect(resolvedValue.cid.toString()).to.equal(cid.toV1().toString()) + }) + + it('should resolve a recursive record with path', async () => { + const key1 = await createEd25519PeerId() + const key2 = await createEd25519PeerId() + await name.publish(key2, cid) + await name.publish(key1, key2) + + const resolvedValue = await name.resolve(key1) + + if (resolvedValue == null) { + throw new Error('Did not resolve entry') + } + + expect(resolvedValue.cid.toString()).to.equal(cid.toV1().toString()) }) it('should emit progress events', async function () { @@ -108,7 +123,7 @@ describe('resolve', () => { customRouting.get.withArgs(customRoutingKey).resolves(marshalledRecord) const result = await name.resolve(peerId) - expect(result.toString()).to.equal(cid.toV1().toString(), 'incorrect record resolved') + expect(result.cid.toString()).to.equal(cid.toV1().toString(), 'incorrect record resolved') expect(datastore.has(dhtKey)).to.be.true('did not cache record locally') }) @@ -129,7 +144,7 @@ describe('resolve', () => { customRouting.get.withArgs(customRoutingKey).resolves(marshalledRecordB) const result = await name.resolve(peerId) - expect(result.toString()).to.equal(cid.toV1().toString(), 'incorrect record resolved') + expect(result.cid.toString()).to.equal(cid.toV1().toString(), 'incorrect record resolved') const cached = await datastore.get(dhtKey) const record = Record.deserialize(cached) From 91cd8103ba15bcdef74a6a9bf8222e81589a973a Mon Sep 17 00:00:00 2001 From: Russell Dempsey <1173416+SgtPooki@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:56:27 -0800 Subject: [PATCH 3/4] fix: interop tests using @helia/ipns --- packages/interop/src/ipns-http.spec.ts | 2 +- packages/interop/src/ipns-pubsub.spec.ts | 6 +++--- packages/interop/src/ipns.spec.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/interop/src/ipns-http.spec.ts b/packages/interop/src/ipns-http.spec.ts index a892bee8..d4c0ec8c 100644 --- a/packages/interop/src/ipns-http.spec.ts +++ b/packages/interop/src/ipns-http.spec.ts @@ -62,7 +62,7 @@ describe('@helia/ipns - http', () => { const key = peerIdFromString(res.name) - const resolvedCid = await name.resolve(key) + const { cid: resolvedCid } = await name.resolve(key) expect(resolvedCid.toString()).to.equal(cid.toString()) }) }) diff --git a/packages/interop/src/ipns-pubsub.spec.ts b/packages/interop/src/ipns-pubsub.spec.ts index bef1f039..260c6dbf 100644 --- a/packages/interop/src/ipns-pubsub.spec.ts +++ b/packages/interop/src/ipns-pubsub.spec.ts @@ -20,7 +20,7 @@ import { createHeliaNode } from './fixtures/create-helia.js' import { createKuboNode } from './fixtures/create-kubo.js' import { keyTypes } from './fixtures/key-types.js' import { waitFor } from './fixtures/wait-for.js' -import type { IPNS } from '@helia/ipns' +import type { IPNS, ResolveResult } from '@helia/ipns' import type { Libp2p, PubSub } from '@libp2p/interface' import type { Keychain } from '@libp2p/keychain' import type { HeliaLibp2p } from 'helia' @@ -161,7 +161,7 @@ keyTypes.filter(keyType => keyType !== 'RSA').forEach(keyType => { key: keyName }) - let resolvedCid: CID | undefined + let resolvedCid: ResolveResult | undefined // we should get an update eventually await waitFor(async () => { @@ -181,7 +181,7 @@ keyTypes.filter(keyType => keyType !== 'RSA').forEach(keyType => { throw new Error('Failed to resolve CID') } - expect(resolvedCid.toString()).to.equal(cid.toString()) + expect(resolvedCid.cid.toString()).to.equal(cid.toString()) }) }) }) diff --git a/packages/interop/src/ipns.spec.ts b/packages/interop/src/ipns.spec.ts index 7bb29b19..8fa1e331 100644 --- a/packages/interop/src/ipns.spec.ts +++ b/packages/interop/src/ipns.spec.ts @@ -176,7 +176,7 @@ keyTypes.forEach(type => { key: keyName }) - const resolvedCid = await name.resolve(key) + const { cid: resolvedCid } = await name.resolve(key) expect(resolvedCid.toString()).to.equal(cid.toString()) }) }) From 7f4bf773852f2a51e669f1f2bd2ad6a17f41807d Mon Sep 17 00:00:00 2001 From: achingbrain Date: Wed, 31 Jan 2024 16:57:14 +0100 Subject: [PATCH 4/4] chore: add string publishing, update docs --- packages/interop/src/ipns-pubsub.spec.ts | 8 +- packages/ipns/README.md | 95 ++++++++++++++++++++--- packages/ipns/src/index.ts | 99 ++++++++++++++++++++---- packages/ipns/test/publish.spec.ts | 37 +++++++++ 4 files changed, 209 insertions(+), 30 deletions(-) diff --git a/packages/interop/src/ipns-pubsub.spec.ts b/packages/interop/src/ipns-pubsub.spec.ts index 260c6dbf..6885b16c 100644 --- a/packages/interop/src/ipns-pubsub.spec.ts +++ b/packages/interop/src/ipns-pubsub.spec.ts @@ -161,12 +161,12 @@ keyTypes.filter(keyType => keyType !== 'RSA').forEach(keyType => { key: keyName }) - let resolvedCid: ResolveResult | undefined + let resolveResult: ResolveResult | undefined // we should get an update eventually await waitFor(async () => { try { - resolvedCid = await name.resolve(peerId) + resolveResult = await name.resolve(peerId) return true } catch { @@ -177,11 +177,11 @@ keyTypes.filter(keyType => keyType !== 'RSA').forEach(keyType => { message: 'Helia could not resolve the IPNS record' }) - if (resolvedCid == null) { + if (resolveResult == null) { throw new Error('Failed to resolve CID') } - expect(resolvedCid.cid.toString()).to.equal(cid.toString()) + expect(resolveResult.cid.toString()).to.equal(cid.toString()) }) }) }) diff --git a/packages/ipns/README.md b/packages/ipns/README.md index b0a72a0f..4901d49d 100644 --- a/packages/ipns/README.md +++ b/packages/ipns/README.md @@ -21,7 +21,7 @@ IPNS operations using a Helia node With IPNSRouting routers: -```typescript +```TypeScript import { createHelia } from 'helia' import { ipns } from '@helia/ipns' import { unixfs } from '@helia/unixfs' @@ -41,7 +41,78 @@ const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4])) await name.publish(peerId, cid) // resolve the name -const cid = name.resolve(peerId) +const result = name.resolve(peerId) + +console.info(result.cid, result.path) +``` + +## Example - Publishing a recursive record + +A recursive record is a one that points to another record rather than to a +value. + +```TypeScript +import { createHelia } from 'helia' +import { ipns } from '@helia/ipns' +import { unixfs } from '@helia/unixfs' + +const helia = await createHelia() +const name = ipns(helia) + +// create a public key to publish as an IPNS name +const keyInfo = await helia.libp2p.services.keychain.createKey('my-key') +const peerId = await helia.libp2p.services.keychain.exportPeerId(keyInfo.name) + +// store some data to publish +const fs = unixfs(helia) +const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4])) + +// publish the name +await name.publish(peerId, cid) + +// create another public key to re-publish the original record +const recursiveKeyInfo = await helia.libp2p.services.keychain.createKey('my-recursive-key') +const recursivePeerId = await helia.libp2p.services.keychain.exportPeerId(recursiveKeyInfo.name) + +// publish the recursive name +await name.publish(recursivePeerId, peerId) + +// resolve the name recursively - it resolves until a CID is found +const result = name.resolve(recursivePeerId) +console.info(result.cid.toString() === cid.toString()) // true +``` + +## Example - Publishing a record with a path + +It is possible to publish CIDs with an associated path. + +```TypeScript +import { createHelia } from 'helia' +import { ipns } from '@helia/ipns' +import { unixfs } from '@helia/unixfs' + +const helia = await createHelia() +const name = ipns(helia) + +// create a public key to publish as an IPNS name +const keyInfo = await helia.libp2p.services.keychain.createKey('my-key') +const peerId = await helia.libp2p.services.keychain.exportPeerId(keyInfo.name) + +// store some data to publish +const fs = unixfs(helia) +const fileCid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4])) + +// store the file in a directory +const dirCid = await fs.mkdir() +const finalDirCid = await fs.cp(fileCid, dirCid, '/foo.txt') + +// publish the name +await name.publish(peerId, `/ipfs/${finalDirCid}/foo.txt) + +// resolve the name +const result = name.resolve(peerId) + +console.info(result.cid, result.path) // QmFoo.. 'foo.txt' ``` ## Example - Using custom PubSub router @@ -60,7 +131,7 @@ This router is only suitable for networks where IPNS updates are frequent and multiple peers are listening on the topic(s), otherwise update messages may fail to be published with "Insufficient peers" errors. -```typescript +```TypeScript import { createHelia, libp2pDefaults } from 'helia' import { ipns } from '@helia/ipns' import { pubsub } from '@helia/ipns/routing' @@ -91,14 +162,14 @@ const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4])) await name.publish(peerId, cid) // resolve the name -const cid = name.resolve(peerId) +const { cid, path } = name.resolve(peerId) ``` ## Example - Using custom DNS over HTTPS resolvers With default DNSResolver resolvers: -```typescript +```TypeScript import { createHelia } from 'helia' import { ipns } from '@helia/ipns' import { unixfs } from '@helia/unixfs' @@ -111,14 +182,14 @@ const name = ipns(helia, { ] }) -const cid = name.resolveDns('some-domain-with-dnslink-entry.com') +const { cid, path } = name.resolveDns('some-domain-with-dnslink-entry.com') ``` ## Example - Resolving a domain with a dnslink entry Calling `resolveDns` with the `@helia/ipns` instance: -```typescript +```TypeScript // resolve a CID from a TXT record in a DNS zone file, using the default // resolver for the current platform eg: // > dig _dnslink.ipfs.io TXT @@ -128,7 +199,7 @@ Calling `resolveDns` with the `@helia/ipns` instance: // ;; ANSWER SECTION: // _dnslink.website.ipfs.io. 60 IN TXT "dnslink=/ipfs/QmWebsite" -const cid = name.resolveDns('ipfs.io') +const { cid, path } = name.resolveDns('ipfs.io') console.info(cid) // QmWebsite @@ -142,11 +213,11 @@ response which can increase browser bundle sizes. If this is a concern, use the DNS-JSON-Over-HTTPS resolver instead. -```typescript +```TypeScript // use DNS-Over-HTTPS import { dnsOverHttps } from '@helia/ipns/dns-resolvers' -const cid = name.resolveDns('ipfs.io', { +const { cid, path } = name.resolveDns('ipfs.io', { resolvers: [ dnsOverHttps('https://mozilla.cloudflare-dns.com/dns-query') ] @@ -158,11 +229,11 @@ const cid = name.resolveDns('ipfs.io', { DNS-JSON-Over-HTTPS resolvers use the RFC 8427 `application/dns-json` and can result in a smaller browser bundle due to the response being plain JSON. -```typescript +```TypeScript // use DNS-JSON-Over-HTTPS import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers' -const cid = name.resolveDns('ipfs.io', { +const { cid, path } = name.resolveDns('ipfs.io', { resolvers: [ dnsJsonOverHttps('https://mozilla.cloudflare-dns.com/dns-query') ] diff --git a/packages/ipns/src/index.ts b/packages/ipns/src/index.ts index ad45bd10..14d52f0d 100644 --- a/packages/ipns/src/index.ts +++ b/packages/ipns/src/index.ts @@ -7,7 +7,7 @@ * * With {@link IPNSRouting} routers: * - * ```typescript + * ```TypeScript * import { createHelia } from 'helia' * import { ipns } from '@helia/ipns' * import { unixfs } from '@helia/unixfs' @@ -27,7 +27,78 @@ * await name.publish(peerId, cid) * * // resolve the name - * const cid = name.resolve(peerId) + * const result = name.resolve(peerId) + * + * console.info(result.cid, result.path) + * ``` + * + * @example Publishing a recursive record + * + * A recursive record is a one that points to another record rather than to a + * value. + * + * ```TypeScript + * import { createHelia } from 'helia' + * import { ipns } from '@helia/ipns' + * import { unixfs } from '@helia/unixfs' + * + * const helia = await createHelia() + * const name = ipns(helia) + * + * // create a public key to publish as an IPNS name + * const keyInfo = await helia.libp2p.services.keychain.createKey('my-key') + * const peerId = await helia.libp2p.services.keychain.exportPeerId(keyInfo.name) + * + * // store some data to publish + * const fs = unixfs(helia) + * const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4])) + * + * // publish the name + * await name.publish(peerId, cid) + * + * // create another public key to re-publish the original record + * const recursiveKeyInfo = await helia.libp2p.services.keychain.createKey('my-recursive-key') + * const recursivePeerId = await helia.libp2p.services.keychain.exportPeerId(recursiveKeyInfo.name) + * + * // publish the recursive name + * await name.publish(recursivePeerId, peerId) + * + * // resolve the name recursively - it resolves until a CID is found + * const result = name.resolve(recursivePeerId) + * console.info(result.cid.toString() === cid.toString()) // true + * ``` + * + * @example Publishing a record with a path + * + * It is possible to publish CIDs with an associated path. + * + * ```TypeScript + * import { createHelia } from 'helia' + * import { ipns } from '@helia/ipns' + * import { unixfs } from '@helia/unixfs' + * + * const helia = await createHelia() + * const name = ipns(helia) + * + * // create a public key to publish as an IPNS name + * const keyInfo = await helia.libp2p.services.keychain.createKey('my-key') + * const peerId = await helia.libp2p.services.keychain.exportPeerId(keyInfo.name) + * + * // store some data to publish + * const fs = unixfs(helia) + * const fileCid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4])) + * + * // store the file in a directory + * const dirCid = await fs.mkdir() + * const finalDirCid = await fs.cp(fileCid, dirCid, '/foo.txt') + * + * // publish the name + * await name.publish(peerId, `/ipfs/${finalDirCid}/foo.txt) + * + * // resolve the name + * const result = name.resolve(peerId) + * + * console.info(result.cid, result.path) // QmFoo.. 'foo.txt' * ``` * * @example Using custom PubSub router @@ -46,7 +117,7 @@ * and multiple peers are listening on the topic(s), otherwise update messages * may fail to be published with "Insufficient peers" errors. * - * ```typescript + * ```TypeScript * import { createHelia, libp2pDefaults } from 'helia' * import { ipns } from '@helia/ipns' * import { pubsub } from '@helia/ipns/routing' @@ -77,14 +148,14 @@ * await name.publish(peerId, cid) * * // resolve the name - * const {cid, path} = name.resolve(peerId) + * const { cid, path } = name.resolve(peerId) * ``` * * @example Using custom DNS over HTTPS resolvers * * With default {@link DNSResolver} resolvers: * - * ```typescript + * ```TypeScript * import { createHelia } from 'helia' * import { ipns } from '@helia/ipns' * import { unixfs } from '@helia/unixfs' @@ -97,14 +168,14 @@ * ] * }) * - * const {cid, path} = name.resolveDns('some-domain-with-dnslink-entry.com') + * const { cid, path } = name.resolveDns('some-domain-with-dnslink-entry.com') * ``` * * @example Resolving a domain with a dnslink entry * * Calling `resolveDns` with the `@helia/ipns` instance: * - * ```typescript + * ```TypeScript * // resolve a CID from a TXT record in a DNS zone file, using the default * // resolver for the current platform eg: * // > dig _dnslink.ipfs.io TXT @@ -114,7 +185,7 @@ * // ;; ANSWER SECTION: * // _dnslink.website.ipfs.io. 60 IN TXT "dnslink=/ipfs/QmWebsite" * - * const {cid, path} = name.resolveDns('ipfs.io') + * const { cid, path } = name.resolveDns('ipfs.io') * * console.info(cid) * // QmWebsite @@ -128,11 +199,11 @@ * * If this is a concern, use the DNS-JSON-Over-HTTPS resolver instead. * - * ```typescript + * ```TypeScript * // use DNS-Over-HTTPS * import { dnsOverHttps } from '@helia/ipns/dns-resolvers' * - * const {cid, path} = name.resolveDns('ipfs.io', { + * const { cid, path } = name.resolveDns('ipfs.io', { * resolvers: [ * dnsOverHttps('https://mozilla.cloudflare-dns.com/dns-query') * ] @@ -144,11 +215,11 @@ * DNS-JSON-Over-HTTPS resolvers use the RFC 8427 `application/dns-json` and can * result in a smaller browser bundle due to the response being plain JSON. * - * ```typescript + * ```TypeScript * // use DNS-JSON-Over-HTTPS * import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers' * - * const {cid, path} = name.resolveDns('ipfs.io', { + * const { cid, path } = name.resolveDns('ipfs.io', { * resolvers: [ * dnsJsonOverHttps('https://mozilla.cloudflare-dns.com/dns-query') * ] @@ -277,7 +348,7 @@ export interface IPNS { * * If the value is a PeerId, a recursive IPNS record will be created. */ - publish(key: PeerId, value: CID | PeerId, options?: PublishOptions): Promise + publish(key: PeerId, value: CID | PeerId | string, options?: PublishOptions): Promise /** * Accepts a public key formatted as a libp2p PeerID and resolves the IPNS record @@ -318,7 +389,7 @@ class DefaultIPNS implements IPNS { this.defaultResolvers = resolvers.length > 0 ? resolvers : [defaultResolver()] } - async publish (key: PeerId, value: CID | PeerId, options: PublishOptions = {}): Promise { + async publish (key: PeerId, value: CID | PeerId | string, options: PublishOptions = {}): Promise { try { let sequenceNumber = 1n const routingKey = peerIdToRoutingKey(key) diff --git a/packages/ipns/test/publish.spec.ts b/packages/ipns/test/publish.spec.ts index 1609cdb0..d8e1ba33 100644 --- a/packages/ipns/test/publish.spec.ts +++ b/packages/ipns/test/publish.spec.ts @@ -3,6 +3,7 @@ import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { expect } from 'aegir/chai' import { MemoryDatastore } from 'datastore-core' +import { base36 } from 'multiformats/bases/base36' import { CID } from 'multiformats/cid' import Sinon from 'sinon' import { type StubbedInstance, stubInterface } from 'sinon-ts' @@ -67,4 +68,40 @@ describe('publish', () => { expect(onProgress).to.have.property('called', true) }) + + it('should publish recursively', async () => { + const key = await createEd25519PeerId() + const record = await name.publish(key, cid, { + offline: true + }) + + expect(record.value).to.equal(`/ipfs/${cid.toV1().toString()}`) + + const recursiveKey = await createEd25519PeerId() + const recursiveRecord = await name.publish(recursiveKey, key, { + offline: true + }) + + expect(recursiveRecord.value).to.equal(`/ipns/${key.toCID().toString(base36)}`) + + const recursiveResult = await name.resolve(recursiveKey) + expect(recursiveResult.cid.toString()).to.equal(cid.toV1().toString()) + }) + + it('should publish record with a path', async () => { + const path = '/foo/bar/baz' + const fullPath = `/ipfs/${cid}/${path}` + + const key = await createEd25519PeerId() + const record = await name.publish(key, fullPath, { + offline: true + }) + + expect(record.value).to.equal(fullPath) + + const result = await name.resolve(key) + + expect(result.cid.toString()).to.equal(cid.toString()) + expect(result.path).to.equal(path) + }) })