@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,834 @@
1
+ /**
2
+ * The store's wire protocol: shapes, the parser ladder, canonical
3
+ * encodings (brief §1.4, §1.12, §2).
4
+ *
5
+ * Two entry points, and they share every rule: {@link decodeMessage}
6
+ * refuses anything the protocol does not define, and
7
+ * {@link encodeMessage} refuses to *produce* anything it would refuse
8
+ * to accept. One validator, both directions — a producer that can emit
9
+ * a frame the parser rejects is a bug that only shows up on the far
10
+ * side of a network.
11
+ *
12
+ * ## The ladder, and where it stops here
13
+ *
14
+ * §1.12 is ordered, cheap checks first, and none of them may run after
15
+ * a mutation. This module implements the rungs that are decidable from
16
+ * the frame alone:
17
+ *
18
+ * 1. **byte length** against the runtime's admissible size → `capacity`
19
+ * (a declared bound was reached), refused **before** parsing;
20
+ * 2. **JSON** under `json.ts` — depth, duplicate keys, finite numbers;
21
+ * 3. **envelope** — `v` known, `k` in the closed set, `q`/`h` present
22
+ * exactly where the kind requires them;
23
+ * 4. **direction** — a replica that receives `act`, or an owner that
24
+ * receives `delta`, refuses `invalid-data`;
25
+ * 6. **payload** — per-kind required/optional fields, unknown fields
26
+ * refused, and every value in its canonical spelling.
27
+ *
28
+ * Rung 4's *admission class* (`not-ready` for gameplay before
29
+ * readiness) and rung 5 (**binding**: handle active, bound to this
30
+ * authenticated peer, per-kind generation checks) are **not here**.
31
+ * They are decidable only against handle state, which is owner dispatch
32
+ * — a later slice. This module deliberately answers "is this a legal
33
+ * frame", never "may this caller do this now", and callers must not
34
+ * read a successful decode as authorization.
35
+ */
36
+ import { STORE_ERROR_CODES } from './errors.js';
37
+ import { parseStoreJson } from './json.js';
38
+ /** The only protocol version this build speaks. */
39
+ export const WIRE_VERSION = 1;
40
+ /** Exact hex lengths (brief §1.12: lowercase only, exact length). */
41
+ export const HANDLE_HEX_LENGTH = 32;
42
+ export const REQUEST_HEX_LENGTH = 16;
43
+ export const INCARNATION_HEX_LENGTH = 16;
44
+ /** Audience bounds (brief §2). */
45
+ export const MAX_AUDIENCE_LABELS = 32;
46
+ export const MAX_AUDIENCE_LABEL_BYTES = 128;
47
+ /**
48
+ * Interest keys per request, and bytes per key. Interest is a spatial
49
+ * filter, not a permission, so its bound is separate from — and larger
50
+ * than — the audience's; the frame budget is what really caps it.
51
+ */
52
+ export const MAX_INTEREST_KEYS = 256;
53
+ export const MAX_INTEREST_KEY_BYTES = 64;
54
+ /** Snapshot bounds (brief §2). */
55
+ export const MAX_SNAPSHOT_BYTES = 1024 * 1024;
56
+ export const MAX_SNAPSHOT_CHUNKS = 255;
57
+ /** Patch bounds (brief §2, applied structurally here, semantically in `patch.ts`). */
58
+ export const MAX_PATCH_OPS = 256;
59
+ export const MAX_PATH_SEGMENTS = 8;
60
+ export const MAX_PATH_SEGMENT_BYTES = 64;
61
+ /** `no.detail` is truncated at the owner to this (brief §2). */
62
+ export const MAX_DETAIL_BYTES = 256;
63
+ /** The largest value a canonical decimal may carry. */
64
+ export const MAX_DECIMAL = 18446744073709551615n;
65
+ const MAX_DECIMAL_DIGITS = 20;
66
+ /** Caller → owner kinds (brief §1.4). */
67
+ export const CALLER_KINDS = [
68
+ 'join',
69
+ 'resume',
70
+ 'resync',
71
+ 'act',
72
+ 'in',
73
+ 'aud',
74
+ 'alive',
75
+ 'leave',
76
+ 'int',
77
+ ];
78
+ /** Owner → caller kinds (brief §1.4). */
79
+ export const OWNER_KINDS = ['man', 'snap', 'delta', 'res', 'ok', 'no'];
80
+ /**
81
+ * The per-kind field table (brief §1.12), in **canonical key order**:
82
+ * `encodeMessage` emits keys in exactly this order, so one message has
83
+ * one spelling.
84
+ */
85
+ const FIELDS = {
86
+ // `store` names WHICH store of this definition the caller wants.
87
+ // Required, and not inferable: a node may host several stores of
88
+ // one definition, every host store on that node sees every frame,
89
+ // and a `join` names no handle — so without this field the store
90
+ // that answers is whichever listener happened to run first. `key`
91
+ // cannot serve: it is the caller's opaque policy token, and two
92
+ // callers of one store carry different ones.
93
+ // `int` — the caller's interest set: which keys of the definition's
94
+ // interest collections it wants delivered. Optional: absent means no
95
+ // filtering, which is every store before interest existed.
96
+ join: { required: ['v', 'k', 'q', 'def', 'ver', 'store', 'key', 'aud'], optional: ['int'] },
97
+ resume: { required: ['v', 'k', 'q', 'h', 'aud'], optional: ['int'] },
98
+ int: { required: ['v', 'k', 'q', 'h', 'int'], optional: [] },
99
+ resync: { required: ['v', 'k', 'q', 'h', 'g', 'have'], optional: [] },
100
+ aud: { required: ['v', 'k', 'q', 'h', 'aud'], optional: [] },
101
+ alive: { required: ['v', 'k', 'q', 'h'], optional: [] },
102
+ leave: { required: ['v', 'k', 'q', 'h'], optional: [] },
103
+ act: { required: ['v', 'k', 'q', 'h', 's', 'name', 'in'], optional: [] },
104
+ in: { required: ['v', 'k', 'h', 'name', 's', 'in'], optional: [] },
105
+ man: { required: ['v', 'k', 'h', 'inc', 'g', 'r', 'n', 'bytes'], optional: ['q'] },
106
+ snap: { required: ['v', 'k', 'h', 'g', 'r', 'i', 'n', 'd'], optional: [] },
107
+ delta: { required: ['v', 'k', 'h', 'g', 'base', 'r', 'ops'], optional: [] },
108
+ res: { required: ['v', 'k', 'q', 'h', 's', 'out'], optional: [] },
109
+ ok: { required: ['v', 'k', 'q', 'h'], optional: [] },
110
+ no: { required: ['v', 'k', 'code'], optional: ['q', 'h', 's', 'detail'] },
111
+ };
112
+ /** Canonical key order for encoding, per kind. */
113
+ const KEY_ORDER = {
114
+ join: ['v', 'k', 'q', 'def', 'ver', 'store', 'key', 'aud', 'int'],
115
+ resume: ['v', 'k', 'q', 'h', 'aud', 'int'],
116
+ int: ['v', 'k', 'q', 'h', 'int'],
117
+ resync: ['v', 'k', 'q', 'h', 'g', 'have'],
118
+ aud: ['v', 'k', 'q', 'h', 'aud'],
119
+ alive: ['v', 'k', 'q', 'h'],
120
+ leave: ['v', 'k', 'q', 'h'],
121
+ act: ['v', 'k', 'q', 'h', 's', 'name', 'in'],
122
+ in: ['v', 'k', 'h', 'name', 's', 'in'],
123
+ man: ['v', 'k', 'q', 'h', 'inc', 'g', 'r', 'n', 'bytes'],
124
+ snap: ['v', 'k', 'h', 'g', 'r', 'i', 'n', 'd'],
125
+ delta: ['v', 'k', 'h', 'g', 'base', 'r', 'ops'],
126
+ res: ['v', 'k', 'q', 'h', 's', 'out'],
127
+ ok: ['v', 'k', 'q', 'h'],
128
+ no: ['v', 'k', 'q', 'h', 'code', 's', 'detail'],
129
+ };
130
+ const CALLER_SET = new Set(CALLER_KINDS);
131
+ const OWNER_SET = new Set(OWNER_KINDS);
132
+ const CODE_SET = new Set(STORE_ERROR_CODES);
133
+ const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/;
134
+ function refuse(stage, reason, code = 'invalid-data') {
135
+ return { ok: false, code, stage, reason };
136
+ }
137
+ /**
138
+ * A field-reader's refusal, in a box application data cannot inhabit.
139
+ *
140
+ * The readers below return either a value or a refusal, and a
141
+ * structural test for that refusal — "an object whose `ok` is
142
+ * `false`" — is a test application data can satisfy: `ok` is a
143
+ * perfectly ordinary key. An `act` whose `in` was `{"ok":false}`
144
+ * therefore came back from `decodeMessage` *as the refusal*, and a
145
+ * payload carrying `code`, `stage` and `reason` supplied the decoder's
146
+ * own purported metadata. A class the payload cannot be an instance of
147
+ * is the distinction that actually holds; nothing about `ok` is
148
+ * reserved in application data any more.
149
+ */
150
+ class Refused {
151
+ refusal;
152
+ constructor(refusal) {
153
+ this.refusal = refusal;
154
+ }
155
+ }
156
+ /** Wrap a refusal for a field reader. */
157
+ function no(stage, reason, code = 'invalid-data') {
158
+ return new Refused(refuse(stage, reason, code));
159
+ }
160
+ /** Whether a field reader refused. Structural collision is impossible. */
161
+ function bad(value) {
162
+ return value instanceof Refused;
163
+ }
164
+ /** UTF-8 byte length without allocating a copy of the bytes. */
165
+ export function utf8Length(text) {
166
+ let bytes = 0;
167
+ for (let i = 0; i < text.length; i += 1) {
168
+ const c = text.codePointAt(i);
169
+ if (c > 0xffff)
170
+ i += 1;
171
+ bytes += c <= 0x7f ? 1 : c <= 0x7ff ? 2 : c <= 0xffff ? 3 : 4;
172
+ }
173
+ return bytes;
174
+ }
175
+ /**
176
+ * Decode one store frame.
177
+ *
178
+ * Never throws for bad input: a refusal is a value, because the
179
+ * dispatcher must count it and answer, and an exception path is how
180
+ * "count it" gets skipped.
181
+ */
182
+ export function decodeMessage(frame, options) {
183
+ // Rung 1 — byte length, before any parse.
184
+ const size = utf8Length(frame);
185
+ if (size > options.maxBytes) {
186
+ return refuse('bytes', 'frame-too-large', 'capacity');
187
+ }
188
+ // Rung 2 — JSON, under the store's rules.
189
+ const parsed = parseStoreJson(frame);
190
+ if (!parsed.ok)
191
+ return refuse('json', parsed.reason);
192
+ const doc = parsed.value;
193
+ if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {
194
+ return refuse('json', 'not-an-object');
195
+ }
196
+ const body = doc;
197
+ // Rung 3 — envelope. `v` and `k` are read before any other field, so
198
+ // an unknown version or kind is refused whole.
199
+ if (body['v'] !== WIRE_VERSION)
200
+ return refuse('envelope', 'unknown-version');
201
+ const kind = body['k'];
202
+ if (typeof kind !== 'string' || !(CALLER_SET.has(kind) || OWNER_SET.has(kind))) {
203
+ return refuse('envelope', 'unknown-kind');
204
+ }
205
+ const k = kind;
206
+ // Rung 4 — direction. An owner never receives its own output kinds,
207
+ // and a replica never receives a caller's.
208
+ const expected = options.as === 'owner' ? CALLER_SET : OWNER_SET;
209
+ if (!expected.has(k))
210
+ return refuse('direction', `wrong-direction:${k}`);
211
+ // Rung 6 — fields, then canonical spellings.
212
+ const table = FIELDS[k];
213
+ const allowed = new Set([...table.required, ...table.optional]);
214
+ for (const key of Object.keys(body)) {
215
+ // Unknown fields are refused, not ignored: forward compatibility is
216
+ // the `v` bump's job, and tolerance is how a typo becomes a
217
+ // silently-ignored security-relevant field.
218
+ if (!allowed.has(key))
219
+ return refuse('payload', 'unknown-field');
220
+ }
221
+ for (const key of table.required) {
222
+ if (!Object.prototype.hasOwnProperty.call(body, key))
223
+ return refuse('payload', 'missing-field');
224
+ }
225
+ // The direction rung above has already established that `k` belongs
226
+ // to this side, which is exactly what `MessageFor<T>` says.
227
+ return buildMessage(k, body);
228
+ }
229
+ function buildMessage(k, body) {
230
+ const hex = (key, length) => {
231
+ const v = body[key];
232
+ if (typeof v !== 'string' || v.length !== length || !isCanonicalHex(v)) {
233
+ return no('payload', `non-canonical-hex:${key}`);
234
+ }
235
+ return v;
236
+ };
237
+ const dec = (key) => {
238
+ const v = body[key];
239
+ if (typeof v !== 'string' || !isCanonicalDecimal(v)) {
240
+ return no('payload', `non-canonical-decimal:${key}`);
241
+ }
242
+ return v;
243
+ };
244
+ const int = (key, min, max) => {
245
+ const v = body[key];
246
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < min || v > max) {
247
+ return no('payload', `bad-integer:${key}`);
248
+ }
249
+ return v;
250
+ };
251
+ const text = (key) => {
252
+ const v = body[key];
253
+ if (typeof v !== 'string' || v.length === 0)
254
+ return no('payload', `bad-string:${key}`);
255
+ return v;
256
+ };
257
+ const obj = (key) => {
258
+ const v = body[key];
259
+ if (typeof v !== 'object' || v === null || Array.isArray(v)) {
260
+ return no('payload', `bad-object:${key}`);
261
+ }
262
+ return v;
263
+ };
264
+ const audience = () => {
265
+ const v = body['aud'];
266
+ if (!Array.isArray(v))
267
+ return no('payload', 'bad-audience');
268
+ if (v.length > MAX_AUDIENCE_LABELS)
269
+ return no('payload', 'audience-too-many', 'capacity');
270
+ const labels = [];
271
+ for (const label of v) {
272
+ if (typeof label !== 'string' || label.length === 0)
273
+ return no('payload', 'bad-audience-label');
274
+ if (utf8Length(label) > MAX_AUDIENCE_LABEL_BYTES) {
275
+ return no('payload', 'audience-label-too-long', 'capacity');
276
+ }
277
+ labels.push(label);
278
+ }
279
+ return labels;
280
+ };
281
+ const interest = (required) => {
282
+ if (!Object.prototype.hasOwnProperty.call(body, 'int')) {
283
+ return required ? no('payload', 'missing-field') : undefined;
284
+ }
285
+ const v = body['int'];
286
+ if (!Array.isArray(v))
287
+ return no('payload', 'bad-interest');
288
+ if (v.length > MAX_INTEREST_KEYS)
289
+ return no('payload', 'interest-too-many', 'capacity');
290
+ const keys = [];
291
+ for (const key of v) {
292
+ if (typeof key !== 'string' || key.length === 0)
293
+ return no('payload', 'bad-interest-key');
294
+ if (utf8Length(key) > MAX_INTEREST_KEY_BYTES)
295
+ return no('payload', 'interest-key-too-long', 'capacity');
296
+ keys.push(key);
297
+ }
298
+ return keys;
299
+ };
300
+ switch (k) {
301
+ case 'int': {
302
+ const q = hex('q', REQUEST_HEX_LENGTH);
303
+ if (bad(q))
304
+ return q.refusal;
305
+ const h = hex('h', HANDLE_HEX_LENGTH);
306
+ if (bad(h))
307
+ return h.refusal;
308
+ const keys = interest(true);
309
+ if (bad(keys))
310
+ return keys.refusal;
311
+ return { ok: true, message: { k, q, h, int: keys } };
312
+ }
313
+ case 'join': {
314
+ const q = hex('q', REQUEST_HEX_LENGTH);
315
+ if (bad(q))
316
+ return q.refusal;
317
+ const def = text('def');
318
+ if (bad(def))
319
+ return def.refusal;
320
+ const ver = int('ver', 0, Number.MAX_SAFE_INTEGER);
321
+ if (bad(ver))
322
+ return ver.refusal;
323
+ const store = text('store');
324
+ if (bad(store))
325
+ return store.refusal;
326
+ const key = text('key');
327
+ if (bad(key))
328
+ return key.refusal;
329
+ const aud = audience();
330
+ if (bad(aud))
331
+ return aud.refusal;
332
+ const keys = interest(false);
333
+ if (bad(keys))
334
+ return keys.refusal;
335
+ return {
336
+ ok: true,
337
+ message: keys === undefined ? { k, q, def, ver, store, key, aud } : { k, q, def, ver, store, key, aud, int: keys },
338
+ };
339
+ }
340
+ case 'resume':
341
+ case 'aud': {
342
+ const q = hex('q', REQUEST_HEX_LENGTH);
343
+ if (bad(q))
344
+ return q.refusal;
345
+ const h = hex('h', HANDLE_HEX_LENGTH);
346
+ if (bad(h))
347
+ return h.refusal;
348
+ const aud = audience();
349
+ if (bad(aud))
350
+ return aud.refusal;
351
+ if (k === 'resume') {
352
+ const keys = interest(false);
353
+ if (bad(keys))
354
+ return keys.refusal;
355
+ return { ok: true, message: keys === undefined ? { k, q, h, aud } : { k, q, h, aud, int: keys } };
356
+ }
357
+ return { ok: true, message: { k, q, h, aud } };
358
+ }
359
+ case 'resync': {
360
+ const q = hex('q', REQUEST_HEX_LENGTH);
361
+ if (bad(q))
362
+ return q.refusal;
363
+ const h = hex('h', HANDLE_HEX_LENGTH);
364
+ if (bad(h))
365
+ return h.refusal;
366
+ const g = dec('g');
367
+ if (bad(g))
368
+ return g.refusal;
369
+ const have = dec('have');
370
+ if (bad(have))
371
+ return have.refusal;
372
+ return { ok: true, message: { k, q, h, g, have } };
373
+ }
374
+ case 'alive':
375
+ case 'leave':
376
+ case 'ok': {
377
+ const q = hex('q', REQUEST_HEX_LENGTH);
378
+ if (bad(q))
379
+ return q.refusal;
380
+ const h = hex('h', HANDLE_HEX_LENGTH);
381
+ if (bad(h))
382
+ return h.refusal;
383
+ return { ok: true, message: { k, q, h } };
384
+ }
385
+ case 'act': {
386
+ const q = hex('q', REQUEST_HEX_LENGTH);
387
+ if (bad(q))
388
+ return q.refusal;
389
+ const h = hex('h', HANDLE_HEX_LENGTH);
390
+ if (bad(h))
391
+ return h.refusal;
392
+ const s = dec('s');
393
+ if (bad(s))
394
+ return s.refusal;
395
+ const name = text('name');
396
+ if (bad(name))
397
+ return name.refusal;
398
+ const input = obj('in');
399
+ if (bad(input))
400
+ return input.refusal;
401
+ return { ok: true, message: { k, q, h, s, name, in: input } };
402
+ }
403
+ case 'in': {
404
+ const h = hex('h', HANDLE_HEX_LENGTH);
405
+ if (bad(h))
406
+ return h.refusal;
407
+ const name = text('name');
408
+ if (bad(name))
409
+ return name.refusal;
410
+ const s = dec('s');
411
+ if (bad(s))
412
+ return s.refusal;
413
+ const input = obj('in');
414
+ if (bad(input))
415
+ return input.refusal;
416
+ return { ok: true, message: { k, h, name, s, in: input } };
417
+ }
418
+ case 'res': {
419
+ const q = hex('q', REQUEST_HEX_LENGTH);
420
+ if (bad(q))
421
+ return q.refusal;
422
+ const h = hex('h', HANDLE_HEX_LENGTH);
423
+ if (bad(h))
424
+ return h.refusal;
425
+ const s = dec('s');
426
+ if (bad(s))
427
+ return s.refusal;
428
+ const out = obj('out');
429
+ if (bad(out))
430
+ return out.refusal;
431
+ return { ok: true, message: { k, q, h, s, out } };
432
+ }
433
+ case 'man': {
434
+ const h = hex('h', HANDLE_HEX_LENGTH);
435
+ if (bad(h))
436
+ return h.refusal;
437
+ const inc = hex('inc', INCARNATION_HEX_LENGTH);
438
+ if (bad(inc))
439
+ return inc.refusal;
440
+ const g = dec('g');
441
+ if (bad(g))
442
+ return g.refusal;
443
+ const r = dec('r');
444
+ if (bad(r))
445
+ return r.refusal;
446
+ const n = int('n', 1, MAX_SNAPSHOT_CHUNKS);
447
+ if (bad(n))
448
+ return n.refusal;
449
+ const bytes = dec('bytes');
450
+ if (bad(bytes))
451
+ return bytes.refusal;
452
+ if (BigInt(bytes) > BigInt(MAX_SNAPSHOT_BYTES)) {
453
+ return refuse('payload', 'snapshot-too-large', 'capacity');
454
+ }
455
+ if (Object.prototype.hasOwnProperty.call(body, 'q')) {
456
+ const q = hex('q', REQUEST_HEX_LENGTH);
457
+ if (bad(q))
458
+ return q.refusal;
459
+ return { ok: true, message: { k, q, h, inc, g, r, n, bytes } };
460
+ }
461
+ return { ok: true, message: { k, h, inc, g, r, n, bytes } };
462
+ }
463
+ case 'snap': {
464
+ const h = hex('h', HANDLE_HEX_LENGTH);
465
+ if (bad(h))
466
+ return h.refusal;
467
+ const g = dec('g');
468
+ if (bad(g))
469
+ return g.refusal;
470
+ const r = dec('r');
471
+ if (bad(r))
472
+ return r.refusal;
473
+ const n = int('n', 1, MAX_SNAPSHOT_CHUNKS);
474
+ if (bad(n))
475
+ return n.refusal;
476
+ const i = int('i', 0, n - 1);
477
+ if (bad(i))
478
+ return i.refusal;
479
+ const d = body['d'];
480
+ if (typeof d !== 'string' || !isCanonicalBase64(d)) {
481
+ return refuse('payload', 'non-canonical-base64:d');
482
+ }
483
+ return { ok: true, message: { k, h, g, r, i, n, d } };
484
+ }
485
+ case 'delta': {
486
+ const h = hex('h', HANDLE_HEX_LENGTH);
487
+ if (bad(h))
488
+ return h.refusal;
489
+ const g = dec('g');
490
+ if (bad(g))
491
+ return g.refusal;
492
+ const base = dec('base');
493
+ if (bad(base))
494
+ return base.refusal;
495
+ const r = dec('r');
496
+ if (bad(r))
497
+ return r.refusal;
498
+ const ops = readOps(body['ops']);
499
+ if (bad(ops))
500
+ return ops.refusal;
501
+ return { ok: true, message: { k, h, g, base, r, ops } };
502
+ }
503
+ case 'no': {
504
+ const code = body['code'];
505
+ if (typeof code !== 'string' || !CODE_SET.has(code)) {
506
+ return refuse('payload', 'unknown-code');
507
+ }
508
+ const hasQ = Object.prototype.hasOwnProperty.call(body, 'q');
509
+ const hasH = Object.prototype.hasOwnProperty.call(body, 'h');
510
+ const hasS = Object.prototype.hasOwnProperty.call(body, 's');
511
+ // A `no` carrying neither names nothing (brief §1.12).
512
+ if (!hasQ && !hasH)
513
+ return refuse('payload', 'no-names-nothing');
514
+ // A `q`-less `no` is an **unsolicited notice about a handle**,
515
+ // and there are exactly TWO (brief §1.12): `closed`, the
516
+ // expiry notice, and `owner-lost`, the owner's goodbye. Both
517
+ // name the handle they are about and neither carries an action
518
+ // sequence. These are relationships inside one frame, so they
519
+ // belong here — unlike "is this `q` outstanding", which needs
520
+ // handle state.
521
+ //
522
+ // `owner-lost` was added because the two are NOT the same
523
+ // event and the replica must not treat them alike: `closed`
524
+ // means the lease lapsed and the replica rejoins, while
525
+ // `owner-lost` is terminal — the incarnation that held the
526
+ // document is gone. Spelling a host's `close()` as `closed`
527
+ // would have every replica rejoin a store that no longer
528
+ // exists, or silently attach to a SUCCESSOR's different
529
+ // document under the same name.
530
+ //
531
+ // Admitting a `q`-less `forbidden` offered a refusal that named
532
+ // no request to attribute it to, and a `q`-less `s` named an
533
+ // action whose reply this cannot be.
534
+ // "Names a handle" needs no separate check here: a `q`-less
535
+ // frame without `h` is already refused above as naming nothing.
536
+ if (!hasQ) {
537
+ if (code !== 'closed' && code !== 'owner-lost') {
538
+ return refuse('payload', 'unsolicited-no-must-be-closed-or-owner-lost');
539
+ }
540
+ if (hasS)
541
+ return refuse('payload', 'unsolicited-no-carries-no-sequence');
542
+ }
543
+ const out = {
544
+ k: 'no',
545
+ code: code,
546
+ };
547
+ if (hasQ) {
548
+ const q = hex('q', REQUEST_HEX_LENGTH);
549
+ if (bad(q))
550
+ return q.refusal;
551
+ out.q = q;
552
+ }
553
+ if (hasH) {
554
+ const h = hex('h', HANDLE_HEX_LENGTH);
555
+ if (bad(h))
556
+ return h.refusal;
557
+ out.h = h;
558
+ }
559
+ if (hasS) {
560
+ const s = dec('s');
561
+ if (bad(s))
562
+ return s.refusal;
563
+ out.s = s;
564
+ }
565
+ if (Object.prototype.hasOwnProperty.call(body, 'detail')) {
566
+ const detail = body['detail'];
567
+ if (typeof detail !== 'string')
568
+ return refuse('payload', 'bad-string:detail');
569
+ if (utf8Length(detail) > MAX_DETAIL_BYTES) {
570
+ return refuse('payload', 'detail-too-long', 'capacity');
571
+ }
572
+ out.detail = detail;
573
+ }
574
+ return { ok: true, message: out };
575
+ }
576
+ }
577
+ }
578
+ function readOps(raw) {
579
+ if (!Array.isArray(raw))
580
+ return no('payload', 'bad-ops');
581
+ if (raw.length > MAX_PATCH_OPS)
582
+ return no('payload', 'ops-too-many', 'capacity');
583
+ const ops = [];
584
+ for (const entry of raw) {
585
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
586
+ return no('payload', 'bad-op');
587
+ }
588
+ const op = entry;
589
+ const o = op['o'];
590
+ if (o !== 'r' && o !== 'x')
591
+ return no('payload', 'bad-op-kind');
592
+ const allowed = o === 'r' ? ['o', 'p', 'val'] : ['o', 'p'];
593
+ for (const key of Object.keys(op)) {
594
+ if (!allowed.includes(key))
595
+ return no('payload', 'unknown-field');
596
+ }
597
+ if (o === 'r' && !Object.prototype.hasOwnProperty.call(op, 'val')) {
598
+ return no('payload', 'missing-field');
599
+ }
600
+ const p = op['p'];
601
+ if (!Array.isArray(p))
602
+ return no('payload', 'bad-path');
603
+ if (p.length > MAX_PATH_SEGMENTS)
604
+ return no('payload', 'path-too-deep', 'capacity');
605
+ const path = [];
606
+ for (const segment of p) {
607
+ if (typeof segment !== 'string' || segment.length === 0)
608
+ return no('payload', 'bad-segment');
609
+ if (utf8Length(segment) > MAX_PATH_SEGMENT_BYTES) {
610
+ return no('payload', 'segment-too-long', 'capacity');
611
+ }
612
+ path.push(segment);
613
+ }
614
+ ops.push(o === 'r' ? { o, p: path, val: op['val'] } : { o, p: path });
615
+ }
616
+ return ops;
617
+ }
618
+ /** Lowercase hex, exact length checked by the caller. */
619
+ export function isCanonicalHex(value) {
620
+ for (let i = 0; i < value.length; i += 1) {
621
+ const c = value.charCodeAt(i);
622
+ const digit = c >= 48 && c <= 57;
623
+ const lower = c >= 97 && c <= 102;
624
+ if (!digit && !lower)
625
+ return false;
626
+ }
627
+ return value.length > 0;
628
+ }
629
+ /**
630
+ * ASCII digits, no sign, no leading zero unless the value is exactly
631
+ * `"0"`, ≤ 20 digits, ≤ 2^64 − 1 (brief §1.12).
632
+ */
633
+ export function isCanonicalDecimal(value) {
634
+ // The digit bound is a **work** bound, not an acceptance rule: every
635
+ // 21-digit value already exceeds 2^64 − 1, so the range check below
636
+ // decides the same answer. It is here so a megabyte of digits is
637
+ // refused without building a `BigInt` from it first.
638
+ if (value.length === 0 || value.length > MAX_DECIMAL_DIGITS)
639
+ return false;
640
+ for (let i = 0; i < value.length; i += 1) {
641
+ const c = value.charCodeAt(i);
642
+ if (c < 48 || c > 57)
643
+ return false;
644
+ }
645
+ if (value.length > 1 && value.charCodeAt(0) === 48)
646
+ return false;
647
+ return BigInt(value) <= MAX_DECIMAL;
648
+ }
649
+ /**
650
+ * Standard alphabet, correctly padded, no whitespace or line breaks.
651
+ *
652
+ * Also rejects a *decodable but non-canonical* tail: base64 pads to a
653
+ * multiple of four, and the bits before the padding must be zero, so
654
+ * `"QQ=="` is canonical for `A` while `"QR=="` decodes to the same byte
655
+ * and is not.
656
+ */
657
+ export function isCanonicalBase64(value) {
658
+ if (value.length === 0)
659
+ return true;
660
+ if (value.length % 4 !== 0)
661
+ return false;
662
+ if (!BASE64.test(value))
663
+ return false;
664
+ const padding = value.endsWith('==') ? 2 : value.endsWith('=') ? 1 : 0;
665
+ if (padding === 0)
666
+ return true;
667
+ const last = value.charCodeAt(value.length - padding - 1);
668
+ const index = base64Index(last);
669
+ if (index < 0)
670
+ return false;
671
+ // 2 padding chars ⇒ 4 spare bits; 1 ⇒ 2 spare bits. They must be 0.
672
+ const spare = padding === 2 ? 0b1111 : 0b11;
673
+ return (index & spare) === 0;
674
+ }
675
+ function base64Index(code) {
676
+ if (code >= 65 && code <= 90)
677
+ return code - 65;
678
+ if (code >= 97 && code <= 122)
679
+ return code - 97 + 26;
680
+ if (code >= 48 && code <= 57)
681
+ return code - 48 + 52;
682
+ if (code === 43)
683
+ return 62;
684
+ if (code === 47)
685
+ return 63;
686
+ return -1;
687
+ }
688
+ /** The numeric value of a canonical decimal field. */
689
+ export function decimalValue(value) {
690
+ if (!isCanonicalDecimal(value)) {
691
+ throw new RangeError(`not a canonical decimal: ${JSON.stringify(value)}`);
692
+ }
693
+ return BigInt(value);
694
+ }
695
+ /**
696
+ * Whether a value survives JSON serialization as itself.
697
+ *
698
+ * `JSON.stringify` is **lossy and silent**: `NaN`, `Infinity` and
699
+ * `-Infinity` become `null`, and `undefined`, functions and symbols
700
+ * are dropped from objects and become `null` in arrays. Validating the
701
+ * serialized text therefore cannot see what was asked for — the
702
+ * evidence is already gone — so an action input of `{n: NaN}` encoded
703
+ * happily as `{"n":null}` and sent a value the caller never wrote.
704
+ *
705
+ * It also TRANSFORMS some values and THROWS on others, all before any
706
+ * scan of the serialized text could see anything: a `toJSON` method
707
+ * replaces its object wholesale (a `Date` ships as a string), a
708
+ * `Map`/`Set` ships as `{}` with its entries gone, an own symbol key
709
+ * is never serialized at all, and a cycle or a `BigInt` raises a bare
710
+ * `TypeError` out of the serializer itself.
711
+ *
712
+ * These are ordinary values at the type level; no cast or exotic
713
+ * object is needed to produce them. So the source values are checked
714
+ * **before** serialization, and an inadmissible one is a refusal
715
+ * rather than a substitution or an escape. `null` itself stays
716
+ * admissible: an intentional null is data.
717
+ */
718
+ export function inadmissibleValue(value, path) {
719
+ return admissibleValue(value, path, new Set());
720
+ }
721
+ /** {@link inadmissibleValue}, with the ancestor chain for cycle detection. */
722
+ function admissibleValue(value, path, ancestors) {
723
+ if (value === null)
724
+ return null;
725
+ switch (typeof value) {
726
+ case 'boolean':
727
+ case 'string':
728
+ return null;
729
+ case 'number':
730
+ // The one the brief names (§1.12): every number is finite.
731
+ return Number.isFinite(value) ? null : `non-finite-number:${path}`;
732
+ case 'object': {
733
+ const object = value;
734
+ // A cycle serializes as a bare `TypeError` — refused here, before
735
+ // the serializer can throw it. Sibling SHARING is fine (the value
736
+ // is duplicated, not transformed), so only ancestors count.
737
+ if (ancestors.has(object))
738
+ return `circular:${path}`;
739
+ // `toJSON` replaces the value wholesale during serialization —
740
+ // `Date` ships as a string and an application object can ship
741
+ // anything at all — so what would leave is not what was checked.
742
+ if ('toJSON' in object)
743
+ return `to-json-transform:${path}`;
744
+ // `Map`/`Set` and their weak forms serialize as `{}`: the
745
+ // entries vanish.
746
+ if (object instanceof Map ||
747
+ object instanceof Set ||
748
+ object instanceof WeakMap ||
749
+ object instanceof WeakSet) {
750
+ return `map-or-set:${path}`;
751
+ }
752
+ // An own symbol key is never serialized, whatever its
753
+ // enumerability: the data vanishes without a trace.
754
+ if (Object.getOwnPropertySymbols(object).length > 0)
755
+ return `symbol-key:${path}`;
756
+ ancestors.add(object);
757
+ try {
758
+ if (Array.isArray(value)) {
759
+ for (let i = 0; i < value.length; i += 1) {
760
+ const bad = admissibleValue(value[i], `${path}[${i}]`, ancestors);
761
+ if (bad !== null)
762
+ return bad;
763
+ }
764
+ return null;
765
+ }
766
+ for (const [key, nested] of Object.entries(value)) {
767
+ const bad = admissibleValue(nested, `${path}.${key}`, ancestors);
768
+ if (bad !== null)
769
+ return bad;
770
+ }
771
+ return null;
772
+ }
773
+ finally {
774
+ ancestors.delete(object);
775
+ }
776
+ }
777
+ default:
778
+ // `undefined`, function, symbol, bigint: each is either dropped,
779
+ // replaced or thrown on by `JSON.stringify`, and none of them is
780
+ // JSON.
781
+ return `not-json:${path}`;
782
+ }
783
+ }
784
+ /** Every value-bearing field of a message, for the check above. */
785
+ function payloadFields(message) {
786
+ switch (message.k) {
787
+ case 'act':
788
+ case 'in':
789
+ return [['in', message.in]];
790
+ case 'res':
791
+ return [['out', message.out]];
792
+ case 'delta':
793
+ return message.ops.flatMap((op, i) => op.o === 'r' ? [[`ops[${i}].val`, op.val]] : []);
794
+ default:
795
+ return [];
796
+ }
797
+ }
798
+ /**
799
+ * Encode a message in its canonical spelling.
800
+ *
801
+ * Validates the **source values** first — because serialization
802
+ * destroys the evidence of a non-finite number — then validates the
803
+ * serialized frame through {@link decodeMessage}'s own rules, so a
804
+ * producer cannot emit a frame the parser would refuse. Throws rather
805
+ * than returning a refusal: this is a local programming error, not
806
+ * untrusted input.
807
+ */
808
+ export function encodeMessage(message) {
809
+ for (const [path, value] of payloadFields(message)) {
810
+ const bad = inadmissibleValue(value, path);
811
+ if (bad !== null) {
812
+ throw new RangeError(`refusing to encode an invalid ${message.k}: ${bad}`);
813
+ }
814
+ }
815
+ const body = { v: WIRE_VERSION, k: message.k };
816
+ const source = message;
817
+ for (const key of KEY_ORDER[message.k]) {
818
+ if (key === 'v' || key === 'k')
819
+ continue;
820
+ if (!Object.prototype.hasOwnProperty.call(source, key))
821
+ continue;
822
+ const value = source[key];
823
+ if (value === undefined)
824
+ continue;
825
+ body[key] = value;
826
+ }
827
+ const text = JSON.stringify(body);
828
+ const side = CALLER_SET.has(message.k) ? 'owner' : 'replica';
829
+ const check = decodeMessage(text, { maxBytes: Number.MAX_SAFE_INTEGER, as: side });
830
+ if (!check.ok) {
831
+ throw new RangeError(`refusing to encode an invalid ${message.k}: ${check.reason}`);
832
+ }
833
+ return text;
834
+ }