onelobby 0.3.1 → 0.5.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/README.md +2 -2
- package/blueprint.schema.json +237 -0
- package/dist/blueprint.d.ts +166 -0
- package/dist/blueprint.js +281 -0
- package/dist/blueprint.js.map +1 -0
- package/dist/chunking.d.ts +4 -0
- package/dist/chunking.js +23 -2
- package/dist/chunking.js.map +1 -1
- package/dist/deflate.d.ts +2 -1
- package/dist/deflate.js +10 -4
- package/dist/deflate.js.map +1 -1
- package/dist/direct/hub-host.d.ts +16 -0
- package/dist/direct/hub-host.js +45 -1
- package/dist/direct/hub-host.js.map +1 -1
- package/dist/direct/hub.d.ts +3 -0
- package/dist/direct/hub.js +32 -6
- package/dist/direct/hub.js.map +1 -1
- package/dist/direct/index.d.ts +8 -2
- package/dist/direct/index.js +26 -4
- package/dist/direct/index.js.map +1 -1
- package/dist/friendly.d.ts +36 -0
- package/dist/friendly.js +44 -0
- package/dist/friendly.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lockstep.d.ts +5 -0
- package/dist/lockstep.js +17 -2
- package/dist/lockstep.js.map +1 -1
- package/dist/onelobby.bundle.mjs +5 -5
- package/dist/onelobby.bundle.mjs.map +4 -4
- package/dist/onelobby.d.ts +34 -2
- package/dist/onelobby.global.js +445 -30
- package/dist/onelobby.global.js.map +3 -3
- package/dist/onelobby.global.min.js +5 -5
- package/dist/onelobby.global.min.js.map +4 -4
- package/dist/onelobby.js +136 -25
- package/dist/onelobby.js.map +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blueprint v1: the `onelobby.json` settings file (docs/design/blueprint.md s.4, spec/blueprint.schema.json).
|
|
3
|
+
* `parseBlueprint` validates by hand (the package has no dependencies), fills the defaults and returns a frozen
|
|
4
|
+
* object. Errors carry the dotted `path` and the `rule` id the shared vectors (spec/vectors/blueprint.json) expect.
|
|
5
|
+
*/
|
|
6
|
+
import { OneLobbyError } from "./signaling.js";
|
|
7
|
+
/** Identity function for TypeScript completion in a `.ts` config (like Vite's `defineConfig`). */
|
|
8
|
+
export function defineBlueprint(x) {
|
|
9
|
+
return x;
|
|
10
|
+
}
|
|
11
|
+
/** `bad_config`: the file is not a valid blueprint. `path` is dotted ("" = root), `rule` a vector rule id. */
|
|
12
|
+
export class BlueprintError extends OneLobbyError {
|
|
13
|
+
path;
|
|
14
|
+
rule;
|
|
15
|
+
constructor(path, rule, detail) {
|
|
16
|
+
super("bad_config", `onelobby.json: ${path ? path + ": " : ""}${detail}`);
|
|
17
|
+
this.path = path;
|
|
18
|
+
this.rule = rule;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const obj = (props, req) => ({ k: "obj", props, req });
|
|
22
|
+
const bool = { k: "bool" };
|
|
23
|
+
const str = (o = {}) => ({ k: "str", ...o });
|
|
24
|
+
const int = (min, max, zeroOr) => ({ k: "int", min, max, zeroOr });
|
|
25
|
+
const en = (...vals) => ({ k: "enum", vals });
|
|
26
|
+
const ROOT = obj({
|
|
27
|
+
version: { k: "const", val: 1 },
|
|
28
|
+
game: obj({ id: str({ pattern: /^[A-Za-z0-9._-]{1,64}$/ }), netVersion: str({ min: 1, max: 32 }), title: str({ max: 64 }) }, ["id"]),
|
|
29
|
+
engine: en("web", "node-browser", "godot-csharp", "unity", "dotnet"),
|
|
30
|
+
hosting: obj({
|
|
31
|
+
direct: bool,
|
|
32
|
+
server: bool,
|
|
33
|
+
lobbyServer: str(),
|
|
34
|
+
directPort: int(1024, 65535),
|
|
35
|
+
openPort: bool,
|
|
36
|
+
relayOnly: bool,
|
|
37
|
+
allowedOrigins: { k: "strarr" },
|
|
38
|
+
}),
|
|
39
|
+
lobby: obj({
|
|
40
|
+
maxPlayers: int(2, 64),
|
|
41
|
+
listed: bool,
|
|
42
|
+
approveJoins: bool,
|
|
43
|
+
password: bool,
|
|
44
|
+
lateJoin: bool,
|
|
45
|
+
rejoin: bool,
|
|
46
|
+
joinLink: bool,
|
|
47
|
+
meta: { k: "meta" },
|
|
48
|
+
}),
|
|
49
|
+
sync: obj({ mode: en("raw", "lockstep", "replication"), tickRate: int(1, 120), hashEveryTicks: int(1, 100000) }),
|
|
50
|
+
players: obj({ identity: en("name", "anonymous", "platform"), maxNameLength: int(1, 24) }),
|
|
51
|
+
chat: obj({ text: bool, voice: bool, voiceMode: en("pushToTalk", "voiceActivity") }),
|
|
52
|
+
session: obj({
|
|
53
|
+
hostLeaves: en("end", "migrate"),
|
|
54
|
+
pause: en("off", "host", "anyone"),
|
|
55
|
+
readyTimeoutMs: int(0, 600000),
|
|
56
|
+
peerTimeoutMs: int(6000, 600000, true),
|
|
57
|
+
rateLimit: { k: "rate" },
|
|
58
|
+
runInBackground: bool,
|
|
59
|
+
}),
|
|
60
|
+
platforms: obj({ steam: { k: "steam" }, epic: { k: "epic" }, discord: bool }),
|
|
61
|
+
ui: obj({ kind: en("drop-in", "generated", "none"), entry: str(), theme: en("light", "dark", "auto") }),
|
|
62
|
+
}, ["game"]);
|
|
63
|
+
/** Allowed (and ignored) anywhere. */
|
|
64
|
+
const IGNORED = new Set(["$schema", "$comment"]);
|
|
65
|
+
class Fail {
|
|
66
|
+
path;
|
|
67
|
+
rule;
|
|
68
|
+
detail;
|
|
69
|
+
constructor(path, rule, detail) {
|
|
70
|
+
this.path = path;
|
|
71
|
+
this.rule = rule;
|
|
72
|
+
this.detail = detail;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const join = (p, k) => (p ? `${p}.${k}` : String(k));
|
|
76
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
77
|
+
function check(n, v, path) {
|
|
78
|
+
switch (n.k) {
|
|
79
|
+
case "obj": {
|
|
80
|
+
if (!isObj(v))
|
|
81
|
+
throw new Fail(path, "type", "must be an object");
|
|
82
|
+
for (const r of n.req ?? [])
|
|
83
|
+
if (!(r in v))
|
|
84
|
+
throw new Fail(join(path, r), "required", "is required");
|
|
85
|
+
for (const k of Object.keys(v))
|
|
86
|
+
if (!IGNORED.has(k) && !(k in n.props))
|
|
87
|
+
throw new Fail(join(path, k), "unknown_key", "unknown key");
|
|
88
|
+
for (const [k, sub] of Object.entries(n.props))
|
|
89
|
+
if (k in v)
|
|
90
|
+
check(sub, v[k], join(path, k));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
case "str": {
|
|
94
|
+
if (typeof v !== "string")
|
|
95
|
+
throw new Fail(path, "type", "must be a string");
|
|
96
|
+
if ((n.min !== undefined && v.length < n.min) || (n.max !== undefined && v.length > n.max))
|
|
97
|
+
throw new Fail(path, "length", `must be ${n.min ?? 0} to ${n.max ?? "any"} characters`);
|
|
98
|
+
if (n.pattern && !n.pattern.test(v))
|
|
99
|
+
throw new Fail(path, "pattern", `must match ${n.pattern.source}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
case "int": {
|
|
103
|
+
if (typeof v !== "number" || !Number.isInteger(v))
|
|
104
|
+
throw new Fail(path, "type", "must be an integer");
|
|
105
|
+
if (n.zeroOr && v === 0)
|
|
106
|
+
return;
|
|
107
|
+
if ((n.min !== undefined && v < n.min) || (n.max !== undefined && v > n.max))
|
|
108
|
+
throw new Fail(path, "range", `must be ${n.zeroOr ? "0 or " : ""}${n.min ?? "any"} to ${n.max ?? "any"}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
case "bool":
|
|
112
|
+
if (typeof v !== "boolean")
|
|
113
|
+
throw new Fail(path, "type", "must be true or false");
|
|
114
|
+
return;
|
|
115
|
+
case "enum":
|
|
116
|
+
if (typeof v !== "string" || !n.vals.includes(v))
|
|
117
|
+
throw new Fail(path, "enum", `must be one of ${n.vals.join(", ")}`);
|
|
118
|
+
return;
|
|
119
|
+
case "const":
|
|
120
|
+
if (v !== n.val)
|
|
121
|
+
throw new Fail(path, "const", `must be ${n.val}`);
|
|
122
|
+
return;
|
|
123
|
+
case "strarr": {
|
|
124
|
+
if (!Array.isArray(v))
|
|
125
|
+
throw new Fail(path, "type", "must be an array of strings");
|
|
126
|
+
v.forEach((x, i) => {
|
|
127
|
+
if (typeof x !== "string")
|
|
128
|
+
throw new Fail(join(path, i), "type", "must be a string");
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
case "meta": {
|
|
133
|
+
if (!isObj(v))
|
|
134
|
+
throw new Fail(path, "type", "must be an object of strings");
|
|
135
|
+
const keys = Object.keys(v);
|
|
136
|
+
if (keys.length > 16)
|
|
137
|
+
throw new Fail(path, "length", "at most 16 keys");
|
|
138
|
+
for (const k of keys) {
|
|
139
|
+
if (k.length > 64)
|
|
140
|
+
throw new Fail(join(path, k), "length", "key is longer than 64 characters");
|
|
141
|
+
const x = v[k];
|
|
142
|
+
if (typeof x !== "string")
|
|
143
|
+
throw new Fail(join(path, k), "type", "must be a string");
|
|
144
|
+
if (x.length > 64)
|
|
145
|
+
throw new Fail(join(path, k), "length", "value is longer than 64 characters");
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
case "rate":
|
|
150
|
+
check(obj({ messages: int(1), perMs: int(1) }, ["messages", "perMs"]), v, path);
|
|
151
|
+
return;
|
|
152
|
+
case "steam":
|
|
153
|
+
if (typeof v === "boolean")
|
|
154
|
+
return;
|
|
155
|
+
if (!isObj(v))
|
|
156
|
+
throw new Fail(path, "type", "must be true, false or { appId }");
|
|
157
|
+
check(obj({ appId: int(1) }), v, path);
|
|
158
|
+
return;
|
|
159
|
+
case "epic": {
|
|
160
|
+
if (typeof v === "boolean")
|
|
161
|
+
return;
|
|
162
|
+
if (!isObj(v))
|
|
163
|
+
throw new Fail(path, "type", "must be true, false or { productId }");
|
|
164
|
+
for (const k of Object.keys(v))
|
|
165
|
+
if (!IGNORED.has(k) && typeof v[k] !== "string")
|
|
166
|
+
throw new Fail(join(path, k), "type", "must be a string");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// ---- defaults and cross rules -----------------------------------------------------------------------------------
|
|
172
|
+
const sect = (x) => (isObj(x) ? x : {});
|
|
173
|
+
function strip(o) {
|
|
174
|
+
if (Array.isArray(o))
|
|
175
|
+
return o.map(strip);
|
|
176
|
+
if (!isObj(o))
|
|
177
|
+
return o;
|
|
178
|
+
const out = {};
|
|
179
|
+
for (const [k, v] of Object.entries(o))
|
|
180
|
+
if (k !== "$comment" && k !== "$schema")
|
|
181
|
+
out[k] = strip(v);
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
function deepFreeze(o) {
|
|
185
|
+
if (typeof o === "object" && o !== null && !Object.isFrozen(o)) {
|
|
186
|
+
Object.freeze(o);
|
|
187
|
+
for (const v of Object.values(o))
|
|
188
|
+
deepFreeze(v);
|
|
189
|
+
}
|
|
190
|
+
return o;
|
|
191
|
+
}
|
|
192
|
+
function fill(input) {
|
|
193
|
+
const game = sect(input.game);
|
|
194
|
+
const engine = input.engine;
|
|
195
|
+
const hosting = sect(input.hosting);
|
|
196
|
+
const lobby = sect(input.lobby);
|
|
197
|
+
const sync = sect(input.sync);
|
|
198
|
+
const players = sect(input.players);
|
|
199
|
+
const chat = sect(input.chat);
|
|
200
|
+
const session = sect(input.session);
|
|
201
|
+
const platforms = sect(input.platforms);
|
|
202
|
+
const ui = sect(input.ui);
|
|
203
|
+
const tickRate = sync.tickRate ?? 20;
|
|
204
|
+
const web = engine === "web";
|
|
205
|
+
const out = {
|
|
206
|
+
version: 1,
|
|
207
|
+
game: { id: game.id, netVersion: game.netVersion ?? "1", ...(game.title !== undefined ? { title: game.title } : {}) },
|
|
208
|
+
...(engine !== undefined ? { engine } : {}),
|
|
209
|
+
hosting: {
|
|
210
|
+
direct: hosting.direct ?? !web,
|
|
211
|
+
server: hosting.server ?? web,
|
|
212
|
+
lobbyServer: hosting.lobbyServer ?? "",
|
|
213
|
+
directPort: hosting.directPort ?? 47810,
|
|
214
|
+
openPort: hosting.openPort ?? true,
|
|
215
|
+
relayOnly: hosting.relayOnly ?? false,
|
|
216
|
+
allowedOrigins: hosting.allowedOrigins ?? [],
|
|
217
|
+
},
|
|
218
|
+
lobby: {
|
|
219
|
+
maxPlayers: lobby.maxPlayers ?? 4,
|
|
220
|
+
listed: lobby.listed ?? false,
|
|
221
|
+
approveJoins: lobby.approveJoins ?? false,
|
|
222
|
+
password: lobby.password ?? false,
|
|
223
|
+
lateJoin: lobby.lateJoin ?? true,
|
|
224
|
+
rejoin: lobby.rejoin ?? true,
|
|
225
|
+
joinLink: lobby.joinLink ?? (web || engine === "node-browser"),
|
|
226
|
+
meta: lobby.meta ?? {},
|
|
227
|
+
},
|
|
228
|
+
sync: { mode: sync.mode ?? "raw", tickRate, hashEveryTicks: sync.hashEveryTicks ?? tickRate },
|
|
229
|
+
players: { identity: players.identity ?? "name", maxNameLength: players.maxNameLength ?? 24 },
|
|
230
|
+
chat: { text: chat.text ?? true, voice: chat.voice ?? false, voiceMode: chat.voiceMode ?? "pushToTalk" },
|
|
231
|
+
session: {
|
|
232
|
+
hostLeaves: session.hostLeaves ?? "end",
|
|
233
|
+
pause: session.pause ?? "off",
|
|
234
|
+
readyTimeoutMs: session.readyTimeoutMs ?? 15000,
|
|
235
|
+
peerTimeoutMs: session.peerTimeoutMs ?? 0,
|
|
236
|
+
...(session.rateLimit !== undefined ? { rateLimit: session.rateLimit } : {}),
|
|
237
|
+
runInBackground: session.runInBackground ?? true,
|
|
238
|
+
},
|
|
239
|
+
platforms: { steam: platforms.steam ?? false, epic: platforms.epic ?? false, discord: platforms.discord ?? false },
|
|
240
|
+
ui: {
|
|
241
|
+
kind: ui.kind ?? (web ? "drop-in" : "generated"),
|
|
242
|
+
...(ui.entry !== undefined ? { entry: ui.entry } : {}),
|
|
243
|
+
theme: ui.theme ?? "auto",
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
function crossRules(b) {
|
|
249
|
+
if (!b.hosting.direct && !b.hosting.server)
|
|
250
|
+
throw new Fail("hosting", "hosting_none", "at least one of direct, server must be true");
|
|
251
|
+
if (b.lobby.listed && b.lobby.approveJoins)
|
|
252
|
+
throw new Fail("lobby", "listed_and_approve", "listed and approveJoins cannot both be true");
|
|
253
|
+
if (b.engine === "web" && b.hosting.direct)
|
|
254
|
+
throw new Fail("hosting.direct", "direct_needs_listener", "engine web cannot host direct (a browser cannot listen on a port)");
|
|
255
|
+
const csharp = b.engine === "godot-csharp" || b.engine === "unity" || b.engine === "dotnet";
|
|
256
|
+
if (b.sync.mode === "replication" && !csharp)
|
|
257
|
+
throw new Fail("sync.mode", "replication_needs_csharp", "replication needs engine godot-csharp, unity or dotnet");
|
|
258
|
+
if (b.sync.mode === "replication" && b.session.hostLeaves === "migrate")
|
|
259
|
+
throw new Fail("session.hostLeaves", "migrate_with_replication", "replication has no host migration; use end");
|
|
260
|
+
if (b.session.hostLeaves === "migrate" && !b.hosting.server)
|
|
261
|
+
throw new Fail("session.hostLeaves", "migrate_needs_server", "migrate needs hosting.server (direct lobbies always end)");
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Validate `json` (an already-parsed onelobby.json), fill every default and return the frozen result.
|
|
265
|
+
* Throws a `BlueprintError` (`OneLobbyError` code `bad_config`, with `path` and `rule`) on the first problem.
|
|
266
|
+
*/
|
|
267
|
+
export function parseBlueprint(json) {
|
|
268
|
+
let b;
|
|
269
|
+
try {
|
|
270
|
+
check(ROOT, json, "");
|
|
271
|
+
b = fill(strip(json));
|
|
272
|
+
crossRules(b);
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
if (e instanceof Fail)
|
|
276
|
+
throw new BlueprintError(e.path, e.rule, e.detail);
|
|
277
|
+
throw e;
|
|
278
|
+
}
|
|
279
|
+
return deepFreeze(b);
|
|
280
|
+
}
|
|
281
|
+
//# sourceMappingURL=blueprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blueprint.js","sourceRoot":"","sources":["../src/blueprint.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Blueprint v1: the `onelobby.json` settings file (docs/design/blueprint.md s.4, spec/blueprint.schema.json).\n * `parseBlueprint` validates by hand (the package has no dependencies), fills the defaults and returns a frozen\n * object. Errors carry the dotted `path` and the `rule` id the shared vectors (spec/vectors/blueprint.json) expect.\n */\nimport { OneLobbyError } from \"./signaling.js\";\n\nexport type BlueprintEngine = \"web\" | \"node-browser\" | \"godot-csharp\" | \"unity\" | \"dotnet\";\n\nexport interface Blueprint {\n readonly version: 1;\n readonly game: { readonly id: string; readonly netVersion: string; readonly title?: string };\n readonly engine?: BlueprintEngine;\n readonly hosting: {\n readonly direct: boolean;\n readonly server: boolean;\n readonly lobbyServer: string;\n readonly directPort: number;\n readonly openPort: boolean;\n readonly relayOnly: boolean;\n readonly allowedOrigins: readonly string[];\n };\n readonly lobby: {\n readonly maxPlayers: number;\n readonly listed: boolean;\n readonly approveJoins: boolean;\n readonly password: boolean;\n readonly lateJoin: boolean;\n readonly rejoin: boolean;\n readonly joinLink: boolean;\n readonly meta: Readonly<Record<string, string>>;\n };\n readonly sync: {\n readonly mode: \"raw\" | \"lockstep\" | \"replication\";\n readonly tickRate: number;\n readonly hashEveryTicks: number;\n };\n readonly players: { readonly identity: \"name\" | \"anonymous\" | \"platform\"; readonly maxNameLength: number };\n readonly chat: { readonly text: boolean; readonly voice: boolean; readonly voiceMode: \"pushToTalk\" | \"voiceActivity\" };\n readonly session: {\n readonly hostLeaves: \"end\" | \"migrate\";\n readonly pause: \"off\" | \"host\" | \"anyone\";\n readonly readyTimeoutMs: number;\n readonly peerTimeoutMs: number;\n readonly rateLimit?: { readonly messages: number; readonly perMs: number };\n readonly runInBackground: boolean;\n };\n readonly platforms: {\n readonly steam: boolean | { readonly appId?: number };\n readonly epic: boolean | { readonly [k: string]: string };\n readonly discord: boolean;\n };\n readonly ui: { readonly kind: \"drop-in\" | \"generated\" | \"none\"; readonly entry?: string; readonly theme: \"light\" | \"dark\" | \"auto\" };\n}\n\n/** What you may write: only `game.id` is required. */\nexport interface BlueprintInput {\n $schema?: string;\n $comment?: string;\n version?: 1;\n game: { $comment?: string; id: string; netVersion?: string; title?: string };\n engine?: BlueprintEngine;\n hosting?: {\n $comment?: string;\n direct?: boolean;\n server?: boolean;\n lobbyServer?: string;\n directPort?: number;\n openPort?: boolean;\n relayOnly?: boolean;\n allowedOrigins?: string[];\n };\n lobby?: {\n $comment?: string;\n maxPlayers?: number;\n listed?: boolean;\n approveJoins?: boolean;\n password?: boolean;\n lateJoin?: boolean;\n rejoin?: boolean;\n joinLink?: boolean;\n meta?: Record<string, string>;\n };\n sync?: { $comment?: string; mode?: \"raw\" | \"lockstep\" | \"replication\"; tickRate?: number; hashEveryTicks?: number };\n players?: { $comment?: string; identity?: \"name\" | \"anonymous\" | \"platform\"; maxNameLength?: number };\n chat?: { $comment?: string; text?: boolean; voice?: boolean; voiceMode?: \"pushToTalk\" | \"voiceActivity\" };\n session?: {\n $comment?: string;\n hostLeaves?: \"end\" | \"migrate\";\n pause?: \"off\" | \"host\" | \"anyone\";\n readyTimeoutMs?: number;\n peerTimeoutMs?: number;\n rateLimit?: { messages: number; perMs: number };\n runInBackground?: boolean;\n };\n platforms?: {\n $comment?: string;\n steam?: boolean | { appId?: number };\n epic?: boolean | { [k: string]: string };\n discord?: boolean;\n };\n ui?: { $comment?: string; kind?: \"drop-in\" | \"generated\" | \"none\"; entry?: string; theme?: \"light\" | \"dark\" | \"auto\" };\n}\n\n/** Identity function for TypeScript completion in a `.ts` config (like Vite's `defineConfig`). */\nexport function defineBlueprint<T extends BlueprintInput>(x: T): T {\n return x;\n}\n\n/** `bad_config`: the file is not a valid blueprint. `path` is dotted (\"\" = root), `rule` a vector rule id. */\nexport class BlueprintError extends OneLobbyError {\n constructor(public path: string, public rule: string, detail: string) {\n super(\"bad_config\", `onelobby.json: ${path ? path + \": \" : \"\"}${detail}`);\n }\n}\n\n// ---- schema as data (key order = spec/blueprint.schema.json) ----------------------------------------------------\n\ntype Node =\n | { k: \"obj\"; req?: string[]; props: Record<string, Node> }\n | { k: \"str\"; pattern?: RegExp; min?: number; max?: number }\n | { k: \"int\"; min?: number; max?: number; zeroOr?: boolean }\n | { k: \"bool\" }\n | { k: \"enum\"; vals: string[] }\n | { k: \"const\"; val: number }\n | { k: \"strarr\" }\n | { k: \"meta\" }\n | { k: \"rate\" }\n | { k: \"steam\" }\n | { k: \"epic\" };\n\nconst obj = (props: Record<string, Node>, req?: string[]): Node => ({ k: \"obj\", props, req });\nconst bool: Node = { k: \"bool\" };\nconst str = (o: { pattern?: RegExp; min?: number; max?: number } = {}): Node => ({ k: \"str\", ...o });\nconst int = (min?: number, max?: number, zeroOr?: boolean): Node => ({ k: \"int\", min, max, zeroOr });\nconst en = (...vals: string[]): Node => ({ k: \"enum\", vals });\n\nconst ROOT: Node = obj(\n {\n version: { k: \"const\", val: 1 },\n game: obj(\n { id: str({ pattern: /^[A-Za-z0-9._-]{1,64}$/ }), netVersion: str({ min: 1, max: 32 }), title: str({ max: 64 }) },\n [\"id\"],\n ),\n engine: en(\"web\", \"node-browser\", \"godot-csharp\", \"unity\", \"dotnet\"),\n hosting: obj({\n direct: bool,\n server: bool,\n lobbyServer: str(),\n directPort: int(1024, 65535),\n openPort: bool,\n relayOnly: bool,\n allowedOrigins: { k: \"strarr\" },\n }),\n lobby: obj({\n maxPlayers: int(2, 64),\n listed: bool,\n approveJoins: bool,\n password: bool,\n lateJoin: bool,\n rejoin: bool,\n joinLink: bool,\n meta: { k: \"meta\" },\n }),\n sync: obj({ mode: en(\"raw\", \"lockstep\", \"replication\"), tickRate: int(1, 120), hashEveryTicks: int(1, 100000) }),\n players: obj({ identity: en(\"name\", \"anonymous\", \"platform\"), maxNameLength: int(1, 24) }),\n chat: obj({ text: bool, voice: bool, voiceMode: en(\"pushToTalk\", \"voiceActivity\") }),\n session: obj({\n hostLeaves: en(\"end\", \"migrate\"),\n pause: en(\"off\", \"host\", \"anyone\"),\n readyTimeoutMs: int(0, 600000),\n peerTimeoutMs: int(6000, 600000, true),\n rateLimit: { k: \"rate\" },\n runInBackground: bool,\n }),\n platforms: obj({ steam: { k: \"steam\" }, epic: { k: \"epic\" }, discord: bool }),\n ui: obj({ kind: en(\"drop-in\", \"generated\", \"none\"), entry: str(), theme: en(\"light\", \"dark\", \"auto\") }),\n },\n [\"game\"],\n);\n\n/** Allowed (and ignored) anywhere. */\nconst IGNORED = new Set([\"$schema\", \"$comment\"]);\n\nclass Fail {\n constructor(public path: string, public rule: string, public detail: string) {}\n}\nconst join = (p: string, k: string | number) => (p ? `${p}.${k}` : String(k));\nconst isObj = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n\nfunction check(n: Node, v: unknown, path: string): void {\n switch (n.k) {\n case \"obj\": {\n if (!isObj(v)) throw new Fail(path, \"type\", \"must be an object\");\n for (const r of n.req ?? []) if (!(r in v)) throw new Fail(join(path, r), \"required\", \"is required\");\n for (const k of Object.keys(v)) if (!IGNORED.has(k) && !(k in n.props)) throw new Fail(join(path, k), \"unknown_key\", \"unknown key\");\n for (const [k, sub] of Object.entries(n.props)) if (k in v) check(sub, v[k], join(path, k));\n return;\n }\n case \"str\": {\n if (typeof v !== \"string\") throw new Fail(path, \"type\", \"must be a string\");\n if ((n.min !== undefined && v.length < n.min) || (n.max !== undefined && v.length > n.max))\n throw new Fail(path, \"length\", `must be ${n.min ?? 0} to ${n.max ?? \"any\"} characters`);\n if (n.pattern && !n.pattern.test(v)) throw new Fail(path, \"pattern\", `must match ${n.pattern.source}`);\n return;\n }\n case \"int\": {\n if (typeof v !== \"number\" || !Number.isInteger(v)) throw new Fail(path, \"type\", \"must be an integer\");\n if (n.zeroOr && v === 0) return;\n if ((n.min !== undefined && v < n.min) || (n.max !== undefined && v > n.max))\n throw new Fail(path, \"range\", `must be ${n.zeroOr ? \"0 or \" : \"\"}${n.min ?? \"any\"} to ${n.max ?? \"any\"}`);\n return;\n }\n case \"bool\":\n if (typeof v !== \"boolean\") throw new Fail(path, \"type\", \"must be true or false\");\n return;\n case \"enum\":\n if (typeof v !== \"string\" || !n.vals.includes(v)) throw new Fail(path, \"enum\", `must be one of ${n.vals.join(\", \")}`);\n return;\n case \"const\":\n if (v !== n.val) throw new Fail(path, \"const\", `must be ${n.val}`);\n return;\n case \"strarr\": {\n if (!Array.isArray(v)) throw new Fail(path, \"type\", \"must be an array of strings\");\n v.forEach((x, i) => {\n if (typeof x !== \"string\") throw new Fail(join(path, i), \"type\", \"must be a string\");\n });\n return;\n }\n case \"meta\": {\n if (!isObj(v)) throw new Fail(path, \"type\", \"must be an object of strings\");\n const keys = Object.keys(v);\n if (keys.length > 16) throw new Fail(path, \"length\", \"at most 16 keys\");\n for (const k of keys) {\n if (k.length > 64) throw new Fail(join(path, k), \"length\", \"key is longer than 64 characters\");\n const x = v[k];\n if (typeof x !== \"string\") throw new Fail(join(path, k), \"type\", \"must be a string\");\n if (x.length > 64) throw new Fail(join(path, k), \"length\", \"value is longer than 64 characters\");\n }\n return;\n }\n case \"rate\":\n check(obj({ messages: int(1), perMs: int(1) }, [\"messages\", \"perMs\"]), v, path);\n return;\n case \"steam\":\n if (typeof v === \"boolean\") return;\n if (!isObj(v)) throw new Fail(path, \"type\", \"must be true, false or { appId }\");\n check(obj({ appId: int(1) }), v, path);\n return;\n case \"epic\": {\n if (typeof v === \"boolean\") return;\n if (!isObj(v)) throw new Fail(path, \"type\", \"must be true, false or { productId }\");\n for (const k of Object.keys(v)) if (!IGNORED.has(k) && typeof v[k] !== \"string\") throw new Fail(join(path, k), \"type\", \"must be a string\");\n return;\n }\n }\n}\n\n// ---- defaults and cross rules -----------------------------------------------------------------------------------\n\nconst sect = (x: unknown): Record<string, unknown> => (isObj(x) ? x : {});\nfunction strip(o: unknown): unknown {\n if (Array.isArray(o)) return o.map(strip);\n if (!isObj(o)) return o;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(o)) if (k !== \"$comment\" && k !== \"$schema\") out[k] = strip(v);\n return out;\n}\nfunction deepFreeze<T>(o: T): T {\n if (typeof o === \"object\" && o !== null && !Object.isFrozen(o)) {\n Object.freeze(o);\n for (const v of Object.values(o)) deepFreeze(v);\n }\n return o;\n}\n\nfunction fill(input: Record<string, unknown>): Blueprint {\n const game = sect(input.game);\n const engine = input.engine as BlueprintEngine | undefined;\n const hosting = sect(input.hosting);\n const lobby = sect(input.lobby);\n const sync = sect(input.sync);\n const players = sect(input.players);\n const chat = sect(input.chat);\n const session = sect(input.session);\n const platforms = sect(input.platforms);\n const ui = sect(input.ui);\n const tickRate = (sync.tickRate as number | undefined) ?? 20;\n const web = engine === \"web\";\n const out = {\n version: 1,\n game: { id: game.id, netVersion: game.netVersion ?? \"1\", ...(game.title !== undefined ? { title: game.title } : {}) },\n ...(engine !== undefined ? { engine } : {}),\n hosting: {\n direct: hosting.direct ?? !web,\n server: hosting.server ?? web,\n lobbyServer: hosting.lobbyServer ?? \"\",\n directPort: hosting.directPort ?? 47810,\n openPort: hosting.openPort ?? true,\n relayOnly: hosting.relayOnly ?? false,\n allowedOrigins: hosting.allowedOrigins ?? [],\n },\n lobby: {\n maxPlayers: lobby.maxPlayers ?? 4,\n listed: lobby.listed ?? false,\n approveJoins: lobby.approveJoins ?? false,\n password: lobby.password ?? false,\n lateJoin: lobby.lateJoin ?? true,\n rejoin: lobby.rejoin ?? true,\n joinLink: lobby.joinLink ?? (web || engine === \"node-browser\"),\n meta: lobby.meta ?? {},\n },\n sync: { mode: sync.mode ?? \"raw\", tickRate, hashEveryTicks: sync.hashEveryTicks ?? tickRate },\n players: { identity: players.identity ?? \"name\", maxNameLength: players.maxNameLength ?? 24 },\n chat: { text: chat.text ?? true, voice: chat.voice ?? false, voiceMode: chat.voiceMode ?? \"pushToTalk\" },\n session: {\n hostLeaves: session.hostLeaves ?? \"end\",\n pause: session.pause ?? \"off\",\n readyTimeoutMs: session.readyTimeoutMs ?? 15000,\n peerTimeoutMs: session.peerTimeoutMs ?? 0,\n ...(session.rateLimit !== undefined ? { rateLimit: session.rateLimit } : {}),\n runInBackground: session.runInBackground ?? true,\n },\n platforms: { steam: platforms.steam ?? false, epic: platforms.epic ?? false, discord: platforms.discord ?? false },\n ui: {\n kind: ui.kind ?? (web ? \"drop-in\" : \"generated\"),\n ...(ui.entry !== undefined ? { entry: ui.entry } : {}),\n theme: ui.theme ?? \"auto\",\n },\n };\n return out as unknown as Blueprint;\n}\n\nfunction crossRules(b: Blueprint): void {\n if (!b.hosting.direct && !b.hosting.server) throw new Fail(\"hosting\", \"hosting_none\", \"at least one of direct, server must be true\");\n if (b.lobby.listed && b.lobby.approveJoins) throw new Fail(\"lobby\", \"listed_and_approve\", \"listed and approveJoins cannot both be true\");\n if (b.engine === \"web\" && b.hosting.direct) throw new Fail(\"hosting.direct\", \"direct_needs_listener\", \"engine web cannot host direct (a browser cannot listen on a port)\");\n const csharp = b.engine === \"godot-csharp\" || b.engine === \"unity\" || b.engine === \"dotnet\";\n if (b.sync.mode === \"replication\" && !csharp) throw new Fail(\"sync.mode\", \"replication_needs_csharp\", \"replication needs engine godot-csharp, unity or dotnet\");\n if (b.sync.mode === \"replication\" && b.session.hostLeaves === \"migrate\") throw new Fail(\"session.hostLeaves\", \"migrate_with_replication\", \"replication has no host migration; use end\");\n if (b.session.hostLeaves === \"migrate\" && !b.hosting.server) throw new Fail(\"session.hostLeaves\", \"migrate_needs_server\", \"migrate needs hosting.server (direct lobbies always end)\");\n}\n\n/**\n * Validate `json` (an already-parsed onelobby.json), fill every default and return the frozen result.\n * Throws a `BlueprintError` (`OneLobbyError` code `bad_config`, with `path` and `rule`) on the first problem.\n */\nexport function parseBlueprint(json: unknown): Blueprint {\n let b: Blueprint;\n try {\n check(ROOT, json, \"\");\n b = fill(strip(json) as Record<string, unknown>);\n crossRules(b);\n } catch (e) {\n if (e instanceof Fail) throw new BlueprintError(e.path, e.rule, e.detail);\n throw e;\n }\n return deepFreeze(b);\n}\n"]}
|
package/dist/chunking.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { type Message } from "./codec.js";
|
|
|
2
2
|
/** Chunk body capacity: 16 KiB frame - 3 header - 8 chunk header, rounded down for headroom. */
|
|
3
3
|
export declare const CHUNK_PAYLOAD = 16000;
|
|
4
4
|
export declare function splitChunks(id: number, bytes: Uint8Array, size?: number): Message[];
|
|
5
|
+
/** Most incomplete chunk sets one reassembler (one peer) holds at once; a new id past this evicts the oldest. */
|
|
6
|
+
export declare const MAX_PARTIAL_SETS = 8;
|
|
5
7
|
/** Reassembles chunked payloads by id. Duplicates are ignored; out-of-order chunks are fine. */
|
|
6
8
|
export declare class Reassembler {
|
|
7
9
|
private parts;
|
|
@@ -13,5 +15,7 @@ export declare class Reassembler {
|
|
|
13
15
|
bytes: Uint8Array;
|
|
14
16
|
}): Uint8Array | null;
|
|
15
17
|
get pending(): number;
|
|
18
|
+
/** Bytes held across incomplete sets. */
|
|
19
|
+
get pendingBytes(): number;
|
|
16
20
|
clear(): void;
|
|
17
21
|
}
|
package/dist/chunking.js
CHANGED
|
@@ -17,15 +17,20 @@ export function splitChunks(id, bytes, size = CHUNK_PAYLOAD) {
|
|
|
17
17
|
}
|
|
18
18
|
return out;
|
|
19
19
|
}
|
|
20
|
+
/** Most incomplete chunk sets one reassembler (one peer) holds at once; a new id past this evicts the oldest. */
|
|
21
|
+
export const MAX_PARTIAL_SETS = 8;
|
|
20
22
|
/** Reassembles chunked payloads by id. Duplicates are ignored; out-of-order chunks are fine. */
|
|
21
23
|
export class Reassembler {
|
|
22
|
-
parts = new Map();
|
|
24
|
+
parts = new Map(); // insertion order = oldest first
|
|
23
25
|
/** Returns the full payload when the last chunk arrives, otherwise null. */
|
|
24
26
|
push(chunk) {
|
|
25
27
|
if (chunk.count === 0 || chunk.index >= chunk.count)
|
|
26
28
|
return null;
|
|
27
29
|
let e = this.parts.get(chunk.id);
|
|
28
30
|
if (!e || e.count !== chunk.count) {
|
|
31
|
+
this.parts.delete(chunk.id);
|
|
32
|
+
while (this.parts.size >= MAX_PARTIAL_SETS)
|
|
33
|
+
this.parts.delete(this.parts.keys().next().value);
|
|
29
34
|
e = { count: chunk.count, got: 0, parts: new Array(chunk.count), bytes: 0 };
|
|
30
35
|
this.parts.set(chunk.id, e);
|
|
31
36
|
}
|
|
@@ -38,8 +43,17 @@ export class Reassembler {
|
|
|
38
43
|
this.parts.delete(chunk.id);
|
|
39
44
|
throw new CodecError("too_long", `chunk set over ${MAX_REASSEMBLED_BYTES} bytes`);
|
|
40
45
|
}
|
|
41
|
-
if (e.got < e.count)
|
|
46
|
+
if (e.got < e.count) {
|
|
47
|
+
// Many half-sent sets must not add up to more than one whole message: the oldest others go first.
|
|
48
|
+
while (this.pendingBytes > MAX_REASSEMBLED_BYTES && this.parts.size > 1) {
|
|
49
|
+
for (const id of this.parts.keys())
|
|
50
|
+
if (id !== chunk.id) {
|
|
51
|
+
this.parts.delete(id);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
42
55
|
return null;
|
|
56
|
+
}
|
|
43
57
|
this.parts.delete(chunk.id);
|
|
44
58
|
const out = new Uint8Array(e.bytes);
|
|
45
59
|
let pos = 0;
|
|
@@ -52,6 +66,13 @@ export class Reassembler {
|
|
|
52
66
|
get pending() {
|
|
53
67
|
return this.parts.size;
|
|
54
68
|
}
|
|
69
|
+
/** Bytes held across incomplete sets. */
|
|
70
|
+
get pendingBytes() {
|
|
71
|
+
let n = 0;
|
|
72
|
+
for (const p of this.parts.values())
|
|
73
|
+
n += p.bytes;
|
|
74
|
+
return n;
|
|
75
|
+
}
|
|
55
76
|
clear() {
|
|
56
77
|
this.parts.clear();
|
|
57
78
|
}
|
package/dist/chunking.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunking.js","sourceRoot":"","sources":["../src/chunking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAgB,MAAM,YAAY,CAAC;AAE7E,gGAAgG;AAChG,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAC;AAEnC,MAAM,UAAU,WAAW,CAAC,EAAU,EAAE,KAAiB,EAAE,IAAI,GAAG,aAAa;IAC7E,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;IAC1D,IAAI,KAAK,GAAG,MAAM;QAAE,MAAM,IAAI,UAAU,CAAC,sCAAsC,CAAC,CAAC;IACjF,MAAM,GAAG,GAAc,EAAE,CAAC;IAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,GAAG,CAAC,IAAI,CAAC;YACP,IAAI,EAAE,OAAO;YACb,EAAE;YACF,KAAK,EAAE,CAAC;YACR,KAAK;YACL,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;SACxE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AASD,gGAAgG;AAChG,MAAM,OAAO,WAAW;IACd,KAAK,GAAG,IAAI,GAAG,EAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"chunking.js","sourceRoot":"","sources":["../src/chunking.ts"],"names":[],"mappings":"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","sourcesContent":["import { CodecError, MAX_REASSEMBLED_BYTES, type Message } from \"./codec.js\";\n\n/** Chunk body capacity: 16 KiB frame - 3 header - 8 chunk header, rounded down for headroom. */\nexport const CHUNK_PAYLOAD = 16000;\n\nexport function splitChunks(id: number, bytes: Uint8Array, size = CHUNK_PAYLOAD): Message[] {\n const count = Math.max(1, Math.ceil(bytes.length / size));\n if (count > 0xffff) throw new RangeError(\"payload needs more than 65535 chunks\");\n const out: Message[] = [];\n for (let i = 0; i < count; i++) {\n out.push({\n type: \"chunk\",\n id,\n index: i,\n count,\n bytes: bytes.subarray(i * size, Math.min(bytes.length, (i + 1) * size)),\n });\n }\n return out;\n}\n\ninterface Partial {\n count: number;\n got: number;\n parts: (Uint8Array | undefined)[];\n bytes: number;\n}\n\n/** Most incomplete chunk sets one reassembler (one peer) holds at once; a new id past this evicts the oldest. */\nexport const MAX_PARTIAL_SETS = 8;\n\n/** Reassembles chunked payloads by id. Duplicates are ignored; out-of-order chunks are fine. */\nexport class Reassembler {\n private parts = new Map<number, Partial>(); // insertion order = oldest first\n\n /** Returns the full payload when the last chunk arrives, otherwise null. */\n push(chunk: { id: number; index: number; count: number; bytes: Uint8Array }): Uint8Array | null {\n if (chunk.count === 0 || chunk.index >= chunk.count) return null;\n let e = this.parts.get(chunk.id);\n if (!e || e.count !== chunk.count) {\n this.parts.delete(chunk.id);\n while (this.parts.size >= MAX_PARTIAL_SETS) this.parts.delete(this.parts.keys().next().value!);\n e = { count: chunk.count, got: 0, parts: new Array(chunk.count), bytes: 0 };\n this.parts.set(chunk.id, e);\n }\n if (e.parts[chunk.index]) return null;\n e.parts[chunk.index] = chunk.bytes;\n e.got++;\n e.bytes += chunk.bytes.length;\n if (e.bytes > MAX_REASSEMBLED_BYTES) {\n this.parts.delete(chunk.id);\n throw new CodecError(\"too_long\", `chunk set over ${MAX_REASSEMBLED_BYTES} bytes`);\n }\n if (e.got < e.count) {\n // Many half-sent sets must not add up to more than one whole message: the oldest others go first.\n while (this.pendingBytes > MAX_REASSEMBLED_BYTES && this.parts.size > 1) {\n for (const id of this.parts.keys()) if (id !== chunk.id) { this.parts.delete(id); break; }\n }\n return null;\n }\n this.parts.delete(chunk.id);\n const out = new Uint8Array(e.bytes);\n let pos = 0;\n for (const p of e.parts) {\n out.set(p!, pos);\n pos += p!.length;\n }\n return out;\n }\n\n get pending(): number {\n return this.parts.size;\n }\n /** Bytes held across incomplete sets. */\n get pendingBytes(): number {\n let n = 0;\n for (const p of this.parts.values()) n += p.bytes;\n return n;\n }\n clear(): void {\n this.parts.clear();\n }\n}\n"]}
|
package/dist/deflate.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Optional zlib deflate for snapshots (CompressionStream where present; .NET ZLibStream is the matching side). */
|
|
2
2
|
export declare function deflateAvailable(): boolean;
|
|
3
3
|
export declare function deflate(bytes: Uint8Array): Promise<Uint8Array>;
|
|
4
|
-
|
|
4
|
+
/** Inflates at most `maxBytes` (default 64 MiB): a few KB can inflate to gigabytes. Over it rejects with CodecError too_long. */
|
|
5
|
+
export declare function inflate(bytes: Uint8Array, maxBytes?: number): Promise<Uint8Array>;
|
package/dist/deflate.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { CodecError, MAX_REASSEMBLED_BYTES } from "./codec.js";
|
|
1
2
|
/** Optional zlib deflate for snapshots (CompressionStream where present; .NET ZLibStream is the matching side). */
|
|
2
3
|
export function deflateAvailable() {
|
|
3
4
|
return typeof CompressionStream !== "undefined" && typeof DecompressionStream !== "undefined";
|
|
4
5
|
}
|
|
5
|
-
async function pipe(bytes, stream) {
|
|
6
|
+
async function pipe(bytes, stream, maxBytes = Infinity) {
|
|
6
7
|
const w = stream.writable.getWriter();
|
|
7
|
-
void w.write(bytes).then(() => w.close());
|
|
8
|
+
void w.write(bytes).then(() => w.close()).catch(() => { });
|
|
8
9
|
const chunks = [];
|
|
9
10
|
const r = stream.readable.getReader();
|
|
10
11
|
let total = 0;
|
|
@@ -14,6 +15,10 @@ async function pipe(bytes, stream) {
|
|
|
14
15
|
break;
|
|
15
16
|
chunks.push(value);
|
|
16
17
|
total += value.length;
|
|
18
|
+
if (total > maxBytes) {
|
|
19
|
+
void r.cancel().catch(() => { });
|
|
20
|
+
throw new CodecError("too_long", `snapshot inflates to more than ${maxBytes} bytes`);
|
|
21
|
+
}
|
|
17
22
|
}
|
|
18
23
|
const out = new Uint8Array(total);
|
|
19
24
|
let pos = 0;
|
|
@@ -26,7 +31,8 @@ async function pipe(bytes, stream) {
|
|
|
26
31
|
export function deflate(bytes) {
|
|
27
32
|
return pipe(bytes, new CompressionStream("deflate"));
|
|
28
33
|
}
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
/** Inflates at most `maxBytes` (default 64 MiB): a few KB can inflate to gigabytes. Over it rejects with CodecError too_long. */
|
|
35
|
+
export function inflate(bytes, maxBytes = MAX_REASSEMBLED_BYTES) {
|
|
36
|
+
return pipe(bytes, new DecompressionStream("deflate"), maxBytes);
|
|
31
37
|
}
|
|
32
38
|
//# sourceMappingURL=deflate.js.map
|
package/dist/deflate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deflate.js","sourceRoot":"","sources":["../src/deflate.ts"],"names":[],"mappings":"AAAA,mHAAmH;AAEnH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,OAAO,iBAAiB,KAAK,WAAW,IAAI,OAAO,mBAAmB,KAAK,WAAW,CAAC;AAChG,CAAC;AAED,KAAK,UAAU,IAAI,
|
|
1
|
+
{"version":3,"file":"deflate.js","sourceRoot":"","sources":["../src/deflate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAE/D,mHAAmH;AAEnH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,OAAO,iBAAiB,KAAK,WAAW,IAAI,OAAO,mBAAmB,KAAK,WAAW,CAAC;AAChG,CAAC;AAED,KAAK,UAAU,IAAI,CACjB,KAAiB,EACjB,MAAsF,EACtF,QAAQ,GAAG,QAAQ;IAEnB,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC;IACtC,KAAK,CAAC,CAAC,KAAK,CAAC,KAA2C,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAChG,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC;IACtC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,SAAS,CAAC;QACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACvC,IAAI,IAAI;YAAE,MAAM;QAChB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnB,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC;QACtB,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;YACrB,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAChC,MAAM,IAAI,UAAU,CAAC,UAAU,EAAE,kCAAkC,QAAQ,QAAQ,CAAC,CAAC;QACvF,CAAC;IACH,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAChB,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC;IAClB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAiB;IACvC,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,iBAAiB,CAAC,SAAS,CAAU,CAAC,CAAC;AAChE,CAAC;AACD,iIAAiI;AACjI,MAAM,UAAU,OAAO,CAAC,KAAiB,EAAE,QAAQ,GAAG,qBAAqB;IACzE,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,mBAAmB,CAAC,SAAS,CAAU,EAAE,QAAQ,CAAC,CAAC;AAC5E,CAAC","sourcesContent":["import { CodecError, MAX_REASSEMBLED_BYTES } from \"./codec.js\";\n\n/** Optional zlib deflate for snapshots (CompressionStream where present; .NET ZLibStream is the matching side). */\n\nexport function deflateAvailable(): boolean {\n return typeof CompressionStream !== \"undefined\" && typeof DecompressionStream !== \"undefined\";\n}\n\nasync function pipe(\n bytes: Uint8Array,\n stream: { readable: ReadableStream<Uint8Array>; writable: WritableStream<Uint8Array> },\n maxBytes = Infinity,\n) {\n const w = stream.writable.getWriter();\n void w.write(bytes as unknown as Uint8Array<ArrayBuffer>).then(() => w.close()).catch(() => {});\n const chunks: Uint8Array[] = [];\n const r = stream.readable.getReader();\n let total = 0;\n for (;;) {\n const { done, value } = await r.read();\n if (done) break;\n chunks.push(value);\n total += value.length;\n if (total > maxBytes) {\n void r.cancel().catch(() => {});\n throw new CodecError(\"too_long\", `snapshot inflates to more than ${maxBytes} bytes`);\n }\n }\n const out = new Uint8Array(total);\n let pos = 0;\n for (const c of chunks) {\n out.set(c, pos);\n pos += c.length;\n }\n return out;\n}\n\nexport function deflate(bytes: Uint8Array): Promise<Uint8Array> {\n return pipe(bytes, new CompressionStream(\"deflate\") as never);\n}\n/** Inflates at most `maxBytes` (default 64 MiB): a few KB can inflate to gigabytes. Over it rejects with CodecError too_long. */\nexport function inflate(bytes: Uint8Array, maxBytes = MAX_REASSEMBLED_BYTES): Promise<Uint8Array> {\n return pipe(bytes, new DecompressionStream(\"deflate\") as never, maxBytes);\n}\n"]}
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { SocketLike } from "../transport.js";
|
|
2
2
|
import { SignalingHub, type HubDeps, type HubOptions } from "./hub.js";
|
|
3
3
|
import { PortMapper, type PortMapperOptions, type PortMapResult } from "./portmap.js";
|
|
4
|
+
/**
|
|
5
|
+
* Whether a WebSocket handshake carrying `origin` is accepted. A web page can open a WebSocket to any address it can
|
|
6
|
+
* reach, so a browser's Origin must be one we expect: in `allowed` ("*" = all), the same host:port as the request's
|
|
7
|
+
* Host header (the page this host serves itself), or loopback (localhost, 127.0.0.1, [::1], any port). A request with
|
|
8
|
+
* no Origin header is a native client and never gets here.
|
|
9
|
+
*/
|
|
10
|
+
export declare function originAllowed(origin: string, host: string | undefined, allowed: readonly string[]): boolean;
|
|
4
11
|
export declare class PortInUseError extends Error {
|
|
5
12
|
readonly code = "port_in_use";
|
|
13
|
+
/** Windows reserved the port (EACCES: an excluded port range, often from Hyper-V, WSL or Docker); not another program. */
|
|
14
|
+
readonly reserved: boolean;
|
|
6
15
|
constructor(port: number, detail: string);
|
|
7
16
|
}
|
|
17
|
+
/** The end of a `port_in_use` message: busy (another program) and reserved (Windows) need different fixes. */
|
|
18
|
+
export declare function portFailureHint(anyBusy: boolean, anyReserved: boolean): string;
|
|
8
19
|
/**
|
|
9
20
|
* Runs a {@link SignalingHub} inside a Node process and listens for joiners' signaling WebSockets on TCP `port`: the
|
|
10
21
|
* Node twin of C#'s `DirectHubHost` (docs/direct-ip.md s.2, s.5 step 3). A plain `net` server with a minimal HTTP/1.1
|
|
@@ -29,6 +40,11 @@ export declare class DirectHubHost {
|
|
|
29
40
|
* `http://<host address>:<port>/` gets a page that joins with `kit.joinDirect(location.host)`. Null (default): 426.
|
|
30
41
|
*/
|
|
31
42
|
http: HttpHandler | null;
|
|
43
|
+
/**
|
|
44
|
+
* Browser `Origin` values allowed to open a signaling socket, exact match, "*" for all. No Origin header (native
|
|
45
|
+
* clients), the host's own page (Origin equal to the request's Host) and localhost pages are always allowed.
|
|
46
|
+
*/
|
|
47
|
+
allowedOrigins: string[];
|
|
32
48
|
/** The bound TCP port (the requested one, or the one the OS picked for 0). */
|
|
33
49
|
readonly port: number;
|
|
34
50
|
portMapper: PortMapper | null;
|
package/dist/direct/hub-host.js
CHANGED
|
@@ -8,6 +8,28 @@ const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
|
8
8
|
const MAX_HEADER_BYTES = 8192;
|
|
9
9
|
const HANDSHAKE_TIMEOUT_MS = 5000;
|
|
10
10
|
const STATUS_TEXT = { 200: "OK", 301: "Moved Permanently", 302: "Found", 404: "Not Found", 500: "Internal Server Error" };
|
|
11
|
+
/**
|
|
12
|
+
* Whether a WebSocket handshake carrying `origin` is accepted. A web page can open a WebSocket to any address it can
|
|
13
|
+
* reach, so a browser's Origin must be one we expect: in `allowed` ("*" = all), the same host:port as the request's
|
|
14
|
+
* Host header (the page this host serves itself), or loopback (localhost, 127.0.0.1, [::1], any port). A request with
|
|
15
|
+
* no Origin header is a native client and never gets here.
|
|
16
|
+
*/
|
|
17
|
+
export function originAllowed(origin, host, allowed) {
|
|
18
|
+
if (allowed.includes("*") || allowed.includes(origin))
|
|
19
|
+
return true;
|
|
20
|
+
let u;
|
|
21
|
+
try {
|
|
22
|
+
u = new URL(origin);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
28
|
+
return false;
|
|
29
|
+
if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]")
|
|
30
|
+
return true;
|
|
31
|
+
return host !== undefined && u.host.toLowerCase() === host.trim().toLowerCase();
|
|
32
|
+
}
|
|
11
33
|
/**
|
|
12
34
|
* Rejects (EADDRINUSE) when another program holds IPv4 TCP or UDP `port`. Windows lets the dual-stack listener bind
|
|
13
35
|
* over a program on 0.0.0.0 (every IPv4 joiner would then reach that program), and the peer links need UDP `port`
|
|
@@ -30,10 +52,22 @@ async function probeIPv4(port) {
|
|
|
30
52
|
}
|
|
31
53
|
export class PortInUseError extends Error {
|
|
32
54
|
code = "port_in_use";
|
|
55
|
+
/** Windows reserved the port (EACCES: an excluded port range, often from Hyper-V, WSL or Docker); not another program. */
|
|
56
|
+
reserved;
|
|
33
57
|
constructor(port, detail) {
|
|
34
|
-
|
|
58
|
+
const reserved = detail === "EACCES";
|
|
59
|
+
super(`port_in_use: cannot listen on port ${port} (${detail}): ${portFailureHint(!reserved, reserved)}`);
|
|
60
|
+
this.reserved = reserved;
|
|
35
61
|
}
|
|
36
62
|
}
|
|
63
|
+
/** The end of a `port_in_use` message: busy (another program) and reserved (Windows) need different fixes. */
|
|
64
|
+
export function portFailureHint(anyBusy, anyReserved) {
|
|
65
|
+
if (!anyReserved)
|
|
66
|
+
return "is another copy of the game hosting?";
|
|
67
|
+
if (!anyBusy)
|
|
68
|
+
return "Windows has reserved it (an excluded port range, often from Hyper-V, WSL or Docker); pick another port";
|
|
69
|
+
return "each one is busy or reserved by Windows (an excluded port range); is another copy of the game hosting?";
|
|
70
|
+
}
|
|
37
71
|
export class DirectHubHost {
|
|
38
72
|
hub;
|
|
39
73
|
/**
|
|
@@ -41,6 +75,11 @@ export class DirectHubHost {
|
|
|
41
75
|
* `http://<host address>:<port>/` gets a page that joins with `kit.joinDirect(location.host)`. Null (default): 426.
|
|
42
76
|
*/
|
|
43
77
|
http = null;
|
|
78
|
+
/**
|
|
79
|
+
* Browser `Origin` values allowed to open a signaling socket, exact match, "*" for all. No Origin header (native
|
|
80
|
+
* clients), the host's own page (Origin equal to the request's Host) and localhost pages are always allowed.
|
|
81
|
+
*/
|
|
82
|
+
allowedOrigins = [];
|
|
44
83
|
/** The bound TCP port (the requested one, or the one the OS picked for 0). */
|
|
45
84
|
port;
|
|
46
85
|
portMapper = null;
|
|
@@ -196,6 +235,11 @@ export class DirectHubHost {
|
|
|
196
235
|
reply("426 Upgrade Required", "this port speaks WebSocket (OneLobby direct host)");
|
|
197
236
|
return false;
|
|
198
237
|
}
|
|
238
|
+
const origin = headers.get("origin");
|
|
239
|
+
if (origin !== undefined && !originAllowed(origin, headers.get("host"), this.allowedOrigins)) {
|
|
240
|
+
reply("403 Forbidden", "this origin may not join (OneLobby direct host)");
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
199
243
|
const accept = createHash("sha1").update(key + WS_GUID).digest("base64");
|
|
200
244
|
sock.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`);
|
|
201
245
|
return true;
|