diff --git a/README.md b/README.md
index fab1e7c..695666c 100644
--- a/README.md
+++ b/README.md
@@ -4,14 +4,16 @@
-# helia-routing-v1-http-api
+# helia-delegated-routing-v1-http-api
[](https://ipfs.tech)
[](https://discuss.ipfs.tech)
-[](https://codecov.io/gh/ipfs/helia-routing-v1-http-api)
-[](https://github.com/ipfs/helia-routing-v1-http-api/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
+[](https://codecov.io/gh/ipfs/helia-delegated-routing-v1-http-api)
+[](https://github.com/ipfs/helia-delegated-routing-v1-http-api/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
-> The Routing V1 HTTP API powered by Helia
+> The Delegated Routing V1 HTTP API powered by Helia
+
+This repo contains a server implementation of the IPFS [Delegated Routing V1 HTTP API](https://specs.ipfs.tech/routing/http-routing-v1/) along with a client that can be used to interact with any compliant server implementation.
## Table of contents
@@ -22,13 +24,13 @@
## Structure
-- [`/packages/client`](./packages/client) A Routing V1 HTTP API client
-- [`/packages/interop`](./packages/interop) Interop tests for the Routing V1 HTTP API server powered by Helia
-- [`/packages/server`](./packages/server) A Routing V1 HTTP API server powered by Helia
+- [`/packages/client`](./packages/client) A Delegated Routing V1 HTTP API client
+- [`/packages/interop`](./packages/interop) Interop tests for the Delegated Routing V1 HTTP API server powered by Helia
+- [`/packages/server`](./packages/server) A Delegated Routing V1 HTTP API server powered by Helia
## API Docs
--
+-
## License
@@ -39,7 +41,7 @@ Licensed under either of
## Contribute
-Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia-routing-v1-http-api/issues).
+Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia-delegated-routing-v1-http-api/issues).
Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general.
diff --git a/package.json b/package.json
index 2d71109..f10fe7d 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,15 @@
{
- "name": "helia-routing-v1-http-api",
+ "name": "helia-delegated-routing-v1-http-api",
"version": "1.0.0",
- "description": "The Routing V1 HTTP API powered by Helia",
+ "description": "The Delegated Routing V1 HTTP API powered by Helia",
"license": "Apache-2.0 OR MIT",
- "homepage": "https://github.com/ipfs/helia-routing-v1-http-api#readme",
+ "homepage": "https://github.com/ipfs/helia-delegated-routing-v1-http-api#readme",
"repository": {
"type": "git",
- "url": "git+https://github.com/ipfs/helia-routing-v1-http-api.git"
+ "url": "git+https://github.com/ipfs/helia-delegated-routing-v1-http-api.git"
},
"bugs": {
- "url": "https://github.com/ipfs/helia-routing-v1-http-api/issues"
+ "url": "https://github.com/ipfs/helia-delegated-routing-v1-http-api/issues"
},
"keywords": [
"ipfs"
diff --git a/packages/client/.aegir.js b/packages/client/.aegir.js
index 411adee..739d8ce 100644
--- a/packages/client/.aegir.js
+++ b/packages/client/.aegir.js
@@ -6,17 +6,51 @@ const options = {
test: {
before: async () => {
const providers = new Map()
+ const peers = new Map()
+ const ipnsGet = new Map()
+ const ipnsPut = new Map()
const echo = new EchoServer()
+ echo.polka.use(body.raw({ type: 'application/vnd.ipfs.ipns-record'}))
echo.polka.use(body.text())
echo.polka.post('/add-providers/:cid', (req, res) => {
providers.set(req.params.cid, req.body)
res.end()
})
echo.polka.get('/routing/v1/providers/:cid', (req, res) => {
- const provs = providers.get(req.params.cid) ?? '[]'
+ const records = providers.get(req.params.cid) ?? '[]'
providers.delete(req.params.cid)
- res.end(provs)
+ res.end(records)
+ })
+ echo.polka.post('/add-peers/:peerId', (req, res) => {
+ peers.set(req.params.peerId, req.body)
+ res.end()
+ })
+ echo.polka.get('/routing/v1/peers/:peerId', (req, res) => {
+ const records = peers.get(req.params.peerId) ?? '[]'
+ peers.delete(req.params.peerId)
+
+ res.end(records)
+ })
+ echo.polka.post('/add-ipns/:peerId', (req, res) => {
+ ipnsGet.set(req.params.peerId, req.body)
+ res.end()
+ })
+ echo.polka.get('/routing/v1/ipns/:peerId', (req, res) => {
+ const record = ipnsGet.get(req.params.peerId) ?? ''
+ ipnsGet.delete(req.params.peerId)
+
+ res.end(record)
+ })
+ echo.polka.put('/routing/v1/ipns/:peerId', (req, res) => {
+ ipnsPut.set(req.params.peerId, req.body)
+ res.end()
+ })
+ echo.polka.get('/get-ipns/:peerId', (req, res) => {
+ const record = ipnsPut.get(req.params.peerId) ?? ''
+ ipnsPut.delete(req.params.peerId)
+
+ res.end(record)
})
await echo.start()
diff --git a/packages/client/README.md b/packages/client/README.md
index 877429b..aa27f43 100644
--- a/packages/client/README.md
+++ b/packages/client/README.md
@@ -4,14 +4,16 @@
-# @helia/routing-v1-http-api-client
+# @helia/delegated-routing-v1-http-api-client
[](https://ipfs.tech)
[](https://discuss.ipfs.tech)
-[](https://codecov.io/gh/ipfs/helia-routing-v1-http-api)
-[](https://github.com/ipfs/helia-routing-v1-http-api/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
+[](https://codecov.io/gh/ipfs/helia-delegated-routing-v1-http-api)
+[](https://github.com/ipfs/helia-delegated-routing-v1-http-api/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
-> A Routing V1 HTTP API client
+> A Delegated Routing V1 HTTP API client
+
+A client implementation of the IPFS [Delegated Routing V1 HTTP API](https://specs.ipfs.tech/routing/http-routing-v1/) that can be used to interact with any compliant server implementation.
## Table of contents
@@ -23,12 +25,12 @@
## Install
```console
-$ npm i @helia/routing-v1-http-api-client
+$ npm i @helia/delegated-routing-v1-http-api-client
```
## API Docs
--
+-
## License
@@ -39,7 +41,7 @@ Licensed under either of
## Contribute
-Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia-routing-v1-http-api/issues).
+Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia-delegated-routing-v1-http-api/issues).
Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general.
diff --git a/packages/client/package.json b/packages/client/package.json
index 697c470..21972f4 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -1,15 +1,15 @@
{
- "name": "@helia/routing-v1-http-api-client",
- "version": "1.0.2",
- "description": "A Routing V1 HTTP API client",
+ "name": "@helia/delegated-routing-v1-http-api-client",
+ "version": "0.0.0",
+ "description": "A Delegated Routing V1 HTTP API client",
"license": "Apache-2.0 OR MIT",
- "homepage": "https://github.com/ipfs/helia-routing-v1-http-api/tree/master/packages/client#readme",
+ "homepage": "https://github.com/ipfs/helia-delegated-routing-v1-http-api/tree/master/packages/client#readme",
"repository": {
"type": "git",
- "url": "git+https://github.com/ipfs/helia-routing-v1-http-api.git"
+ "url": "git+https://github.com/ipfs/helia-delegated-routing-v1-http-api.git"
},
"bugs": {
- "url": "https://github.com/ipfs/helia-routing-v1-http-api/issues"
+ "url": "https://github.com/ipfs/helia-delegated-routing-v1-http-api/issues"
},
"keywords": [
"IPFS"
@@ -132,10 +132,11 @@
"dependencies": {
"@libp2p/interface": "^0.1.2",
"@libp2p/logger": "^3.0.2",
- "@libp2p/peer-id": "^3.0.2",
+ "@libp2p/peer-id": "^3.0.3",
"@multiformats/multiaddr": "^12.1.3",
"any-signal": "^4.1.1",
"browser-readablestream-to-it": "^2.0.3",
+ "ipns": "^7.0.1",
"it-all": "^3.0.2",
"iterable-ndjson": "^1.1.0",
"multiformats": "^12.1.1",
@@ -143,7 +144,7 @@
"p-queue": "^7.3.4"
},
"devDependencies": {
- "@libp2p/peer-id-factory": "^3.0.3",
+ "@libp2p/peer-id-factory": "^3.0.5",
"aegir": "^41.0.0",
"body-parser": "^1.20.2"
}
diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts
index 6a3c7a9..0fcac0c 100644
--- a/packages/client/src/client.ts
+++ b/packages/client/src/client.ts
@@ -4,31 +4,25 @@ import { peerIdFromString } from '@libp2p/peer-id'
import { multiaddr } from '@multiformats/multiaddr'
import { anySignal } from 'any-signal'
import toIt from 'browser-readablestream-to-it'
+import { unmarshal, type IPNSRecord, marshal, peerIdToRoutingKey } from 'ipns'
+import { ipnsValidator } from 'ipns/validator'
// @ts-expect-error no types
import ndjson from 'iterable-ndjson'
import defer from 'p-defer'
import PQueue from 'p-queue'
-import type { RoutingV1HttpApiClient, RoutingV1HttpApiClientInit } from './index.js'
+import type { DelegatedRoutingV1HttpApiClient, DelegatedRoutingV1HttpApiClientInit, PeerRecord } from './index.js'
import type { AbortOptions } from '@libp2p/interface'
-import type { PeerInfo } from '@libp2p/interface/peer-info'
-import type { Multiaddr } from '@multiformats/multiaddr'
+import type { PeerId } from '@libp2p/interface/peer-id'
import type { CID } from 'multiformats'
-const log = logger('routing-v1-http-api-client')
-
-interface RoutingV1HttpApiGetProvidersResponse {
- Protocol: string
- Schema: string
- ID: string
- Addrs: Multiaddr[]
-}
+const log = logger('delegated-routing-v1-http-api-client')
const defaultValues = {
concurrentRequests: 4,
timeout: 30e3
}
-export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient {
+export class DefaultDelegatedRoutingV1HttpApiClient implements DelegatedRoutingV1HttpApiClient {
private started: boolean
private readonly httpQueue: PQueue
private readonly shutDownController: AbortController
@@ -38,7 +32,7 @@ export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient {
/**
* Create a new DelegatedContentRouting instance
*/
- constructor (url: string | URL, init: RoutingV1HttpApiClientInit = {}) {
+ constructor (url: string | URL, init: DelegatedRoutingV1HttpApiClientInit = {}) {
this.started = false
this.shutDownController = new AbortController()
this.httpQueue = new PQueue({
@@ -62,8 +56,8 @@ export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient {
this.started = false
}
- async * getProviders (cid: CID, options: AbortOptions | undefined = {}): AsyncGenerator {
- log('findProviders starts: %c', cid)
+ async * getProviders (cid: CID, options: AbortOptions = {}): AsyncGenerator {
+ log('getProviders starts: %c', cid)
const signal = anySignal([this.shutDownController.signal, options.signal, AbortSignal.timeout(this.timeout)])
const onStart = defer()
@@ -77,46 +71,196 @@ export class DefaultRoutingV1HttpApiClient implements RoutingV1HttpApiClient {
try {
await onStart.promise
- // https://github.com/ipfs/specs/blob/main/routing/ROUTING_V1_HTTP.md#api
+ // https://specs.ipfs.tech/routing/http-routing-v1/
const resource = `${this.clientUrl}routing/v1/providers/${cid.toString()}`
const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal }
- const a = await fetch(resource, getOptions)
+ const res = await fetch(resource, getOptions)
- if (a.body == null) {
+ if (res.body == null) {
throw new CodeError('Routing response had no body', 'ERR_BAD_RESPONSE')
}
- for await (const event of ndjson(toIt(a.body))) {
- if (event.Protocol !== 'transport-bitswap') {
- continue
+ const contentType = res.headers.get('Content-Type')
+ if (contentType === 'application/json') {
+ const body = await res.json()
+
+ for (const provider of body.Providers) {
+ const record = this.#handleProviderRecords(provider)
+ if (record != null) {
+ yield record
+ }
}
+ } else {
+ for await (const provider of ndjson(toIt(res.body))) {
+ const record = this.#handleProviderRecords(provider)
+ if (record != null) {
+ yield record
+ }
+ }
+ }
+ } catch (err) {
+ log.error('getProviders errored:', err)
+ } finally {
+ signal.clear()
+ onFinish.resolve()
+ log('getProviders finished: %c', cid)
+ }
+ }
+
+ async * getPeerInfo (peerId: PeerId, options: AbortOptions | undefined = {}): AsyncGenerator {
+ log('getPeers starts: %c', peerId)
+
+ const signal = anySignal([this.shutDownController.signal, options.signal, AbortSignal.timeout(this.timeout)])
+ const onStart = defer()
+ const onFinish = defer()
+
+ void this.httpQueue.add(async () => {
+ onStart.resolve()
+ return onFinish.promise
+ })
- yield this.#mapProvider(event)
+ try {
+ await onStart.promise
+
+ // https://specs.ipfs.tech/routing/http-routing-v1/
+ const resource = `${this.clientUrl}routing/v1/peers/${peerId.toCID().toString()}`
+ const getOptions = { headers: { Accept: 'application/x-ndjson' }, signal }
+ const res = await fetch(resource, getOptions)
+
+ if (res.body == null) {
+ throw new CodeError('Routing response had no body', 'ERR_BAD_RESPONSE')
+ }
+
+ const contentType = res.headers.get('Content-Type')
+ if (contentType === 'application/json') {
+ const body = await res.json()
+
+ for (const peer of body.Peers) {
+ const record = this.#handlePeerRecords(peerId, peer)
+ if (record != null) {
+ yield record
+ }
+ }
+ } else {
+ for await (const peer of ndjson(toIt(res.body))) {
+ const record = this.#handlePeerRecords(peerId, peer)
+ if (record != null) {
+ yield record
+ }
+ }
}
} catch (err) {
- log.error('findProviders errored:', err)
+ log.error('getPeers errored:', err)
} finally {
signal.clear()
onFinish.resolve()
- log('findProviders finished: %c', cid)
+ log('getPeers finished: %c', peerId)
}
}
- #mapProvider (event: RoutingV1HttpApiGetProvidersResponse): PeerInfo {
- const peer = peerIdFromString(event.ID)
- const ma: Multiaddr[] = []
+ async getIPNS (peerId: PeerId, options: AbortOptions = {}): Promise {
+ log('getIPNS starts: %c', peerId)
+
+ const signal = anySignal([this.shutDownController.signal, options.signal, AbortSignal.timeout(this.timeout)])
+ const onStart = defer()
+ const onFinish = defer()
+
+ void this.httpQueue.add(async () => {
+ onStart.resolve()
+ return onFinish.promise
+ })
- for (const strAddr of event.Addrs) {
- const addr = multiaddr(strAddr)
- ma.push(addr)
+ try {
+ await onStart.promise
+
+ // https://specs.ipfs.tech/routing/http-routing-v1/
+ const resource = `${this.clientUrl}routing/v1/ipns/${peerId.toCID().toString()}`
+ const getOptions = { headers: { Accept: 'application/vnd.ipfs.ipns-record' }, signal }
+ const res = await fetch(resource, getOptions)
+
+ if (res.body == null) {
+ throw new CodeError('GET ipns response had no body', 'ERR_BAD_RESPONSE')
+ }
+
+ const body = new Uint8Array(await res.arrayBuffer())
+ await ipnsValidator(peerIdToRoutingKey(peerId), body)
+ return unmarshal(body)
+ } finally {
+ signal.clear()
+ onFinish.resolve()
+ log('getIPNS finished: %c', peerId)
+ }
+ }
+
+ async putIPNS (peerId: PeerId, record: IPNSRecord, options: AbortOptions = {}): Promise {
+ log('getIPNS starts: %c', peerId)
+
+ const signal = anySignal([this.shutDownController.signal, options.signal, AbortSignal.timeout(this.timeout)])
+ const onStart = defer()
+ const onFinish = defer()
+
+ void this.httpQueue.add(async () => {
+ onStart.resolve()
+ return onFinish.promise
+ })
+
+ try {
+ await onStart.promise
+
+ const body = marshal(record)
+
+ // https://specs.ipfs.tech/routing/http-routing-v1/
+ const resource = `${this.clientUrl}routing/v1/ipns/${peerId.toCID().toString()}`
+ const getOptions = { method: 'PUT', headers: { 'Content-Type': 'application/vnd.ipfs.ipns-record' }, body, signal }
+ const res = await fetch(resource, getOptions)
+ if (res.status !== 200) {
+ throw new CodeError('PUT ipns response had status other than 200', 'ERR_BAD_RESPONSE')
+ }
+ } finally {
+ signal.clear()
+ onFinish.resolve()
+ log('getIPNS finished: %c', peerId)
+ }
+ }
+
+ #handleProviderRecords (record: any): PeerRecord | undefined {
+ if (record.Schema === 'peer') {
+ // Peer schema can have additional, user-defined, fields.
+ record.ID = peerIdFromString(record.ID)
+ record.Addrs = record.Addrs.map(multiaddr)
+ return record
+ }
+
+ if (record.Schema === 'bitswap') {
+ // Bitswap schema is deprecated, was incorrectly used when server had no
+ // information about actual protocols, so we convert it to peer result
+ // without protocol information
+ return {
+ Schema: 'peer',
+ ID: peerIdFromString(record.ID),
+ Addrs: record.Addrs.map(multiaddr),
+ Protocols: record.Protocol != null ? [record.Protocol] : []
+ }
}
- const pi = {
- id: peer,
- multiaddrs: ma,
- protocols: []
+ if (record.ID != null && Array.isArray(record.Addrs)) {
+ return {
+ Schema: 'peer',
+ ID: peerIdFromString(record.ID),
+ Addrs: record.Addrs.map(multiaddr),
+ Protocols: Array.isArray(record.Protocols) ? record.Protocols : []
+ }
}
+ }
- return pi
+ #handlePeerRecords (peerId: PeerId, record: any): PeerRecord | undefined {
+ if (record.Schema === 'peer') {
+ // Peer schema can have additional, user-defined, fields.
+ record.ID = peerIdFromString(record.ID)
+ record.Addrs = record.Addrs.map(multiaddr)
+ if (peerId.equals(record.ID)) {
+ return record
+ }
+ }
}
}
diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts
index 480538c..7f96905 100644
--- a/packages/client/src/index.ts
+++ b/packages/client/src/index.ts
@@ -17,12 +17,21 @@
* ```
*/
-import { DefaultRoutingV1HttpApiClient } from './client.js'
+import { DefaultDelegatedRoutingV1HttpApiClient } from './client.js'
import type { AbortOptions } from '@libp2p/interface'
-import type { PeerInfo } from '@libp2p/interface/peer-info'
+import type { PeerId } from '@libp2p/interface/peer-id'
+import type { Multiaddr } from '@multiformats/multiaddr'
+import type { IPNSRecord } from 'ipns'
import type { CID } from 'multiformats/cid'
-export interface RoutingV1HttpApiClientInit {
+export interface PeerRecord {
+ Schema: 'peer'
+ ID: PeerId
+ Addrs?: Multiaddr[]
+ Protocols?: string[]
+}
+
+export interface DelegatedRoutingV1HttpApiClientInit {
/**
* A concurrency limit to avoid request flood in web browser (default: 4)
*
@@ -36,12 +45,27 @@ export interface RoutingV1HttpApiClientInit {
timeout?: number
}
-export interface RoutingV1HttpApiClient {
+export interface DelegatedRoutingV1HttpApiClient {
/**
* Returns an async generator of PeerInfos that can provide the content
* for the passed CID
*/
- getProviders(cid: CID, options?: AbortOptions): AsyncGenerator
+ getProviders(cid: CID, options?: AbortOptions): AsyncGenerator
+
+ /**
+ * Returns an async generator of PeerInfos for the provided PeerId
+ */
+ getPeerInfo(peerId: PeerId, options?: AbortOptions): AsyncGenerator
+
+ /**
+ * Returns a promise of a IPNSRecord for the given PeerId
+ */
+ getIPNS(peerId: PeerId, options?: AbortOptions): Promise
+
+ /**
+ * Publishes the given IPNSRecord for the provided PeerId
+ */
+ putIPNS(peerId: PeerId, record: IPNSRecord, options?: AbortOptions): Promise
/**
* Shut down any currently running HTTP requests and clear up any resources
@@ -53,6 +77,6 @@ export interface RoutingV1HttpApiClient {
/**
* Create and return a client to use with a Routing V1 HTTP API server
*/
-export function createRoutingV1HttpApiClient (url: URL, init: RoutingV1HttpApiClientInit = {}): RoutingV1HttpApiClient {
- return new DefaultRoutingV1HttpApiClient(url, init)
+export function createDelegatedRoutingV1HttpApiClient (url: URL, init: DelegatedRoutingV1HttpApiClientInit = {}): DelegatedRoutingV1HttpApiClient {
+ return new DefaultDelegatedRoutingV1HttpApiClient(url, init)
}
diff --git a/packages/client/test/index.spec.ts b/packages/client/test/index.spec.ts
index c106a36..c4e4774 100644
--- a/packages/client/test/index.spec.ts
+++ b/packages/client/test/index.spec.ts
@@ -2,9 +2,10 @@
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import { expect } from 'aegir/chai'
+import { create as createIpnsRecord, marshal as marshalIpnsRecord } from 'ipns'
import all from 'it-all'
import { CID } from 'multiformats/cid'
-import { createRoutingV1HttpApiClient, type RoutingV1HttpApiClient } from '../src/index.js'
+import { createDelegatedRoutingV1HttpApiClient, type DelegatedRoutingV1HttpApiClient } from '../src/index.js'
if (process.env.ECHO_SERVER == null) {
throw new Error('Echo server not configured correctly')
@@ -12,11 +13,11 @@ if (process.env.ECHO_SERVER == null) {
const serverUrl = process.env.ECHO_SERVER
-describe('routing-v1-http-api-client', () => {
- let client: RoutingV1HttpApiClient
+describe('delegated-routing-v1-http-api-client', () => {
+ let client: DelegatedRoutingV1HttpApiClient
beforeEach(() => {
- client = createRoutingV1HttpApiClient(new URL(serverUrl))
+ client = createDelegatedRoutingV1HttpApiClient(new URL(serverUrl))
})
afterEach(async () => {
@@ -33,11 +34,14 @@ describe('routing-v1-http-api-client', () => {
ID: (await createEd25519PeerId()).toString(),
Addrs: ['/ip4/41.41.41.41/tcp/1234']
}, {
- Protocol: 'transport-bitswap',
- Schema: 'bitswap',
+ Protocols: ['transport-bitswap'],
+ Schema: 'peer',
Metadata: 'gBI=',
ID: (await createEd25519PeerId()).toString(),
Addrs: ['/ip4/42.42.42.42/tcp/1234']
+ }, {
+ ID: (await createEd25519PeerId()).toString(),
+ Addrs: ['/ip4/43.43.43.43/tcp/1234']
}]
const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')
@@ -50,8 +54,8 @@ describe('routing-v1-http-api-client', () => {
const provs = await all(client.getProviders(cid))
expect(provs.map(prov => ({
- id: prov.id.toString(),
- addrs: prov.multiaddrs.map(ma => ma.toString())
+ id: prov.ID.toString(),
+ addrs: prov.Addrs?.map(ma => ma.toString())
}))).to.deep.equal(providers.map(prov => ({
id: prov.ID,
addrs: prov.Addrs
@@ -103,4 +107,103 @@ describe('routing-v1-http-api-client', () => {
const provs = await all(client.getProviders(cid))
expect(provs).to.be.empty()
})
+
+ it('should find peers and only accepts correct peer records', async () => {
+ const peerId = await createEd25519PeerId()
+
+ const records = [{
+ Protocol: 'transport-bitswap',
+ Schema: 'bitswap',
+ Metadata: 'gBI=',
+ ID: peerId.toString(),
+ Addrs: ['/ip4/41.41.41.41/tcp/1234']
+ }, {
+ Protocol: 'transport-saddle',
+ Schema: 'horse-ride',
+ Metadata: 'gBI=',
+ ID: peerId.toString(),
+ Addrs: ['/ip4/41.41.41.41/tcp/1234']
+ }, {
+ Protocols: ['transport-bitswap'],
+ Schema: 'peer',
+ Metadata: 'gBI=',
+ ID: peerId.toString(),
+ Addrs: ['/ip4/42.42.42.42/tcp/1234']
+ }, {
+ Protocols: ['transport-bitswap'],
+ Schema: 'peer',
+ Metadata: 'gBI=',
+ ID: (await createEd25519PeerId()).toString(),
+ Addrs: ['/ip4/42.42.42.42/tcp/1234']
+ }]
+
+ // load peer for the router to fetch
+ await fetch(`${process.env.ECHO_SERVER}/add-peers/${peerId.toCID().toString()}`, {
+ method: 'POST',
+ body: records.map(prov => JSON.stringify(prov)).join('\n')
+ })
+
+ const peerRecords = await all(client.getPeerInfo(peerId))
+ expect(peerRecords.map(peerRecord => ({
+ ...peerRecord,
+ ID: peerRecord.ID.toString(),
+ Addrs: peerRecord.Addrs?.map(ma => ma.toString())
+ }))).to.deep.equal([
+ records[2]
+ ])
+ })
+
+ it('should get ipns record', async () => {
+ const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')
+ const peerId = await createEd25519PeerId()
+ const record = await createIpnsRecord(peerId, cid, 0, 1000)
+
+ // load record for the router to fetch
+ await fetch(`${process.env.ECHO_SERVER}/add-ipns/${peerId.toCID().toString()}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/vnd.ipfs.ipns-record'
+ },
+ body: marshalIpnsRecord(record)
+ })
+
+ const ipnsRecord = await client.getIPNS(peerId)
+ expect(marshalIpnsRecord(ipnsRecord)).to.equalBytes(marshalIpnsRecord(record))
+ })
+
+ it('get ipns record fails with bad record', async () => {
+ const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')
+ const peerId = await createEd25519PeerId()
+ const record = await createIpnsRecord(await createEd25519PeerId(), cid, 0, 1000)
+
+ // load record for the router to fetch
+ await fetch(`${process.env.ECHO_SERVER}/add-ipns/${peerId.toCID().toString()}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/vnd.ipfs.ipns-record'
+ },
+ body: marshalIpnsRecord(record)
+ })
+
+ await expect(client.getIPNS(peerId)).to.be.rejected()
+ })
+
+ it('should put ipns', async () => {
+ const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')
+ const peerId = await createEd25519PeerId()
+ const record = await createIpnsRecord(peerId, cid, 0, 1000)
+
+ await client.putIPNS(peerId, record)
+
+ // load record that our client just PUT to remote server
+ const res = await fetch(`${process.env.ECHO_SERVER}/get-ipns/${peerId.toCID().toString()}`, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/vnd.ipfs.ipns-record'
+ }
+ })
+
+ const receivedRecord = new Uint8Array(await res.arrayBuffer())
+ expect(marshalIpnsRecord(record)).to.equalBytes(receivedRecord)
+ })
})
diff --git a/packages/interop/README.md b/packages/interop/README.md
index 4419c69..22ff88f 100644
--- a/packages/interop/README.md
+++ b/packages/interop/README.md
@@ -4,14 +4,14 @@
-# @helia/routing-v1-http-api-interop
+# @helia/delegated-routing-v1-http-api-interop
[](https://ipfs.tech)
[](https://discuss.ipfs.tech)
-[](https://codecov.io/gh/ipfs/helia-routing-v1-http-api)
-[](https://github.com/ipfs/helia-routing-v1-http-api/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
+[](https://codecov.io/gh/ipfs/helia-delegated-routing-v1-http-api)
+[](https://github.com/ipfs/helia-delegated-routing-v1-http-api/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
-> Interop tests for the Routing V1 HTTP API server powered by Helia
+> Interop tests for the Delegated Routing V1 HTTP API server powered by Helia
## Table of contents
@@ -22,7 +22,7 @@
## Install
```console
-$ npm i @helia/routing-v1-http-api-interop
+$ npm i @helia/delegated-routing-v1-http-api-interop
```
## License
@@ -34,7 +34,7 @@ Licensed under either of
## Contribute
-Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia-routing-v1-http-api/issues).
+Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia-delegated-routing-v1-http-api/issues).
Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general.
diff --git a/packages/interop/package.json b/packages/interop/package.json
index 7b07fd4..81ae4fb 100644
--- a/packages/interop/package.json
+++ b/packages/interop/package.json
@@ -1,15 +1,15 @@
{
- "name": "@helia/routing-v1-http-api-interop",
- "version": "1.0.0",
- "description": "Interop tests for the Routing V1 HTTP API server powered by Helia",
+ "name": "@helia/delegated-routing-v1-http-api-interop",
+ "version": "0.0.0",
+ "description": "Interop tests for the Delegated Routing V1 HTTP API server powered by Helia",
"license": "Apache-2.0 OR MIT",
- "homepage": "https://github.com/ipfs/helia-routing-v1-http-api/tree/master/packages/interop#readme",
+ "homepage": "https://github.com/ipfs/helia-delegated-routing-v1-http-api/tree/master/packages/interop#readme",
"repository": {
"type": "git",
- "url": "git+https://github.com/ipfs/helia-routing-v1-http-api.git"
+ "url": "git+https://github.com/ipfs/helia-delegated-routing-v1-http-api.git"
},
"bugs": {
- "url": "https://github.com/ipfs/helia-routing-v1-http-api/issues"
+ "url": "https://github.com/ipfs/helia-delegated-routing-v1-http-api/issues"
},
"keywords": [
"IPFS"
@@ -43,18 +43,19 @@
"test": "aegir test -t node",
"test:node": "aegir test -t node --cov"
},
- "dependencies": {
- "@helia/routing-v1-http-api-client": "^1.0.0",
- "@helia/routing-v1-http-api-server": "^1.0.0",
- "helia": "^2.0.1"
- },
"devDependencies": {
+ "@helia/delegated-routing-v1-http-api-client": "~0.0.0",
+ "@helia/delegated-routing-v1-http-api-server": "~0.0.0",
"@helia/interface": "^2.0.0",
+ "@helia/ipns": "^2.0.1",
"@libp2p/interface": "^0.1.2",
"@libp2p/kad-dht": "^10.0.6",
+ "@libp2p/peer-id-factory": "^3.0.5",
"aegir": "^41.0.0",
"fastify": "^4.17.0",
- "libp2p": "^0.46.10",
+ "helia": "^2.0.1",
+ "ipns": "^7.0.1",
+ "it-first": "^3.0.3",
"multiformats": "^12.1.1"
},
"private": true
diff --git a/packages/interop/test/fixtures/create-helia.ts b/packages/interop/test/fixtures/create-helia.ts
index 88e322b..164680c 100644
--- a/packages/interop/test/fixtures/create-helia.ts
+++ b/packages/interop/test/fixtures/create-helia.ts
@@ -1,6 +1,4 @@
-import { kadDHT } from '@libp2p/kad-dht'
import { createHelia as createNode, type HeliaInit } from 'helia'
-import { identifyService } from 'libp2p/identify'
import type { Helia } from '@helia/interface'
import type { Libp2p } from '@libp2p/interface'
import type { KadDHT } from '@libp2p/kad-dht'
@@ -8,11 +6,7 @@ import type { KadDHT } from '@libp2p/kad-dht'
export async function createHelia (init?: Partial): Promise>> {
const helia = await createNode({
libp2p: {
- peerDiscovery: [],
- services: {
- identify: identifyService(),
- dht: kadDHT()
- }
+ peerDiscovery: []
}
})
diff --git a/packages/interop/test/index.spec.ts b/packages/interop/test/index.spec.ts
index 9000fbb..2261173 100644
--- a/packages/interop/test/index.spec.ts
+++ b/packages/interop/test/index.spec.ts
@@ -1,34 +1,39 @@
/* eslint-env mocha */
-import { createRoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
-import { createRoutingV1HttpApiServer } from '@helia/routing-v1-http-api-server'
+import { createDelegatedRoutingV1HttpApiClient } from '@helia/delegated-routing-v1-http-api-client'
+import { createDelegatedRoutingV1HttpApiServer } from '@helia/delegated-routing-v1-http-api-server'
+import { ipns } from '@helia/ipns'
+import { dht } from '@helia/ipns/routing'
+import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import { expect } from 'aegir/chai'
+import { create as createIpnsRecord } from 'ipns'
+import first from 'it-first'
import { CID } from 'multiformats/cid'
import * as raw from 'multiformats/codecs/raw'
import { sha256 } from 'multiformats/hashes/sha2'
import { createHelia } from './fixtures/create-helia.js'
+import type { DelegatedRoutingV1HttpApiClient } from '@helia/delegated-routing-v1-http-api-client'
import type { Helia } from '@helia/interface'
-import type { RoutingV1HttpApiClient } from '@helia/routing-v1-http-api-client'
import type { Libp2p } from '@libp2p/interface'
import type { KadDHT } from '@libp2p/kad-dht'
import type { FastifyInstance } from 'fastify'
-describe('routing-v1-http-api interop', () => {
+describe('delegated-routing-v1-http-api interop', () => {
let network: Array>>
let server: FastifyInstance
- let client: RoutingV1HttpApiClient
+ let client: DelegatedRoutingV1HttpApiClient
beforeEach(async () => {
network = await Promise.all(
new Array(10).fill(0).map(async () => createHelia())
)
- server = await createRoutingV1HttpApiServer(network[0])
+ server = await createDelegatedRoutingV1HttpApiServer(network[0])
const address = server.server.address()
const port = typeof address === 'string' ? address : address?.port
- client = createRoutingV1HttpApiClient(new URL(`http://127.0.0.1:${port}`))
+ client = createDelegatedRoutingV1HttpApiClient(new URL(`http://127.0.0.1:${port}`))
for (const node of network) {
for (const remote of network) {
@@ -64,7 +69,7 @@ describe('routing-v1-http-api interop', () => {
for await (const prov of client.getProviders(cid)) {
// should be a node in this test network
- if (network.map(node => node.libp2p.peerId.toString()).includes(prov.id.toString())) {
+ if (network.map(node => node.libp2p.peerId.toString()).includes(prov.ID.toString())) {
foundProvider = true
break
}
@@ -72,4 +77,44 @@ describe('routing-v1-http-api interop', () => {
expect(foundProvider).to.be.true()
})
+
+ it('should find peer info', async () => {
+ const result = await first(client.getPeerInfo(network[2].libp2p.peerId))
+
+ if (result == null) {
+ throw new Error('PeerInfo not found')
+ }
+
+ expect(result.ID.toString()).to.equal(network[2].libp2p.peerId.toString())
+ })
+
+ it('should get an IPNS record', async () => {
+ // publish a record using a remote host
+ const i = ipns(network[5], [
+ dht(network[5])
+ ])
+ const cid = CID.parse('bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354')
+ const peerId = await createEd25519PeerId()
+ await i.publish(peerId, cid)
+
+ // use client to resolve the published record
+ const record = await client.getIPNS(peerId)
+ expect(record.value).to.equal(`/ipfs/${cid.toString()}`)
+ })
+
+ it('should put an IPNS record', async () => {
+ // publish a record using the client
+ const cid = CID.parse('bafybeiczsscdsbs7ffqz55asqdf3smv6klcw3gofszvwlyarci47bgf354')
+ const peerId = await createEd25519PeerId()
+ const record = await createIpnsRecord(peerId, cid, 0, 1000 * 60 * 60 * 24)
+
+ await client.putIPNS(peerId, record)
+
+ // resolve the record using a remote host
+ const i = ipns(network[8], [
+ dht(network[8])
+ ])
+ const result = await i.resolve(peerId)
+ expect(result.toString()).to.equal(cid.toString())
+ })
})
diff --git a/packages/server/README.md b/packages/server/README.md
index e276b76..f4e3a44 100644
--- a/packages/server/README.md
+++ b/packages/server/README.md
@@ -4,14 +4,16 @@
-# @helia/routing-v1-http-api-server
+# @helia/delegated-routing-v1-http-api-server
[](https://ipfs.tech)
[](https://discuss.ipfs.tech)
-[](https://codecov.io/gh/ipfs/helia-routing-v1-http-api)
-[](https://github.com/ipfs/helia-routing-v1-http-api/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
+[](https://codecov.io/gh/ipfs/helia-delegated-routing-v1-http-api)
+[](https://github.com/ipfs/helia-delegated-routing-v1-http-api/actions/workflows/js-test-and-release.yml?query=branch%3Amain)
-> A Routing V1 HTTP API server powered by Helia
+> A Delegated Routing V1 HTTP API server powered by Helia
+
+A server implementation of the IPFS [Delegated Routing V1 HTTP API](https://specs.ipfs.tech/routing/http-routing-v1/).
## Table of contents
@@ -23,12 +25,12 @@
## Install
```console
-$ npm i @helia/routing-v1-http-api-server
+$ npm i @helia/delegated-routing-v1-http-api-server
```
## API Docs
--
+-
## License
@@ -39,7 +41,7 @@ Licensed under either of
## Contribute
-Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia-routing-v1-http-api/issues).
+Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia-delegated-routing-v1-http-api/issues).
Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general.
diff --git a/packages/server/package.json b/packages/server/package.json
index 4c47219..e359203 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,15 +1,15 @@
{
- "name": "@helia/routing-v1-http-api-server",
- "version": "1.0.3",
- "description": "A Routing V1 HTTP API server powered by Helia",
+ "name": "@helia/delegated-routing-v1-http-api-server",
+ "version": "0.0.0",
+ "description": "A Delegated Routing V1 HTTP API server powered by Helia",
"license": "Apache-2.0 OR MIT",
- "homepage": "https://github.com/ipfs/helia-routing-v1-http-api/tree/master/packages/server#readme",
+ "homepage": "https://github.com/ipfs/helia-delegated-routing-v1-http-api/tree/master/packages/server#readme",
"repository": {
"type": "git",
- "url": "git+https://github.com/ipfs/helia-routing-v1-http-api.git"
+ "url": "git+https://github.com/ipfs/helia-delegated-routing-v1-http-api.git"
},
"bugs": {
- "url": "https://github.com/ipfs/helia-routing-v1-http-api/issues"
+ "url": "https://github.com/ipfs/helia-delegated-routing-v1-http-api/issues"
},
"keywords": [
"IPFS"
@@ -153,11 +153,14 @@
"@fastify/cors": "^8.3.0",
"@helia/interface": "^2.0.0",
"@libp2p/interface": "^0.1.2",
+ "@libp2p/peer-id": "^3.0.3",
"fastify": "^4.17.0",
- "multiformats": "^12.1.1"
+ "ipns": "^7.0.1",
+ "multiformats": "^12.1.1",
+ "raw-body": "^2.5.2"
},
"devDependencies": {
- "@libp2p/peer-id-factory": "^3.0.3",
+ "@libp2p/peer-id-factory": "^3.0.5",
"@multiformats/multiaddr": "^12.1.3",
"@types/sinon": "^10.0.15",
"aegir": "^41.0.0",
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index 1bcc253..bf52b62 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -67,7 +67,7 @@ export interface ServerInit {
/**
* Create and return a Routing V1 HTTP API server
*/
-export async function createRoutingV1HttpApiServer (helia: Helia, init: ServerInit = {}): Promise {
+export async function createDelegatedRoutingV1HttpApiServer (helia: Helia, init: ServerInit = {}): Promise {
const server = init.fastify ?? fastify()
await server.register(cors, {
origin: '*',
diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts
index 959e4ff..69b027f 100644
--- a/packages/server/src/routes/index.ts
+++ b/packages/server/src/routes/index.ts
@@ -1,7 +1,13 @@
+import getIpnsV1 from './routing/v1/ipns/get.js'
+import putIpnsV1 from './routing/v1/ipns/put.js'
+import getPeersV1 from './routing/v1/peers/get.js'
import getProvidersV1 from './routing/v1/providers/get.js'
import type { Helia } from '@helia/interface'
import type { FastifyInstance } from 'fastify'
export default function routes (fastify: FastifyInstance, helia: Helia): void {
getProvidersV1(fastify, helia)
+ getPeersV1(fastify, helia)
+ getIpnsV1(fastify, helia)
+ putIpnsV1(fastify, helia)
}
diff --git a/packages/server/src/routes/routing/v1/ipns/get.ts b/packages/server/src/routes/routing/v1/ipns/get.ts
new file mode 100644
index 0000000..57ff1c3
--- /dev/null
+++ b/packages/server/src/routes/routing/v1/ipns/get.ts
@@ -0,0 +1,56 @@
+import { peerIdFromCID } from '@libp2p/peer-id'
+import { peerIdToRoutingKey } from 'ipns'
+import { CID } from 'multiformats/cid'
+import type { Helia } from '@helia/interface'
+import type { PeerId } from '@libp2p/interface/peer-id'
+import type { FastifyInstance } from 'fastify'
+
+interface Params {
+ name: string
+}
+
+export default function getIpnsV1 (fastify: FastifyInstance, helia: Helia): void {
+ fastify.route<{ Params: Params }>({
+ method: 'GET',
+ url: '/routing/v1/ipns/:name',
+ schema: {
+ // request needs to have a querystring with a `name` parameter
+ params: {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'string'
+ }
+ },
+ required: ['name']
+ }
+ },
+ handler: async (request, reply) => {
+ let peerId: PeerId
+ const controller = new AbortController()
+
+ request.raw.on('close', () => {
+ controller.abort()
+ })
+
+ try {
+ // PeerId must be encoded as a Libp2p-key CID.
+ const { name: cidStr } = request.params
+ const peerCid = CID.parse(cidStr)
+ peerId = peerIdFromCID(peerCid)
+ } catch (err) {
+ fastify.log.error('could not parse CID from params', err)
+ return reply.code(422).type('text/html').send('Unprocessable Entity')
+ }
+
+ const rawRecord = await helia.libp2p.contentRouting.get(peerIdToRoutingKey(peerId), {
+ signal: controller.signal
+ })
+
+ return reply
+ .header('Content-Type', 'application/vnd.ipfs.ipns-record')
+ // one cannot simply send rawRecord https://github.com/fastify/fastify/issues/5118
+ .send(Buffer.from(rawRecord, 0, rawRecord.byteLength))
+ }
+ })
+}
diff --git a/packages/server/src/routes/routing/v1/ipns/put.ts b/packages/server/src/routes/routing/v1/ipns/put.ts
new file mode 100644
index 0000000..c603107
--- /dev/null
+++ b/packages/server/src/routes/routing/v1/ipns/put.ts
@@ -0,0 +1,65 @@
+import { peerIdFromCID } from '@libp2p/peer-id'
+import { peerIdToRoutingKey } from 'ipns'
+import { ipnsValidator } from 'ipns/validator'
+import { CID } from 'multiformats/cid'
+import getRawBody from 'raw-body'
+import type { Helia } from '@helia/interface'
+import type { PeerId } from '@libp2p/interface/peer-id'
+import type { FastifyInstance } from 'fastify'
+
+interface Params {
+ name: string
+}
+
+export default function putIpnsV1 (fastify: FastifyInstance, helia: Helia): void {
+ fastify.addContentTypeParser('application/vnd.ipfs.ipns-record', function (request, payload, done) {
+ getRawBody(payload)
+ .then(buff => { done(null, buff) })
+ .catch(err => { done(err) })
+ })
+
+ fastify.route<{ Params: Params }>({
+ method: 'PUT',
+ url: '/routing/v1/ipns/:name',
+ schema: {
+ // request needs to have a querystring with a `name` parameter
+ params: {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'string'
+ }
+ },
+ required: ['name']
+ }
+ },
+ handler: async (request, reply) => {
+ let peerId: PeerId
+ const controller = new AbortController()
+
+ request.raw.on('close', () => {
+ controller.abort()
+ })
+
+ try {
+ // PeerId must be encoded as a Libp2p-key CID.
+ const { name: cidStr } = request.params
+ const peerCid = CID.parse(cidStr)
+ peerId = peerIdFromCID(peerCid)
+ } catch (err) {
+ fastify.log.error('could not parse CID from params', err)
+ return reply.code(422).type('text/html').send('Unprocessable Entity')
+ }
+
+ // @ts-expect-error request.body does not have a type
+ const body: Uint8Array = request.body
+ await ipnsValidator(peerIdToRoutingKey(peerId), body)
+
+ await helia.libp2p.contentRouting.put(peerIdToRoutingKey(peerId), body, {
+ signal: controller.signal
+ })
+
+ return reply.send()
+ }
+ })
+}
diff --git a/packages/server/src/routes/routing/v1/peers/get.ts b/packages/server/src/routes/routing/v1/peers/get.ts
new file mode 100644
index 0000000..9aaf980
--- /dev/null
+++ b/packages/server/src/routes/routing/v1/peers/get.ts
@@ -0,0 +1,72 @@
+import { PassThrough } from 'node:stream'
+import { peerIdFromCID } from '@libp2p/peer-id'
+import { CID } from 'multiformats/cid'
+import type { Helia } from '@helia/interface'
+import type { PeerId } from '@libp2p/interface/peer-id'
+import type { FastifyInstance } from 'fastify'
+
+interface Params {
+ peerId: string
+}
+
+export default function getPeersV1 (fastify: FastifyInstance, helia: Helia): void {
+ fastify.route<{ Params: Params }>({
+ method: 'GET',
+ url: '/routing/v1/peers/:peerId',
+ schema: {
+ // request needs to have a querystring with a `name` parameter
+ params: {
+ type: 'object',
+ properties: {
+ peerId: {
+ type: 'string'
+ }
+ },
+ required: ['peerId']
+ }
+ },
+ handler: async (request, reply) => {
+ let peerId: PeerId
+ const controller = new AbortController()
+
+ request.raw.on('close', () => {
+ controller.abort()
+ })
+
+ try {
+ const { peerId: cidStr } = request.params
+ const peerCid = CID.parse(cidStr)
+ peerId = peerIdFromCID(peerCid)
+ } catch (err) {
+ fastify.log.error('could not parse CID from params', err)
+ return reply.code(422).type('text/html').send('Unprocessable Entity')
+ }
+
+ const peerInfo = await helia.libp2p.peerRouting.findPeer(peerId, {
+ signal: controller.signal
+ })
+ const peerRecord = {
+ Schema: 'peer',
+ ID: peerInfo.id.toString(),
+ Addrs: peerInfo.multiaddrs.map(ma => ma.toString())
+ }
+
+ if (request.headers.accept?.includes('application/x-ndjson') === true) {
+ const stream = new PassThrough()
+ stream.push(JSON.stringify(peerRecord) + '\n')
+ stream.end()
+
+ // these are .thenables but not .catchables?
+ return reply
+ .header('Content-Type', 'application/x-ndjson')
+ .send(stream)
+ } else {
+ return reply
+ .header('Content-Type', 'application/json')
+ .send({
+ Peers: [peerRecord]
+ })
+ }
+ }
+ })
+}
diff --git a/packages/server/src/routes/routing/v1/providers/get.ts b/packages/server/src/routes/routing/v1/providers/get.ts
index 4d9e6f3..184cd5d 100644
--- a/packages/server/src/routes/routing/v1/providers/get.ts
+++ b/packages/server/src/routes/routing/v1/providers/get.ts
@@ -8,15 +8,15 @@ interface Params {
cid: string
}
-interface Provider {
- Protocol: string
+interface PeerRecord {
Schema: string
+ Protocols?: string[]
ID: string
Addrs: string[]
}
interface Providers {
- Providers: Provider[]
+ Providers: PeerRecord[]
}
const MAX_PROVIDERS = 100
@@ -39,48 +39,57 @@ export default function getProvidersV1 (fastify: FastifyInstance, helia: Helia):
},
handler: async (request, reply) => {
let cid: CID
+ const controller = new AbortController()
+
+ request.raw.on('close', () => {
+ controller.abort()
+ })
try {
const { cid: cidStr } = request.params
cid = CID.parse(cidStr)
} catch (err) {
- // these are .thenables but not .catchables?
- reply.code(422).type('text/html').send('Unprocessable Entity') // eslint-disable-line @typescript-eslint/no-floating-promises
- return
+ fastify.log.error('could not parse CID from params', err)
+ return reply.code(422).type('text/html').send('Unprocessable Entity')
}
if (request.headers.accept?.includes('application/x-ndjson') === true) {
const stream = new PassThrough()
- try {
- let found = 0
+ // wait until we have the first result
+ const iterable = streamingHandler(cid, helia, {
+ signal: controller.signal
+ })
+ const result = await iterable.next()
- for await (const prov of streamingHandler(cid, helia)) {
- if (found === 0) {
- // these are .thenables but not .catchables?
- reply.header('Content-Type', 'application/x-ndjson') // eslint-disable-line @typescript-eslint/no-floating-promises
- reply.send(stream) // eslint-disable-line @typescript-eslint/no-floating-promises
- }
+ // if we have a value, send the value in a stream
+ if (result.done !== true) {
+ stream.push(JSON.stringify(result.value) + '\n')
- found++
-
- stream.push(JSON.stringify(prov) + '\n')
- }
-
- if (found > 0) {
- return
- }
- } finally {
- stream.end()
+ // iterate over the rest of the results
+ void Promise.resolve().then(async () => {
+ for await (const prov of iterable) {
+ stream.push(JSON.stringify(prov) + '\n')
+ }
+ })
+ .catch(err => {
+ fastify.log.error('could send stream of providers', err)
+ })
+ .finally(() => {
+ stream.end()
+ })
+
+ return reply
+ .header('Content-Type', 'application/x-ndjson')
+ .send(stream)
}
} else {
- const result = await nonStreamingHandler(cid, helia)
+ const result = await nonStreamingHandler(cid, helia, {
+ signal: controller.signal
+ })
if (result.Providers.length > 0) {
- // this is .thenable but not .catchable?
- reply.header('Content-Type', 'application/json') // eslint-disable-line @typescript-eslint/no-floating-promises
-
- return reply.send(result)
+ return reply.header('Content-Type', 'application/json').send(result)
}
}
@@ -89,13 +98,12 @@ export default function getProvidersV1 (fastify: FastifyInstance, helia: Helia):
})
}
-async function * streamingHandler (cid: CID, helia: Helia, options?: AbortOptions): AsyncGenerator {
+async function * streamingHandler (cid: CID, helia: Helia, options?: AbortOptions): AsyncGenerator {
let provs = 0
for await (const prov of helia.libp2p.contentRouting.findProviders(cid, options)) {
yield {
- Protocol: 'transport-bitswap',
- Schema: 'bitswap',
+ Schema: 'peer',
ID: prov.id.toString(),
Addrs: prov.multiaddrs.map(ma => ma.toString())
}
@@ -114,8 +122,7 @@ async function nonStreamingHandler (cid: CID, helia: Helia, options?: AbortOptio
try {
for await (const prov of helia.libp2p.contentRouting.findProviders(cid, options)) {
providers.push({
- Protocol: 'transport-bitswap',
- Schema: 'bitswap',
+ Schema: 'peer',
ID: prov.id.toString(),
Addrs: prov.multiaddrs.map(ma => ma.toString())
})
diff --git a/packages/server/test/index.spec.ts b/packages/server/test/index.spec.ts
index 652eb13..6fc1ac2 100644
--- a/packages/server/test/index.spec.ts
+++ b/packages/server/test/index.spec.ts
@@ -3,21 +3,23 @@
import { createEd25519PeerId } from '@libp2p/peer-id-factory'
import { multiaddr } from '@multiformats/multiaddr'
import { expect } from 'aegir/chai'
+import { create as createIpnsRecord, marshal as marshalIpnsRecord, peerIdToRoutingKey } from 'ipns'
+import { CID } from 'multiformats'
import { stubInterface } from 'sinon-ts'
-import { createRoutingV1HttpApiServer } from '../src/index.js'
+import { createDelegatedRoutingV1HttpApiServer } from '../src/index.js'
import type { Helia } from '@helia/interface'
import type { PeerInfo } from '@libp2p/interface/peer-info'
import type { FastifyInstance } from 'fastify'
import type { StubbedInstance } from 'sinon-ts'
-describe('routing-v1-http-api-server', () => {
+describe('delegated-routing-v1-http-api-server', () => {
let helia: StubbedInstance
let server: FastifyInstance
let url: URL
beforeEach(async () => {
helia = stubInterface()
- server = await createRoutingV1HttpApiServer(helia, {
+ server = await createDelegatedRoutingV1HttpApiServer(helia, {
listen: {
host: '127.0.0.1',
port: 0
@@ -47,7 +49,7 @@ describe('routing-v1-http-api-server', () => {
expect(res.headers.get('access-control-allow-methods')).to.equal('GET, OPTIONS')
})
- it('returns 422 if the CID is invalid', async () => {
+ it('GET providers returns 422 if the CID is invalid', async () => {
const res = await fetch(`${url}routing/v1/providers/derp`, {
method: 'GET'
})
@@ -55,7 +57,7 @@ describe('routing-v1-http-api-server', () => {
expect(res.status).to.equal(422)
})
- it('returns 404 if the CID is missing', async () => {
+ it('GET providers returns 404 if the CID is missing', async () => {
const res = await fetch(`${url}routing/v1/providers`, {
method: 'GET'
})
@@ -63,7 +65,7 @@ describe('routing-v1-http-api-server', () => {
expect(res.status).to.equal(404)
})
- it('returns 404 if no providers are found', async () => {
+ it('GET providers returns 404 if no providers are found', async () => {
helia.libp2p = {
// @ts-expect-error incomplete implementation
contentRouting: {
@@ -78,7 +80,7 @@ describe('routing-v1-http-api-server', () => {
expect(res.status).to.equal(404)
})
- it('returns 404 if no providers are found when streaming', async () => {
+ it('GET providers returns 404 if no providers are found when streaming', async () => {
helia.libp2p = {
// @ts-expect-error incomplete implementation
contentRouting: {
@@ -96,7 +98,7 @@ describe('routing-v1-http-api-server', () => {
expect(res.status).to.equal(404)
})
- it('returns providers', async () => {
+ it('GET providers returns providers', async () => {
const provider1: PeerInfo = {
id: await createEd25519PeerId(),
multiaddrs: [
@@ -130,17 +132,15 @@ describe('routing-v1-http-api-server', () => {
const json = await res.json()
- expect(json).to.have.nested.property('Providers[0].Protocol', 'transport-bitswap')
- expect(json).to.have.nested.property('Providers[0].Schema', 'bitswap')
+ expect(json).to.have.nested.property('Providers[0].Schema', 'peer')
expect(json).to.have.nested.property('Providers[0].ID', provider1.id.toString())
expect(json).to.have.deep.nested.property('Providers[0].Addrs', provider1.multiaddrs.map(ma => ma.toString()))
- expect(json).to.have.nested.property('Providers[1].Protocol', 'transport-bitswap')
- expect(json).to.have.nested.property('Providers[1].Schema', 'bitswap')
+ expect(json).to.have.nested.property('Providers[1].Schema', 'peer')
expect(json).to.have.nested.property('Providers[1].ID', provider2.id.toString())
expect(json).to.have.deep.nested.property('Providers[1].Addrs', provider2.multiaddrs.map(ma => ma.toString()))
})
- it('returns provider stream', async () => {
+ it('GET providers returns provider stream', async () => {
const provider1: PeerInfo = {
id: await createEd25519PeerId(),
multiaddrs: [
@@ -180,13 +180,130 @@ describe('routing-v1-http-api-server', () => {
.filter(Boolean)
.map(str => JSON.parse(str))
- expect(json).to.have.nested.property('[0].Protocol', 'transport-bitswap')
- expect(json).to.have.nested.property('[0].Schema', 'bitswap')
+ expect(json).to.have.nested.property('[0].Schema', 'peer')
expect(json).to.have.nested.property('[0].ID', provider1.id.toString())
expect(json).to.have.deep.nested.property('[0].Addrs', provider1.multiaddrs.map(ma => ma.toString()))
- expect(json).to.have.nested.property('[1].Protocol', 'transport-bitswap')
- expect(json).to.have.nested.property('[1].Schema', 'bitswap')
+ expect(json).to.have.nested.property('[1].Schema', 'peer')
expect(json).to.have.nested.property('[1].ID', provider2.id.toString())
expect(json).to.have.deep.nested.property('[1].Addrs', provider2.multiaddrs.map(ma => ma.toString()))
})
+
+ it('GET peers returns 422 if peer id is not cid', async () => {
+ const res = await fetch(`${url}routing/v1/peers/${(await createEd25519PeerId()).toString()}`, {
+ method: 'GET'
+ })
+
+ expect(res.status).to.equal(422)
+ })
+
+ it('GET peers returns 422 if cid is not peer id', async () => {
+ const res = await fetch(`${url}routing/v1/peers/bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4`, {
+ method: 'GET'
+ })
+
+ expect(res.status).to.equal(422)
+ })
+
+ it('GET peers returns peer records for get peers', async () => {
+ const peer: PeerInfo = {
+ id: await createEd25519PeerId(),
+ multiaddrs: [
+ multiaddr('/ip4/123.123.123.123/tcp/123')
+ ],
+ protocols: ['transport-bitswap']
+ }
+
+ helia.libp2p = {
+ // @ts-expect-error incomplete implementation
+ peerRouting: {
+ findPeer: async function () {
+ return peer
+ }
+ }
+ }
+
+ const res = await fetch(`${url}routing/v1/peers/${peer.id.toCID().toString()}`, {
+ method: 'GET'
+ })
+ expect(res.status).to.equal(200)
+
+ const json = await res.json()
+ expect(json).to.have.nested.property('Peers[0].Schema', 'peer')
+ expect(json).to.have.nested.property('Peers[0].ID', peer.id.toString())
+ expect(json).to.have.deep.nested.property('Peers[0].Addrs', peer.multiaddrs.map(ma => ma.toString()))
+ })
+
+ it('GET ipns returns 422 if peer id is not cid', async () => {
+ const res = await fetch(`${url}routing/v1/ipns/${(await createEd25519PeerId()).toString()}`, {
+ method: 'GET'
+ })
+
+ expect(res.status).to.equal(422)
+ })
+
+ it('GET ipns returns 422 if cid is not peer id', async () => {
+ const res = await fetch(`${url}routing/v1/ipns/bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4`, {
+ method: 'GET'
+ })
+
+ expect(res.status).to.equal(422)
+ })
+
+ it('GET ipns returns record', async () => {
+ const peerId = await createEd25519PeerId()
+ const cid = CID.parse('bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4')
+ const record = await createIpnsRecord(peerId, cid, 0, 1000)
+
+ helia.libp2p = {
+ // @ts-expect-error incomplete implementation
+ contentRouting: {
+ get: async function () {
+ return marshalIpnsRecord(record)
+ }
+ }
+ }
+
+ const res = await fetch(`${url}routing/v1/ipns/${peerId.toCID().toString()}`, {
+ method: 'GET',
+ headers: {
+ accept: 'application/vnd.ipfs.ipns-record'
+ }
+ })
+
+ expect(res.status).to.equal(200)
+ const arrayBuffer = await res.arrayBuffer()
+ expect(new Uint8Array(arrayBuffer)).to.equalBytes(marshalIpnsRecord(record))
+ })
+
+ it('PUT ipns puts record', async () => {
+ const peerId = await createEd25519PeerId()
+ const cid = CID.parse('bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4')
+ const record = await createIpnsRecord(peerId, cid, 0, 1000)
+ const marshalledRecord = marshalIpnsRecord(record)
+
+ let putKey: Uint8Array = new Uint8Array()
+ let putValue: Uint8Array = new Uint8Array()
+
+ helia.libp2p = {
+ // @ts-expect-error incomplete implementation
+ contentRouting: {
+ put: async function (key: Uint8Array, value: Uint8Array) {
+ putKey = key
+ putValue = value
+ }
+ }
+ }
+
+ const res = await fetch(`${url}routing/v1/ipns/${peerId.toCID().toString()}`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/vnd.ipfs.ipns-record'
+ },
+ body: marshalledRecord
+ })
+
+ expect(res.status).to.equal(200)
+ expect(putKey).to.equalBytes(peerIdToRoutingKey(peerId))
+ expect(putValue).to.equalBytes(marshalledRecord)
+ })
})