@statewalker/httpeers-libp2p 0.1.3

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/dist/browser.d.ts +33 -0
  4. package/dist/browser.d.ts.map +1 -0
  5. package/dist/browser.js +77 -0
  6. package/dist/duplex.d.ts +86 -0
  7. package/dist/duplex.d.ts.map +1 -0
  8. package/dist/duplex.js +101 -0
  9. package/dist/hop-reserve.d.ts +105 -0
  10. package/dist/hop-reserve.d.ts.map +1 -0
  11. package/dist/hop-reserve.js +269 -0
  12. package/dist/hub-link.d.ts +131 -0
  13. package/dist/hub-link.d.ts.map +1 -0
  14. package/dist/hub-link.js +217 -0
  15. package/dist/hub-relay.d.ts +129 -0
  16. package/dist/hub-relay.d.ts.map +1 -0
  17. package/dist/hub-relay.js +154 -0
  18. package/dist/identity.d.ts +84 -0
  19. package/dist/identity.d.ts.map +1 -0
  20. package/dist/identity.js +91 -0
  21. package/dist/index.d.ts +28 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +26 -0
  24. package/dist/link.d.ts +49 -0
  25. package/dist/link.d.ts.map +1 -0
  26. package/dist/link.js +88 -0
  27. package/dist/multiaddr-parts.d.ts +21 -0
  28. package/dist/multiaddr-parts.d.ts.map +1 -0
  29. package/dist/multiaddr-parts.js +27 -0
  30. package/dist/node.d.ts +33 -0
  31. package/dist/node.d.ts.map +1 -0
  32. package/dist/node.js +60 -0
  33. package/dist/reservation.d.ts +245 -0
  34. package/dist/reservation.d.ts.map +1 -0
  35. package/dist/reservation.js +396 -0
  36. package/dist/serve-peer.d.ts +91 -0
  37. package/dist/serve-peer.d.ts.map +1 -0
  38. package/dist/serve-peer.js +68 -0
  39. package/dist/timers.d.ts +36 -0
  40. package/dist/timers.d.ts.map +1 -0
  41. package/dist/timers.js +38 -0
  42. package/dist/transport.d.ts +314 -0
  43. package/dist/transport.d.ts.map +1 -0
  44. package/dist/transport.js +410 -0
  45. package/package.json +73 -0
  46. package/src/browser.ts +118 -0
  47. package/src/duplex.ts +172 -0
  48. package/src/hop-reserve.ts +295 -0
  49. package/src/hub-link.ts +281 -0
  50. package/src/hub-relay.ts +188 -0
  51. package/src/identity.ts +155 -0
  52. package/src/index.ts +114 -0
  53. package/src/link.ts +143 -0
  54. package/src/multiaddr-parts.ts +29 -0
  55. package/src/node.ts +63 -0
  56. package/src/reservation.ts +564 -0
  57. package/src/serve-peer.ts +141 -0
  58. package/src/timers.ts +47 -0
  59. package/src/transport.ts +538 -0
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Dialing the relay, and waiting for the reservation it grants afterwards.
3
+ *
4
+ * TRANSPORT-NEUTRAL ON PURPOSE, AND THAT IS WHY IT LIVES HERE. Both halves
5
+ * of this module touch nothing but `Libp2p.dial` and `Libp2p.getMultiaddrs`,
6
+ * so they are identical for a browser node (`browser/node-profile.ts`), a
7
+ * Node hub (`hub/node-profile.ts`) and a test peer (`tests/e2e/harness.ts`).
8
+ * They used to live in `browser/node-profile.ts`, whose top-level imports
9
+ * include `@libp2p/webrtc` -- which is why `tests/e2e/harness.ts` grew a
10
+ * second, hand-copied poll loop rather than import them (Task 14 reported
11
+ * that duplication as a finding). `@libp2p/webrtc` now loads under Node
12
+ * (its `node-datachannel` binary is installed, Task 20 Step 1), so the
13
+ * import would work today; the copy is still gone because ONE poll loop
14
+ * with one timing contract is the point, not because importing was
15
+ * impossible.
16
+ *
17
+ * READINESS IS POLLED, NOT AWAITED (design note 17 §4). `node.dial(relay)`
18
+ * resolving means the connection to the relay opened -- it says nothing
19
+ * about whether the relay has finished granting a reservation. That
20
+ * reservation lands asynchronously afterwards, so `waitForCircuitReservation`
21
+ * polls `getMultiaddrs()` until a `p2p-circuit` address actually appears, on
22
+ * the same schedule (250 ms x 40 attempts = 10 s ceiling) the validated
23
+ * relay test used (`prototypes/16-.../src/relay.test.ts`). Treating `dial`'s
24
+ * resolution as "ready" is exactly the race that note documents this project
25
+ * already got bitten by once.
26
+ */
27
+ import { multiaddr } from "@multiformats/multiaddr";
28
+ import { requestRelayReservation } from "./hop-reserve.js";
29
+ import { lastPeerIdOf } from "./multiaddr-parts.js";
30
+ import { globalTimers } from "./timers.js";
31
+ /** Dial the relay named by `relayAddr` (`httpeers.json`'s `relayAddrs[0]`). Resolving means the link is up -- NOT that a circuit reservation exists yet; see `waitForCircuitReservation`. */
32
+ export async function dialRelay(node, relayAddr) {
33
+ await node.dial(multiaddr(relayAddr));
34
+ }
35
+ /** How often `waitForCircuitReservation` re-checks `getMultiaddrs()`. */
36
+ export const RESERVATION_POLL_INTERVAL_MS = 250;
37
+ /** How many times it checks before giving up -- 40 x 250 ms = 10 s, the ceiling the validated relay test (`relay.test.ts`) used. */
38
+ export const RESERVATION_POLL_ATTEMPTS = 40;
39
+ /**
40
+ * Poll `node.getMultiaddrs()` until a `p2p-circuit` address appears,
41
+ * returning it as a string. See this module's own "READINESS IS POLLED,
42
+ * NOT AWAITED" note for why this exists instead of trusting `dialRelay`'s
43
+ * resolution: the reservation is granted by the relay asynchronously,
44
+ * after the dial itself has already resolved.
45
+ */
46
+ export async function waitForCircuitReservation(node, init = {}) {
47
+ const intervalMs = init.intervalMs ?? RESERVATION_POLL_INTERVAL_MS;
48
+ const attempts = init.attempts ?? RESERVATION_POLL_ATTEMPTS;
49
+ for (let attempt = 0; attempt < attempts; attempt++) {
50
+ const found = node.getMultiaddrs().find((addr) => addr.toString().includes("p2p-circuit"));
51
+ if (found != null)
52
+ return found.toString();
53
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
54
+ }
55
+ throw new Error(`reservation: no p2p-circuit reservation appeared within ${attempts * intervalMs}ms of dialing the relay -- ` +
56
+ "the relay may be unreachable, or its reservation limits already exhausted.");
57
+ }
58
+ /** How long one restore attempt waits for the reservation after its dial resolved -- 20 x 250 ms = 5 s. */
59
+ const RESTORE_POLL_ATTEMPTS = 20;
60
+ /** The backstop check, for a loss no event reported. */
61
+ const SUPERVISOR_CHECK_INTERVAL_MS = 10_000;
62
+ /** How soon after starting the supervisor first asks the relay. See `scheduleRenewal`. */
63
+ const INITIAL_VERIFY_DELAY_MS = 1_000;
64
+ /**
65
+ * The floor and ceiling on the renewal cadence, and the cadence used when the
66
+ * relay has not told us a TTL yet.
67
+ *
68
+ * A QUARTER OF THE TTL is the rule (`renewalIntervalMs`). Against the relay
69
+ * this project runs -- `apps/relay/src/limits.ts`, TTL two hours, restated
70
+ * there precisely so a library change is visible -- that is thirty minutes,
71
+ * and it means three consecutive renewals can fail before the reservation
72
+ * would lapse on its own. A half would leave room for one, which is no margin
73
+ * at all; an eighth would double the traffic for a margin nobody needs.
74
+ *
75
+ * The CEILING is the same thirty minutes, so a relay that one day advertises a
76
+ * longer TTL does not stretch the cadence with it: the incident was not a
77
+ * reservation that expired, it was one that vanished, and how fast that is
78
+ * repaired is bounded by this number, not by the TTL. The FLOOR keeps a relay
79
+ * with an implausibly short TTL from being asked every few seconds.
80
+ */
81
+ export const MIN_RENEWAL_INTERVAL_MS = 60_000;
82
+ export const MAX_RENEWAL_INTERVAL_MS = 30 * 60_000;
83
+ export const DEFAULT_RENEWAL_INTERVAL_MS = 20 * 60_000;
84
+ /** The renewal cadence for a reservation the relay says lasts `ttlMs`. See the constants above. */
85
+ export function renewalIntervalMs(ttlMs) {
86
+ if (ttlMs == null || !(ttlMs > 0))
87
+ return DEFAULT_RENEWAL_INTERVAL_MS;
88
+ return Math.min(Math.max(ttlMs / 4, MIN_RENEWAL_INTERVAL_MS), MAX_RENEWAL_INTERVAL_MS);
89
+ }
90
+ /**
91
+ * The actual wait before the next renewal: `intervalMs` jittered DOWN into its
92
+ * upper quarter, never up.
93
+ *
94
+ * Down, because the interval is already the deadline -- stretching it past a
95
+ * quarter of the TTL would spend the margin the quarter exists to buy. The
96
+ * spread is what keeps a fleet of hubs that started together, or came back
97
+ * together after a relay restart, from asking the relay in the same instant
98
+ * for ever after.
99
+ */
100
+ export function renewalDelayMs(intervalMs, random = Math.random) {
101
+ return intervalMs * 0.75 + intervalMs * 0.25 * random();
102
+ }
103
+ /**
104
+ * How long a lost reservation may go unrepaired before the hub should call
105
+ * itself unhealthy.
106
+ *
107
+ * TWO MINUTES. A restore normally completes in seconds: the loss is noticed
108
+ * either at once (the address disappears) or at the next renewal, the restore
109
+ * dials and waits at most five seconds for the reservation, and the retry
110
+ * backoff caps at thirty. Two minutes is therefore at least four attempts
111
+ * including capped retries -- long enough that a relay restart or a dropped
112
+ * link never shows up as an unhealthy appliance, short enough to be a small
113
+ * fraction of the two-hour TTL, so a hub that truly cannot get back is
114
+ * reported long before members would notice on their own.
115
+ */
116
+ export const RESERVATION_LOSS_GRACE_MS = 120_000;
117
+ /** Is this state healthy -- reserved, or lost for less than `graceMs`? See `RESERVATION_LOSS_GRACE_MS`. */
118
+ export function reservationHealthy(state, graceMs = RESERVATION_LOSS_GRACE_MS, now = Date.now()) {
119
+ if (state.status === "reserved")
120
+ return true;
121
+ if (state.status === "stopped")
122
+ return false;
123
+ return state.lostSince != null && now - state.lostSince < graceMs;
124
+ }
125
+ /**
126
+ * Keep this node's reservation on the relay at `relayAddr`: renew it against
127
+ * the relay on a schedule, and re-dial whenever it is lost. Call once the
128
+ * first reservation has landed.
129
+ */
130
+ export function superviseRelay(init) {
131
+ const { node, relayAddr } = init;
132
+ const minDelayMs = init.minRetryDelayMs ?? 1_000;
133
+ const maxDelayMs = init.maxRetryDelayMs ?? 30_000;
134
+ const relayPeerId = lastPeerIdOf(multiaddr(relayAddr)) ?? null;
135
+ const timers = init.timers ?? globalTimers;
136
+ const now = init.now ?? Date.now;
137
+ const random = init.random ?? Math.random;
138
+ const log = init.log ?? defaultRelayLog;
139
+ const verify = init.verify === false
140
+ ? undefined
141
+ : (init.verify ??
142
+ (relayPeerId != null
143
+ ? (options) => requestRelayReservation(node, relayPeerId, options)
144
+ : undefined));
145
+ const state = {
146
+ status: "reserved",
147
+ relayAddr,
148
+ relayPeerId,
149
+ verifiedAt: null,
150
+ expiresAt: null,
151
+ lostSince: null,
152
+ consecutiveFailures: 0,
153
+ renewals: 0,
154
+ restores: 0,
155
+ lastError: null,
156
+ };
157
+ let retryTimer;
158
+ let renewalTimer;
159
+ let restoring = false;
160
+ let renewing = false;
161
+ let stopped = false;
162
+ const snapshot = () => ({ ...state });
163
+ const report = (event, reason) => {
164
+ const where = relayPeerId ?? relayAddr;
165
+ log(`relay: ${event} on ${where}${reason != null ? ` -- ${reason}` : ""}` +
166
+ ` (failures=${state.consecutiveFailures}, renewals=${state.renewals}, restores=${state.restores})`, { event, state: snapshot() });
167
+ };
168
+ /**
169
+ * The address check, used ONLY to prove a loss. `true` here means nothing:
170
+ * it is precisely the belief that was wrong during the incident.
171
+ */
172
+ const addressGone = () => !node.getMultiaddrs().some((addr) => {
173
+ const s = addr.toString();
174
+ return relayPeerId == null
175
+ ? s.includes("/p2p-circuit")
176
+ : s.includes(`/p2p/${relayPeerId}/p2p-circuit`);
177
+ });
178
+ const markReserved = (grant, event) => {
179
+ state.status = "reserved";
180
+ state.verifiedAt = now();
181
+ state.expiresAt = grant.expiresAt;
182
+ state.lostSince = null;
183
+ state.consecutiveFailures = 0;
184
+ if (event === "renewed")
185
+ state.renewals++;
186
+ if (event === "restored")
187
+ state.restores++;
188
+ report(event);
189
+ };
190
+ const markLost = (reason) => {
191
+ state.lastError = reason;
192
+ if (state.status === "lost")
193
+ return;
194
+ state.status = "lost";
195
+ state.lostSince = now();
196
+ report("lost", reason);
197
+ };
198
+ const scheduleRetry = (delayMs) => {
199
+ timers.clearTimeout(retryTimer);
200
+ retryTimer = timers.setTimeout(() => {
201
+ retryTimer = undefined;
202
+ void restore();
203
+ }, delayMs);
204
+ };
205
+ /**
206
+ * THE FIRST RENEWAL IS PROMPT, the rest are on the TTL's cadence.
207
+ *
208
+ * A supervisor is started the moment a reservation lands, so one request a
209
+ * second later costs a two-byte round trip and buys two things the caller
210
+ * cannot get otherwise: the relay's own expiry -- which is what every later
211
+ * interval is derived from, instead of the blind `DEFAULT_RENEWAL_INTERVAL_MS`
212
+ * -- and a `verifiedAt` an operator can read straight after a restart rather
213
+ * than twenty minutes into one.
214
+ */
215
+ const scheduleRenewal = (first = false) => {
216
+ if (stopped || verify == null)
217
+ return;
218
+ timers.clearTimeout(renewalTimer);
219
+ const interval = init.renewalIntervalMs ?? renewalIntervalMs(ttlOf(state, now()));
220
+ const delayMs = first && init.renewalIntervalMs == null
221
+ ? INITIAL_VERIFY_DELAY_MS
222
+ : renewalDelayMs(interval, random);
223
+ renewalTimer = timers.setTimeout(() => {
224
+ renewalTimer = undefined;
225
+ void renew();
226
+ }, delayMs);
227
+ };
228
+ /**
229
+ * One relay-verified renewal. This is the whole point of the module: the
230
+ * request renews a reservation that is there and re-creates one that is not,
231
+ * so the ordinary case never reaches `restore` at all.
232
+ */
233
+ async function renew() {
234
+ if (stopped || verify == null || renewing || restoring) {
235
+ scheduleRenewal();
236
+ return;
237
+ }
238
+ renewing = true;
239
+ try {
240
+ const grant = await verify({});
241
+ if (stopped)
242
+ return;
243
+ markReserved(grant, state.status === "reserved" ? "renewed" : "restored");
244
+ }
245
+ catch (error) {
246
+ if (stopped)
247
+ return;
248
+ state.consecutiveFailures++;
249
+ const reason = messageOf(error);
250
+ state.lastError = reason;
251
+ report("renewal-failed", reason);
252
+ markLost(reason);
253
+ scheduleRetry(0);
254
+ }
255
+ finally {
256
+ renewing = false;
257
+ scheduleRenewal();
258
+ }
259
+ }
260
+ /**
261
+ * Get the reservation back the hard way: re-dial (or the caller's `restore`),
262
+ * then ASK THE RELAY. A restore that only re-listens has proved nothing --
263
+ * that is the same local belief that failed before -- so when a `verify` is
264
+ * available, its answer is what ends the outage.
265
+ */
266
+ async function restore() {
267
+ if (stopped || restoring)
268
+ return;
269
+ restoring = true;
270
+ report("restoring", state.lastError ?? undefined);
271
+ try {
272
+ if (init.restore != null) {
273
+ await init.restore();
274
+ }
275
+ else {
276
+ await dialRelay(node, relayAddr);
277
+ await waitForCircuitReservation(node, { attempts: RESTORE_POLL_ATTEMPTS });
278
+ }
279
+ if (stopped)
280
+ return;
281
+ if (verify != null) {
282
+ markReserved(await verify({}), "restored");
283
+ }
284
+ else {
285
+ if (addressGone())
286
+ throw new Error("restore resolved without a reservation");
287
+ markReserved({ expiresAt: null, ttlMs: null }, "restored");
288
+ }
289
+ }
290
+ catch (error) {
291
+ if (stopped)
292
+ return;
293
+ state.consecutiveFailures++;
294
+ const reason = messageOf(error);
295
+ state.lastError = reason;
296
+ markLost(reason);
297
+ report("restore-failed", reason);
298
+ scheduleRetry(retryDelayMs(state.consecutiveFailures - 1, minDelayMs, maxDelayMs, random));
299
+ }
300
+ finally {
301
+ restoring = false;
302
+ }
303
+ }
304
+ /** The fast loss detector: an address that has gone is proof; one that is there is not. */
305
+ const check = () => {
306
+ if (stopped || restoring || renewing || retryTimer != null)
307
+ return;
308
+ if (!addressGone())
309
+ return;
310
+ markLost("the node's own circuit address is gone (the relay connection closed)");
311
+ scheduleRetry(0);
312
+ };
313
+ node.addEventListener("connection:close", check);
314
+ node.addEventListener("self:peer:update", check);
315
+ const interval = timers.setInterval(check, init.checkIntervalMs ?? SUPERVISOR_CHECK_INTERVAL_MS);
316
+ report("reserved");
317
+ scheduleRenewal(true);
318
+ return {
319
+ poke() {
320
+ if (stopped)
321
+ return;
322
+ timers.clearTimeout(retryTimer);
323
+ retryTimer = undefined;
324
+ state.consecutiveFailures = 0;
325
+ if (verify != null)
326
+ void renew();
327
+ else
328
+ check();
329
+ },
330
+ state: snapshot,
331
+ stop() {
332
+ if (stopped)
333
+ return;
334
+ stopped = true;
335
+ state.status = "stopped";
336
+ timers.clearTimeout(retryTimer);
337
+ timers.clearTimeout(renewalTimer);
338
+ retryTimer = undefined;
339
+ renewalTimer = undefined;
340
+ timers.clearInterval(interval);
341
+ node.removeEventListener("connection:close", check);
342
+ node.removeEventListener("self:peer:update", check);
343
+ report("stopped");
344
+ },
345
+ };
346
+ }
347
+ /** What is left of the reservation the relay last granted, for the next renewal's cadence. */
348
+ function ttlOf(state, nowMs) {
349
+ return state.expiresAt != null ? state.expiresAt - nowMs : null;
350
+ }
351
+ function messageOf(error) {
352
+ return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
353
+ }
354
+ /** One line per transition on stdout. Replaceable, but never absent — see the module comment. */
355
+ const defaultRelayLog = (message) => {
356
+ console.log(message);
357
+ };
358
+ /**
359
+ * The wait before retry number `failures` (0-based): doubling from
360
+ * `minDelayMs`, capped at `maxDelayMs`, then jittered into its upper half
361
+ * so a relay restart does not bring every peer back in the same instant.
362
+ */
363
+ export function retryDelayMs(failures, minDelayMs, maxDelayMs, random = Math.random) {
364
+ const base = Math.min(maxDelayMs, minDelayMs * 2 ** failures);
365
+ return base / 2 + (random() * base) / 2;
366
+ }
367
+ /**
368
+ * Which of a node's addresses a peer should actually be told.
369
+ *
370
+ * After reserving, a node carries several multiaddrs at once and they are NOT
371
+ * interchangeable: its own transport address is not reachable through a relay
372
+ * at all, the bare circuit relays every byte, and the `/webrtc` circuit uses
373
+ * the relay for the handshake only and then talks directly. Handing out the
374
+ * wrong one is a connection that fails for a reason nobody can read from the
375
+ * error.
376
+ *
377
+ * Each assembly in the prototype picked one inline, and differently — the kind
378
+ * of triplication the extraction exists to end.
379
+ */
380
+ export function circuitAddrs(node) {
381
+ const found = {};
382
+ for (const addr of node.getMultiaddrs()) {
383
+ const value = addr.toString();
384
+ // Not a circuit: the node's own transport address. A dialler reaching it
385
+ // needs no relay, so it does not belong in either field.
386
+ if (!value.includes("/p2p-circuit"))
387
+ continue;
388
+ // Order matters: every webrtc circuit also contains `/p2p-circuit`, so the
389
+ // narrower test comes first or every address lands in `bare`.
390
+ if (value.includes("/webrtc"))
391
+ found.webrtc ??= value;
392
+ else
393
+ found.bare ??= value;
394
+ }
395
+ return found;
396
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * A mount table, served to the mesh.
3
+ *
4
+ * THE SEAM THREE ASSEMBLIES COLLAPSE INTO. The prototype builds a peer three
5
+ * times — the Node hub, the browser hub and the test hub — each wiring a node,
6
+ * a reservation, identity registration, a router and mounts in its own order,
7
+ * and the coverage audit found that the one symbol every assembly turns on was
8
+ * exported nowhere. This is it.
9
+ *
10
+ * WHAT IT DOES NOT DO, and the omission is the design. It does not verify a
11
+ * token, evaluate a policy, or know what a hub is. `access` is a middleware
12
+ * the caller supplies — `withAccess` from `@statewalker/httpeers-access`, or
13
+ * nothing at all for a peer that trusts its transport — because this package
14
+ * must not learn about Biscuit and `httpeers-access` must not learn about
15
+ * libp2p. They meet at a `FetchHandler`, which is the only contract either
16
+ * needs.
17
+ *
18
+ * IDENTITY IS REGISTERED BEFORE ANYTHING DOWNSTREAM RUNS. `serveTransport`
19
+ * gives every inbound stream its own `Duplex` closing over that stream's
20
+ * Noise-proven `remotePeer`, and calls `registerPeer` on the request before
21
+ * the handler sees it. That ordering is the whole basis of the binding check:
22
+ * a token's subject is compared against what the TRANSPORT proved, never
23
+ * against anything the caller said.
24
+ */
25
+ import type { Libp2p } from "@libp2p/interface";
26
+ import type { FetchHandler, Mounts, PeerIdStr } from "@statewalker/httpeers-core";
27
+ import type { CallOnLimitedConnection } from "./link.js";
28
+ export interface ServePeerInit {
29
+ node: Libp2p;
30
+ /**
31
+ * What this peer serves. A value, or a factory taking the peer's own id —
32
+ * a hub's endpoints need to know which mesh they are, and the alternative
33
+ * is for this package to learn what a hub is.
34
+ */
35
+ mounts: Mounts | ((self: PeerIdStr) => Mounts);
36
+ /**
37
+ * Everything between the transport and the mounts: identity binding and
38
+ * policy. Omitted, requests reach the mounts with only the transport's
39
+ * proof — which is a legitimate shape for a peer whose mounts are public,
40
+ * and a serious mistake for one whose are not.
41
+ *
42
+ * Passed through to the router, which applies it to LOCAL handling only.
43
+ * That distinction is load-bearing and easy to lose: wrapping the whole
44
+ * router instead would run policy over FORWARDED requests too, and an
45
+ * outbound call is not this peer's business to authorize.
46
+ */
47
+ access?: (next: FetchHandler) => FetchHandler;
48
+ /**
49
+ * May this request be forwarded to `target`? **Deny by default**, and the
50
+ * default is inherited rather than restated here.
51
+ *
52
+ * Relaying is a distinct capability, not a side effect of knowing how to
53
+ * route: without it any peer can make this one dial a third party and pump
54
+ * a stream on its behalf. The hole is invisible for a while because it
55
+ * fails CLOSED at the far end — the third party rejects the tokenless
56
+ * request, so every observable outcome looks right. The damage is the work
57
+ * done, not the answer given.
58
+ */
59
+ allowForward?: (request: Request, target: PeerIdStr) => Promise<boolean>;
60
+ protocol?: string;
61
+ /** Forwarded to the transport; see `transport.ts` for what each bounds. */
62
+ maxInboundStreams?: number;
63
+ maxOutboundStreams?: number;
64
+ drainTimeoutMs?: number;
65
+ /**
66
+ * Accept streams on limited connections (relay circuits) -- SERVING ONLY. A
67
+ * HUB sets it, so members that can reach it only through the public relay
68
+ * are served; a member never does, so the hub's relay limits still bound
69
+ * member-to-member traffic.
70
+ */
71
+ serveOnLimitedConnection?: boolean;
72
+ /**
73
+ * Open streams on limited connections to the peers this allows -- CALLING
74
+ * ONLY, and it covers every outbound path of this peer (`call`, and the
75
+ * router's forwarding behind `dispatch`). A member in relay mode passes
76
+ * `(peerId) => peerId === hubPeerId`. See `CallOnLimitedConnection`.
77
+ */
78
+ callOnLimitedConnection?: CallOnLimitedConnection;
79
+ }
80
+ export interface Peer {
81
+ peerId: PeerIdStr;
82
+ /** Every multiaddr this node is reachable on, right now. */
83
+ addrs(): string[];
84
+ /** Inbound, after identity binding and policy. Exposed so an edge can reuse it locally. */
85
+ dispatch: FetchHandler;
86
+ /** Call another peer. The request's URL path is what the far side routes on. */
87
+ call(peerId: PeerIdStr, request: Request): Promise<Response>;
88
+ stop(): Promise<void>;
89
+ }
90
+ export declare function servePeer(init: ServePeerInit): Promise<Peer>;
91
+ //# sourceMappingURL=serve-peer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve-peer.d.ts","sourceRoot":"","sources":["../src/serve-peer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAElF,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAGzD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,SAAS,KAAK,MAAM,CAAC,CAAC;IAC/C;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,YAAY,CAAC;IAC9C;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,uBAAuB,CAAC;CACnD;AAED,MAAM,WAAW,IAAI;IACnB,MAAM,EAAE,SAAS,CAAC;IAClB,4DAA4D;IAC5D,KAAK,IAAI,MAAM,EAAE,CAAC;IAClB,2FAA2F;IAC3F,QAAQ,EAAE,YAAY,CAAC;IACvB,gFAAgF;IAChF,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7D,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA6ClE"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * A mount table, served to the mesh.
3
+ *
4
+ * THE SEAM THREE ASSEMBLIES COLLAPSE INTO. The prototype builds a peer three
5
+ * times — the Node hub, the browser hub and the test hub — each wiring a node,
6
+ * a reservation, identity registration, a router and mounts in its own order,
7
+ * and the coverage audit found that the one symbol every assembly turns on was
8
+ * exported nowhere. This is it.
9
+ *
10
+ * WHAT IT DOES NOT DO, and the omission is the design. It does not verify a
11
+ * token, evaluate a policy, or know what a hub is. `access` is a middleware
12
+ * the caller supplies — `withAccess` from `@statewalker/httpeers-access`, or
13
+ * nothing at all for a peer that trusts its transport — because this package
14
+ * must not learn about Biscuit and `httpeers-access` must not learn about
15
+ * libp2p. They meet at a `FetchHandler`, which is the only contract either
16
+ * needs.
17
+ *
18
+ * IDENTITY IS REGISTERED BEFORE ANYTHING DOWNSTREAM RUNS. `serveTransport`
19
+ * gives every inbound stream its own `Duplex` closing over that stream's
20
+ * Noise-proven `remotePeer`, and calls `registerPeer` on the request before
21
+ * the handler sees it. That ordering is the whole basis of the binding check:
22
+ * a token's subject is compared against what the TRANSPORT proved, never
23
+ * against anything the caller said.
24
+ */
25
+ import { createPeerRouter } from "@statewalker/httpeers-core";
26
+ import { createRemote, PROTOCOL, serveTransport } from "./transport.js";
27
+ export async function servePeer(init) {
28
+ const peerId = init.node.peerId.toString();
29
+ const protocol = init.protocol ?? PROTOCOL;
30
+ const mounts = typeof init.mounts === "function" ? init.mounts(peerId) : init.mounts;
31
+ const remote = createRemote({
32
+ node: init.node,
33
+ protocol,
34
+ maxOutboundStreams: init.maxOutboundStreams,
35
+ callOnLimitedConnection: init.callOnLimitedConnection,
36
+ });
37
+ const dispatch = createPeerRouter({
38
+ selfPeerId: peerId,
39
+ mounts,
40
+ remote,
41
+ access: init.access,
42
+ allowForward: init.allowForward,
43
+ });
44
+ const stopServing = await serveTransport({
45
+ node: init.node,
46
+ dispatch,
47
+ protocol,
48
+ maxInboundStreams: init.maxInboundStreams,
49
+ maxOutboundStreams: init.maxOutboundStreams,
50
+ drainTimeoutMs: init.drainTimeoutMs,
51
+ runOnLimitedConnection: init.serveOnLimitedConnection,
52
+ });
53
+ let stopped = false;
54
+ return {
55
+ peerId,
56
+ addrs: () => init.node.getMultiaddrs().map((a) => a.toString()),
57
+ dispatch,
58
+ call: async (target, request) => remote(target, request),
59
+ async stop() {
60
+ // Idempotent: an assembly that tears down twice — a page unloading while
61
+ // a test also cleans up — must not see the second call throw.
62
+ if (stopped)
63
+ return;
64
+ stopped = true;
65
+ await stopServing();
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The timer seam: the four calls anything long-lived in this package schedules
3
+ * with, behind an interface instead of the global functions.
4
+ *
5
+ * WHY, AND IT IS NOT TESTABILITY. A browser throttles `setTimeout` and
6
+ * `setInterval` in a background tab -- to once a minute, and on some engines
7
+ * far less -- and a hub running in a tab is a hub whose relay reservation is
8
+ * renewed on the browser's schedule rather than on the relay's. The fix is
9
+ * `worker-timers`, which schedules from a Web Worker and is not throttled; the
10
+ * fix is NOT to shorten the interval, which changes nothing about what the
11
+ * browser does to it. That wiring is deliberately not here yet (the Node hub is
12
+ * what runs today); this seam is what makes it a one-line change later:
13
+ *
14
+ * import { clearInterval, clearTimeout, setInterval, setTimeout } from "worker-timers";
15
+ * const timers: Timers = { setTimeout, clearTimeout, setInterval, clearInterval };
16
+ *
17
+ * Tests get the seam for free, and that is the cheaper of the two reasons.
18
+ *
19
+ * `TimerHandle` IS `unknown` ON PURPOSE. Node's timers return an object,
20
+ * browsers return a number, `worker-timers` returns a number of its own that
21
+ * only its own `clearTimeout` understands. An implementation must therefore
22
+ * only ever hand its OWN handles back to its OWN clear functions -- which is
23
+ * exactly what an opaque type states and a `number` alias would quietly
24
+ * permit callers to violate.
25
+ */
26
+ /** A handle from one `Timers` implementation, meaningful only to that same one. */
27
+ export type TimerHandle = unknown;
28
+ export interface Timers {
29
+ setTimeout(handler: () => void, delayMs: number): TimerHandle;
30
+ clearTimeout(handle: TimerHandle): void;
31
+ setInterval(handler: () => void, delayMs: number): TimerHandle;
32
+ clearInterval(handle: TimerHandle): void;
33
+ }
34
+ /** The global timers, which is what everything uses unless it is told otherwise. */
35
+ export declare const globalTimers: Timers;
36
+ //# sourceMappingURL=timers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timers.d.ts","sourceRoot":"","sources":["../src/timers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,mFAAmF;AACnF,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC;AAElC,MAAM,WAAW,MAAM;IACrB,UAAU,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,CAAC;IAC9D,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IACxC,WAAW,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,CAAC;IAC/D,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;CAC1C;AAED,oFAAoF;AACpF,eAAO,MAAM,YAAY,EAAE,MAS1B,CAAC"}
package/dist/timers.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The timer seam: the four calls anything long-lived in this package schedules
3
+ * with, behind an interface instead of the global functions.
4
+ *
5
+ * WHY, AND IT IS NOT TESTABILITY. A browser throttles `setTimeout` and
6
+ * `setInterval` in a background tab -- to once a minute, and on some engines
7
+ * far less -- and a hub running in a tab is a hub whose relay reservation is
8
+ * renewed on the browser's schedule rather than on the relay's. The fix is
9
+ * `worker-timers`, which schedules from a Web Worker and is not throttled; the
10
+ * fix is NOT to shorten the interval, which changes nothing about what the
11
+ * browser does to it. That wiring is deliberately not here yet (the Node hub is
12
+ * what runs today); this seam is what makes it a one-line change later:
13
+ *
14
+ * import { clearInterval, clearTimeout, setInterval, setTimeout } from "worker-timers";
15
+ * const timers: Timers = { setTimeout, clearTimeout, setInterval, clearInterval };
16
+ *
17
+ * Tests get the seam for free, and that is the cheaper of the two reasons.
18
+ *
19
+ * `TimerHandle` IS `unknown` ON PURPOSE. Node's timers return an object,
20
+ * browsers return a number, `worker-timers` returns a number of its own that
21
+ * only its own `clearTimeout` understands. An implementation must therefore
22
+ * only ever hand its OWN handles back to its OWN clear functions -- which is
23
+ * exactly what an opaque type states and a `number` alias would quietly
24
+ * permit callers to violate.
25
+ */
26
+ /** The global timers, which is what everything uses unless it is told otherwise. */
27
+ export const globalTimers = {
28
+ setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
29
+ clearTimeout: (handle) => {
30
+ if (handle !== undefined)
31
+ clearTimeout(handle);
32
+ },
33
+ setInterval: (handler, delayMs) => setInterval(handler, delayMs),
34
+ clearInterval: (handle) => {
35
+ if (handle !== undefined)
36
+ clearInterval(handle);
37
+ },
38
+ };