@statewalker/httpeers-libp2p 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -134
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -1,173 +1,219 @@
|
|
|
1
1
|
# @statewalker/httpeers-libp2p
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
## What it is
|
|
4
|
+
|
|
5
|
+
The httpeers transport: libp2p nodes, identity keys, reachability through
|
|
6
|
+
circuit relays, and `servePeer`, which puts a mount table on the wire and calls
|
|
7
|
+
other peers. It is the only httpeers package that imports libp2p; everything
|
|
8
|
+
above it speaks `FetchHandler` and `Mounts`.
|
|
9
|
+
|
|
10
|
+
## Why it exists
|
|
11
|
+
|
|
12
|
+
A hub, a Node member and a page all need the same arrangement: a node, a
|
|
13
|
+
router, an inbound protocol handler and an outbound dialler — and, for a peer
|
|
14
|
+
that cannot accept inbound connections, a relay reservation that stays alive.
|
|
15
|
+
Getting reachability right is most of the work (a reservation that silently
|
|
16
|
+
lapses makes a peer unreachable with no error anywhere), so it is written once,
|
|
17
|
+
here, and every assembly takes it as a library instead of wiring libp2p by hand.
|
|
18
|
+
|
|
19
|
+
## How to use
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pnpm add @statewalker/httpeers-libp2p
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
libp2p and its transports are regular dependencies. Under Node,
|
|
26
|
+
`@libp2p/webrtc` needs the native `node-datachannel` module; with pnpm 10 it
|
|
27
|
+
must be listed in `onlyBuiltDependencies`, or pnpm skips its build script and
|
|
28
|
+
the import fails at runtime (see the repository root README).
|
|
29
|
+
|
|
30
|
+
| Import | Gives | Runs in |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `.` | `createNode`, `servePeer`, identity (`generateKey`, `peerIdOf`, `signerOf`, `identityStore`), reachability, duplex | Node and browsers |
|
|
33
|
+
| `./node` | `nodeTransports()` (TCP), `fileBytesStore` | Node only (`@libp2p/tcp`, `node:fs`) |
|
|
34
|
+
| `./browser` | `browserTransports()` (WebSockets, circuit relay, WebRTC), `idbBytesStore` | browsers (IndexedDB) |
|
|
35
|
+
|
|
36
|
+
The root never reaches `@libp2p/tcp` or a `node:` builtin; `tests/boundary.test.ts`
|
|
37
|
+
asserts it. `@libp2p/webrtc` is at the root because a page needs it and a Node
|
|
38
|
+
process can load it too.
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
A Node peer serving a mount table and calling another peer:
|
|
5
43
|
|
|
6
44
|
```ts
|
|
45
|
+
import { createMounts } from "@statewalker/httpeers-core";
|
|
7
46
|
import { createNode, generateKey, servePeer } from "@statewalker/httpeers-libp2p";
|
|
8
47
|
import { nodeTransports } from "@statewalker/httpeers-libp2p/node";
|
|
9
48
|
|
|
10
|
-
const node = await createNode({
|
|
11
|
-
|
|
49
|
+
const node = await createNode({
|
|
50
|
+
privateKey: await generateKey(),
|
|
51
|
+
transports: nodeTransports(),
|
|
52
|
+
listen: ["/ip4/0.0.0.0/tcp/0"],
|
|
53
|
+
});
|
|
54
|
+
const mounts = createMounts();
|
|
55
|
+
const peer = await servePeer({ node, mounts, access: guard }); // guard: e.g. withAccess(...)
|
|
12
56
|
|
|
13
|
-
await peer.call(
|
|
57
|
+
const res = await peer.call(otherPeerId, new Request("http://peer.local/hello"));
|
|
14
58
|
```
|
|
15
59
|
|
|
16
|
-
|
|
17
|
-
it imports `libp2p`.
|
|
60
|
+
Keeping a relay reservation alive and exposing its health:
|
|
18
61
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
A hub, a Node member and a page were each wiring a node, a router, an inbound
|
|
22
|
-
handler and an outbound dialler by hand. `servePeer` is that arrangement, once:
|
|
23
|
-
it owns the wire and the router, and it takes `access` as a parameter rather
|
|
24
|
-
than knowing what a token is.
|
|
62
|
+
```ts
|
|
63
|
+
import { reservationHealthy, superviseRelay } from "@statewalker/httpeers-libp2p";
|
|
25
64
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
──takes──> access: (handler) => handler ← withAccess, from httpeers-access
|
|
65
|
+
const supervisor = superviseRelay({ node, relayAddr });
|
|
66
|
+
const healthy = reservationHealthy(supervisor.state()); // reserved, or lost < 2 min
|
|
29
67
|
```
|
|
30
68
|
|
|
31
|
-
|
|
32
|
-
has no crypto policy; the graph runs `core → access` and `core → libp2p`, and
|
|
33
|
-
`tests/boundary.test.ts` fails if either starts pointing at the other.
|
|
69
|
+
Signing tokens with the node's own key:
|
|
34
70
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
71
|
+
```ts
|
|
72
|
+
import { signerOf } from "@statewalker/httpeers-libp2p";
|
|
73
|
+
import { mintToken } from "@statewalker/httpeers-access/issuer";
|
|
74
|
+
|
|
75
|
+
const signer = signerOf(hubKey); // { mesh: peerIdOf(hubKey), seed }
|
|
76
|
+
const token = await mintToken({ signer, sub, roles: ["member"], ttlMs: 60_000 });
|
|
77
|
+
```
|
|
38
78
|
|
|
39
|
-
##
|
|
79
|
+
## Internals
|
|
40
80
|
|
|
41
|
-
`
|
|
42
|
-
its node factory, so importing the transport module at all dragged a Node-only
|
|
43
|
-
transport into a browser bundle — in a package whose whole claim was that one
|
|
44
|
-
implementation runs on both.
|
|
81
|
+
### `servePeer` owns the wire and takes policy as a parameter
|
|
45
82
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
83
|
+
```
|
|
84
|
+
servePeer ──owns──> the protocol handler (/httpeers/1.0.0), the router, the outbound dialler
|
|
85
|
+
──takes──> access: (handler) => handler e.g. withAccess from httpeers-access
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`httpeers-access` has no transport and this package has no access policy; the
|
|
89
|
+
graph runs `core → access` and `core → libp2p`, and the boundary test fails if
|
|
90
|
+
either points at the other. `Peer.dispatch` is the inbound router *after*
|
|
91
|
+
identity binding and policy, so a local edge reuses the same decision path.
|
|
92
|
+
Inbound identity comes from the Noise handshake and is bound with
|
|
93
|
+
`registerPeer` by `@statewalker/httpeers-bridge`.
|
|
51
94
|
|
|
52
|
-
|
|
53
|
-
Note what is *not* forbidden: `@libp2p/webrtc` is at the root, because a page
|
|
54
|
-
needs it and a Node process can load it too — though only if its native
|
|
55
|
-
dependency actually built. See the note on `node-datachannel` at the repository
|
|
56
|
-
root.
|
|
95
|
+
### `createNode` requires transports
|
|
57
96
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
97
|
+
There is no default transport list. A default containing `tcp()` would drag a
|
|
98
|
+
Node-only transport into every browser bundle that imports the root. Pass
|
|
99
|
+
`nodeTransports()` or `browserTransports()`; with none, `createNode` throws
|
|
100
|
+
`createNode: at least one transport is required. Use \`nodeTransports()\` …`.
|
|
101
|
+
The root uses an explicit export list, never `export *`, so nothing Node-only
|
|
102
|
+
leaks in through a barrel.
|
|
62
103
|
|
|
63
|
-
|
|
104
|
+
### A browser is reachable only through a relay reservation
|
|
64
105
|
|
|
65
|
-
A
|
|
66
|
-
accepts
|
|
67
|
-
that:
|
|
106
|
+
A page cannot listen for inbound connections. It reserves a slot on a relay and
|
|
107
|
+
accepts a WebRTC upgrade brokered over it:
|
|
68
108
|
|
|
69
109
|
| Module | What it does |
|
|
70
110
|
|---|---|
|
|
71
|
-
| `reservation` | `dialRelay`, `waitForCircuitReservation`, `circuitAddrs`, `superviseRelay
|
|
72
|
-
| `hop-reserve` | `requestRelayReservation` —
|
|
111
|
+
| `reservation` | `dialRelay`, `waitForCircuitReservation`, `circuitAddrs`, `superviseRelay`, `reservationHealthy`, `renewalIntervalMs` |
|
|
112
|
+
| `hop-reserve` | `requestRelayReservation` — a circuit-relay v2 `HOP RESERVE` on the existing connection |
|
|
73
113
|
| `timers` | the injectable `Timers` seam (`worker-timers` in a background tab) |
|
|
74
|
-
| `hub-link` | `reachHub`, `reachHubRelayed`, `reserveOnHub
|
|
75
|
-
| `hub-relay` | `hubRelayService`, `membershipGater`, `releaseReservation` — a hub relaying for
|
|
76
|
-
| `identity` | `generateKey`, `peerIdOf`, `signerOf`,
|
|
77
|
-
| `duplex` |
|
|
78
|
-
|
|
79
|
-
**A reservation yields two addresses and only one works.** The bare
|
|
80
|
-
`/p2p-circuit`
|
|
81
|
-
the protocol; the `/webrtc`-suffixed one is the one to
|
|
82
|
-
returns both, labelled
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
the node's own belief
|
|
87
|
-
hours while the relay
|
|
88
|
-
every member
|
|
89
|
-
address list
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
libp2p client and prints
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
failed reservation
|
|
116
|
-
on"), with the relay's status only in the text.
|
|
117
|
-
throws
|
|
118
|
-
`
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
`
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
114
|
+
| `hub-link` | `reachHub`, `reachHubRelayed`, `reserveOnHub`, `leaveRelay`, `superviseHubReservation` |
|
|
115
|
+
| `hub-relay` | `hubRelayService`, `membershipGater`, `releaseReservation` — a hub relaying for its own members only |
|
|
116
|
+
| `identity` | `generateKey`, `peerIdOf`, `signerOf`, `identityStore` |
|
|
117
|
+
| `duplex` | `serveDuplex`, `openDuplex`, `createDuplexMounts` — see below |
|
|
118
|
+
|
|
119
|
+
**A reservation yields two addresses, and only one works.** The bare
|
|
120
|
+
`/p2p-circuit` address is a *limited* connection on which libp2p silently
|
|
121
|
+
refuses the httpeers protocol; the `/webrtc`-suffixed one is the one to
|
|
122
|
+
publish. `circuitAddrs` returns both, labelled.
|
|
123
|
+
|
|
124
|
+
### Only the relay knows whether you are reserved
|
|
125
|
+
|
|
126
|
+
`node.getMultiaddrs()` is the node's own belief. A node can keep listing a
|
|
127
|
+
circuit address for hours while the relay holds no reservation: the WebSocket
|
|
128
|
+
is still up, and every member trying to reach it gets `NO_RESERVATION`. So the
|
|
129
|
+
address list proves a **loss** (the address is gone), never health.
|
|
130
|
+
|
|
131
|
+
`superviseRelay` therefore asks the relay, with a `HOP RESERVE` over the
|
|
132
|
+
existing connection, on a schedule derived from the TTL the relay granted
|
|
133
|
+
(`renewalIntervalMs`: a quarter of it, jittered down into its upper quarter;
|
|
134
|
+
22.5–30 minutes against a two-hour TTL, clamped to 1–30 minutes). libp2p's own
|
|
135
|
+
`addRelay` is not usable for this: it short-circuits on its cached entry, is not
|
|
136
|
+
reachable from `Libp2p`, and blacklists a relay locally on failure. A relay
|
|
137
|
+
keys reservations by peer, so one request renews an entry that exists and
|
|
138
|
+
re-creates one that does not, keeping live circuits.
|
|
139
|
+
|
|
140
|
+
`supervisor.state()` reports status, `verifiedAt`, `expiresAt`, `lostSince`,
|
|
141
|
+
consecutive failures, renewal and restore counts and the last error.
|
|
142
|
+
`reservationHealthy(state)` is the rule for a health check: reserved, or lost
|
|
143
|
+
for less than `RESERVATION_LOSS_GRACE_MS` (2 minutes). Every transition logs one
|
|
144
|
+
line naming the relay and the reason.
|
|
145
|
+
|
|
146
|
+
`scripts/probe-hub.mjs <hubPeerId>` checks reachability from **outside**, as a
|
|
147
|
+
member would: it reads the relay address from
|
|
148
|
+
`https://relay.httpeers.net/.well-known/httpeers-relay.json`, dials
|
|
149
|
+
`<relay>/p2p-circuit/p2p/<hub>` with an independent libp2p client and prints
|
|
150
|
+
one JSON line. Run it from this package's directory so its imports resolve.
|
|
151
|
+
It is the first check when members report a hub as unreachable.
|
|
152
|
+
|
|
153
|
+
### A refused hub reservation says which refusal it was
|
|
154
|
+
|
|
155
|
+
libp2p reports every failed reservation the same way ("Some configured
|
|
156
|
+
addresses failed to be listened on"), with the relay's status only in the text.
|
|
157
|
+
`reserveOnHub` reads it out and throws `HubReservationError` with `status` and
|
|
158
|
+
`refusal`:
|
|
159
|
+
|
|
160
|
+
| `refusal` | Relay status | Message starts |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `store-full` | `RESERVATION_REFUSED` | `hub-link: the hub (…) refused a reservation with RESERVATION_REFUSED: the hub's reservation store is full` |
|
|
163
|
+
| `resource-limit` | `RESOURCE_LIMIT_EXCEEDED` | `… refused a reservation with RESOURCE_LIMIT_EXCEEDED: the hub's relay is at a resource limit.` |
|
|
164
|
+
| `not-a-member` | `PERMISSION_DENIED` | `… this peer is not a member as far as the hub is concerned` |
|
|
165
|
+
| `no-relay` | — | `… does not relay at all -- it has no circuit-relay service` |
|
|
166
|
+
| `no-answer` | — | `… gave no answer to a reservation request.` |
|
|
167
|
+
| `other` | any other | `… refused a reservation with <status>.` |
|
|
168
|
+
|
|
169
|
+
A full store is not a membership problem, and the message says so.
|
|
170
|
+
`tests/hub-link.test.ts` pins each case.
|
|
171
|
+
|
|
172
|
+
### A hub's reservation store is sized for a mesh
|
|
173
|
+
|
|
174
|
+
libp2p's relay default holds 15 reservations for two hours each and keeps one
|
|
175
|
+
after its holder disconnects; a hub on that default refuses every member with
|
|
132
176
|
`RESERVATION_REFUSED` after fifteen distinct peers. `hubRelayService` sizes the
|
|
133
177
|
store at `HUB_MAX_RESERVATIONS` (4096; `{ maxReservations }` overrides it),
|
|
134
178
|
releases a reservation when its holder's last connection closes, and
|
|
135
|
-
`releaseReservation(relay, peerId)` frees a revoked member's slot at once.
|
|
136
|
-
|
|
137
|
-
keep
|
|
179
|
+
`releaseReservation(relay, peerId)` frees a revoked member's slot at once. The
|
|
180
|
+
per-circuit data limits stay at libp2p's defaults (128 KiB, 2 minutes): they
|
|
181
|
+
keep the hub a signalling channel. The store bounds how *many* members are
|
|
138
182
|
reachable through the hub, not how *much* may cross it.
|
|
139
183
|
|
|
140
|
-
|
|
184
|
+
`membershipGater` takes a thunk, not a value: the hub's node must exist before
|
|
185
|
+
the member store that answers "is this a member", and the thunk is read at
|
|
186
|
+
decision time.
|
|
141
187
|
|
|
142
|
-
|
|
143
|
-
node from one key and mint with another, and every token's `mesh` claim names a
|
|
144
|
-
peer nobody is talking to — a failure that looks like a policy bug.
|
|
188
|
+
### `signerOf` keeps the mesh name and the signing key the same
|
|
145
189
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
190
|
+
A hub signs membership tokens with the key its node speaks with. Build the node
|
|
191
|
+
from one key and mint with another, and every token's `mesh` claim names a peer
|
|
192
|
+
nobody is talking to — which looks like a policy bug. `signerOf(key)` returns
|
|
193
|
+
`{ mesh: peerIdOf(key), seed }`. Its return type is declared structurally, so
|
|
194
|
+
this package does not depend on `httpeers-access`; a mismatch is a compile error
|
|
195
|
+
in the assembly that wires the two together.
|
|
196
|
+
|
|
197
|
+
### Duplex is a second protocol for a different shape
|
|
149
198
|
|
|
150
|
-
|
|
151
|
-
`
|
|
152
|
-
|
|
153
|
-
|
|
199
|
+
A fetch contract cannot express a WebSocket: both sides talking, neither input
|
|
200
|
+
closed. `serveDuplex` / `openDuplex` run one `Duplex` per libp2p stream on
|
|
201
|
+
`/httpeers-duplex/1.0.0`, addressed by path through their own mount table
|
|
202
|
+
(`createDuplexMounts`).
|
|
154
203
|
|
|
155
|
-
|
|
204
|
+
### Dependencies
|
|
156
205
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
than a flag.
|
|
206
|
+
libp2p (`libp2p`, `@libp2p/*`, `@chainsafe/libp2p-noise`, `@chainsafe/libp2p-yamux`,
|
|
207
|
+
`@multiformats/multiaddr`), `@statewalker/httpeers-core`,
|
|
208
|
+
`@statewalker/httpeers-bridge` (HTTP over the link), and
|
|
209
|
+
`@statewalker/webrun-streams`, `@statewalker/webrun-streams-libp2p`,
|
|
210
|
+
`@statewalker/webrun-http-streams` (duplex streams over libp2p).
|
|
163
211
|
|
|
164
|
-
|
|
212
|
+
Tests: `pnpm --filter @statewalker/httpeers-libp2p test`. Files run serially
|
|
213
|
+
because they start real libp2p nodes. `tests/serve-peer.test.ts` stands up two
|
|
214
|
+
nodes over a real Noise handshake and checks that what arrives carries the
|
|
215
|
+
identity the transport proved, not what the caller said.
|
|
165
216
|
|
|
166
|
-
|
|
167
|
-
`tests/hub-link.test.ts` pins how `reserveOnHub` reads each refusal out of
|
|
168
|
-
libp2p's error text. `tests/serve-peer.test.ts` stands up two real nodes over a real Noise
|
|
169
|
-
handshake and checks that what arrives carries the identity the **transport**
|
|
170
|
-
proved rather than anything the caller said.
|
|
217
|
+
## License
|
|
171
218
|
|
|
172
|
-
|
|
173
|
-
`@statewalker/httpeers-conformance`.
|
|
219
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/httpeers-libp2p",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The transport: nodes, identity, and servePeer — the one place in the extraction that knows what libp2p is.",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/statewalker/httpeers/issues"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/statewalker/httpeers/tree/main/packages/httpeers-libp2p#readme",
|
|
6
10
|
"license": "MIT",
|
|
7
11
|
"repository": {
|
|
8
12
|
"type": "git",
|
|
9
|
-
"url": "git+
|
|
13
|
+
"url": "git+https://github.com/statewalker/httpeers.git",
|
|
10
14
|
"directory": "packages/httpeers-libp2p"
|
|
11
15
|
},
|
|
12
16
|
"main": "./dist/index.js",
|
|
@@ -46,19 +50,19 @@
|
|
|
46
50
|
"@libp2p/webrtc": "^6.0.33",
|
|
47
51
|
"@libp2p/websockets": "^10.1.21",
|
|
48
52
|
"@multiformats/multiaddr": "^13.0.3",
|
|
49
|
-
"@statewalker/webrun-http-streams": "^0.
|
|
53
|
+
"@statewalker/webrun-http-streams": "^0.3.0",
|
|
50
54
|
"@statewalker/webrun-streams": "^0.2.0",
|
|
51
55
|
"@statewalker/webrun-streams-libp2p": "^0.1.6",
|
|
52
56
|
"libp2p": "^3.3.11",
|
|
53
|
-
"@statewalker/httpeers-
|
|
54
|
-
"@statewalker/httpeers-
|
|
57
|
+
"@statewalker/httpeers-core": "^0.1.3",
|
|
58
|
+
"@statewalker/httpeers-bridge": "^0.2.0"
|
|
55
59
|
},
|
|
56
60
|
"devDependencies": {
|
|
57
61
|
"@biomejs/biome": "^2.5.15",
|
|
58
62
|
"@types/node": "^26.6.4",
|
|
59
63
|
"typescript": "^7.0.2",
|
|
60
64
|
"vitest": "^5.0.3",
|
|
61
|
-
"@statewalker/httpeers-access": "^0.1.
|
|
65
|
+
"@statewalker/httpeers-access": "^0.1.4"
|
|
62
66
|
},
|
|
63
67
|
"publishConfig": {
|
|
64
68
|
"access": "public"
|