@net-mesh/browser 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +517 -0
  2. package/LICENSE-APACHE +190 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +666 -0
  5. package/dist/async-queue.d.ts +26 -0
  6. package/dist/async-queue.js +62 -0
  7. package/dist/credential.d.ts +70 -0
  8. package/dist/credential.js +95 -0
  9. package/dist/errors.d.ts +400 -0
  10. package/dist/errors.js +726 -0
  11. package/dist/events.d.ts +232 -0
  12. package/dist/events.js +298 -0
  13. package/dist/identity.d.ts +40 -0
  14. package/dist/identity.js +78 -0
  15. package/dist/index.bundle.js +2 -0
  16. package/dist/index.d.ts +38 -0
  17. package/dist/index.js +46 -0
  18. package/dist/leader/events.d.ts +111 -0
  19. package/dist/leader/events.js +116 -0
  20. package/dist/leader/index.d.ts +14 -0
  21. package/dist/leader/index.js +11 -0
  22. package/dist/leader/session.d.ts +539 -0
  23. package/dist/leader/session.js +770 -0
  24. package/dist/leader/wasm.d.ts +123 -0
  25. package/dist/leader/wasm.js +39 -0
  26. package/dist/lobby.d.ts +183 -0
  27. package/dist/lobby.js +504 -0
  28. package/dist/local.d.ts +80 -0
  29. package/dist/local.js +160 -0
  30. package/dist/net_leaf.d.ts +1494 -0
  31. package/dist/net_leaf.js +3753 -0
  32. package/dist/net_leaf_bg.wasm +0 -0
  33. package/dist/net_leaf_bg.wasm.d.ts +137 -0
  34. package/dist/netcode/client.d.ts +102 -0
  35. package/dist/netcode/client.js +296 -0
  36. package/dist/netcode/clock.d.ts +45 -0
  37. package/dist/netcode/clock.js +54 -0
  38. package/dist/netcode/host.d.ts +118 -0
  39. package/dist/netcode/host.js +276 -0
  40. package/dist/netcode/index.d.ts +19 -0
  41. package/dist/netcode/index.js +14 -0
  42. package/dist/netcode/interpolate.d.ts +50 -0
  43. package/dist/netcode/interpolate.js +100 -0
  44. package/dist/netcode/wire.d.ts +125 -0
  45. package/dist/netcode/wire.js +117 -0
  46. package/dist/node.d.ts +908 -0
  47. package/dist/node.js +1085 -0
  48. package/dist/org.d.ts +412 -0
  49. package/dist/org.js +567 -0
  50. package/dist/peer-driver.d.ts +117 -0
  51. package/dist/peer-driver.js +224 -0
  52. package/dist/store/assembly.d.ts +176 -0
  53. package/dist/store/assembly.js +325 -0
  54. package/dist/store/chunker.d.ts +83 -0
  55. package/dist/store/chunker.js +190 -0
  56. package/dist/store/core.d.ts +136 -0
  57. package/dist/store/core.js +477 -0
  58. package/dist/store/definition.d.ts +23 -0
  59. package/dist/store/definition.js +83 -0
  60. package/dist/store/errors.d.ts +77 -0
  61. package/dist/store/errors.js +94 -0
  62. package/dist/store/host.d.ts +247 -0
  63. package/dist/store/host.js +675 -0
  64. package/dist/store/index.d.ts +50 -0
  65. package/dist/store/index.js +40 -0
  66. package/dist/store/interest.d.ts +40 -0
  67. package/dist/store/interest.js +77 -0
  68. package/dist/store/inventory.d.ts +81 -0
  69. package/dist/store/inventory.js +140 -0
  70. package/dist/store/join.d.ts +118 -0
  71. package/dist/store/join.js +724 -0
  72. package/dist/store/json.d.ts +50 -0
  73. package/dist/store/json.js +283 -0
  74. package/dist/store/ledger.d.ts +187 -0
  75. package/dist/store/ledger.js +292 -0
  76. package/dist/store/owner.d.ts +616 -0
  77. package/dist/store/owner.js +1711 -0
  78. package/dist/store/patch.d.ts +70 -0
  79. package/dist/store/patch.js +166 -0
  80. package/dist/store/player.d.ts +65 -0
  81. package/dist/store/player.js +261 -0
  82. package/dist/store/replica.d.ts +187 -0
  83. package/dist/store/replica.js +740 -0
  84. package/dist/store/state.d.ts +42 -0
  85. package/dist/store/state.js +165 -0
  86. package/dist/store/types.d.ts +200 -0
  87. package/dist/store/types.js +25 -0
  88. package/dist/store/visibility.d.ts +118 -0
  89. package/dist/store/visibility.js +282 -0
  90. package/dist/store/wire.d.ts +309 -0
  91. package/dist/store/wire.js +834 -0
  92. package/dist/stream.d.ts +174 -0
  93. package/dist/stream.js +238 -0
  94. package/dist/three/index.d.ts +109 -0
  95. package/dist/three/index.js +130 -0
  96. package/dist/udp-probe.d.ts +159 -0
  97. package/dist/udp-probe.js +245 -0
  98. package/dist/wasm.d.ts +642 -0
  99. package/dist/wasm.js +98 -0
  100. package/dist/world/border.d.ts +84 -0
  101. package/dist/world/border.js +99 -0
  102. package/dist/world/handoff.d.ts +187 -0
  103. package/dist/world/handoff.js +222 -0
  104. package/dist/world/index.d.ts +13 -0
  105. package/dist/world/index.js +9 -0
  106. package/dist/world/link.d.ts +179 -0
  107. package/dist/world/link.js +476 -0
  108. package/dist/world/regions.d.ts +154 -0
  109. package/dist/world/regions.js +314 -0
  110. package/package.json +72 -0
@@ -0,0 +1,232 @@
1
+ /**
2
+ * The event surface.
3
+ *
4
+ * The wasm node hands the page one JSON **string** per event through
5
+ * `on_event`. This module turns that string into a typed, discriminated
6
+ * object and fans it out to typed listeners and/or an async iterator.
7
+ *
8
+ * Two conventions, both deliberate:
9
+ *
10
+ * - **Tag values stay verbatim** (`channel_message`, not
11
+ * `channelMessage`). They are the leaf's own protocol vocabulary and
12
+ * appear in Rust logs, browser logs and Playwright assertions; a
13
+ * translation layer would only add a place to disagree. Field names
14
+ * *are* camelCased, because they are TypeScript property accesses.
15
+ * - **`u64` fields are exact decimal strings.** `JSON.parse` rounds
16
+ * integer literals above 2^53 — `18446744073709551615` parses to
17
+ * `18446744073709552000` — which would silently mis-route a page
18
+ * filtering `channel_message` by hash. {@link parseEvent} re-quotes
19
+ * big integer literals in the known `u64` keys *before* parsing, so
20
+ * no precision is lost whether the leaf emits numbers or strings.
21
+ */
22
+ import { type LeafError } from './errors.js';
23
+ /**
24
+ * The `u64` keys in the leaf's event JSON, as they appear on the wire.
25
+ *
26
+ * Exported so a mismatch with the Rust side is one grep away: any new
27
+ * 64-bit field the leaf emits must be listed here or it will be
28
+ * rounded by `JSON.parse`.
29
+ */
30
+ export declare const U64_EVENT_KEYS: readonly ["node_id", "peer_node", "channel_hash", "origin_hash", "stream_id", "dialog", "not_after", "call_id", "seq", "generation", "incarnation", "version", "from", "to"];
31
+ /** The node finished its handshake with an anchor and has a session. */
32
+ export interface ConnectedEvent {
33
+ readonly type: 'connected';
34
+ /** This node's mesh id, hex. */
35
+ readonly nodeIdHex: string;
36
+ /** The anchor's mesh id, exact decimal. */
37
+ readonly peerNode: string;
38
+ /**
39
+ * The `rtc_addr` that anchor published, verbatim from its
40
+ * `GET /rtc/anchor`. The subject of the STUN probe that turns an
41
+ * ICE timeout into `udp-blocked`; `null` when the anchor published
42
+ * none.
43
+ */
44
+ readonly rtcAddr: string | null;
45
+ }
46
+ /** The session went away. */
47
+ export interface DisconnectedEvent {
48
+ readonly type: 'disconnected';
49
+ readonly reason: string;
50
+ }
51
+ /** A message on a subscribed channel. */
52
+ export interface ChannelMessageEvent {
53
+ readonly type: 'channel_message';
54
+ /** The channel's hash, exact decimal. */
55
+ readonly channelHash: string;
56
+ /** The publisher's origin hash, exact decimal. */
57
+ readonly originHash: string;
58
+ readonly payload: Uint8Array;
59
+ }
60
+ /** A frame on an open stream, after the consumer-side `seq` reorder. */
61
+ export interface StreamDataEvent {
62
+ readonly type: 'stream_data';
63
+ /**
64
+ * The peer this frame came from, exact decimal — the other half of
65
+ * the identity of a stream.
66
+ *
67
+ * A stream id is the application's label and is scoped to a
68
+ * session, so the SAME id is the ordinary case for two streams to
69
+ * two different peers. Without this, a wrapper filtering the
70
+ * node-wide event vector by id alone takes one peer's payload for
71
+ * the other's (R4-10).
72
+ *
73
+ * Always present: a frame only exists on an authenticated session,
74
+ * and an anchor-addressed stream's peer is the anchor.
75
+ */
76
+ readonly peerNode: string;
77
+ /**
78
+ * Which incarnation of that peer's session carried it, exact
79
+ * decimal.
80
+ *
81
+ * Provenance, not identity: the filter keys on `(peerNode,
82
+ * streamId)` only, because a replaced session retires its receive
83
+ * cursors, so no frame arrives under a dead incarnation. It is
84
+ * here so a page attributing a payload to a session reads an exact
85
+ * value rather than deriving one — which is also why
86
+ * `incarnation` is in {@link U64_EVENT_KEYS}.
87
+ */
88
+ readonly incarnation: string;
89
+ /** The stream's id, exact decimal. */
90
+ readonly streamId: string;
91
+ readonly seq: number;
92
+ readonly payload: Uint8Array;
93
+ }
94
+ /** A signed capability announcement the leaf ingested. */
95
+ export interface AnnouncementEvent {
96
+ readonly type: 'announcement';
97
+ /** The announcing node's mesh id, exact decimal. */
98
+ readonly nodeId: string;
99
+ readonly capabilities: readonly string[];
100
+ readonly rtcAddr: string | null;
101
+ /** Whether the leaf verified the signature. */
102
+ readonly verified: boolean;
103
+ }
104
+ /** A session-independent signalling envelope addressed to this leaf. */
105
+ export interface SignalEvent {
106
+ readonly type: 'signal';
107
+ /** Sender mesh id, exact decimal. */
108
+ readonly from: string;
109
+ /** Recipient mesh id, exact decimal. */
110
+ readonly to: string;
111
+ /** Dialog id, exact decimal. */
112
+ readonly dialog: string;
113
+ readonly kind: string;
114
+ readonly payload: Uint8Array;
115
+ /** Replay bound, exact decimal. */
116
+ readonly notAfter: string;
117
+ }
118
+ /** An nRPC reply arrived for an in-flight call. */
119
+ export interface RpcResponseEvent {
120
+ readonly type: 'rpc_response';
121
+ /** The call's id, exact decimal. */
122
+ readonly callId: string;
123
+ readonly status: number;
124
+ }
125
+ /** The leaf dropped an inbound frame, and why. */
126
+ export interface DroppedEvent {
127
+ readonly type: 'dropped';
128
+ readonly reason: string;
129
+ readonly subprotocol: number;
130
+ }
131
+ /** An RTC attempt failed, with the leaf's own typed classification. */
132
+ export interface RtcFailureEvent {
133
+ readonly type: 'rtc_failure';
134
+ /** Re-typed from the leaf's `Display` text by {@link fromWasmError}. */
135
+ readonly error: LeafError;
136
+ }
137
+ /**
138
+ * A tag this version of the package does not know, carried through
139
+ * intact rather than dropped — a newer leaf must not go silent against
140
+ * an older page.
141
+ */
142
+ export interface UnknownEvent {
143
+ readonly type: 'unknown';
144
+ /** The tag as it arrived, or `null` when the JSON had none. */
145
+ readonly tag: string | null;
146
+ /** The parsed object, or the raw text when it was not JSON at all. */
147
+ readonly raw: unknown;
148
+ }
149
+ /** Every event the leaf can deliver. */
150
+ export type LeafEvent = ConnectedEvent | DisconnectedEvent | ChannelMessageEvent | StreamDataEvent | AnnouncementEvent | SignalEvent | RpcResponseEvent | DroppedEvent | RtcFailureEvent | UnknownEvent;
151
+ /** `LeafEvent` narrowed by its tag. */
152
+ export type LeafEventOf<T extends LeafEvent['type']> = Extract<LeafEvent, {
153
+ type: T;
154
+ }>;
155
+ /**
156
+ * Parse one event JSON string. Never throws: malformed input becomes
157
+ * an {@link UnknownEvent}, because this runs inside a callback the
158
+ * wasm module invokes and an exception there would unwind through
159
+ * Rust.
160
+ */
161
+ export declare function parseEvent(json: string): LeafEvent;
162
+ /** A listener's cancel handle. Calling it twice is harmless. */
163
+ export type Unsubscribe = () => void;
164
+ /**
165
+ * The fan-out behind `node.on` / `node.onEvent` / `node.events()`.
166
+ *
167
+ * One `on_event` callback is registered with the wasm node for the
168
+ * node's lifetime; everything above it is TypeScript. A throwing
169
+ * listener is reported and skipped — it must not take the others down,
170
+ * and it must not unwind into Rust.
171
+ *
172
+ * Generic over the event union so §8's session surface, whose union is
173
+ * `LeafEvent` plus its five lifecycle tags, reuses this fan-out instead
174
+ * of growing a second one beside it. The default parameter keeps the
175
+ * direct surface's type exactly as it was.
176
+ */
177
+ export declare class EventHub<E extends {
178
+ readonly type: string;
179
+ } = LeafEvent> {
180
+ private readonly typed;
181
+ private readonly any;
182
+ private readonly queues;
183
+ /** Listen for one tag. */
184
+ on<T extends E['type']>(type: T, handler: (event: Extract<E, {
185
+ type: T;
186
+ }>) => void): Unsubscribe;
187
+ /** Listen for every event. */
188
+ onAny(handler: (event: E) => void): Unsubscribe;
189
+ /**
190
+ * Consume events as an async iterable.
191
+ *
192
+ * Events that arrive while the consumer is not awaiting are buffered
193
+ * in arrival order, and buffering stops the moment the loop is left
194
+ * (`break`, `return`, or an exception) — so a page that stops
195
+ * consuming stops paying. A page that starts a loop and never awaits
196
+ * it will grow the buffer; leave the loop instead of abandoning it.
197
+ */
198
+ events(): AsyncIterableIterator<E>;
199
+ /**
200
+ * Feed one raw JSON string in — the wasm callback's only job.
201
+ *
202
+ * `parse` defaults to {@link parseEvent}; a surface with a wider
203
+ * union passes its own parser. Like `parseEvent`, a parser handed
204
+ * here must never throw: this runs inside a callback the wasm module
205
+ * invokes, and an exception would unwind through Rust.
206
+ */
207
+ deliver(json: string, parse?: (json: string) => E): void;
208
+ /** Feed one already-parsed event in. */
209
+ dispatch(event: E): void;
210
+ /** Release every listener and end every iterator. */
211
+ close(): void;
212
+ }
213
+ /**
214
+ * `JSON.parse`, minus the `u64` rounding.
215
+ *
216
+ * Unquoted integer literals under the {@link U64_EVENT_KEYS} keys are
217
+ * re-quoted first, so a 64-bit id survives as an exact decimal string.
218
+ * A value the leaf already quoted is left alone, which is why this is
219
+ * a no-op against the current Rust side and still a guard against a
220
+ * regression there.
221
+ */
222
+ export declare function parseJsonPreservingU64(json: string): unknown;
223
+ /**
224
+ * Standard padded base64 to bytes.
225
+ *
226
+ * `atob`/`btoa` are the one decoder present in every browser this
227
+ * package targets *and* in every Node the package supports (>=20), so
228
+ * there is no second path to keep honest.
229
+ */
230
+ export declare function fromBase64(text: string): Uint8Array;
231
+ /** Bytes to standard padded base64. */
232
+ export declare function toBase64(data: Uint8Array): string;
package/dist/events.js ADDED
@@ -0,0 +1,298 @@
1
+ /**
2
+ * The event surface.
3
+ *
4
+ * The wasm node hands the page one JSON **string** per event through
5
+ * `on_event`. This module turns that string into a typed, discriminated
6
+ * object and fans it out to typed listeners and/or an async iterator.
7
+ *
8
+ * Two conventions, both deliberate:
9
+ *
10
+ * - **Tag values stay verbatim** (`channel_message`, not
11
+ * `channelMessage`). They are the leaf's own protocol vocabulary and
12
+ * appear in Rust logs, browser logs and Playwright assertions; a
13
+ * translation layer would only add a place to disagree. Field names
14
+ * *are* camelCased, because they are TypeScript property accesses.
15
+ * - **`u64` fields are exact decimal strings.** `JSON.parse` rounds
16
+ * integer literals above 2^53 — `18446744073709551615` parses to
17
+ * `18446744073709552000` — which would silently mis-route a page
18
+ * filtering `channel_message` by hash. {@link parseEvent} re-quotes
19
+ * big integer literals in the known `u64` keys *before* parsing, so
20
+ * no precision is lost whether the leaf emits numbers or strings.
21
+ */
22
+ import { AsyncQueue } from './async-queue.js';
23
+ import { fromWasmError } from './errors.js';
24
+ /**
25
+ * The `u64` keys in the leaf's event JSON, as they appear on the wire.
26
+ *
27
+ * Exported so a mismatch with the Rust side is one grep away: any new
28
+ * 64-bit field the leaf emits must be listed here or it will be
29
+ * rounded by `JSON.parse`.
30
+ */
31
+ export const U64_EVENT_KEYS = [
32
+ 'node_id',
33
+ 'peer_node',
34
+ 'channel_hash',
35
+ 'origin_hash',
36
+ 'stream_id',
37
+ 'dialog',
38
+ 'not_after',
39
+ 'call_id',
40
+ 'seq',
41
+ 'generation',
42
+ 'incarnation',
43
+ 'version',
44
+ 'from',
45
+ 'to',
46
+ ];
47
+ /**
48
+ * Parse one event JSON string. Never throws: malformed input becomes
49
+ * an {@link UnknownEvent}, because this runs inside a callback the
50
+ * wasm module invokes and an exception there would unwind through
51
+ * Rust.
52
+ */
53
+ export function parseEvent(json) {
54
+ let raw;
55
+ try {
56
+ raw = parseJsonPreservingU64(json);
57
+ }
58
+ catch {
59
+ return { type: 'unknown', tag: null, raw: json };
60
+ }
61
+ if (raw === null || typeof raw !== 'object') {
62
+ return { type: 'unknown', tag: null, raw };
63
+ }
64
+ // Every read below goes through a checked accessor, so an index
65
+ // signature is the whole of what this assertion claims.
66
+ const fields = raw;
67
+ const tag = typeof fields.type === 'string' ? fields.type : null;
68
+ switch (tag) {
69
+ case 'connected':
70
+ return {
71
+ type: 'connected',
72
+ nodeIdHex: str(fields, 'node_id_hex'),
73
+ peerNode: str(fields, 'peer_node'),
74
+ rtcAddr: optionalStr(fields, 'rtc_addr'),
75
+ };
76
+ case 'disconnected':
77
+ return { type: 'disconnected', reason: str(fields, 'reason') };
78
+ case 'channel_message':
79
+ return {
80
+ type: 'channel_message',
81
+ channelHash: str(fields, 'channel_hash'),
82
+ originHash: str(fields, 'origin_hash'),
83
+ payload: bytes(fields, 'payload'),
84
+ };
85
+ case 'stream_data':
86
+ return {
87
+ type: 'stream_data',
88
+ peerNode: str(fields, 'peer_node'),
89
+ incarnation: str(fields, 'incarnation'),
90
+ streamId: str(fields, 'stream_id'),
91
+ seq: num(fields, 'seq'),
92
+ payload: bytes(fields, 'payload'),
93
+ };
94
+ case 'announcement':
95
+ return {
96
+ type: 'announcement',
97
+ nodeId: str(fields, 'node_id'),
98
+ capabilities: strArray(fields, 'capabilities'),
99
+ rtcAddr: optionalStr(fields, 'rtc_addr'),
100
+ verified: fields.verified === true,
101
+ };
102
+ case 'signal':
103
+ return {
104
+ type: 'signal',
105
+ from: str(fields, 'from'),
106
+ to: str(fields, 'to'),
107
+ dialog: str(fields, 'dialog'),
108
+ kind: str(fields, 'kind'),
109
+ payload: bytes(fields, 'payload'),
110
+ notAfter: str(fields, 'not_after'),
111
+ };
112
+ case 'rpc_response':
113
+ return {
114
+ type: 'rpc_response',
115
+ callId: str(fields, 'call_id'),
116
+ status: num(fields, 'status'),
117
+ };
118
+ case 'dropped':
119
+ return {
120
+ type: 'dropped',
121
+ reason: str(fields, 'reason'),
122
+ subprotocol: num(fields, 'subprotocol'),
123
+ };
124
+ case 'rtc_failure':
125
+ return { type: 'rtc_failure', error: fromWasmError(str(fields, 'error')) };
126
+ default:
127
+ return { type: 'unknown', tag, raw: fields };
128
+ }
129
+ }
130
+ /**
131
+ * The fan-out behind `node.on` / `node.onEvent` / `node.events()`.
132
+ *
133
+ * One `on_event` callback is registered with the wasm node for the
134
+ * node's lifetime; everything above it is TypeScript. A throwing
135
+ * listener is reported and skipped — it must not take the others down,
136
+ * and it must not unwind into Rust.
137
+ *
138
+ * Generic over the event union so §8's session surface, whose union is
139
+ * `LeafEvent` plus its five lifecycle tags, reuses this fan-out instead
140
+ * of growing a second one beside it. The default parameter keeps the
141
+ * direct surface's type exactly as it was.
142
+ */
143
+ export class EventHub {
144
+ typed = new Map();
145
+ any = new Set();
146
+ queues = new Set();
147
+ /** Listen for one tag. */
148
+ on(type, handler) {
149
+ const handlers = this.typed.get(type) ?? new Set();
150
+ this.typed.set(type, handlers);
151
+ // Variance the compiler cannot unify: the map erases the tag, and
152
+ // `dispatch` only ever hands a handler the tag it registered for.
153
+ const erased = handler;
154
+ handlers.add(erased);
155
+ return () => {
156
+ handlers.delete(erased);
157
+ };
158
+ }
159
+ /** Listen for every event. */
160
+ onAny(handler) {
161
+ this.any.add(handler);
162
+ return () => {
163
+ this.any.delete(handler);
164
+ };
165
+ }
166
+ /**
167
+ * Consume events as an async iterable.
168
+ *
169
+ * Events that arrive while the consumer is not awaiting are buffered
170
+ * in arrival order, and buffering stops the moment the loop is left
171
+ * (`break`, `return`, or an exception) — so a page that stops
172
+ * consuming stops paying. A page that starts a loop and never awaits
173
+ * it will grow the buffer; leave the loop instead of abandoning it.
174
+ */
175
+ events() {
176
+ const queue = new AsyncQueue(() => this.queues.delete(queue));
177
+ this.queues.add(queue);
178
+ return queue;
179
+ }
180
+ /**
181
+ * Feed one raw JSON string in — the wasm callback's only job.
182
+ *
183
+ * `parse` defaults to {@link parseEvent}; a surface with a wider
184
+ * union passes its own parser. Like `parseEvent`, a parser handed
185
+ * here must never throw: this runs inside a callback the wasm module
186
+ * invokes, and an exception would unwind through Rust.
187
+ */
188
+ deliver(json, parse) {
189
+ // Without a parser the events are `LeafEvent`s, which is exact for
190
+ // the default `E` and a subset of any wider union built on it —
191
+ // the only instantiations that exist. A surface whose union is not
192
+ // a superset of `LeafEvent` must pass its own parser.
193
+ this.dispatch(parse ? parse(json) : parseEvent(json));
194
+ }
195
+ /** Feed one already-parsed event in. */
196
+ dispatch(event) {
197
+ for (const handler of this.typed.get(event.type) ?? []) {
198
+ invoke(handler, event);
199
+ }
200
+ for (const handler of this.any) {
201
+ invoke(handler, event);
202
+ }
203
+ for (const queue of this.queues) {
204
+ queue.push(event);
205
+ }
206
+ }
207
+ /** Release every listener and end every iterator. */
208
+ close() {
209
+ this.typed.clear();
210
+ this.any.clear();
211
+ for (const queue of [...this.queues]) {
212
+ queue.end();
213
+ }
214
+ this.queues.clear();
215
+ }
216
+ }
217
+ function invoke(handler, event) {
218
+ try {
219
+ handler(event);
220
+ }
221
+ catch (error) {
222
+ // A listener's bug is not the node's: report it where a page can
223
+ // see it and keep the remaining listeners running. Throwing here
224
+ // would unwind through the wasm callback frame.
225
+ console.error('[@net-mesh/browser] event listener threw', error);
226
+ }
227
+ }
228
+ /**
229
+ * `JSON.parse`, minus the `u64` rounding.
230
+ *
231
+ * Unquoted integer literals under the {@link U64_EVENT_KEYS} keys are
232
+ * re-quoted first, so a 64-bit id survives as an exact decimal string.
233
+ * A value the leaf already quoted is left alone, which is why this is
234
+ * a no-op against the current Rust side and still a guard against a
235
+ * regression there.
236
+ */
237
+ export function parseJsonPreservingU64(json) {
238
+ const keys = U64_EVENT_KEYS.join('|');
239
+ const exact = json.replace(new RegExp(`("(?:${keys})"\\s*:\\s*)(\\d+)`, 'g'), '$1"$2"');
240
+ return JSON.parse(exact);
241
+ }
242
+ function str(fields, key) {
243
+ const value = fields[key];
244
+ if (typeof value === 'string')
245
+ return value;
246
+ if (typeof value === 'number' || typeof value === 'boolean')
247
+ return String(value);
248
+ return '';
249
+ }
250
+ function optionalStr(fields, key) {
251
+ const value = fields[key];
252
+ return typeof value === 'string' && value.length > 0 ? value : null;
253
+ }
254
+ function num(fields, key) {
255
+ const value = fields[key];
256
+ if (typeof value === 'number')
257
+ return value;
258
+ if (typeof value === 'string') {
259
+ const parsed = Number(value);
260
+ return Number.isFinite(parsed) ? parsed : 0;
261
+ }
262
+ return 0;
263
+ }
264
+ function strArray(fields, key) {
265
+ const value = fields[key];
266
+ if (!Array.isArray(value))
267
+ return [];
268
+ return value.filter((item) => typeof item === 'string');
269
+ }
270
+ function bytes(fields, key) {
271
+ const value = fields[key];
272
+ if (value instanceof Uint8Array)
273
+ return value;
274
+ if (typeof value !== 'string')
275
+ return new Uint8Array(0);
276
+ return fromBase64(value);
277
+ }
278
+ /**
279
+ * Standard padded base64 to bytes.
280
+ *
281
+ * `atob`/`btoa` are the one decoder present in every browser this
282
+ * package targets *and* in every Node the package supports (>=20), so
283
+ * there is no second path to keep honest.
284
+ */
285
+ export function fromBase64(text) {
286
+ const binary = atob(text);
287
+ const out = new Uint8Array(binary.length);
288
+ for (let i = 0; i < binary.length; i += 1)
289
+ out[i] = binary.charCodeAt(i);
290
+ return out;
291
+ }
292
+ /** Bytes to standard padded base64. */
293
+ export function toBase64(data) {
294
+ let binary = '';
295
+ for (const byte of data)
296
+ binary += String.fromCharCode(byte);
297
+ return btoa(binary);
298
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `rememberedIdentity` — the same player on every visit, for `connect()`.
3
+ *
4
+ * ```ts
5
+ * const node = await connect({ credentialB64, bootstrapUrl, ...rememberedIdentity() });
6
+ * ```
7
+ *
8
+ * `connect()` without `entitySecretHex` makes a new node — a new player —
9
+ * on every page load. Games (stores, lobbies) run on `connect()`, so this
10
+ * keeps the node's two secrets in `localStorage` and hands them back on
11
+ * every visit: one player per **browser profile**. Clearing site data
12
+ * makes a new player, and the same person on two devices is two players.
13
+ *
14
+ * **The trust boundary is the origin.** Anything running on the page's
15
+ * origin can read `localStorage`, and could equally drive the node; the
16
+ * leaf's own IndexedDB custody (`openSession()`) is no stronger against
17
+ * same-origin script (see `leaf/src/identity.rs`). Do not use this on an
18
+ * origin that runs code you do not trust.
19
+ *
20
+ * When storage is unavailable (a private window with storage blocked, a
21
+ * sandboxed frame), the secrets are fresh and not kept: the page still
22
+ * connects, as a new player.
23
+ */
24
+ /** The two secrets `connect()` takes to be a particular node. */
25
+ export interface IdentitySecrets {
26
+ /** Ed25519 entity secret, 32 bytes hex — who the player is. */
27
+ readonly entitySecretHex: string;
28
+ /** X25519 Noise static secret, 32 bytes hex. */
29
+ readonly noiseSecretHex: string;
30
+ }
31
+ /** The `localStorage` key {@link rememberedIdentity} uses by default. */
32
+ export declare const DEFAULT_IDENTITY_KEY = "net-mesh:identity";
33
+ type Storage = Pick<globalThis.Storage, 'getItem' | 'setItem'>;
34
+ /**
35
+ * This origin's player secrets, created on first use and kept in
36
+ * `localStorage` under `key`. Pass a different `key` for a second,
37
+ * separate player on the same origin (a local two-player test).
38
+ */
39
+ export declare function rememberedIdentity(key?: string, storage?: Storage): IdentitySecrets;
40
+ export {};
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `rememberedIdentity` — the same player on every visit, for `connect()`.
3
+ *
4
+ * ```ts
5
+ * const node = await connect({ credentialB64, bootstrapUrl, ...rememberedIdentity() });
6
+ * ```
7
+ *
8
+ * `connect()` without `entitySecretHex` makes a new node — a new player —
9
+ * on every page load. Games (stores, lobbies) run on `connect()`, so this
10
+ * keeps the node's two secrets in `localStorage` and hands them back on
11
+ * every visit: one player per **browser profile**. Clearing site data
12
+ * makes a new player, and the same person on two devices is two players.
13
+ *
14
+ * **The trust boundary is the origin.** Anything running on the page's
15
+ * origin can read `localStorage`, and could equally drive the node; the
16
+ * leaf's own IndexedDB custody (`openSession()`) is no stronger against
17
+ * same-origin script (see `leaf/src/identity.rs`). Do not use this on an
18
+ * origin that runs code you do not trust.
19
+ *
20
+ * When storage is unavailable (a private window with storage blocked, a
21
+ * sandboxed frame), the secrets are fresh and not kept: the page still
22
+ * connects, as a new player.
23
+ */
24
+ /** The `localStorage` key {@link rememberedIdentity} uses by default. */
25
+ export const DEFAULT_IDENTITY_KEY = 'net-mesh:identity';
26
+ function randomHex(bytes) {
27
+ const buffer = new Uint8Array(bytes);
28
+ globalThis.crypto.getRandomValues(buffer);
29
+ return Array.from(buffer, byte => byte.toString(16).padStart(2, '0')).join('');
30
+ }
31
+ const HEX_32 = /^[0-9a-f]{64}$/;
32
+ function parse(raw) {
33
+ if (raw === null)
34
+ return null;
35
+ try {
36
+ const value = JSON.parse(raw);
37
+ if (typeof value.entitySecretHex === 'string' && typeof value.noiseSecretHex === 'string'
38
+ && HEX_32.test(value.entitySecretHex) && HEX_32.test(value.noiseSecretHex)) {
39
+ return { entitySecretHex: value.entitySecretHex, noiseSecretHex: value.noiseSecretHex };
40
+ }
41
+ }
42
+ catch {
43
+ // Not ours, or damaged: replaced below.
44
+ }
45
+ return null;
46
+ }
47
+ /**
48
+ * This origin's player secrets, created on first use and kept in
49
+ * `localStorage` under `key`. Pass a different `key` for a second,
50
+ * separate player on the same origin (a local two-player test).
51
+ */
52
+ export function rememberedIdentity(key = DEFAULT_IDENTITY_KEY, storage) {
53
+ let store = storage;
54
+ try {
55
+ store ??= globalThis.localStorage;
56
+ }
57
+ catch {
58
+ // Accessing `localStorage` throws where storage is blocked.
59
+ store = undefined;
60
+ }
61
+ let existing = null;
62
+ try {
63
+ existing = parse(store?.getItem(key) ?? null);
64
+ }
65
+ catch {
66
+ store = undefined;
67
+ }
68
+ if (existing !== null)
69
+ return existing;
70
+ const fresh = { entitySecretHex: randomHex(32), noiseSecretHex: randomHex(32) };
71
+ try {
72
+ store?.setItem(key, JSON.stringify(fresh));
73
+ }
74
+ catch {
75
+ // Full or blocked: this visit's player is not kept.
76
+ }
77
+ return fresh;
78
+ }