@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.
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/browser-platform.d.ts +30 -0
- package/dist/browser-platform.d.ts.map +1 -0
- package/dist/browser-platform.js +30 -0
- package/dist/browser-profile.d.ts +85 -0
- package/dist/browser-profile.d.ts.map +1 -0
- package/dist/browser-profile.js +120 -0
- package/dist/browser.d.ts +48 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +51 -0
- package/dist/connection-kind.d.ts +36 -0
- package/dist/connection-kind.d.ts.map +1 -0
- package/dist/connection-kind.js +52 -0
- package/dist/describe-error.d.ts +18 -0
- package/dist/describe-error.d.ts.map +1 -0
- package/dist/describe-error.js +72 -0
- package/dist/edge-control.d.ts +50 -0
- package/dist/edge-control.d.ts.map +1 -0
- package/dist/edge-control.js +69 -0
- package/dist/edge-dispatch.d.ts +188 -0
- package/dist/edge-dispatch.d.ts.map +1 -0
- package/dist/edge-dispatch.js +142 -0
- package/dist/edge-guard.d.ts +45 -0
- package/dist/edge-guard.d.ts.map +1 -0
- package/dist/edge-guard.js +62 -0
- package/dist/edge.d.ts +97 -0
- package/dist/edge.d.ts.map +1 -0
- package/dist/edge.js +104 -0
- package/dist/gateway.d.ts +49 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +102 -0
- package/dist/identity.d.ts +78 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +117 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/join-blob.d.ts +125 -0
- package/dist/join-blob.d.ts.map +1 -0
- package/dist/join-blob.js +195 -0
- package/dist/join.d.ts +343 -0
- package/dist/join.d.ts.map +1 -0
- package/dist/join.js +409 -0
- package/dist/kv.d.ts +20 -0
- package/dist/kv.d.ts.map +1 -0
- package/dist/kv.js +10 -0
- package/dist/mesh-config.d.ts +26 -0
- package/dist/mesh-config.d.ts.map +1 -0
- package/dist/mesh-config.js +39 -0
- package/dist/mesh-memory.d.ts +55 -0
- package/dist/mesh-memory.d.ts.map +1 -0
- package/dist/mesh-memory.js +74 -0
- package/dist/node.d.ts +29 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +42 -0
- package/dist/page-wake.d.ts +24 -0
- package/dist/page-wake.d.ts.map +1 -0
- package/dist/page-wake.js +31 -0
- package/dist/peer-request.d.ts +29 -0
- package/dist/peer-request.d.ts.map +1 -0
- package/dist/peer-request.js +11 -0
- package/dist/reset.d.ts +56 -0
- package/dist/reset.d.ts.map +1 -0
- package/dist/reset.js +103 -0
- package/dist/session.d.ts +214 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +403 -0
- package/dist/start-member.d.ts +176 -0
- package/dist/start-member.d.ts.map +1 -0
- package/dist/start-member.js +307 -0
- package/package.json +120 -0
- package/src/browser-platform.ts +53 -0
- package/src/browser-profile.ts +179 -0
- package/src/browser.ts +103 -0
- package/src/connection-kind.ts +53 -0
- package/src/describe-error.ts +76 -0
- package/src/edge-control.ts +94 -0
- package/src/edge-dispatch.ts +295 -0
- package/src/edge-guard.ts +67 -0
- package/src/edge.ts +175 -0
- package/src/gateway.ts +141 -0
- package/src/identity.ts +146 -0
- package/src/index.ts +95 -0
- package/src/join-blob.ts +229 -0
- package/src/join.ts +632 -0
- package/src/kv.ts +21 -0
- package/src/mesh-config.ts +41 -0
- package/src/mesh-memory.ts +104 -0
- package/src/node.ts +49 -0
- package/src/page-wake.ts +59 -0
- package/src/peer-request.ts +39 -0
- package/src/reset.ts +127 -0
- package/src/session.ts +579 -0
- package/src/start-member.ts +476 -0
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The NODE half of `MemberPlatform` — the counterpart to
|
|
3
|
+
* `./browser-profile.ts`'s `createBrowserNode`.
|
|
4
|
+
*
|
|
5
|
+
* WHAT DIFFERS FROM THE BROWSER PROFILE, AND WHAT DELIBERATELY DOES NOT:
|
|
6
|
+
*
|
|
7
|
+
* - `tcp()` is added. It is the one transport a page cannot have and the
|
|
8
|
+
* one a Node member wants, for the direct member-to-member hop a page
|
|
9
|
+
* makes over WebRTC instead.
|
|
10
|
+
* - The LISTEN ADDRESSES ARE THE SAME — `/p2p-circuit` and `/webrtc`,
|
|
11
|
+
* plus a loopback TCP port. If a Node member listened differently it
|
|
12
|
+
* would reserve somewhere else, and the rung would be testing a
|
|
13
|
+
* different design rather than the same one on another platform.
|
|
14
|
+
* - The connection gater is permissive, which the browser profile also
|
|
15
|
+
* does for loopback and private addresses (`dialNeedsPermissiveGater`).
|
|
16
|
+
* Node has no browser default to relax, so this is the same policy
|
|
17
|
+
* arrived at from the other side.
|
|
18
|
+
* - There is no `mountEdge`: `MemberHandle.fetch` is the edge under Node.
|
|
19
|
+
* - There is no `watchWake`: a Node process gets no wake events, so the
|
|
20
|
+
* supervisor's backstop timer is the whole story.
|
|
21
|
+
*/
|
|
22
|
+
import type { Ed25519PrivateKey, Libp2p } from "@libp2p/interface";
|
|
23
|
+
import type { MemberPlatform } from "./start-member.js";
|
|
24
|
+
export declare function createNodeMemberNode(init: {
|
|
25
|
+
privateKey?: Ed25519PrivateKey;
|
|
26
|
+
}): Promise<Libp2p>;
|
|
27
|
+
/** The whole Node platform: a node factory, and nothing else. */
|
|
28
|
+
export declare const nodePlatform: MemberPlatform;
|
|
29
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAKnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,wBAAsB,oBAAoB,CAAC,IAAI,EAAE;IAC/C,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC,GAAG,OAAO,CAAC,MAAM,CAAC,CAUlB;AAED,iEAAiE;AACjE,eAAO,MAAM,YAAY,EAAE,cAAqD,CAAC"}
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The NODE half of `MemberPlatform` — the counterpart to
|
|
3
|
+
* `./browser-profile.ts`'s `createBrowserNode`.
|
|
4
|
+
*
|
|
5
|
+
* WHAT DIFFERS FROM THE BROWSER PROFILE, AND WHAT DELIBERATELY DOES NOT:
|
|
6
|
+
*
|
|
7
|
+
* - `tcp()` is added. It is the one transport a page cannot have and the
|
|
8
|
+
* one a Node member wants, for the direct member-to-member hop a page
|
|
9
|
+
* makes over WebRTC instead.
|
|
10
|
+
* - The LISTEN ADDRESSES ARE THE SAME — `/p2p-circuit` and `/webrtc`,
|
|
11
|
+
* plus a loopback TCP port. If a Node member listened differently it
|
|
12
|
+
* would reserve somewhere else, and the rung would be testing a
|
|
13
|
+
* different design rather than the same one on another platform.
|
|
14
|
+
* - The connection gater is permissive, which the browser profile also
|
|
15
|
+
* does for loopback and private addresses (`dialNeedsPermissiveGater`).
|
|
16
|
+
* Node has no browser default to relax, so this is the same policy
|
|
17
|
+
* arrived at from the other side.
|
|
18
|
+
* - There is no `mountEdge`: `MemberHandle.fetch` is the edge under Node.
|
|
19
|
+
* - There is no `watchWake`: a Node process gets no wake events, so the
|
|
20
|
+
* supervisor's backstop timer is the whole story.
|
|
21
|
+
*/
|
|
22
|
+
import { noise } from "@chainsafe/libp2p-noise";
|
|
23
|
+
import { yamux } from "@chainsafe/libp2p-yamux";
|
|
24
|
+
import { circuitRelayTransport } from "@libp2p/circuit-relay-v2";
|
|
25
|
+
import { identify } from "@libp2p/identify";
|
|
26
|
+
import { tcp } from "@libp2p/tcp";
|
|
27
|
+
import { webRTC } from "@libp2p/webrtc";
|
|
28
|
+
import { webSockets } from "@libp2p/websockets";
|
|
29
|
+
import { createLibp2p } from "libp2p";
|
|
30
|
+
export async function createNodeMemberNode(init) {
|
|
31
|
+
return createLibp2p({
|
|
32
|
+
...(init.privateKey != null ? { privateKey: init.privateKey } : {}),
|
|
33
|
+
addresses: { listen: ["/ip4/127.0.0.1/tcp/0", "/p2p-circuit", "/webrtc"] },
|
|
34
|
+
transports: [webSockets(), circuitRelayTransport(), webRTC(), tcp()],
|
|
35
|
+
connectionEncrypters: [noise()],
|
|
36
|
+
streamMuxers: [yamux()],
|
|
37
|
+
connectionGater: { denyDialMultiaddr: async () => false },
|
|
38
|
+
services: { identify: identify() },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** The whole Node platform: a node factory, and nothing else. */
|
|
42
|
+
export const nodePlatform = { createNode: createNodeMemberNode };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page events that mean "the relay link may have died while nothing was
|
|
3
|
+
* watching, and may be reachable again now" -- turned into one callback.
|
|
4
|
+
*/
|
|
5
|
+
/** The parts of `window` this reads. */
|
|
6
|
+
export type WakeWindow = Pick<EventTarget, "addEventListener" | "removeEventListener">;
|
|
7
|
+
/** The parts of `document` this reads. */
|
|
8
|
+
export type WakeDocument = Pick<EventTarget, "addEventListener" | "removeEventListener"> & {
|
|
9
|
+
readonly visibilityState: string;
|
|
10
|
+
};
|
|
11
|
+
export interface WatchPageWakeInit {
|
|
12
|
+
win?: WakeWindow;
|
|
13
|
+
doc?: WakeDocument;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Call `onWake` whenever the page comes back; returns the unsubscribe.
|
|
17
|
+
*
|
|
18
|
+
* Why these events, and not a timer: a suspended page's timers are
|
|
19
|
+
* throttled or stopped, so the relay supervisor's own backoff may not fire
|
|
20
|
+
* for minutes after the page is usable again. See
|
|
21
|
+
* `tests/browser-page-wake.test.ts` for what each event means.
|
|
22
|
+
*/
|
|
23
|
+
export declare function watchPageWake(onWake: () => void, init?: WatchPageWakeInit): () => void;
|
|
24
|
+
//# sourceMappingURL=page-wake.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-wake.d.ts","sourceRoot":"","sources":["../src/page-wake.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,wCAAwC;AACxC,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,kBAAkB,GAAG,qBAAqB,CAAC,CAAC;AAEvF,0CAA0C;AAC1C,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,WAAW,EAAE,kBAAkB,GAAG,qBAAqB,CAAC,GAAG;IACzF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,GAAG,CAAC,EAAE,YAAY,CAAC;CACpB;AAeD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,IAAI,EAAE,IAAI,GAAE,iBAAsB,GAAG,MAAM,IAAI,CAmB1F"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page events that mean "the relay link may have died while nothing was
|
|
3
|
+
* watching, and may be reachable again now" -- turned into one callback.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Call `onWake` whenever the page comes back; returns the unsubscribe.
|
|
7
|
+
*
|
|
8
|
+
* Why these events, and not a timer: a suspended page's timers are
|
|
9
|
+
* throttled or stopped, so the relay supervisor's own backoff may not fire
|
|
10
|
+
* for minutes after the page is usable again. See
|
|
11
|
+
* `tests/browser-page-wake.test.ts` for what each event means.
|
|
12
|
+
*/
|
|
13
|
+
export function watchPageWake(onWake, init = {}) {
|
|
14
|
+
const win = init.win ?? window;
|
|
15
|
+
const doc = init.doc ?? document;
|
|
16
|
+
const wake = () => onWake();
|
|
17
|
+
const onVisibility = () => {
|
|
18
|
+
if (doc.visibilityState === "visible")
|
|
19
|
+
onWake();
|
|
20
|
+
};
|
|
21
|
+
win.addEventListener("online", wake);
|
|
22
|
+
win.addEventListener("pageshow", wake);
|
|
23
|
+
doc.addEventListener("resume", wake);
|
|
24
|
+
doc.addEventListener("visibilitychange", onVisibility);
|
|
25
|
+
return () => {
|
|
26
|
+
win.removeEventListener("online", wake);
|
|
27
|
+
win.removeEventListener("pageshow", wake);
|
|
28
|
+
doc.removeEventListener("resume", wake);
|
|
29
|
+
doc.removeEventListener("visibilitychange", onVisibility);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `Request` addressed at a mesh path.
|
|
3
|
+
*
|
|
4
|
+
* `Peer.call(peerId, request)` takes a whole `Request`, because that is the
|
|
5
|
+
* contract every other seam in this system speaks — the prototype's peer took
|
|
6
|
+
* `(peerId, path, init)` instead, which meant it owned a second, narrower idea
|
|
7
|
+
* of what a call is and could not carry anything a `Request` can (a stream, an
|
|
8
|
+
* `AbortSignal`, a `Headers` instance a caller already had).
|
|
9
|
+
*
|
|
10
|
+
* The origin is a placeholder and is never dialled: the far side routes on the
|
|
11
|
+
* pathname alone. It is spelled `peer.local` rather than `localhost` so that a
|
|
12
|
+
* stray log line cannot be mistaken for a loopback request that really went out.
|
|
13
|
+
*/
|
|
14
|
+
export interface PeerRequestInit extends RequestInit {
|
|
15
|
+
/**
|
|
16
|
+
* A membership token, placed in `MESH_TOKEN_HEADER` -- never
|
|
17
|
+
* `Authorization`, which stays the caller's to use for the application it
|
|
18
|
+
* is calling.
|
|
19
|
+
*
|
|
20
|
+
* The prototype's `peer.call` took this as its own option and built the
|
|
21
|
+
* header internally. Keeping it here rather than making every call site
|
|
22
|
+
* write the header keeps those sites reading as intent — and keeps ONE
|
|
23
|
+
* place that decides the scheme, which is what a token's format being
|
|
24
|
+
* changed later will want.
|
|
25
|
+
*/
|
|
26
|
+
token?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function peerRequest(path: string, init?: PeerRequestInit): Request;
|
|
29
|
+
//# sourceMappingURL=peer-request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer-request.d.ts","sourceRoot":"","sources":["../src/peer-request.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAgB,SAAQ,WAAW;IAClD;;;;;;;;;;OAUG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,eAAoB,GAAG,OAAO,CAQ7E"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { setMeshToken } from "@statewalker/httpeers-core";
|
|
2
|
+
export function peerRequest(path, init = {}) {
|
|
3
|
+
const { token, headers, ...rest } = init;
|
|
4
|
+
const merged = new Headers(headers);
|
|
5
|
+
if (token != null)
|
|
6
|
+
setMeshToken(merged, token);
|
|
7
|
+
return new Request(`http://peer.local${path.startsWith("/") ? path : `/${path}`}`, {
|
|
8
|
+
...rest,
|
|
9
|
+
headers: merged,
|
|
10
|
+
});
|
|
11
|
+
}
|
package/dist/reset.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting a stuck origin back to a first visit — and NOTHING ELSE IN IT.
|
|
3
|
+
*
|
|
4
|
+
* ITS OWN ENTRY POINT ON PURPOSE. This is what a rescue page loads when the
|
|
5
|
+
* app will not start, so it must not depend on the app starting. Importing it
|
|
6
|
+
* through `./browser` would drag in the session, the member lifecycle,
|
|
7
|
+
* `httpeers-access` and its Biscuit engine, crypto and transport code -- all to
|
|
8
|
+
* unregister a ServiceWorker. If any of that is what broke, the rescue page
|
|
9
|
+
* breaks with it.
|
|
10
|
+
*
|
|
11
|
+
* This was measured when the engine was WebAssembly: importing
|
|
12
|
+
* `@statewalker/httpeers-member/browser` failed outright until the wasm loader
|
|
13
|
+
* was wired. The engine is pure TypeScript now, but the rule stands for every
|
|
14
|
+
* other reason an app fails to start.
|
|
15
|
+
*
|
|
16
|
+
* So: zero imports. Everything here is a platform global.
|
|
17
|
+
*/
|
|
18
|
+
/** What a reset actually removed, so a page can report it rather than guess. */
|
|
19
|
+
export interface ResetResult {
|
|
20
|
+
/** ServiceWorker registrations unregistered on this origin. */
|
|
21
|
+
serviceWorkers: number;
|
|
22
|
+
/** IndexedDB databases deleted, by name. */
|
|
23
|
+
databases: string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Put this origin back to a first-visit state: no worker, no databases, no
|
|
27
|
+
* stored values.
|
|
28
|
+
*
|
|
29
|
+
* WHY THIS EXISTS AT ALL. A ServiceWorker is the stickiest thing a static site
|
|
30
|
+
* can install, and the obvious remedies do not work — measured in a real
|
|
31
|
+
* browser, not inferred:
|
|
32
|
+
*
|
|
33
|
+
* - **A reload does not remove one.** Registration is durable state on the
|
|
34
|
+
* ORIGIN, independent of any page's lifetime.
|
|
35
|
+
* - **`unregister()` does not evict the worker already controlling this
|
|
36
|
+
* page.** It removes the registration; the active worker keeps serving
|
|
37
|
+
* open clients until they are unloaded. So a reset control that reports
|
|
38
|
+
* success without reloading is still being served by the thing it says it
|
|
39
|
+
* removed. **Reload after calling this.**
|
|
40
|
+
* - **Unregistering alone is a half-reset.** The identity that makes a page
|
|
41
|
+
* "the same peer" is in IndexedDB. Clear only the worker and the page
|
|
42
|
+
* comes back as the same member, which is exactly the outcome people
|
|
43
|
+
* report as "it did not work".
|
|
44
|
+
*
|
|
45
|
+
* EVERYTHING ON THE ORIGIN, NOT A LIST OF NAMES. The state spans idb-keyval's
|
|
46
|
+
* store (identity, remembered mesh), the hub's database, the libp2p key store,
|
|
47
|
+
* the ServiceWorker adapter's claimed-keys table and `localStorage` — five
|
|
48
|
+
* places across three packages today. A hand-maintained list would go stale
|
|
49
|
+
* the first time one moved, silently, leaving the half-reset above. An origin
|
|
50
|
+
* here belongs to one app, so "all of it" is both simpler and correct.
|
|
51
|
+
*
|
|
52
|
+
* NEVER THROWS. It runs on a rescue page that must work when everything else
|
|
53
|
+
* is broken, including on a first visit with nothing to remove.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resetBrowserState(): Promise<ResetResult>;
|
|
56
|
+
//# sourceMappingURL=reset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reset.d.ts","sourceRoot":"","sources":["../src/reset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAoBH,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,CAoD9D"}
|
package/dist/reset.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting a stuck origin back to a first visit — and NOTHING ELSE IN IT.
|
|
3
|
+
*
|
|
4
|
+
* ITS OWN ENTRY POINT ON PURPOSE. This is what a rescue page loads when the
|
|
5
|
+
* app will not start, so it must not depend on the app starting. Importing it
|
|
6
|
+
* through `./browser` would drag in the session, the member lifecycle,
|
|
7
|
+
* `httpeers-access` and its Biscuit engine, crypto and transport code -- all to
|
|
8
|
+
* unregister a ServiceWorker. If any of that is what broke, the rescue page
|
|
9
|
+
* breaks with it.
|
|
10
|
+
*
|
|
11
|
+
* This was measured when the engine was WebAssembly: importing
|
|
12
|
+
* `@statewalker/httpeers-member/browser` failed outright until the wasm loader
|
|
13
|
+
* was wired. The engine is pure TypeScript now, but the rule stands for every
|
|
14
|
+
* other reason an app fails to start.
|
|
15
|
+
*
|
|
16
|
+
* So: zero imports. Everything here is a platform global.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Put this origin back to a first-visit state: no worker, no databases, no
|
|
20
|
+
* stored values.
|
|
21
|
+
*
|
|
22
|
+
* WHY THIS EXISTS AT ALL. A ServiceWorker is the stickiest thing a static site
|
|
23
|
+
* can install, and the obvious remedies do not work — measured in a real
|
|
24
|
+
* browser, not inferred:
|
|
25
|
+
*
|
|
26
|
+
* - **A reload does not remove one.** Registration is durable state on the
|
|
27
|
+
* ORIGIN, independent of any page's lifetime.
|
|
28
|
+
* - **`unregister()` does not evict the worker already controlling this
|
|
29
|
+
* page.** It removes the registration; the active worker keeps serving
|
|
30
|
+
* open clients until they are unloaded. So a reset control that reports
|
|
31
|
+
* success without reloading is still being served by the thing it says it
|
|
32
|
+
* removed. **Reload after calling this.**
|
|
33
|
+
* - **Unregistering alone is a half-reset.** The identity that makes a page
|
|
34
|
+
* "the same peer" is in IndexedDB. Clear only the worker and the page
|
|
35
|
+
* comes back as the same member, which is exactly the outcome people
|
|
36
|
+
* report as "it did not work".
|
|
37
|
+
*
|
|
38
|
+
* EVERYTHING ON THE ORIGIN, NOT A LIST OF NAMES. The state spans idb-keyval's
|
|
39
|
+
* store (identity, remembered mesh), the hub's database, the libp2p key store,
|
|
40
|
+
* the ServiceWorker adapter's claimed-keys table and `localStorage` — five
|
|
41
|
+
* places across three packages today. A hand-maintained list would go stale
|
|
42
|
+
* the first time one moved, silently, leaving the half-reset above. An origin
|
|
43
|
+
* here belongs to one app, so "all of it" is both simpler and correct.
|
|
44
|
+
*
|
|
45
|
+
* NEVER THROWS. It runs on a rescue page that must work when everything else
|
|
46
|
+
* is broken, including on a first visit with nothing to remove.
|
|
47
|
+
*/
|
|
48
|
+
export async function resetBrowserState() {
|
|
49
|
+
let serviceWorkers = 0;
|
|
50
|
+
try {
|
|
51
|
+
const registrations = (await navigator.serviceWorker?.getRegistrations()) ?? [];
|
|
52
|
+
for (const registration of registrations) {
|
|
53
|
+
try {
|
|
54
|
+
await registration.unregister();
|
|
55
|
+
serviceWorkers += 1;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* one that refuses must not stop the rest */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* no ServiceWorker support, or an insecure context */
|
|
64
|
+
}
|
|
65
|
+
const databases = [];
|
|
66
|
+
try {
|
|
67
|
+
// `indexedDB.databases()` is unsupported in Firefox, where enumeration is
|
|
68
|
+
// impossible and this half is a no-op. The worker and the stores below are
|
|
69
|
+
// still cleared, so a reset there is partial rather than absent -- worth
|
|
70
|
+
// knowing, and better than refusing to run.
|
|
71
|
+
const found = (await indexedDB.databases?.()) ?? [];
|
|
72
|
+
for (const { name } of found) {
|
|
73
|
+
if (name == null || name === "")
|
|
74
|
+
continue;
|
|
75
|
+
await new Promise((resolve) => {
|
|
76
|
+
const request = indexedDB.deleteDatabase(name);
|
|
77
|
+
request.onsuccess = () => resolve();
|
|
78
|
+
request.onerror = () => resolve();
|
|
79
|
+
// A database still open in another tab blocks. Resolving anyway keeps
|
|
80
|
+
// the reset moving; the page reload that follows closes this tab's
|
|
81
|
+
// handle, and the delete completes then.
|
|
82
|
+
request.onblocked = () => resolve();
|
|
83
|
+
});
|
|
84
|
+
databases.push(name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
/* enumeration unsupported */
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
localStorage?.clear();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* blocked site data */
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
sessionStorage?.clear();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* blocked site data */
|
|
101
|
+
}
|
|
102
|
+
return { serviceWorkers, databases };
|
|
103
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page session: one persistent identity, resume-or-redeem on start, and
|
|
3
|
+
* the three operator controls -- join, disconnect, reset identity -- with
|
|
4
|
+
* no DOM anywhere in it.
|
|
5
|
+
*
|
|
6
|
+
* WHY EVERY CONSUMER PAGE SHARES THIS. Two pages differ in exactly one
|
|
7
|
+
* thing: what they mount and advertise. Everything else -- read the saved
|
|
8
|
+
* identity, try to resume, say which of the two "cannot resume" situations
|
|
9
|
+
* this is, offer an invitation field, disconnect without surrendering
|
|
10
|
+
* membership, reset the key -- is identical between them, down to the
|
|
11
|
+
* wording, and duplicating it would have been duplicating the wording too.
|
|
12
|
+
*
|
|
13
|
+
* NO DOM, AND NOTHING BROWSER-ONLY IMPORTED. This module decides things --
|
|
14
|
+
* which mesh to try, resume or redeem, what an operator is told -- and
|
|
15
|
+
* deciding is exactly what is worth testing, so `tests/session.test.ts`
|
|
16
|
+
* exercises every branch under Node with no browser, no relay and no
|
|
17
|
+
* ServiceWorker. That is only possible because the platform, the two stores
|
|
18
|
+
* and the reload are INJECTED rather than defaulted: `browserPlatform`
|
|
19
|
+
* reaches `@libp2p/webrtc`, whose Node build loads a native module, so a
|
|
20
|
+
* default here would have made importing this module a browser-only act.
|
|
21
|
+
* `./browser.ts`'s `createSession` is where those defaults live instead.
|
|
22
|
+
*
|
|
23
|
+
* Pages render `SessionState` and call the four methods; they hold no state
|
|
24
|
+
* of their own.
|
|
25
|
+
*
|
|
26
|
+
* THE CONFIG IS RESOLVED HERE, because `startMember` requires one and will
|
|
27
|
+
* not fetch anything itself (see `StartMemberInit.config`). That moved the
|
|
28
|
+
* `httpeers.json` read up into this module, which is the only place that
|
|
29
|
+
* knows which of the three candidates -- a blob's mesh, the remembered one,
|
|
30
|
+
* the deployment's -- applies to the attempt at hand.
|
|
31
|
+
*
|
|
32
|
+
* WHICH MESH IS TRIED, AND THE ONE RULE THAT MUST NOT BEND. When someone
|
|
33
|
+
* supplies an invitation, the invitation decides: a join blob names its own
|
|
34
|
+
* mesh, and a BARE invitation id still means "the mesh `httpeers.json`
|
|
35
|
+
* names", i.e. the Node hub's -- the contract `./join-blob.ts` states and
|
|
36
|
+
* the reason both deployments keep working. Only a RESUME (nobody supplied
|
|
37
|
+
* anything) consults `./mesh-memory.ts`, and only because a mesh whose hub
|
|
38
|
+
* is a browser page cannot be named any other way after the `?join=` query
|
|
39
|
+
* has gone. See `./mesh-memory.ts`.
|
|
40
|
+
*
|
|
41
|
+
* DISCONNECT IS NOT LEAVING. `MemberHandle.stop()` drops presence and
|
|
42
|
+
* keeps membership, which is what makes the page resumable afterwards with
|
|
43
|
+
* no new invitation; see that method's own doc comment. This module never
|
|
44
|
+
* calls anything that surrenders membership, and the one control that
|
|
45
|
+
* destroys something -- `resetIdentity` -- says what it costs and is named
|
|
46
|
+
* separately.
|
|
47
|
+
*/
|
|
48
|
+
import type { Ed25519PrivateKey } from "@libp2p/interface";
|
|
49
|
+
import type { RuleSet } from "@statewalker/httpeers-access";
|
|
50
|
+
import type { MeshConfig as HttpeersConfig, Mounts } from "@statewalker/httpeers-core";
|
|
51
|
+
import type { IdentityStoreInit } from "./identity.js";
|
|
52
|
+
import type { AdvertisementInput } from "./join.js";
|
|
53
|
+
import type { MeshMemory } from "./mesh-memory.js";
|
|
54
|
+
import type { HubLink, JoinMethod, MemberHandle, MemberPlatform, MemberState, StartMemberInit } from "./start-member.js";
|
|
55
|
+
/** Why this page is asking for an invitation. The two that matter are the first two -- see `SessionPhase`. */
|
|
56
|
+
export type NeedsInvitationReason =
|
|
57
|
+
/** This browser has never held an identity for this origin: a genuine first run. */
|
|
58
|
+
"no-identity"
|
|
59
|
+
/** This browser holds an identity, the hub was reachable, and it does not know it. */
|
|
60
|
+
| "unknown-identity"
|
|
61
|
+
/** An invitation was supplied and the hub refused it (spent, expired, unknown). */
|
|
62
|
+
| "invitation-refused"
|
|
63
|
+
/** Nothing was supplied and nothing is remembered, so there is no mesh to even try. */
|
|
64
|
+
| "no-mesh";
|
|
65
|
+
/**
|
|
66
|
+
* Where the session is. Every one of these is a distinct thing to say on
|
|
67
|
+
* screen, which is the only reason there are six.
|
|
68
|
+
*
|
|
69
|
+
* `needs-invitation` CARRIES ITS REASON because "no saved identity yet" and
|
|
70
|
+
* "saved identity, but this hub does not know it" are not the same
|
|
71
|
+
* situation and must not read the same (Task 28, requirement 3). The first
|
|
72
|
+
* is a page that has never run; the second is a hub reset or a revoked
|
|
73
|
+
* membership, and an operator told only "please paste an invitation" would
|
|
74
|
+
* have no way to tell which had happened -- or that their saved identity is
|
|
75
|
+
* intact and simply unrecognised.
|
|
76
|
+
*
|
|
77
|
+
* `blocked` is the duplicate-identity refusal, kept apart from `failed`
|
|
78
|
+
* because it is not a fault to retry: two tabs are running one libp2p
|
|
79
|
+
* identity, and something has to give before this page can proceed.
|
|
80
|
+
*/
|
|
81
|
+
/**
|
|
82
|
+
* What `starting` reports.
|
|
83
|
+
*
|
|
84
|
+
* `"loading-config"` is the SESSION'S OWN step, not the member's: resolving
|
|
85
|
+
* which mesh to try can mean an `httpeers.json` fetch, which is the one part
|
|
86
|
+
* of a start that happens before `startMember` exists to have a state. Every
|
|
87
|
+
* other value is `MemberState` verbatim, forwarded.
|
|
88
|
+
*/
|
|
89
|
+
export type SessionStartState = "loading-config" | MemberState;
|
|
90
|
+
export type SessionPhase = {
|
|
91
|
+
kind: "checking";
|
|
92
|
+
} | {
|
|
93
|
+
kind: "starting";
|
|
94
|
+
peerState: SessionStartState;
|
|
95
|
+
} | {
|
|
96
|
+
kind: "live";
|
|
97
|
+
joinedBy: JoinMethod;
|
|
98
|
+
note: string | null;
|
|
99
|
+
} | {
|
|
100
|
+
kind: "needs-invitation";
|
|
101
|
+
reason: NeedsInvitationReason;
|
|
102
|
+
message: string;
|
|
103
|
+
} | {
|
|
104
|
+
kind: "disconnected";
|
|
105
|
+
message: string;
|
|
106
|
+
} | {
|
|
107
|
+
kind: "blocked";
|
|
108
|
+
message: string;
|
|
109
|
+
} | {
|
|
110
|
+
kind: "failed";
|
|
111
|
+
message: string;
|
|
112
|
+
};
|
|
113
|
+
/** Which controls make sense in the current phase. Derived here, once, rather than by each page re-deriving it from `phase.kind`. */
|
|
114
|
+
export interface SessionControls {
|
|
115
|
+
join: boolean;
|
|
116
|
+
disconnect: boolean;
|
|
117
|
+
reconnect: boolean;
|
|
118
|
+
reset: boolean;
|
|
119
|
+
}
|
|
120
|
+
export interface SessionState {
|
|
121
|
+
phase: SessionPhase;
|
|
122
|
+
/** The peerId this origin holds, or `null` when it has never had one. Known before anything is dialled. */
|
|
123
|
+
identity: string | null;
|
|
124
|
+
/** Live only while `phase.kind === "live"`. */
|
|
125
|
+
handle: MemberHandle | null;
|
|
126
|
+
/** How the live member reaches its hub (`MemberHandle.hubLink`, fixed for the member's run); `null` unless live. */
|
|
127
|
+
hubLink: HubLink | null;
|
|
128
|
+
controls: SessionControls;
|
|
129
|
+
}
|
|
130
|
+
/** `startMember`'s shape, as a seam -- see the module comment. */
|
|
131
|
+
export type StartPeer = (init: StartMemberInit) => Promise<MemberHandle>;
|
|
132
|
+
/** The identity store's three operations, as a seam. Defaults to `./identity.ts` over the real IndexedDB. */
|
|
133
|
+
export interface IdentityStore {
|
|
134
|
+
read(): Promise<Ed25519PrivateKey | null>;
|
|
135
|
+
loadOrCreate(): Promise<Ed25519PrivateKey>;
|
|
136
|
+
clear(): Promise<void>;
|
|
137
|
+
}
|
|
138
|
+
export declare function createIdentityStore(init: IdentityStoreInit): IdentityStore;
|
|
139
|
+
export interface PeerSessionInit {
|
|
140
|
+
/** Everything `startMember` needs that is this PAGE's own, and nothing that is the session's. */
|
|
141
|
+
key: string;
|
|
142
|
+
mounts: Mounts;
|
|
143
|
+
/** This page's own rule set, already built -- see `StartMemberInit.rules`. */
|
|
144
|
+
rules: RuleSet;
|
|
145
|
+
advertisements?: () => AdvertisementInput[];
|
|
146
|
+
/**
|
|
147
|
+
* How this host builds a node, mounts an edge and hears about wakes.
|
|
148
|
+
* REQUIRED, and deliberately: see the module comment on why a default
|
|
149
|
+
* would make importing this module browser-only.
|
|
150
|
+
*/
|
|
151
|
+
platform: MemberPlatform;
|
|
152
|
+
/** This page's `location.search`, read once at `start()`. */
|
|
153
|
+
search: string;
|
|
154
|
+
/** Fired on every state change, including the first. */
|
|
155
|
+
onChange: (state: SessionState) => void;
|
|
156
|
+
/** Where this origin's key lives. Required for the same reason `./mesh-memory.ts`'s backend is. */
|
|
157
|
+
identity: IdentityStore;
|
|
158
|
+
/** The mesh this origin last joined. Required for the same reason. */
|
|
159
|
+
meshMemory: MeshMemory;
|
|
160
|
+
/**
|
|
161
|
+
* Reloading the host after a reset. REQUIRED because `location` is the one
|
|
162
|
+
* DOM global this module would otherwise have to name; `./browser.ts`
|
|
163
|
+
* supplies it.
|
|
164
|
+
*/
|
|
165
|
+
reload: () => void;
|
|
166
|
+
startPeer?: StartPeer;
|
|
167
|
+
/**
|
|
168
|
+
* The mesh this DEPLOYMENT names (`httpeers.json`).
|
|
169
|
+
*
|
|
170
|
+
* READ FOR TWO REASONS. It is the fallback mesh -- what a bare invitation
|
|
171
|
+
* id means, and what a page with an identity but no remembered mesh tries.
|
|
172
|
+
* And it explains a failure: when a page cannot resume against the mesh it
|
|
173
|
+
* REMEMBERS and the deployment now names a different hub, that is almost
|
|
174
|
+
* always a re-run of `pnpm bootstrap` (or a switch between the Node hub and
|
|
175
|
+
* a hub page), and saying so is the difference between a two-minute fix and
|
|
176
|
+
* an hour.
|
|
177
|
+
*/
|
|
178
|
+
readDeploymentConfig?: () => Promise<HttpeersConfig | null>;
|
|
179
|
+
}
|
|
180
|
+
export interface PeerSession {
|
|
181
|
+
/** Read the saved identity and try to resume. Call once, on page load. */
|
|
182
|
+
start(): Promise<void>;
|
|
183
|
+
/** Join with what someone typed or pasted: a blob, a bare invitation id, or a whole join link. */
|
|
184
|
+
join(text: string): Promise<void>;
|
|
185
|
+
/** Stop this peer: drop presence, KEEP membership -- see `MemberHandle.stop`. */
|
|
186
|
+
disconnect(): Promise<void>;
|
|
187
|
+
/** Come back after a `disconnect`, by resuming. No invitation involved. */
|
|
188
|
+
reconnect(): Promise<void>;
|
|
189
|
+
/** Forget this origin's key, so the next run is a different peer. Destructive -- see below. */
|
|
190
|
+
resetIdentity(): Promise<void>;
|
|
191
|
+
state(): SessionState;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Where a deployment publishes the mesh it names.
|
|
195
|
+
*
|
|
196
|
+
* A SITE-ABSOLUTE PATH, so every page of an origin reads the same file
|
|
197
|
+
* whatever directory it was served from. It left with `peer-runtime.ts` and
|
|
198
|
+
* is declared here because this module is now the only thing that reads it.
|
|
199
|
+
*/
|
|
200
|
+
export declare const DEFAULT_HTTPEERS_CONFIG_URL = "/httpeers.json";
|
|
201
|
+
/**
|
|
202
|
+
* The sentence added to a "this hub does not know you" message when the
|
|
203
|
+
* deployment has visibly moved underneath the page.
|
|
204
|
+
*
|
|
205
|
+
* THE CASE THIS EXISTS FOR: `pnpm bootstrap` was re-run, so `httpeers.json`
|
|
206
|
+
* now names a hub with a different peerId than the one this page last
|
|
207
|
+
* joined. Everything about the page is fine, the hub it is talking to is
|
|
208
|
+
* fine, and the membership is simply in a mesh that no longer exists. With
|
|
209
|
+
* nothing said, that presents as "please paste an invitation" for reasons
|
|
210
|
+
* an operator cannot see. Exported for its own test.
|
|
211
|
+
*/
|
|
212
|
+
export declare function describeMeshDrift(tried: string, deployment: HttpeersConfig | null): string | null;
|
|
213
|
+
export declare function createPeerSession(init: PeerSessionInit): PeerSession;
|
|
214
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|