@statewalker/httpeers-member 0.1.4

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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +203 -0
  3. package/dist/browser-platform.d.ts +30 -0
  4. package/dist/browser-platform.d.ts.map +1 -0
  5. package/dist/browser-platform.js +30 -0
  6. package/dist/browser-profile.d.ts +85 -0
  7. package/dist/browser-profile.d.ts.map +1 -0
  8. package/dist/browser-profile.js +120 -0
  9. package/dist/browser.d.ts +48 -0
  10. package/dist/browser.d.ts.map +1 -0
  11. package/dist/browser.js +51 -0
  12. package/dist/connection-kind.d.ts +36 -0
  13. package/dist/connection-kind.d.ts.map +1 -0
  14. package/dist/connection-kind.js +52 -0
  15. package/dist/describe-error.d.ts +18 -0
  16. package/dist/describe-error.d.ts.map +1 -0
  17. package/dist/describe-error.js +72 -0
  18. package/dist/edge-control.d.ts +50 -0
  19. package/dist/edge-control.d.ts.map +1 -0
  20. package/dist/edge-control.js +69 -0
  21. package/dist/edge-dispatch.d.ts +188 -0
  22. package/dist/edge-dispatch.d.ts.map +1 -0
  23. package/dist/edge-dispatch.js +142 -0
  24. package/dist/edge-guard.d.ts +45 -0
  25. package/dist/edge-guard.d.ts.map +1 -0
  26. package/dist/edge-guard.js +62 -0
  27. package/dist/edge.d.ts +97 -0
  28. package/dist/edge.d.ts.map +1 -0
  29. package/dist/edge.js +104 -0
  30. package/dist/gateway.d.ts +49 -0
  31. package/dist/gateway.d.ts.map +1 -0
  32. package/dist/gateway.js +102 -0
  33. package/dist/identity.d.ts +78 -0
  34. package/dist/identity.d.ts.map +1 -0
  35. package/dist/identity.js +117 -0
  36. package/dist/index.d.ts +39 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +37 -0
  39. package/dist/join-blob.d.ts +125 -0
  40. package/dist/join-blob.d.ts.map +1 -0
  41. package/dist/join-blob.js +195 -0
  42. package/dist/join.d.ts +343 -0
  43. package/dist/join.d.ts.map +1 -0
  44. package/dist/join.js +409 -0
  45. package/dist/kv.d.ts +20 -0
  46. package/dist/kv.d.ts.map +1 -0
  47. package/dist/kv.js +10 -0
  48. package/dist/mesh-config.d.ts +26 -0
  49. package/dist/mesh-config.d.ts.map +1 -0
  50. package/dist/mesh-config.js +39 -0
  51. package/dist/mesh-memory.d.ts +55 -0
  52. package/dist/mesh-memory.d.ts.map +1 -0
  53. package/dist/mesh-memory.js +74 -0
  54. package/dist/node.d.ts +29 -0
  55. package/dist/node.d.ts.map +1 -0
  56. package/dist/node.js +42 -0
  57. package/dist/page-wake.d.ts +24 -0
  58. package/dist/page-wake.d.ts.map +1 -0
  59. package/dist/page-wake.js +31 -0
  60. package/dist/peer-request.d.ts +29 -0
  61. package/dist/peer-request.d.ts.map +1 -0
  62. package/dist/peer-request.js +11 -0
  63. package/dist/reset.d.ts +56 -0
  64. package/dist/reset.d.ts.map +1 -0
  65. package/dist/reset.js +103 -0
  66. package/dist/session.d.ts +214 -0
  67. package/dist/session.d.ts.map +1 -0
  68. package/dist/session.js +403 -0
  69. package/dist/start-member.d.ts +176 -0
  70. package/dist/start-member.d.ts.map +1 -0
  71. package/dist/start-member.js +307 -0
  72. package/package.json +120 -0
  73. package/src/browser-platform.ts +53 -0
  74. package/src/browser-profile.ts +179 -0
  75. package/src/browser.ts +103 -0
  76. package/src/connection-kind.ts +53 -0
  77. package/src/describe-error.ts +76 -0
  78. package/src/edge-control.ts +94 -0
  79. package/src/edge-dispatch.ts +295 -0
  80. package/src/edge-guard.ts +67 -0
  81. package/src/edge.ts +175 -0
  82. package/src/gateway.ts +141 -0
  83. package/src/identity.ts +146 -0
  84. package/src/index.ts +95 -0
  85. package/src/join-blob.ts +229 -0
  86. package/src/join.ts +632 -0
  87. package/src/kv.ts +21 -0
  88. package/src/mesh-config.ts +41 -0
  89. package/src/mesh-memory.ts +104 -0
  90. package/src/node.ts +49 -0
  91. package/src/page-wake.ts +59 -0
  92. package/src/peer-request.ts +39 -0
  93. package/src/reset.ts +127 -0
  94. package/src/session.ts +579 -0
  95. package/src/start-member.ts +476 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikhail Kotelnikov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # @statewalker/httpeers-member
2
+
3
+ Everything a participant does: hold an identity, redeem or resume, connect,
4
+ keep the link, advertise, discover, call, and serve.
5
+
6
+ ```ts
7
+ import { startMember } from "@statewalker/httpeers-member";
8
+ import { nodePlatform } from "@statewalker/httpeers-member/node";
9
+
10
+ const member = await startMember({
11
+ key: "peers",
12
+ mounts, // what this peer serves
13
+ rules, // the mesh's policy
14
+ config: { relayAddrs, hubPeerId },
15
+ platform: nodePlatform,
16
+ invitationId, // omit to resume an existing membership
17
+ });
18
+
19
+ await member.fetch(new Request(`http://local/peers/${otherPeer}/hello`));
20
+ ```
21
+
22
+ ## One lifecycle, and a platform object with three methods
23
+
24
+ The prototype had **two** lifecycles — a 644-line `startBrowserPeer` and a Node
25
+ equivalent — that agreed on every step and diverged on three. `startMember` is
26
+ both of them, with the three differences injected:
27
+
28
+ ```ts
29
+ interface MemberPlatform {
30
+ createNode(init): Promise<Libp2p>; // required
31
+ mountEdge?(init): Promise<MemberEdge>; // a page only
32
+ watchWake?(onWake): () => void; // a page only
33
+ }
34
+ ```
35
+
36
+ That is the extraction's central claim, so `browser/peer-runtime.ts` was
37
+ deliberately **not ported**: porting it would have walked the duplication back
38
+ in through the front door. There is no browser peer runtime in this package,
39
+ only `browserPlatform()`.
40
+
41
+ | Import | Holds |
42
+ |---|---|
43
+ | `.` | `startMember`, the session, the join blob, the gateway, the edge dispatch, identity, mesh memory |
44
+ | `./node` | `nodePlatform` — a value, because a Node member has nothing per-instance to build |
45
+ | `./browser` | `browserPlatform()`, `createSession()`, `mountEdge`, `watchPageWake`, the IndexedDB backends |
46
+
47
+ ## `MemberHandle.fetch` IS the edge, on both platforms
48
+
49
+ A Node caller writes `member.fetch(...)`; a page writes
50
+ `fetch(`${baseUrl}${peer}/x`)` and the ServiceWorker hands it to the same
51
+ handler. `baseUrl` is the browser-only extra, which is why it is optional.
52
+
53
+ **The edge is a local ingress and strips the proven-peer header.** A page
54
+ builds the requests that enter its own edge, so a header there is a claim, not
55
+ a fact. `createEdgeDispatch` used to branch on "does this already have a
56
+ binding" to mean "it arrived from the network" — safe while that binding was a
57
+ WeakMap no page could write, and a forgery hole the moment it became a header.
58
+ The branch is gone: a network request never reaches the edge, because
59
+ `serveTransport` dispatches to `peer.dispatch` directly.
60
+
61
+ **A member must be told it may route its own traffic.** The edge hands
62
+ `/{peerId}/{path}` to the peer's router, and that router refuses to forward
63
+ unless asked — relaying is its own capability, deny by default. `startMember`
64
+ passes `forwardLocalOnly` (from `httpeers-core`), which allows what originated
65
+ at this edge and refuses anything that arrived from the network. Omitting it
66
+ does not fail to compile; it fails at runtime with `403 this peer does not
67
+ relay for you`, from the **caller's own** router. That was a real defect here,
68
+ found by standing up a live mesh.
69
+
70
+ ## `mountEdge` never waits for a controller that cannot come
71
+
72
+ The edge is `webrun-http-browser`'s `SwHttpAdapter` (`^0.5.0`). A **hard
73
+ reload** (Ctrl+Shift+R; Firefox's `location.reload(true)`) loads the page
74
+ bypassing its ServiceWorker, while that worker is already active and claimed its
75
+ clients long ago — so no `controllerchange` ever comes. Up to 0.4 `start()`
76
+ then waited forever, and llm-chat's `mesh.html` shipped sitting at "Joining…
77
+ (mounting-edge)" for good after one.
78
+
79
+ Since 0.5 the library recovers it, and `mountEdge` relies on that:
80
+
81
+ - uncontrolled under an active worker → `start()` sends a `CLAIM` request, the
82
+ stock `sw-worker` (every app's `/sw.js`) answers with `clients.claim()`, and
83
+ the page is taken over **in place, without a reload**;
84
+ - a worker that does not answer `CLAIM` (an older `/sw.js` still installed in a
85
+ returning visitor's browser, or a custom `serviceWorkerUrl`) → after the
86
+ bound, **one reload** (`reloadIfUncontrolled`, kept as the last resort; the
87
+ library guards it with a `sessionStorage` marker, so it cannot loop);
88
+ - still uncontrolled after that, or no usable `sessionStorage` → throw
89
+ `UncontrolledPageError` ("close the tab and reopen it").
90
+
91
+ Every wait is bounded (`controlTimeoutMs`, default `DEFAULT_CONTROL_TIMEOUT_MS`
92
+ = 30 s, passed to the adapter as its `timeout`); a worker that never activates
93
+ or never answers fails with an `Error` that says what to do. `edge-control.ts`
94
+ maps the library's `ServiceWorkerControlError` onto those errors, matching on
95
+ `name`/`reason` rather than `instanceof`. `tests/edge-control.test.ts` pins the
96
+ mapping; `httpeers-browser-conformance`'s `pnpm run test:reload` runs first
97
+ visit, reload, a hard reload (asserting it recovers with no extra navigation) and
98
+ a second tab, then — against a worker that ignores `CLAIM` — the fallback reload
99
+ and a spent guard, in real Chromium and Firefox.
100
+
101
+ ## Direct or relay: how a member reaches its hub
102
+
103
+ `MemberHandle.hubLink()` is `"direct"` or `"relay"`, decided once per join.
104
+ **Direct** is a WebRTC upgrade to the hub plus a reservation on it, which is
105
+ what lets other members reach this one. **Relay** is a kept, limited circuit
106
+ through the public relay: the member calls the hub over it and nothing else
107
+ can reach the member. A member lands in relay mode when
108
+
109
+ - the WebRTC upgrade fails (a hub in Docker on a bridge network), or
110
+ - the upgrade works but the hub **refuses the reservation for want of capacity**
111
+ (`RESERVATION_REFUSED`, `RESOURCE_LIMIT_EXCEEDED`) or does not answer. The
112
+ WebRTC link is then closed and the member continues exactly as in the first
113
+ case. `MemberHandle.hubLinkNote` says why, and the session shows it as the
114
+ live phase's note (the join widget: `Connected (relay)` plus the reason).
115
+
116
+ Before this, a full reservation store on the hub failed the whole join while a
117
+ relay-mode member worked. Any other refusal — `PERMISSION_DENIED` from a hub
118
+ that has just accepted this peer, a hub with no relay service — still fails the
119
+ join: those are faults to report, not conditions to route around
120
+ (`fallsBackToRelay` in `start-member.ts`). Relay mode does not retry the
121
+ upgrade; a reconnect decides again.
122
+
123
+ ## `createGateway` — the mesh as ordinary HTTP
124
+
125
+ ```ts
126
+ const gateway = createGateway({ source: member, basePath: "", edgeKey: "peers" });
127
+ ```
128
+
129
+ A `FetchHandler` dispatching `/{peerId}/{path}`, so a client with no peer
130
+ object, no token and no knowledge of libp2p can `fetch()` a mesh resource.
131
+ `GatewaySource` is structurally a subset of `MemberHandle`, so **a member is
132
+ one** — no adapter.
133
+
134
+ ## Joining: a blob, a bare id, and one rule that must not bend
135
+
136
+ | What someone supplies | Which mesh |
137
+ |---|---|
138
+ | a join **blob** (`eyJ…`) | the one the blob names — a hub page's |
139
+ | a **bare invitation id** | the one the deployment's `httpeers.json` names |
140
+ | nothing (a resume) | the one this origin remembers, else the deployment's |
141
+
142
+ Only a **resume** consults the remembered mesh. A bare id still means the
143
+ deployment's, because that is the contract a Node-hub invitation travels under;
144
+ reversing it would silently point those joins at whatever mesh the page last
145
+ saw. `tests/session.test.ts` pins all three.
146
+
147
+ `invitationFromQrText` lives here rather than in `@statewalker/httpeers-qr`,
148
+ because it is entirely about the join format — a QR package that knew it could
149
+ not be used for anything else. It is a **predicate, never a parser**: it cannot
150
+ throw, because a live scanner that died on a malformed URL would die on the
151
+ first poster.
152
+
153
+ ## The session, and why it is isomorphic
154
+
155
+ `createPeerSession` holds the four operator controls — join, disconnect,
156
+ reconnect, reset identity — and the state a page renders. It has **no DOM and
157
+ no browser imports**, so all fifteen of its branches are tested under Node with
158
+ no browser, no relay and no ServiceWorker.
159
+
160
+ That is load-bearing rather than tidy: `browserPlatform` reaches
161
+ `@libp2p/webrtc`, whose Node build loads a native module, so a browser default
162
+ inside the session would have made importing it a browser-only act. The
163
+ defaults live in `./browser`'s `createSession` instead.
164
+
165
+ **Disconnect is not leaving.** `stop()` drops presence and keeps membership, so
166
+ reconnecting needs no new invitation. `resetIdentity` is the destructive one,
167
+ named separately and documented with what it costs.
168
+
169
+ ## The two "cannot resume" states are different, and say so
170
+
171
+ A page with no saved identity has never joined; a page whose identity the hub
172
+ does not recognise has had its membership revoked, or is talking to a hub that
173
+ was reset. Both would otherwise render as "please paste an invitation", and an
174
+ operator could not tell which had happened. When a resume fails *and* the
175
+ deployment now names a different hub, the session says so — that is almost
176
+ always a re-run of `pnpm bootstrap`, and saying it is the difference between a
177
+ two-minute fix and an hour.
178
+
179
+ ## Isomorphism, measured by reachability
180
+
181
+ `tests/boundary.test.ts` walks the import closure of `index.ts` and fails if it
182
+ reaches `node:`, `@libp2p/tcp`, `@libp2p/webrtc`, `idb-keyval`, a ServiceWorker
183
+ adapter or a DOM-only global. It does **not** exempt filenames: `edge.ts` and
184
+ `page-wake.ts` are browser-only by construction and simply are not reachable
185
+ from the root, which the test asserts explicitly. Verified non-vacuous by
186
+ re-exporting `mountEdge` from `index.ts` and watching three named tests fail.
187
+
188
+ ## Tests
189
+
190
+ **116.** `tests/edge-control.test.ts` pins the error `mountEdge` throws when
191
+ its ServiceWorker cannot take control. `tests/reservation-fallback.test.ts` pins which refused reservations
192
+ drop a join to relay mode and which fail it. `tests/consumer.test.ts` compiles a real dependent against all three
193
+ entry points under three tsconfig shapes — the `NodeNext` row is the only one
194
+ that honours the `exports` map, and for a subpath there is no legacy `types`
195
+ field to fall back to.
196
+
197
+ A member is also exercised for real in `@statewalker/httpeers-conformance`:
198
+ two live members redeem invitations from a live hub over a relay, call each
199
+ other, and the far side reports back a subject read out of a verified Biscuit
200
+ that matches the peer its handshake proved. `member-relay-fallback.test.ts` and
201
+ `member-reservation-fallback.test.ts` there run the two ways into relay mode
202
+ against a live hub: an upgrade that fails, and a hub whose reservation store is
203
+ full.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The three things a page does differently, as one `MemberPlatform`.
3
+ *
4
+ * THIS IS WHAT THE EXTRACTION IS FOR. The prototype had two lifecycles —
5
+ * `startBrowserPeer` (644 lines) and a Node equivalent — that agreed on every
6
+ * step and diverged on three: how a node is built, whether an edge is mounted,
7
+ * and whether the process gets wake events. Rung 01 replaced both with
8
+ * `startMember` plus an injected platform, and porting `peer-runtime.ts` here
9
+ * would have brought the duplication back through the front door.
10
+ *
11
+ * So there is no browser peer runtime in this package. There is one lifecycle
12
+ * and this object.
13
+ */
14
+ import type { MemberPlatform } from "./start-member.js";
15
+ export interface BrowserPlatformInit {
16
+ /**
17
+ * Where the ServiceWorker script lives. Omitting it is not an option the
18
+ * edge survives — see `mountEdge`.
19
+ */
20
+ serviceWorkerUrl?: string;
21
+ /**
22
+ * Relax libp2p's dial gater for loopback and private addresses.
23
+ *
24
+ * A page dialling `127.0.0.1` is a development setup, and the default gater
25
+ * refuses it silently. Off in production, where refusing is right.
26
+ */
27
+ dev?: boolean;
28
+ }
29
+ export declare function browserPlatform(init?: BrowserPlatformInit): MemberPlatform;
30
+ //# sourceMappingURL=browser-platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-platform.d.ts","sourceRoot":"","sources":["../src/browser-platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,wBAAgB,eAAe,CAAC,IAAI,GAAE,mBAAwB,GAAG,cAAc,CAiB9E"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The three things a page does differently, as one `MemberPlatform`.
3
+ *
4
+ * THIS IS WHAT THE EXTRACTION IS FOR. The prototype had two lifecycles —
5
+ * `startBrowserPeer` (644 lines) and a Node equivalent — that agreed on every
6
+ * step and diverged on three: how a node is built, whether an edge is mounted,
7
+ * and whether the process gets wake events. Rung 01 replaced both with
8
+ * `startMember` plus an injected platform, and porting `peer-runtime.ts` here
9
+ * would have brought the duplication back through the front door.
10
+ *
11
+ * So there is no browser peer runtime in this package. There is one lifecycle
12
+ * and this object.
13
+ */
14
+ import { createBrowserNode } from "./browser-profile.js";
15
+ import { mountEdge } from "./edge.js";
16
+ import { watchPageWake } from "./page-wake.js";
17
+ export function browserPlatform(init = {}) {
18
+ return {
19
+ createNode: async ({ privateKey }) => createBrowserNode({ privateKey, dev: init.dev ?? false }),
20
+ // A PAGE ONLY. `MemberHandle.fetch` is the edge on both platforms; what a
21
+ // page additionally gets is a same-origin URL a plain `fetch()` reaches it
22
+ // through, which is what `baseUrl` is and why it is optional on the handle.
23
+ mountEdge: async ({ key, dispatch }) => mountEdge({ key, dispatch, serviceWorkerUrl: init.serviceWorkerUrl }),
24
+ // A PAGE ONLY. A Node process gets no wake events, so there the
25
+ // supervisor's backstop timer is the whole story. A tab that was suspended
26
+ // and is now visible again should re-check its hub link immediately rather
27
+ // than wait out a backoff the user can feel.
28
+ watchWake: (onWake) => watchPageWake(onWake),
29
+ };
30
+ }
@@ -0,0 +1,85 @@
1
+ import type { Ed25519PrivateKey, Libp2p } from "@libp2p/interface";
2
+ import { type IsMember } from "@statewalker/httpeers-libp2p";
3
+ /**
4
+ * Re-exported from `./identity.ts`, which is where they live now -- see
5
+ * this module's own note on the move. A page that imports
6
+ * `IDENTITY_STORAGE_KEY` from here keeps working.
7
+ */
8
+ export { clearIdentity, decodeIdentity, encodeIdentity, IDENTITY_STORAGE_KEY, loadOrCreateIdentity, peerIdOf, } from "./identity.js";
9
+ /**
10
+ * Does dialing this address need the permissive gater?
11
+ *
12
+ * libp2p's browser default refuses to dial loopback and private-range
13
+ * addresses. `dev` used to be derived from the PAGE's hostname
14
+ * (`localhost`/`127.0.0.1`), which asks the wrong question: what the gater
15
+ * objects to is the address being DIALLED, not where the page came from. Open
16
+ * the same page at `http://192.168.1.5:5177` and the hostname test says
17
+ * "production" while `httpeers.json` still names a loopback relay -- so every
18
+ * address in the dial is denied and the failure surfaces as
19
+ * `DialDeniedError`, whose message blames the relay for being unreachable
20
+ * when the relay is running perfectly well.
21
+ *
22
+ * Asking it of the address fixes both that case and the LAN one (a page on
23
+ * `192.168.1.5` dialling a relay on `192.168.1.5`, also a private address,
24
+ * also denied). A public relay address returns false and the gater stays at
25
+ * its default, which is the behaviour a deployment wants.
26
+ */
27
+ export declare function dialNeedsPermissiveGater(addr: string): boolean;
28
+ export interface CreateBrowserNodeInit {
29
+ /**
30
+ * Relaxes the libp2p connection gater to allow insecure (`ws://`) and
31
+ * private/loopback dials. libp2p 3.x's browser-only default gater denies
32
+ * BOTH on its own (`libp2p/dist/src/config/connection-gater.browser.js`)
33
+ * -- fine in production, where a real deployment's relay is `wss://` on
34
+ * a real DNS name (`../setup/main.ts`'s `relayAddrFamily`), but fatal in
35
+ * local development, where the relay is `ws://127.0.0.1:<port>/ws`: with
36
+ * the default gater EVERY dial this node makes -- to the relay itself,
37
+ * and to every derived `/p2p-circuit/webrtc/p2p/...` peer address -- is
38
+ * denied before any handshake even starts. Precedent:
39
+ * `workspaces/webrun-wire/apps/p2p-demo/lib/browser-node.ts`, which
40
+ * applies exactly this relaxation gated on `import.meta.env.DEV`.
41
+ * Required here (no default), rather than silently inferred from
42
+ * `import.meta.env.DEV`, because THIS file has no bundler-injected `env`
43
+ * of its own to read -- inferring it here would mean guessing at a value
44
+ * only the caller (a Vite-built page, see `../static-server/main.ts`'s
45
+ * "DIST DIRECTORY LAYOUT" note) actually knows.
46
+ */
47
+ dev: boolean;
48
+ /**
49
+ * This node's signing key. Defaults to this origin's persisted identity
50
+ * (`./identity.ts`'s `loadOrCreateIdentity`) -- which is what both
51
+ * ordinary pages want and neither has to say.
52
+ *
53
+ * SUPPLIED BY A CALLER THAT NEEDS THE KEY FOR SOMETHING ELSE TOO, which
54
+ * today means the hub page: `createLibp2p` never gives a key back out,
55
+ * and the hub must hand the SAME key to `createPeer` (`mintToken` signs
56
+ * with it, and a mesh whose hub node and hub signer were different keys
57
+ * would mint tokens no peer could match to the hub it is talking to).
58
+ * Loading it once and passing it here is the only way to guarantee they
59
+ * are the same key rather than two reads that happen to agree.
60
+ */
61
+ privateKey: Ed25519PrivateKey;
62
+ /**
63
+ * Makes this node a HUB: it relays signalling for the peers this answers
64
+ * true for, and for nobody else. See `../hub-relay.ts`. Only the hub page
65
+ * passes it.
66
+ */
67
+ isMember?: IsMember;
68
+ }
69
+ /**
70
+ * Build this origin's browser libp2p node -- identity loaded/generated,
71
+ * transports and encryption wired, NOT yet dialled anywhere. See
72
+ * `dialRelay` / `waitForCircuitReservation` for the next two steps, kept
73
+ * separate so a caller (`peer-runtime.ts`) can report distinct lifecycle
74
+ * states for "connecting to the relay" and "waiting for the reservation"
75
+ * rather than one opaque await.
76
+ */
77
+ export declare function createBrowserNode(init: CreateBrowserNodeInit): Promise<Libp2p>;
78
+ /**
79
+ * Re-exported from `../reservation.ts`, which is where they live now -- see
80
+ * this module's own note on the move. A page that imports them from here
81
+ * keeps working, and a reader looking for "what happens after
82
+ * `createBrowserNode`" still finds the answer named in this file.
83
+ */
84
+ export { dialRelay, RESERVATION_POLL_ATTEMPTS, RESERVATION_POLL_INTERVAL_MS, type WaitForCircuitReservationInit, waitForCircuitReservation, } from "@statewalker/httpeers-libp2p";
85
+ //# sourceMappingURL=browser-profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-profile.d.ts","sourceRoot":"","sources":["../src/browser-profile.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGnE,OAAO,EAAmB,KAAK,QAAQ,EAAmB,MAAM,8BAA8B,CAAC;AAG/F;;;;GAIG;AACH,OAAO,EACL,aAAa,EACb,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,QAAQ,GACT,MAAM,eAAe,CAAC;AAEvB;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAO9D;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;;;;;;;;OAiBG;IACH,GAAG,EAAE,OAAO,CAAC;IACb;;;;;;;;;;;;OAYG;IACH,UAAU,EAAE,iBAAiB,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAwBpF;AASD;;;;;GAKG;AACH,OAAO,EACL,SAAS,EACT,yBAAyB,EACzB,4BAA4B,EAC5B,KAAK,6BAA6B,EAClC,yBAAyB,GAC1B,MAAM,8BAA8B,CAAC"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The browser libp2p profile.
3
+ *
4
+ * A browser cannot listen for inbound TCP the way `../hub/main.ts` /
5
+ * `../relay/main.ts` do (`transport-duplex.ts`'s `createNode`, TCP-only,
6
+ * is Node-side and stays Node-side). Instead this node reserves a relayed
7
+ * address on the deployment's relay and accepts a WebRTC upgrade brokered
8
+ * over it -- WebSockets to dial the relay in the first place,
9
+ * Circuit-Relay-v2 for the reservation and for dialling other peers'
10
+ * relayed addresses, WebRTC for the peer-to-peer upgrade once a relayed
11
+ * connection exists. `addresses.listen: ['/p2p-circuit', '/webrtc']` and
12
+ * this transport list are the exact configuration design note 17 §4 (the
13
+ * relay prototype) and the validated browser-WebRTC prototype (repo notes,
14
+ * `httpeers-plan/prototypes/21-browser-webrtc-prototype-validated/app.ts`)
15
+ * both landed on -- not a guess assembled from the libp2p docs.
16
+ *
17
+ * DIALING THE RELAY AND WAITING FOR THE RESERVATION LIVE IN
18
+ * `../reservation.ts`, re-exported below so this module still reads as the
19
+ * one place a page asks "how do I get a browser node onto the mesh". They
20
+ * moved there in Task 20 because they touch nothing transport-specific and
21
+ * three callers now need them -- this file, `../hub/main.ts`, and
22
+ * `tests/e2e/harness.ts`, the last of which had hand-copied the poll loop
23
+ * rather than import it through this module's `@libp2p/webrtc` dependency.
24
+ *
25
+ * IDENTITY PERSISTS PER ORIGIN, IN INDEXEDDB -- and it lives in
26
+ * `./identity.ts` now, re-exported below. It moved there in Task 24
27
+ * because the hub page needs the key itself, not just a node built from
28
+ * it: `createLibp2p` never hands a generated key back out, and the hub must
29
+ * pass the very same key to `createPeer` so its `mintToken` closure signs
30
+ * as the mesh. See that module for the format contract and for why
31
+ * per-origin identities are deliberate.
32
+ */
33
+ import { noise } from "@chainsafe/libp2p-noise";
34
+ import { yamux } from "@chainsafe/libp2p-yamux";
35
+ import { circuitRelayTransport } from "@libp2p/circuit-relay-v2";
36
+ import { identify } from "@libp2p/identify";
37
+ import { webRTC } from "@libp2p/webrtc";
38
+ import { webSockets } from "@libp2p/websockets";
39
+ import { hubRelayService, membershipGater } from "@statewalker/httpeers-libp2p";
40
+ import { createLibp2p } from "libp2p";
41
+ /**
42
+ * Re-exported from `./identity.ts`, which is where they live now -- see
43
+ * this module's own note on the move. A page that imports
44
+ * `IDENTITY_STORAGE_KEY` from here keeps working.
45
+ */
46
+ export { clearIdentity, decodeIdentity, encodeIdentity, IDENTITY_STORAGE_KEY, loadOrCreateIdentity, peerIdOf, } from "./identity.js";
47
+ /**
48
+ * Does dialing this address need the permissive gater?
49
+ *
50
+ * libp2p's browser default refuses to dial loopback and private-range
51
+ * addresses. `dev` used to be derived from the PAGE's hostname
52
+ * (`localhost`/`127.0.0.1`), which asks the wrong question: what the gater
53
+ * objects to is the address being DIALLED, not where the page came from. Open
54
+ * the same page at `http://192.168.1.5:5177` and the hostname test says
55
+ * "production" while `httpeers.json` still names a loopback relay -- so every
56
+ * address in the dial is denied and the failure surfaces as
57
+ * `DialDeniedError`, whose message blames the relay for being unreachable
58
+ * when the relay is running perfectly well.
59
+ *
60
+ * Asking it of the address fixes both that case and the LAN one (a page on
61
+ * `192.168.1.5` dialling a relay on `192.168.1.5`, also a private address,
62
+ * also denied). A public relay address returns false and the gater stays at
63
+ * its default, which is the behaviour a deployment wants.
64
+ */
65
+ export function dialNeedsPermissiveGater(addr) {
66
+ const ip = /\/ip4\/([0-9.]+)/.exec(addr)?.[1];
67
+ if (ip == null)
68
+ return /\/dns4?\/localhost(\/|$)/.test(addr);
69
+ if (ip === "127.0.0.1" || ip.startsWith("127."))
70
+ return true;
71
+ if (ip.startsWith("10.") || ip.startsWith("192.168."))
72
+ return true;
73
+ const m = /^172\.(\d+)\./.exec(ip);
74
+ return m != null && Number(m[1]) >= 16 && Number(m[1]) <= 31;
75
+ }
76
+ /**
77
+ * Build this origin's browser libp2p node -- identity loaded/generated,
78
+ * transports and encryption wired, NOT yet dialled anywhere. See
79
+ * `dialRelay` / `waitForCircuitReservation` for the next two steps, kept
80
+ * separate so a caller (`peer-runtime.ts`) can report distinct lifecycle
81
+ * states for "connecting to the relay" and "waiting for the reservation"
82
+ * rather than one opaque await.
83
+ */
84
+ export async function createBrowserNode(init) {
85
+ // REQUIRED now. It used to fall back to `loadOrCreateIdentity()`, which
86
+ // reached for IndexedDB from a function a Node member also calls — see
87
+ // `./kv.js`. The caller holds the key; this only starts a node with it.
88
+ const privateKey = init.privateKey;
89
+ return createLibp2p({
90
+ privateKey,
91
+ // ONLY A HUB RESERVES ON THE PUBLIC RELAY. A member is reached through
92
+ // its hub (`../hub-link.ts`), so it listens for WebRTC upgrades and
93
+ // nothing else; a `/p2p-circuit` entry here would make libp2p reserve on
94
+ // the first relay it meets -- the public one -- and spend its capacity.
95
+ addresses: { listen: init.isMember != null ? ["/p2p-circuit", "/webrtc"] : ["/webrtc"] },
96
+ transports: [webSockets(), webRTC(), circuitRelayTransport()],
97
+ connectionEncrypters: [noise()],
98
+ streamMuxers: [yamux()],
99
+ connectionGater: {
100
+ ...(init.dev ? { denyDialMultiaddr: async () => false } : {}),
101
+ // The thunk is read at DECISION time, so it closes the ordering cycle:
102
+ // this node must exist before the hub that answers `isMember` does.
103
+ ...(init.isMember != null ? membershipGater(() => init.isMember) : {}),
104
+ },
105
+ services: nodeServices(init.isMember),
106
+ });
107
+ }
108
+ /** The relay service only on a hub, and only behind its membership test. */
109
+ function nodeServices(isMember) {
110
+ return isMember != null
111
+ ? { identify: identify(), relay: hubRelayService() }
112
+ : { identify: identify() };
113
+ }
114
+ /**
115
+ * Re-exported from `../reservation.ts`, which is where they live now -- see
116
+ * this module's own note on the move. A page that imports them from here
117
+ * keeps working, and a reader looking for "what happens after
118
+ * `createBrowserNode`" still finds the answer named in this file.
119
+ */
120
+ export { dialRelay, RESERVATION_POLL_ATTEMPTS, RESERVATION_POLL_INTERVAL_MS, waitForCircuitReservation, } from "@statewalker/httpeers-libp2p";
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The BROWSER half of a member: the platform object, the two IndexedDB
3
+ * stores, and one function that assembles a page session out of them.
4
+ *
5
+ * EVERYTHING BROWSER-BOUND IS BEHIND THIS ENTRY, and that is a load-bearing
6
+ * arrangement rather than tidiness. `idb-keyval` needs a real IndexedDB;
7
+ * `@libp2p/webrtc`'s Node build loads a native module and throws on import;
8
+ * `@statewalker/webrun-http-browser/sw` is a ServiceWorker adapter. Any of
9
+ * them reachable from `./index.ts` would make a Node member fail at import
10
+ * time, which `tests/boundary.test.ts` now measures by reachability.
11
+ *
12
+ * `createSession` IS THE ONLY THING HERE THAT IS NOT A RE-EXPORT. It is the
13
+ * defaults `session.ts` deliberately does not carry: the platform, the two
14
+ * stores, `location.search` and the reload. A page that wants to substitute
15
+ * any of them passes it and this steps aside.
16
+ */
17
+ import type { AsyncBytesBackend, AsyncKeyValueBackend } from "./kv.js";
18
+ import type { PeerSession, PeerSessionInit } from "./session.js";
19
+ export { type BrowserPlatformInit, browserPlatform } from "./browser-platform.js";
20
+ export { type CreateBrowserNodeInit, createBrowserNode, dialNeedsPermissiveGater, } from "./browser-profile.js";
21
+ export { DEFAULT_CONTROL_TIMEOUT_MS, DEFAULT_SERVICE_WORKER_URL, type MountEdgeInit, mountEdge, UncontrolledPageError, } from "./edge.js";
22
+ export { type WakeDocument, type WakeWindow, type WatchPageWakeInit, watchPageWake, } from "./page-wake.js";
23
+ /** The real thing: the same IndexedDB store `@statewalker/webrun-http-browser` already uses. */
24
+ export declare function idbBackend(): AsyncKeyValueBackend;
25
+ export declare function idbBytesBackend(): AsyncBytesBackend;
26
+ /**
27
+ * `PeerSessionInit` with every browser-supplied part made optional.
28
+ *
29
+ * The four that become optional are exactly the four `session.ts` refuses to
30
+ * default, and they are listed there with the reason. `serviceWorkerUrl` and
31
+ * `dev` are here rather than there because they are `browserPlatform`'s
32
+ * arguments, and a page that passes its own `platform` has no use for them.
33
+ */
34
+ export interface CreateSessionInit extends Omit<PeerSessionInit, "platform" | "identity" | "meshMemory" | "reload" | "search"> {
35
+ platform?: PeerSessionInit["platform"];
36
+ identity?: PeerSessionInit["identity"];
37
+ meshMemory?: PeerSessionInit["meshMemory"];
38
+ reload?: () => void;
39
+ /** Defaults to `location.search`, read once at `start()`. */
40
+ search?: string;
41
+ /** Where the ServiceWorker script lives — see `mountEdge`. */
42
+ serviceWorkerUrl?: string;
43
+ /** Relax libp2p's dial gater for loopback and private addresses. Off in production. */
44
+ dev?: boolean;
45
+ }
46
+ export declare function createSession(init: CreateSessionInit): PeerSession;
47
+ export { type ResetResult, resetBrowserState } from "./reset.js";
48
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEvE,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGjE,OAAO,EAAE,KAAK,mBAAmB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACL,KAAK,qBAAqB,EAC1B,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,KAAK,aAAa,EAClB,SAAS,EACT,qBAAqB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,aAAa,GACd,MAAM,gBAAgB,CAAC;AASxB,gGAAgG;AAChG,wBAAgB,UAAU,IAAI,oBAAoB,CAMjD;AAED,wBAAgB,eAAe,IAAI,iBAAiB,CAMnD;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBACf,SAAQ,IAAI,CAAC,eAAe,EAAE,UAAU,GAAG,UAAU,GAAG,YAAY,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC3F,QAAQ,CAAC,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;IACvC,UAAU,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IAC3C,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,uFAAuF;IACvF,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,WAAW,CAWlE;AACD,OAAO,EAAE,KAAK,WAAW,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The BROWSER half of a member: the platform object, the two IndexedDB
3
+ * stores, and one function that assembles a page session out of them.
4
+ *
5
+ * EVERYTHING BROWSER-BOUND IS BEHIND THIS ENTRY, and that is a load-bearing
6
+ * arrangement rather than tidiness. `idb-keyval` needs a real IndexedDB;
7
+ * `@libp2p/webrtc`'s Node build loads a native module and throws on import;
8
+ * `@statewalker/webrun-http-browser/sw` is a ServiceWorker adapter. Any of
9
+ * them reachable from `./index.ts` would make a Node member fail at import
10
+ * time, which `tests/boundary.test.ts` now measures by reachability.
11
+ *
12
+ * `createSession` IS THE ONLY THING HERE THAT IS NOT A RE-EXPORT. It is the
13
+ * defaults `session.ts` deliberately does not carry: the platform, the two
14
+ * stores, `location.search` and the reload. A page that wants to substitute
15
+ * any of them passes it and this steps aside.
16
+ */
17
+ import { del, get, set } from "idb-keyval";
18
+ import { browserPlatform } from "./browser-platform.js";
19
+ import { createMeshMemory } from "./mesh-memory.js";
20
+ import { createIdentityStore, createPeerSession } from "./session.js";
21
+ export { browserPlatform } from "./browser-platform.js";
22
+ export { createBrowserNode, dialNeedsPermissiveGater, } from "./browser-profile.js";
23
+ export { DEFAULT_CONTROL_TIMEOUT_MS, DEFAULT_SERVICE_WORKER_URL, mountEdge, UncontrolledPageError, } from "./edge.js";
24
+ export { watchPageWake, } from "./page-wake.js";
25
+ /** The real thing: the same IndexedDB store `@statewalker/webrun-http-browser` already uses. */
26
+ export function idbBackend() {
27
+ return {
28
+ get: async (key) => (await get(key)) ?? undefined,
29
+ set: async (key, value) => await set(key, value),
30
+ delete: async (key) => await del(key),
31
+ };
32
+ }
33
+ export function idbBytesBackend() {
34
+ return {
35
+ get: async (key) => (await get(key)) ?? undefined,
36
+ set: async (key, value) => await set(key, value),
37
+ delete: async (key) => await del(key),
38
+ };
39
+ }
40
+ export function createSession(init) {
41
+ return createPeerSession({
42
+ ...init,
43
+ search: init.search ?? location.search,
44
+ platform: init.platform ??
45
+ browserPlatform({ serviceWorkerUrl: init.serviceWorkerUrl, dev: init.dev ?? false }),
46
+ identity: init.identity ?? createIdentityStore({ backend: idbBytesBackend() }),
47
+ meshMemory: init.meshMemory ?? createMeshMemory({ backend: idbBackend() }),
48
+ reload: init.reload ?? (() => location.reload()),
49
+ });
50
+ }
51
+ export { resetBrowserState } from "./reset.js";
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Is this peer reached directly, or through the relay?
3
+ *
4
+ * WHY THIS EXISTS. libp2p does not upgrade a circuit to WebRTC on its own:
5
+ * `join.ts`'s `preDialPeer` dials `<relay>/p2p-circuit/webrtc/p2p/<peer>`
6
+ * explicitly, and the circuit's only job there is to carry the SDP exchange.
7
+ * When that dial fails -- symmetric NAT on a mobile carrier, a VPN, no reachable
8
+ * STUN -- `edge-dispatch.ts` SWALLOWS the failure by design and the call is
9
+ * attempted anyway. `remote()` then dials by peer id, which resolves through the
10
+ * peerStore to a bare `/p2p-circuit` address, and every byte crosses the relay.
11
+ *
12
+ * That fallback works. What it does not do is announce itself, and the silence
13
+ * is the problem: it is 10-100x slower, it spends the relay operator's
14
+ * bandwidth instead of going peer-to-peer, and while the relay applied a
15
+ * per-connection data cap it truncated transfers mid-stream. The visible
16
+ * symptom was broken images in a gallery, with one of them loading fine in a
17
+ * fresh tab -- which reads as random corruption rather than as a transport
18
+ * that quietly changed underneath.
19
+ *
20
+ * Reading the open connection's own address is the whole diagnosis, and it is
21
+ * one call. Strings in, so this module needs no libp2p import and is testable
22
+ * under plain Node.
23
+ */
24
+ export type ConnectionKind = "direct" | "relayed" | "none";
25
+ /**
26
+ * Classify the connections open to one peer, from their multiaddrs.
27
+ *
28
+ * `/webrtc` present means the WebRTC upgrade completed and the relay is no
29
+ * longer in the data path. A `/p2p-circuit` WITHOUT it means it did not, and
30
+ * traffic is being relayed. Direct wins when both are open, because that is the
31
+ * one libp2p will actually use.
32
+ */
33
+ export declare function classifyConnection(remoteAddrs: readonly string[]): ConnectionKind;
34
+ /** One line a page can render unconditionally, saying what the kind means rather than naming it. */
35
+ export declare function describeConnection(kind: ConnectionKind): string;
36
+ //# sourceMappingURL=connection-kind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-kind.d.ts","sourceRoot":"","sources":["../src/connection-kind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;AAE3D;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,CAMjF;AAED,oGAAoG;AACpG,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAS/D"}