@zaisma/game-server 1.22.0 → 1.23.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/bench/glass.js +135 -40
- package/package.json +1 -1
- package/src/doors.d.ts +68 -0
- package/src/doors.js +24 -0
- package/src/internal.d.ts +1 -0
- package/src/internal.js +1 -0
- package/src/room.d.ts +64 -0
- package/src/room.js +35 -6
package/package.json
CHANGED
package/src/doors.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Door contracts (docs/DOORS.md). Every way into Zaisma, of every kind, states who may call it, what it may
|
|
3
|
+
* reach and what it trusts from other doors, and goes through checkDoor before its own code runs. Here, in the
|
|
4
|
+
* game server package's internal part, so a room on a developer's own server goes through the same check;
|
|
5
|
+
* never in the package's published index.
|
|
6
|
+
*/
|
|
7
|
+
/** Callers the door's guard proves itself, from what it read of the request or the room. */
|
|
8
|
+
export type DoorCallerProvenByGuard =
|
|
9
|
+
/** Guests too. A door that shows a signed-in person more trusts `session` as well. */
|
|
10
|
+
"anyone"
|
|
11
|
+
/** A session that works. */
|
|
12
|
+
| "signed_in"
|
|
13
|
+
/** A session with a finished account, and no developer ban unless the door keeps it open to them. */
|
|
14
|
+
| "developer"
|
|
15
|
+
/** Cloudflare Access to /admin. */
|
|
16
|
+
| "admin"
|
|
17
|
+
/** Anyone in the room, spectators too. */
|
|
18
|
+
| "room_member"
|
|
19
|
+
/** A player in the room, not a spectator. */
|
|
20
|
+
| "room_player"
|
|
21
|
+
/** The room's host. */
|
|
22
|
+
| "room_host";
|
|
23
|
+
/** Callers a named function proves, because the proof takes the door's own data (a body's signature, a token's row). */
|
|
24
|
+
export type DoorCallerProvenByFunction =
|
|
25
|
+
/** The API router: an API key's access, or a session from Zaisma's own pages. */
|
|
26
|
+
"api_caller"
|
|
27
|
+
/** A tester or preview link's token in the address. */
|
|
28
|
+
| "link_holder"
|
|
29
|
+
/** A Sign in with Zaisma code or access token. */
|
|
30
|
+
| "token_holder"
|
|
31
|
+
/** Stripe or GitHub, by the signature on the body. */
|
|
32
|
+
| "signed_sender"
|
|
33
|
+
/** A game's own page, by the request's Origin. */
|
|
34
|
+
| "game_page"
|
|
35
|
+
/** A signed player pass. */
|
|
36
|
+
| "pass_holder";
|
|
37
|
+
export type DoorCaller = DoorCallerProvenByGuard | DoorCallerProvenByFunction;
|
|
38
|
+
/**
|
|
39
|
+
* What a door may touch. The first seven are the API's own words (ApiRoute's reaches, apps/site/src/developer.ts):
|
|
40
|
+
* `account`, `player_save`, `game`, `webhook`, `sign_in_app` and `forum_topic` are the caller's own or what the caller may read,
|
|
41
|
+
* and `nothing_private` is what anyone may see. The `any_` ones and `zaisma_settings` are only ever an admin's.
|
|
42
|
+
*/
|
|
43
|
+
export type DoorReach = "account" | "player_save" | "game" | "webhook" | "sign_in_app" | "forum_topic" | "nothing_private" | "any_game" | "any_account" | "zaisma_settings" | "room" | "purchase";
|
|
44
|
+
/** What a door takes from another door, as the security map names them (scripts/security-map/doors.mjs). */
|
|
45
|
+
export type DoorTrust = "session" | "api_key" | "oauth_code" | "oauth_access" | "tester_link" | "preview_link" | "player_pass" | "admin_access" | "player_id" | "stripe_signature" | "github_signature" | "game_origin" | "purchase_receipt" | "passkey" | "email_code";
|
|
46
|
+
/** The function that proves a caller; a test checks it exists. */
|
|
47
|
+
export interface DoorProof {
|
|
48
|
+
source_path: string;
|
|
49
|
+
symbol_name: string;
|
|
50
|
+
}
|
|
51
|
+
export interface DoorContractFields {
|
|
52
|
+
reaches: DoorReach;
|
|
53
|
+
trusts: readonly DoorTrust[];
|
|
54
|
+
}
|
|
55
|
+
/** A caller a function proves can't be stated without naming the function, so it doesn't build. */
|
|
56
|
+
export type DoorContract = DoorContractFields & ({
|
|
57
|
+
whoMayCall: DoorCallerProvenByGuard;
|
|
58
|
+
} | {
|
|
59
|
+
whoMayCall: DoorCallerProvenByFunction;
|
|
60
|
+
provenBy: DoorProof;
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* The one check every door goes through. `proven` is who the door's guard found the caller to be; a caller a
|
|
64
|
+
* named function proves passes here, and that function refuses them, before anything else of the door runs.
|
|
65
|
+
*/
|
|
66
|
+
export declare function checkDoor(contract: DoorContract, proven: readonly DoorCaller[]): boolean;
|
|
67
|
+
/** Where a door's path parts come from: `:name` parts, each a [^/]+ unless named here, and a final `*` for the rest. */
|
|
68
|
+
export declare function doorPathPattern(path: string, partPatterns?: Readonly<Record<string, string>>): RegExp;
|
package/src/doors.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Door contracts (docs/DOORS.md). Every way into Zaisma, of every kind, states who may call it, what it may
|
|
3
|
+
* reach and what it trusts from other doors, and goes through checkDoor before its own code runs. Here, in the
|
|
4
|
+
* game server package's internal part, so a room on a developer's own server goes through the same check;
|
|
5
|
+
* never in the package's published index.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The one check every door goes through. `proven` is who the door's guard found the caller to be; a caller a
|
|
9
|
+
* named function proves passes here, and that function refuses them, before anything else of the door runs.
|
|
10
|
+
*/
|
|
11
|
+
export function checkDoor(contract, proven) {
|
|
12
|
+
return contract.whoMayCall === "anyone" || "provenBy" in contract || proven.includes(contract.whoMayCall);
|
|
13
|
+
}
|
|
14
|
+
/** Where a door's path parts come from: `:name` parts, each a [^/]+ unless named here, and a final `*` for the rest. */
|
|
15
|
+
export function doorPathPattern(path, partPatterns = {}) {
|
|
16
|
+
const parts = path.split("/").map((part) => {
|
|
17
|
+
if (part === "*")
|
|
18
|
+
return ".*";
|
|
19
|
+
if (part.startsWith(":"))
|
|
20
|
+
return `(?<${part.slice(1)}>${partPatterns[part.slice(1)] ?? "[^/]+"})`;
|
|
21
|
+
return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22
|
+
});
|
|
23
|
+
return new RegExp(`^${parts.join("/")}$`);
|
|
24
|
+
}
|
package/src/internal.d.ts
CHANGED
package/src/internal.js
CHANGED
package/src/room.d.ts
CHANGED
|
@@ -1334,6 +1334,70 @@ export interface StateSyncHooks {
|
|
|
1334
1334
|
export declare const NO_MESSAGE_TYPE = "(none)";
|
|
1335
1335
|
export declare const BYTES_MESSAGE_TYPE = "(bytes)";
|
|
1336
1336
|
export declare const ZAISMA_MESSAGE_TYPE = "(zaisma)";
|
|
1337
|
+
/**
|
|
1338
|
+
* Every message a room takes (docs/DOORS.md). receive's switch is over these names, so a new one without a row
|
|
1339
|
+
* doesn't build. A spectator may send only what's for room members. hello comes first and proves the pass; chat is the host's to take before receive (readChatMessage);
|
|
1340
|
+
* bytes is every binary message (receiveBytes). A type with no row, maybe from a newer SDK, is counted and ignored.
|
|
1341
|
+
*/
|
|
1342
|
+
export declare const ROOM_MESSAGE_DOORS: {
|
|
1343
|
+
readonly hello: {
|
|
1344
|
+
readonly whoMayCall: "pass_holder";
|
|
1345
|
+
readonly reaches: "room";
|
|
1346
|
+
readonly trusts: readonly ["player_pass"];
|
|
1347
|
+
readonly provenBy: {
|
|
1348
|
+
readonly source_path: "packages/game-server/src/pass.ts";
|
|
1349
|
+
readonly symbol_name: "verifyPass";
|
|
1350
|
+
};
|
|
1351
|
+
};
|
|
1352
|
+
readonly probe_answer: import("./doors.ts").DoorContractFields & {
|
|
1353
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1354
|
+
};
|
|
1355
|
+
readonly room: import("./doors.ts").DoorContractFields & {
|
|
1356
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1357
|
+
};
|
|
1358
|
+
readonly kick: import("./doors.ts").DoorContractFields & {
|
|
1359
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1360
|
+
};
|
|
1361
|
+
readonly here: import("./doors.ts").DoorContractFields & {
|
|
1362
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1363
|
+
};
|
|
1364
|
+
readonly idle: import("./doors.ts").DoorContractFields & {
|
|
1365
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1366
|
+
};
|
|
1367
|
+
readonly hidden: import("./doors.ts").DoorContractFields & {
|
|
1368
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1369
|
+
};
|
|
1370
|
+
readonly shown: import("./doors.ts").DoorContractFields & {
|
|
1371
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1372
|
+
};
|
|
1373
|
+
readonly back: import("./doors.ts").DoorContractFields & {
|
|
1374
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1375
|
+
};
|
|
1376
|
+
readonly message: import("./doors.ts").DoorContractFields & {
|
|
1377
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1378
|
+
};
|
|
1379
|
+
readonly bytes: import("./doors.ts").DoorContractFields & {
|
|
1380
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1381
|
+
};
|
|
1382
|
+
readonly chat: import("./doors.ts").DoorContractFields & {
|
|
1383
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1384
|
+
};
|
|
1385
|
+
readonly block: import("./doors.ts").DoorContractFields & {
|
|
1386
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1387
|
+
};
|
|
1388
|
+
readonly unblock: import("./doors.ts").DoorContractFields & {
|
|
1389
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1390
|
+
};
|
|
1391
|
+
readonly friend_request: import("./doors.ts").DoorContractFields & {
|
|
1392
|
+
whoMayCall: import("./doors.ts").DoorCallerProvenByGuard;
|
|
1393
|
+
};
|
|
1394
|
+
readonly "zaisma:purchase": {
|
|
1395
|
+
readonly whoMayCall: "room_player";
|
|
1396
|
+
readonly reaches: "purchase";
|
|
1397
|
+
readonly trusts: readonly ["player_pass", "purchase_receipt"];
|
|
1398
|
+
};
|
|
1399
|
+
};
|
|
1400
|
+
export type RoomMessageType = keyof typeof ROOM_MESSAGE_DOORS;
|
|
1337
1401
|
export declare const OTHER_MESSAGE_TYPE = "(other)";
|
|
1338
1402
|
/** State sync's updates (stateSyncEnabled). */
|
|
1339
1403
|
export declare const STATE_MESSAGE_TYPE = "(state)";
|
package/src/room.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { CHAT_CHECK_LIMITS, CHAT_ROOM_LIMITS } from "./chat.js";
|
|
10
10
|
import { DatabaseError } from "./database.js";
|
|
11
|
+
import { checkDoor } from "./doors.js";
|
|
11
12
|
import { isRoomCode, roomCode } from "./names.js";
|
|
12
13
|
import { readSchedules } from "./schedules.js";
|
|
13
14
|
import { roomCallChecks } from "./room-calls.js";
|
|
@@ -502,6 +503,35 @@ export const STATE_SYNC = Symbol("stateSync");
|
|
|
502
503
|
export const NO_MESSAGE_TYPE = "(none)";
|
|
503
504
|
export const BYTES_MESSAGE_TYPE = "(bytes)";
|
|
504
505
|
export const ZAISMA_MESSAGE_TYPE = "(zaisma)";
|
|
506
|
+
const ROOM_MEMBER = { whoMayCall: "room_member", reaches: "room", trusts: ["player_pass"] };
|
|
507
|
+
const ROOM_PLAYER = { whoMayCall: "room_player", reaches: "room", trusts: ["player_pass"] };
|
|
508
|
+
const ROOM_HOST = { whoMayCall: "room_host", reaches: "room", trusts: ["player_pass"] };
|
|
509
|
+
/**
|
|
510
|
+
* Every message a room takes (docs/DOORS.md). receive's switch is over these names, so a new one without a row
|
|
511
|
+
* doesn't build. A spectator may send only what's for room members. hello comes first and proves the pass; chat is the host's to take before receive (readChatMessage);
|
|
512
|
+
* bytes is every binary message (receiveBytes). A type with no row, maybe from a newer SDK, is counted and ignored.
|
|
513
|
+
*/
|
|
514
|
+
export const ROOM_MESSAGE_DOORS = {
|
|
515
|
+
hello: { whoMayCall: "pass_holder", reaches: "room", trusts: ["player_pass"], provenBy: { source_path: "packages/game-server/src/pass.ts", symbol_name: "verifyPass" } },
|
|
516
|
+
probe_answer: ROOM_MEMBER,
|
|
517
|
+
room: ROOM_HOST,
|
|
518
|
+
kick: ROOM_HOST,
|
|
519
|
+
here: ROOM_PLAYER,
|
|
520
|
+
idle: ROOM_PLAYER,
|
|
521
|
+
hidden: ROOM_PLAYER,
|
|
522
|
+
shown: ROOM_PLAYER,
|
|
523
|
+
back: ROOM_PLAYER,
|
|
524
|
+
message: ROOM_PLAYER,
|
|
525
|
+
bytes: ROOM_PLAYER,
|
|
526
|
+
chat: ROOM_PLAYER,
|
|
527
|
+
block: ROOM_PLAYER,
|
|
528
|
+
unblock: ROOM_PLAYER,
|
|
529
|
+
friend_request: ROOM_PLAYER,
|
|
530
|
+
"zaisma:purchase": { whoMayCall: "room_player", reaches: "purchase", trusts: ["player_pass", "purchase_receipt"] },
|
|
531
|
+
};
|
|
532
|
+
const isRoomMessageType = (type) => typeof type === "string" && Object.hasOwn(ROOM_MESSAGE_DOORS, type);
|
|
533
|
+
const PLAYER_PROVEN = ["room_member", "room_player"];
|
|
534
|
+
const HOST_PROVEN = ["room_member", "room_player", "room_host"];
|
|
505
535
|
export const OTHER_MESSAGE_TYPE = "(other)";
|
|
506
536
|
/** State sync's updates (stateSyncEnabled). */
|
|
507
537
|
export const STATE_MESSAGE_TYPE = "(state)";
|
|
@@ -1759,7 +1789,10 @@ export class RoomCore {
|
|
|
1759
1789
|
const presence = this.presence.get(playerId);
|
|
1760
1790
|
if (presence)
|
|
1761
1791
|
presence.seenAtMs = this.now();
|
|
1762
|
-
|
|
1792
|
+
const type = isRoomMessageType(message.type) ? message.type : null;
|
|
1793
|
+
if (type && !checkDoor(ROOM_MESSAGE_DOORS[type], this.hostPlayerId === playerId ? HOST_PROVEN : PLAYER_PROVEN))
|
|
1794
|
+
return "not_allowed";
|
|
1795
|
+
switch (type) {
|
|
1763
1796
|
case "room":
|
|
1764
1797
|
return this.hostChange(playerId, message);
|
|
1765
1798
|
case "kick":
|
|
@@ -1838,7 +1871,7 @@ export class RoomCore {
|
|
|
1838
1871
|
if (text.length <= ROOM_LIMITS.maximumGameMessageSizeBytes) {
|
|
1839
1872
|
try {
|
|
1840
1873
|
const message = JSON.parse(text);
|
|
1841
|
-
if (message?.type === "
|
|
1874
|
+
if (isRoomMessageType(message?.type) && ROOM_MESSAGE_DOORS[message.type].whoMayCall === "room_member" && this.answer(spectatorId, message.probeNumber))
|
|
1842
1875
|
return null;
|
|
1843
1876
|
}
|
|
1844
1877
|
catch {
|
|
@@ -1910,8 +1943,6 @@ export class RoomCore {
|
|
|
1910
1943
|
* text between strangers), so the kicked player is told the host removed them.
|
|
1911
1944
|
*/
|
|
1912
1945
|
hostKick(playerId, message) {
|
|
1913
|
-
if (this.hostPlayerId !== playerId)
|
|
1914
|
-
return "not_allowed";
|
|
1915
1946
|
const kickedId = message.playerId;
|
|
1916
1947
|
if (typeof kickedId !== "string" || kickedId === playerId || !this.players.has(kickedId))
|
|
1917
1948
|
return "bad_message";
|
|
@@ -1920,8 +1951,6 @@ export class RoomCore {
|
|
|
1920
1951
|
}
|
|
1921
1952
|
/** The host hides, shows, opens, closes, turns friends' Join on or off, names or labels the room, when the game server allows it. */
|
|
1922
1953
|
hostChange(playerId, message) {
|
|
1923
|
-
if (this.hostPlayerId !== playerId)
|
|
1924
|
-
return "not_allowed";
|
|
1925
1954
|
const asked = message.room;
|
|
1926
1955
|
if (!asked || typeof asked !== "object" || Array.isArray(asked))
|
|
1927
1956
|
return "bad_message";
|