@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.
- package/CHANGELOG.md +517 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-MIT +21 -0
- package/README.md +666 -0
- package/dist/async-queue.d.ts +26 -0
- package/dist/async-queue.js +62 -0
- package/dist/credential.d.ts +70 -0
- package/dist/credential.js +95 -0
- package/dist/errors.d.ts +400 -0
- package/dist/errors.js +726 -0
- package/dist/events.d.ts +232 -0
- package/dist/events.js +298 -0
- package/dist/identity.d.ts +40 -0
- package/dist/identity.js +78 -0
- package/dist/index.bundle.js +2 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +46 -0
- package/dist/leader/events.d.ts +111 -0
- package/dist/leader/events.js +116 -0
- package/dist/leader/index.d.ts +14 -0
- package/dist/leader/index.js +11 -0
- package/dist/leader/session.d.ts +539 -0
- package/dist/leader/session.js +770 -0
- package/dist/leader/wasm.d.ts +123 -0
- package/dist/leader/wasm.js +39 -0
- package/dist/lobby.d.ts +183 -0
- package/dist/lobby.js +504 -0
- package/dist/local.d.ts +80 -0
- package/dist/local.js +160 -0
- package/dist/net_leaf.d.ts +1494 -0
- package/dist/net_leaf.js +3753 -0
- package/dist/net_leaf_bg.wasm +0 -0
- package/dist/net_leaf_bg.wasm.d.ts +137 -0
- package/dist/netcode/client.d.ts +102 -0
- package/dist/netcode/client.js +296 -0
- package/dist/netcode/clock.d.ts +45 -0
- package/dist/netcode/clock.js +54 -0
- package/dist/netcode/host.d.ts +118 -0
- package/dist/netcode/host.js +276 -0
- package/dist/netcode/index.d.ts +19 -0
- package/dist/netcode/index.js +14 -0
- package/dist/netcode/interpolate.d.ts +50 -0
- package/dist/netcode/interpolate.js +100 -0
- package/dist/netcode/wire.d.ts +125 -0
- package/dist/netcode/wire.js +117 -0
- package/dist/node.d.ts +908 -0
- package/dist/node.js +1085 -0
- package/dist/org.d.ts +412 -0
- package/dist/org.js +567 -0
- package/dist/peer-driver.d.ts +117 -0
- package/dist/peer-driver.js +224 -0
- package/dist/store/assembly.d.ts +176 -0
- package/dist/store/assembly.js +325 -0
- package/dist/store/chunker.d.ts +83 -0
- package/dist/store/chunker.js +190 -0
- package/dist/store/core.d.ts +136 -0
- package/dist/store/core.js +477 -0
- package/dist/store/definition.d.ts +23 -0
- package/dist/store/definition.js +83 -0
- package/dist/store/errors.d.ts +77 -0
- package/dist/store/errors.js +94 -0
- package/dist/store/host.d.ts +247 -0
- package/dist/store/host.js +675 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.js +40 -0
- package/dist/store/interest.d.ts +40 -0
- package/dist/store/interest.js +77 -0
- package/dist/store/inventory.d.ts +81 -0
- package/dist/store/inventory.js +140 -0
- package/dist/store/join.d.ts +118 -0
- package/dist/store/join.js +724 -0
- package/dist/store/json.d.ts +50 -0
- package/dist/store/json.js +283 -0
- package/dist/store/ledger.d.ts +187 -0
- package/dist/store/ledger.js +292 -0
- package/dist/store/owner.d.ts +616 -0
- package/dist/store/owner.js +1711 -0
- package/dist/store/patch.d.ts +70 -0
- package/dist/store/patch.js +166 -0
- package/dist/store/player.d.ts +65 -0
- package/dist/store/player.js +261 -0
- package/dist/store/replica.d.ts +187 -0
- package/dist/store/replica.js +740 -0
- package/dist/store/state.d.ts +42 -0
- package/dist/store/state.js +165 -0
- package/dist/store/types.d.ts +200 -0
- package/dist/store/types.js +25 -0
- package/dist/store/visibility.d.ts +118 -0
- package/dist/store/visibility.js +282 -0
- package/dist/store/wire.d.ts +309 -0
- package/dist/store/wire.js +834 -0
- package/dist/stream.d.ts +174 -0
- package/dist/stream.js +238 -0
- package/dist/three/index.d.ts +109 -0
- package/dist/three/index.js +130 -0
- package/dist/udp-probe.d.ts +159 -0
- package/dist/udp-probe.js +245 -0
- package/dist/wasm.d.ts +642 -0
- package/dist/wasm.js +98 -0
- package/dist/world/border.d.ts +84 -0
- package/dist/world/border.js +99 -0
- package/dist/world/handoff.d.ts +187 -0
- package/dist/world/handoff.js +222 -0
- package/dist/world/index.d.ts +13 -0
- package/dist/world/index.js +9 -0
- package/dist/world/link.d.ts +179 -0
- package/dist/world/link.js +476 -0
- package/dist/world/regions.d.ts +154 -0
- package/dist/world/regions.js +314 -0
- 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
|
+
}
|