@irtio/protocol 0.2.0 → 0.4.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/dist/index.d.ts +66 -6
- package/dist/index.js +36 -4
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -257,19 +257,31 @@ declare function deltaPayload(codecBytes: Uint8Array): Uint8Array;
|
|
|
257
257
|
declare function writePayload(codecBytes: Uint8Array): Uint8Array;
|
|
258
258
|
/**
|
|
259
259
|
* The CORRECT frame payload: the schema-codec delta bytes with the judged client write tick
|
|
260
|
-
* (u32) appended (week 8, D19)
|
|
261
|
-
*
|
|
262
|
-
*
|
|
260
|
+
* (u32) appended (week 8, D19), and after it the server tick that write was *applied* at (u32,
|
|
261
|
+
* bug 1). Additive in both steps: the codec's delta decoder never reads past the delta body, so
|
|
262
|
+
* a decoder that does not know about a suffix ignores it; a reader that does (`decodeDeltaFrom`)
|
|
263
|
+
* is left positioned exactly at the first one.
|
|
264
|
+
*
|
|
265
|
+
* The two ticks are on different clocks and that is the point. `clientTick` is the client's own
|
|
266
|
+
* stamp, opaque to the server and echoed verbatim; `appliedTick` is the room's tick counter when
|
|
267
|
+
* the write landed. A predicting client stamps on its *predicted* clock, which leads authority,
|
|
268
|
+
* so without the second number it has no way to learn where on the server's clock its intent
|
|
269
|
+
* actually took effect, and its replay puts it a whole prediction lead too late.
|
|
263
270
|
*/
|
|
264
|
-
declare function correctPayload(codecBytes: Uint8Array, clientTick: number): Uint8Array;
|
|
271
|
+
declare function correctPayload(codecBytes: Uint8Array, clientTick: number, appliedTick?: number): Uint8Array;
|
|
265
272
|
/**
|
|
266
273
|
* Reads the `clientTick` suffix a reader is positioned at after `decodeDeltaFrom`, or
|
|
267
274
|
* `undefined` for a pre-week-8 CORRECT payload with no suffix.
|
|
268
275
|
*/
|
|
269
276
|
declare function readCorrectClientTick(r: ByteReader): number | undefined;
|
|
277
|
+
/**
|
|
278
|
+
* Reads the `appliedTick` suffix that follows `clientTick`, or `undefined` from a server that
|
|
279
|
+
* does not send one. Call only after `readCorrectClientTick`, which leaves the reader on it.
|
|
280
|
+
*/
|
|
281
|
+
declare function readCorrectAppliedTick(r: ByteReader): number | undefined;
|
|
270
282
|
declare function encodeDeltaFrame(codecBytes: Uint8Array): Uint8Array;
|
|
271
283
|
declare function encodeWriteFrame(codecBytes: Uint8Array): Uint8Array;
|
|
272
|
-
declare function encodeCorrectFrame(codecBytes: Uint8Array, clientTick: number): Uint8Array;
|
|
284
|
+
declare function encodeCorrectFrame(codecBytes: Uint8Array, clientTick: number, appliedTick?: number): Uint8Array;
|
|
273
285
|
|
|
274
286
|
interface Call {
|
|
275
287
|
readonly reqId: number;
|
|
@@ -372,4 +384,52 @@ declare const relaySchema: Schema<{
|
|
|
372
384
|
declare const RELAY_HASH8: Uint8Array;
|
|
373
385
|
declare function isRelayHash8(hash8: Uint8Array): boolean;
|
|
374
386
|
|
|
375
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Browser-origin policy, shared by every component that has to answer "may this page connect?"
|
|
389
|
+
*
|
|
390
|
+
* Three components ask that question about the same project and must answer it identically: the
|
|
391
|
+
* router (from the control plane's fresh list), the guest supervisor (from the list frozen into
|
|
392
|
+
* its boot env), and `irtio dev` (from `IRT_ORIGINS`). It lived only in the supervisor until the
|
|
393
|
+
* router was given the list it had been fetching and discarding, at which point one copy of the
|
|
394
|
+
* rules became the only honest arrangement.
|
|
395
|
+
*
|
|
396
|
+
* The rules, in the order they are applied:
|
|
397
|
+
*
|
|
398
|
+
* 1. No `Origin` header at all — bots, native clients, tests, `curl` — is NOT a browser and is
|
|
399
|
+
* allowed unless the caller opts out. Origin locking is a same-origin-policy backstop; it
|
|
400
|
+
* protects a browser user from a page they did not open, and there is no such user here.
|
|
401
|
+
* 2. `*` in the list allows every origin.
|
|
402
|
+
* 3. Localhost is ALWAYS allowed, on any port and any scheme. A project's list starts blank,
|
|
403
|
+
* and a developer running `vite dev` against hosted rooms must not have to register a port
|
|
404
|
+
* number before anything works. Nothing is protected by refusing it: an attacker who can
|
|
405
|
+
* serve the victim a page from their own machine has already won.
|
|
406
|
+
* 4. Otherwise the origin must appear in the list verbatim.
|
|
407
|
+
*/
|
|
408
|
+
/**
|
|
409
|
+
* Is `origin` a page served from the connecting developer's own machine?
|
|
410
|
+
*
|
|
411
|
+
* Matches on the HOSTNAME only, so every port passes — a dev server's port is an implementation
|
|
412
|
+
* detail that changes per tool and per run. `*.localhost` (which resolves to loopback in every
|
|
413
|
+
* current browser) counts too. A `file://` page sends the literal `null`, which does not parse
|
|
414
|
+
* as a URL and is therefore not local: it is indistinguishable from a sandboxed iframe on a
|
|
415
|
+
* hostile site, and lumping the two together would silently widen rule 3 to the open internet.
|
|
416
|
+
*/
|
|
417
|
+
declare function isLocalhostOrigin(origin: string): boolean;
|
|
418
|
+
/**
|
|
419
|
+
* `origins: ['*']` allows every browser origin; an empty list allows only localhost. A missing
|
|
420
|
+
* `Origin` (bots, native clients, tests) is allowed unless `allowNoOrigin` is explicitly `false`.
|
|
421
|
+
*/
|
|
422
|
+
declare function originAllowed(origins: readonly string[], allowNoOrigin: boolean, origin: string | undefined): boolean;
|
|
423
|
+
/**
|
|
424
|
+
* Splits an `IRT_ORIGINS`-shaped value into a list. The distinction that matters is UNSET vs
|
|
425
|
+
* EMPTY, and it is the caller's to make: unset means "no policy configured, allow everything"
|
|
426
|
+
* (`['*']`, what a hand-run supervisor and every pre-blank-default deployment expect), while an
|
|
427
|
+
* empty string means "a policy IS configured and it lists nothing" — localhost only.
|
|
428
|
+
*
|
|
429
|
+
* Getting that backwards fails OPEN, which is why it is one function with one test rather than a
|
|
430
|
+
* `?? '*'` at each call site. `deploy/host/rootfs/tenant-init` has the same trap in shell:
|
|
431
|
+
* `${IRT_ORIGINS:-*}` treats empty as unset, `${IRT_ORIGINS-*}` does not.
|
|
432
|
+
*/
|
|
433
|
+
declare function parseOriginList(raw: string | undefined): readonly string[];
|
|
434
|
+
|
|
435
|
+
export { type Call, type ClientCallable, type Credential, ERROR_CATALOGUE, type ErrorCatalogueEntry, ErrorCode, type ErrorCodeDef, type ErrorCodeName, type ErrorPayload, type Frame, FrameType, type Hello, type Msg, type MsgTarget, PRESENCE_COLLECTION, PROTOCOL_VERSION, type Ping, type Pong, type PresenceRecord, RELAY_HASH8, type Reply, type Welcome, builtinRpcs, correctPayload, decodeCall, decodeErrorPayload, decodeFrame, decodeHello, decodeMsg, decodePing, decodePong, decodeReply, decodeWelcome, deltaPayload, encodeCall, encodeCorrectFrame, encodeDeltaFrame, encodeErrorPayload, encodeFrame, encodeHello, encodeMsg, encodePing, encodePong, encodeReply, encodeWelcome, encodeWriteFrame, errorByCode, formatError, isFrameType, isLocalhostOrigin, isRelayHash8, originAllowed, parseOriginList, presenceEntity, readCorrectAppliedTick, readCorrectClientTick, relaySchema, requestOwnership, rpcByIdOf, rpcIdOf, rpcTable, withBuiltins, writePayload };
|
package/dist/index.js
CHANGED
|
@@ -222,23 +222,28 @@ function deltaPayload(codecBytes) {
|
|
|
222
222
|
function writePayload(codecBytes) {
|
|
223
223
|
return codecBytes;
|
|
224
224
|
}
|
|
225
|
-
function correctPayload(codecBytes, clientTick) {
|
|
226
|
-
const
|
|
225
|
+
function correctPayload(codecBytes, clientTick, appliedTick) {
|
|
226
|
+
const extra = appliedTick === void 0 ? 4 : 8;
|
|
227
|
+
const w = new ByteWriter(codecBytes.length + extra);
|
|
227
228
|
w.bytes(codecBytes);
|
|
228
229
|
w.u32(clientTick);
|
|
230
|
+
if (appliedTick !== void 0) w.u32(appliedTick);
|
|
229
231
|
return w.finish();
|
|
230
232
|
}
|
|
231
233
|
function readCorrectClientTick(r) {
|
|
232
234
|
return r.remaining >= 4 ? r.u32() : void 0;
|
|
233
235
|
}
|
|
236
|
+
function readCorrectAppliedTick(r) {
|
|
237
|
+
return r.remaining >= 4 ? r.u32() : void 0;
|
|
238
|
+
}
|
|
234
239
|
function encodeDeltaFrame(codecBytes) {
|
|
235
240
|
return encodeFrame(FrameType.DELTA, deltaPayload(codecBytes));
|
|
236
241
|
}
|
|
237
242
|
function encodeWriteFrame(codecBytes) {
|
|
238
243
|
return encodeFrame(FrameType.WRITE, writePayload(codecBytes));
|
|
239
244
|
}
|
|
240
|
-
function encodeCorrectFrame(codecBytes, clientTick) {
|
|
241
|
-
return encodeFrame(FrameType.CORRECT, correctPayload(codecBytes, clientTick));
|
|
245
|
+
function encodeCorrectFrame(codecBytes, clientTick, appliedTick) {
|
|
246
|
+
return encodeFrame(FrameType.CORRECT, correctPayload(codecBytes, clientTick, appliedTick));
|
|
242
247
|
}
|
|
243
248
|
|
|
244
249
|
// src/rpc.ts
|
|
@@ -400,6 +405,29 @@ function isRelayHash8(hash8) {
|
|
|
400
405
|
for (const b of hash8) if (b !== 0) return false;
|
|
401
406
|
return true;
|
|
402
407
|
}
|
|
408
|
+
|
|
409
|
+
// src/origin.ts
|
|
410
|
+
var LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
|
|
411
|
+
function isLocalhostOrigin(origin) {
|
|
412
|
+
let url;
|
|
413
|
+
try {
|
|
414
|
+
url = new URL(origin);
|
|
415
|
+
} catch {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
const host = url.hostname.toLowerCase();
|
|
419
|
+
return LOCAL_HOSTNAMES.has(host) || host.endsWith(".localhost");
|
|
420
|
+
}
|
|
421
|
+
function originAllowed(origins, allowNoOrigin, origin) {
|
|
422
|
+
if (origin === void 0 || origin === "") return allowNoOrigin;
|
|
423
|
+
if (origins.includes("*")) return true;
|
|
424
|
+
if (isLocalhostOrigin(origin)) return true;
|
|
425
|
+
return origins.includes(origin);
|
|
426
|
+
}
|
|
427
|
+
function parseOriginList(raw) {
|
|
428
|
+
if (raw === void 0) return ["*"];
|
|
429
|
+
return raw.split(",").map((o) => o.trim()).filter((o) => o.length > 0);
|
|
430
|
+
}
|
|
403
431
|
export {
|
|
404
432
|
ERROR_CATALOGUE,
|
|
405
433
|
ErrorCode,
|
|
@@ -434,8 +462,12 @@ export {
|
|
|
434
462
|
errorByCode,
|
|
435
463
|
formatError,
|
|
436
464
|
isFrameType,
|
|
465
|
+
isLocalhostOrigin,
|
|
437
466
|
isRelayHash8,
|
|
467
|
+
originAllowed,
|
|
468
|
+
parseOriginList,
|
|
438
469
|
presenceEntity,
|
|
470
|
+
readCorrectAppliedTick,
|
|
439
471
|
readCorrectClientTick,
|
|
440
472
|
relaySchema,
|
|
441
473
|
requestOwnership,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "irtio wire protocol: frames, framing, session payloads, error codes, built-in presence",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dist"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@irtio/schema": "0.
|
|
23
|
+
"@irtio/schema": "0.4.0"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "tsup",
|