diff --git a/packages/helia/package.json b/packages/helia/package.json index 18ada688..475d5eba 100644 --- a/packages/helia/package.json +++ b/packages/helia/package.json @@ -58,7 +58,7 @@ "@chainsafe/libp2p-noise": "^14.0.0", "@chainsafe/libp2p-yamux": "^6.0.1", "@helia/block-brokers": "^1.0.0", - "@helia/delegated-routing-v1-http-api-client": "^2.0.2", + "@helia/delegated-routing-v1-http-api-client": "^3.0.0", "@helia/interface": "^3.0.1", "@helia/routers": "^0.0.0", "@helia/utils": "^0.0.0", @@ -83,7 +83,7 @@ "datastore-core": "^9.2.6", "interface-blockstore": "^5.2.7", "interface-datastore": "^8.2.9", - "ipns": "^8.0.3", + "ipns": "^9.0.0", "libp2p": "^1.1.1", "multiformats": "^13.0.0" }, diff --git a/packages/interface/src/routing.ts b/packages/interface/src/routing.ts index 622f49b4..7263e4f1 100644 --- a/packages/interface/src/routing.ts +++ b/packages/interface/src/routing.ts @@ -22,6 +22,13 @@ export interface RoutingOptions extends AbortOptions, ProgressOptions { * @default true */ useCache?: boolean + + /** + * Pass `false` to not perform validation + * + * @default true + */ + validate?: boolean } /** diff --git a/packages/interop/package.json b/packages/interop/package.json index a127b35e..00e90de7 100644 --- a/packages/interop/package.json +++ b/packages/interop/package.json @@ -61,9 +61,12 @@ "@helia/car": "^2.0.1", "@helia/dag-cbor": "^2.0.1", "@helia/dag-json": "^2.0.1", + "@helia/http": "^0.9.0", + "@helia/interface": "^3.0.1", "@helia/ipns": "^4.0.0", "@helia/json": "^2.0.1", "@helia/mfs": "^2.0.1", + "@helia/routers": "^0.0.0", "@helia/strings": "^2.0.1", "@helia/unixfs": "^2.0.1", "@ipld/car": "^5.2.5", diff --git a/packages/interop/src/fixtures/create-helia-http.ts b/packages/interop/src/fixtures/create-helia-http.ts new file mode 100644 index 00000000..54514afd --- /dev/null +++ b/packages/interop/src/fixtures/create-helia-http.ts @@ -0,0 +1,8 @@ +import { createHeliaHTTP as createHelia, type HeliaHTTPInit } from '@helia/http' +import type { Helia } from '@helia/interface' + +export async function createHeliaHTTP (init: Partial = {}): Promise { + return createHelia({ + ...init + }) +} diff --git a/packages/interop/src/fixtures/create-kubo.browser.ts b/packages/interop/src/fixtures/create-kubo.browser.ts index 48a6633b..8eacb7da 100644 --- a/packages/interop/src/fixtures/create-kubo.browser.ts +++ b/packages/interop/src/fixtures/create-kubo.browser.ts @@ -12,7 +12,16 @@ export async function createKuboNode (): Promise { Swarm: [ '/ip4/0.0.0.0/tcp/0', '/ip4/0.0.0.0/tcp/0/ws' - ] + ], + Gateway: '/ip4/127.0.0.1/tcp/8180' + }, + Gateway: { + NoFetch: true, + ExposeRoutingAPI: true, + HTTPHeaders: { + 'Access-Control-Allow-Origin': ['*'], + 'Access-Control-Allow-Methods': ['GET', 'POST', 'PUT', 'OPTIONS'] + } } } }, diff --git a/packages/interop/src/fixtures/create-kubo.ts b/packages/interop/src/fixtures/create-kubo.ts index fa7718b4..a632b905 100644 --- a/packages/interop/src/fixtures/create-kubo.ts +++ b/packages/interop/src/fixtures/create-kubo.ts @@ -14,7 +14,16 @@ export async function createKuboNode (): Promise { Swarm: [ '/ip4/0.0.0.0/tcp/4001', '/ip4/0.0.0.0/tcp/4002/ws' - ] + ], + Gateway: '/ip4/127.0.0.1/tcp/8180' + }, + Gateway: { + NoFetch: true, + ExposeRoutingAPI: true, + HTTPHeaders: { + 'Access-Control-Allow-Origin': ['*'], + 'Access-Control-Allow-Methods': ['GET', 'POST', 'PUT', 'OPTIONS'] + } } } }, diff --git a/packages/interop/src/ipns-http.spec.ts b/packages/interop/src/ipns-http.spec.ts new file mode 100644 index 00000000..a892bee8 --- /dev/null +++ b/packages/interop/src/ipns-http.spec.ts @@ -0,0 +1,68 @@ +/* eslint-env mocha */ + +import { ipns } from '@helia/ipns' +import { delegatedHTTPRouting } from '@helia/routers' +import { peerIdFromString } from '@libp2p/peer-id' +import { expect } from 'aegir/chai' +import { isNode } from 'wherearewe' +import { createHeliaHTTP } from './fixtures/create-helia-http.js' +import { createKuboNode } from './fixtures/create-kubo.js' +import type { Helia } from '@helia/interface' +import type { IPNS } from '@helia/ipns' +import type { Controller } from 'ipfsd-ctl' + +describe('@helia/ipns - http', () => { + let helia: Helia + let kubo: Controller + let name: IPNS + + /** + * Ensure that for the CID we are going to publish, the resolver has a peer ID that + * is KAD-closer to the routing key so we can predict the the resolver will receive + * the DHT record containing the IPNS record + */ + beforeEach(async () => { + kubo = await createKuboNode() + helia = await createHeliaHTTP({ + routers: [ + delegatedHTTPRouting('http://127.0.0.1:8180') + ] + }) + name = ipns(helia) + }) + + afterEach(async () => { + if (helia != null) { + await helia.stop() + } + + if (kubo != null) { + await kubo.stop() + } + }) + + it('should publish on kubo and resolve on helia', async function () { + if (!isNode) { + // https://github.com/protocol/bifrost-community/issues/4#issuecomment-1898417008 + return this.skip() + } + + const keyName = 'my-ipns-key' + const { cid } = await kubo.api.add(Uint8Array.from([0, 1, 2, 3, 4])) + + await kubo.api.key.gen(keyName, { + // @ts-expect-error the types say upper-case E, Kubo errors unless it's a + // lower case e + type: 'ed25519' + }) + + const res = await kubo.api.name.publish(cid, { + key: keyName + }) + + const key = peerIdFromString(res.name) + + const resolvedCid = await name.resolve(key) + expect(resolvedCid.toString()).to.equal(cid.toString()) + }) +}) diff --git a/packages/interop/tsconfig.json b/packages/interop/tsconfig.json index d12ad905..bc845a1b 100644 --- a/packages/interop/tsconfig.json +++ b/packages/interop/tsconfig.json @@ -23,6 +23,12 @@ { "path": "../helia" }, + { + "path": "../http" + }, + { + "path": "../interface" + }, { "path": "../ipns" }, @@ -32,6 +38,9 @@ { "path": "../mfs" }, + { + "path": "../routers" + }, { "path": "../strings" }, diff --git a/packages/ipns/package.json b/packages/ipns/package.json index 4c9b5ee9..26de3a4d 100644 --- a/packages/ipns/package.json +++ b/packages/ipns/package.json @@ -173,7 +173,7 @@ "dns-packet": "^5.6.0", "hashlru": "^2.3.0", "interface-datastore": "^8.2.9", - "ipns": "^8.0.3", + "ipns": "^9.0.0", "is-ipfs": "^8.0.1", "multiformats": "^13.0.0", "p-queue": "^8.0.1", diff --git a/packages/ipns/src/index.ts b/packages/ipns/src/index.ts index e956c8d2..6a82dca0 100644 --- a/packages/ipns/src/index.ts +++ b/packages/ipns/src/index.ts @@ -420,21 +420,44 @@ class DefaultIPNS implements IPNS { } const records: Uint8Array[] = [] + let foundInvalid = 0 await Promise.all( routers.map(async (router) => { + let record: Uint8Array + + try { + record = await router.get(routingKey, { + ...options, + validate: false + }) + } catch (err: any) { + if (router === this.localStore && err.code === 'ERR_NOT_FOUND') { + log('did not have record locally') + } else { + log.error('error finding IPNS record', err) + } + + return + } + try { - const record = await router.get(routingKey, options) await ipnsValidator(routingKey, record) records.push(record) } catch (err) { + // we found a record, but the validator rejected it + foundInvalid++ log.error('error finding IPNS record', err) } }) ) if (records.length === 0) { + if (foundInvalid > 0) { + throw new CodeError(`${foundInvalid > 1 ? `${foundInvalid} records` : 'Record'} found for routing key ${foundInvalid > 1 ? 'were' : 'was'} invalid`, 'ERR_RECORDS_FAILED_VALIDATION') + } + throw new CodeError('Could not find record for routing key', 'ERR_NOT_FOUND') } diff --git a/packages/ipns/src/routing/index.ts b/packages/ipns/src/routing/index.ts index 4ce905b8..972dc4ad 100644 --- a/packages/ipns/src/routing/index.ts +++ b/packages/ipns/src/routing/index.ts @@ -9,7 +9,12 @@ export interface PutOptions extends AbortOptions, ProgressOptions { } export interface GetOptions extends AbortOptions, ProgressOptions { - + /** + * Pass false to not perform validation actions + * + * @default true + */ + validate?: boolean } export interface IPNSRouting { diff --git a/packages/routers/package.json b/packages/routers/package.json index 54908e24..16be7e13 100644 --- a/packages/routers/package.json +++ b/packages/routers/package.json @@ -53,17 +53,17 @@ "test:electron-main": "aegir test -t electron-main" }, "dependencies": { - "@helia/delegated-routing-v1-http-api-client": "^2.0.2", + "@helia/delegated-routing-v1-http-api-client": "^3.0.0", "@helia/interface": "^3.0.1", "@libp2p/interface": "^1.1.1", - "@libp2p/peer-id": "^4.0.5", - "ipns": "^8.0.3", + "ipns": "^9.0.0", "it-first": "^3.0.4", "it-map": "^3.0.5", "multiformats": "^13.0.0", "uint8arrays": "^5.0.1" }, "devDependencies": { + "@libp2p/peer-id": "^4.0.5", "@libp2p/peer-id-factory": "^4.0.5", "aegir": "^42.1.0", "it-drain": "^3.0.5", diff --git a/packages/routers/src/delegated-http-routing.ts b/packages/routers/src/delegated-http-routing.ts index 1c76fdf9..f54882cf 100644 --- a/packages/routers/src/delegated-http-routing.ts +++ b/packages/routers/src/delegated-http-routing.ts @@ -1,7 +1,6 @@ import { createDelegatedRoutingV1HttpApiClient } from '@helia/delegated-routing-v1-http-api-client' import { CodeError } from '@libp2p/interface' -import { peerIdFromBytes } from '@libp2p/peer-id' -import { marshal, unmarshal } from 'ipns' +import { marshal, unmarshal, peerIdFromRoutingKey } from 'ipns' import first from 'it-first' import map from 'it-map' import { equals as uint8ArrayEquals } from 'uint8arrays/equals' @@ -17,10 +16,6 @@ function isIPNSKey (key: Uint8Array): boolean { return uint8ArrayEquals(key.subarray(0, IPNS_PREFIX.byteLength), IPNS_PREFIX) } -const peerIdFromRoutingKey = (key: Uint8Array): PeerId => { - return peerIdFromBytes(key.slice(IPNS_PREFIX.length)) -} - class DelegatedHTTPRouter implements Routing { private readonly client: DelegatedRoutingV1HttpApiClient