@zaisma/game-server 1.21.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaisma/game-server",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "Run your Zaisma game server: the same rooms on Zaisma or on your own server.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://zaisma.com",
package/sdk/zaisma.d.ts CHANGED
@@ -198,7 +198,7 @@ declare namespace Zaisma {
198
198
  const server: Server;
199
199
 
200
200
  /**
201
- * Invite friends. On Zaisma it opens the page's Share, the same as the game bar's Share button: the
201
+ * Invite friends. On Zaisma it opens Zaisma's invite sheet, the same as the Zaisma overlay's Share: the
202
202
  * room's QR code, link and code, Copy and the share sheet ("opened", or "failed" when the page couldn't
203
203
  * open it). On your own site it opens the share sheet on phones, otherwise copies the room's invite link
204
204
  * (the game's page without a room). Call it from a button in your game, like on a "waiting for players"
@@ -227,16 +227,6 @@ declare namespace Zaisma {
227
227
  */
228
228
  function checkChat(text: string): Promise<ChatCheck>;
229
229
 
230
- /**
231
- * The Players panel, where a player can mute, block and report the others and see their friends. On
232
- * Zaisma it opens the game bar's own Players, the same as its button (leaving full screen if the
233
- * game is in it), so a Players button in your game, in your game's style, does exactly what the bar's
234
- * does. On your own site, where there's no bar, it draws Zaisma's ready-made panel.
235
- * A game with its own netcode offers them in its own menu. Or build your own panel
236
- * with room.mute, room.block and room.report.
237
- */
238
- function openPlayersPanel(room: Room): void;
239
-
240
230
  /**
241
231
  * The signed-in player's Zaisma friends who are playing this game right now, for your own list of
242
232
  * them: to join a friend's match, watch it or invite them ({@link Friends.list}). A game never sees
@@ -605,7 +595,7 @@ declare namespace Zaisma {
605
595
 
606
596
  /**
607
597
  * Whether Zaisma's page covers the game right now, so a game can pause or hold the player's input while it does.
608
- * coveredBy is "sheet" while one of Zaisma's sheets is open over the game (buying, Players, Report: the player is
598
+ * coveredBy is "sheet" while one of Zaisma's sheets is open over the game (buying, the Zaisma overlay, Report: the player is
609
599
  * busy with it), "card" while one of its cards is over the game's corner (Still there?, Keep your progress; the
610
600
  * game stays in view and playable), and null otherwise. Always null outside Zaisma's page (your own site, a
611
601
  * local test). Listen for "coveredBy" to hear each change.
package/sdk/zaisma.js CHANGED
@@ -2574,138 +2574,9 @@
2574
2574
  });
2575
2575
  });
2576
2576
 
2577
- /** Hand a player report to the Zaisma page. If no Zaisma page answers (the game runs somewhere else), use our own form. */
2578
- function handOff(room, reported, opened, fallback) {
2579
- if (!config.site || window.parent === window) return fallback();
2580
- var isAnswered = false;
2581
- function listen(event) {
2582
- if (event.origin === config.site && event.source === window.parent && event.data && event.data.type === "zaisma:report-open") {
2583
- isAnswered = true;
2584
- window.removeEventListener("message", listen);
2585
- opened();
2586
- }
2587
- }
2588
- window.addEventListener("message", listen);
2589
- window.parent.postMessage({ type: "zaisma:report", code: room.roomCode, player: { id: reported.id, name: reported.name } }, config.site);
2590
- setTimeout(function () {
2591
- window.removeEventListener("message", listen);
2592
- if (!isAnswered) fallback();
2593
- }, 800);
2594
- }
2595
-
2596
2577
  // The first four go to Zaisma; the rest to the game's developer.
2597
2578
  var REASONS = [["real_life_threat", "Threatening them in real life"], ["personal_details", "Sharing someone's personal details"], ["following_after_block", "Following me after I blocked them"], ["sexual", "Sexual behavior"], ["harassment", "Harassing or bullying"], ["cheating", "Cheating"], ["other", "Something else"]];
2598
2579
 
2599
- /** A ready-made Players panel: mute, block and report. Games open it from their own menu. */
2600
- function openPlayersPanel(room) {
2601
- // On Zaisma, the page's own Players (the bar's), so a game's Players button opens the same one.
2602
- if (onZaismaPage()) {
2603
- window.parent.postMessage({ type: "zaisma:players-open" }, config.site);
2604
- return;
2605
- }
2606
- var old = document.getElementById("zaisma-safety");
2607
- if (old) old.remove();
2608
- var wrap = document.createElement("div");
2609
- wrap.id = "zaisma-safety";
2610
- wrap.setAttribute("role", "dialog");
2611
- wrap.setAttribute("aria-label", "Players");
2612
- // Zaisma's Steel look: gunmetal plates with the cut corner.
2613
- wrap.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:rgba(4,5,7,.6);display:grid;place-items:center;font:15px/1.4 system-ui,-apple-system,sans-serif;color:#eceef0";
2614
- var box = document.createElement("div");
2615
- box.style.cssText = "background:linear-gradient(180deg,#262a31,#1b1e23);box-shadow:inset 1px 1px 0 rgba(255,255,255,.09),inset -1px -1px 0 rgba(0,0,0,.7),0 14px 32px rgba(0,0,0,.42);clip-path:polygon(0 0,calc(100% - 18px) 0,100% 18px,100% 100%,0 100%);padding:18px;width:min(420px,92vw);max-height:80vh;overflow:auto";
2616
- wrap.appendChild(box);
2617
- var buttonStyle = "font:inherit;font-size:13px;font-weight:600;min-height:36px;padding:0 12px;border:0;background:linear-gradient(180deg,#30353d,#22262c);box-shadow:inset 1px 1px 0 rgba(255,255,255,.13),inset -1px -1px 0 #000;clip-path:polygon(0 0,calc(100% - 7px) 0,100% 7px,100% 100%,0 100%);color:#eceef0;cursor:pointer";
2618
- function close() { wrap.remove(); }
2619
- wrap.addEventListener("click", function (event) { if (event.target === wrap) close(); });
2620
- wrap.addEventListener("keydown", function (event) { if (event.key === "Escape") close(); });
2621
-
2622
- function render(note) {
2623
- box.textContent = "";
2624
- var head = document.createElement("div");
2625
- head.style.cssText = "display:flex;justify-content:space-between;align-items:center;margin-bottom:10px";
2626
- var heading = document.createElement("strong");
2627
- heading.textContent = "Players";
2628
- var closeButton = document.createElement("button");
2629
- closeButton.textContent = "Close";
2630
- closeButton.style.cssText = buttonStyle;
2631
- closeButton.onclick = close;
2632
- head.append(heading, closeButton);
2633
- box.appendChild(head);
2634
- if (note) {
2635
- var noteText = document.createElement("p");
2636
- noteText.textContent = note;
2637
- noteText.style.cssText = "margin:0 0 10px;color:#b9e7a6";
2638
- box.appendChild(noteText);
2639
- }
2640
- var others = room.players.filter(function (each) { return !room.me || each.id !== room.me.id; });
2641
- if (!others.length) {
2642
- var none = document.createElement("p");
2643
- none.textContent = "Nobody else is here.";
2644
- none.style.color = "#969ba4";
2645
- box.appendChild(none);
2646
- }
2647
- others.forEach(function (other) {
2648
- var row = document.createElement("div");
2649
- row.style.cssText = "display:flex;gap:6px;align-items:center;padding:10px 0;box-shadow:inset 0 1px 0 #000,inset 0 2px 0 rgba(255,255,255,.05)";
2650
- // Only what this player did themselves shows: a block someone made of them never does.
2651
- var isBlocked = hasBlocked(other.id);
2652
- var isMuted = Boolean(room._muted && room._muted.has(other.id));
2653
- var name = document.createElement("span");
2654
- name.textContent = other.name + (isBlocked ? " (blocked)" : isMuted ? " (muted)" : "");
2655
- name.style.flex = "1";
2656
- row.appendChild(name);
2657
- function button(text, onclick) {
2658
- var each = document.createElement("button");
2659
- each.textContent = text;
2660
- each.style.cssText = buttonStyle;
2661
- each.onclick = onclick;
2662
- row.appendChild(each);
2663
- }
2664
- // A lasting block is undone on zaisma.com/friends; a session one here.
2665
- if (!lastingBlocked.has(other.id)) button(isBlocked ? "Unblock" : isMuted ? "Unmute" : "Mute", function () { isBlocked || isMuted ? room.unmute(other.id) : room.mute(other.id); render(); });
2666
- if (!isBlocked) {
2667
- button("Block", function () {
2668
- room.block(other.id).then(function (isDone) {
2669
- render(!isDone ? undefined : lastingBlocked.has(other.id) ? other.name + " is blocked. Unblock them on your Zaisma friends page." : other.name + " is blocked for this session.");
2670
- }, function () { render(); });
2671
- });
2672
- }
2673
- button("Report", function () { handOff(room, other, close, function () { reportForm(other); }); });
2674
- box.appendChild(row);
2675
- });
2676
- }
2677
-
2678
- function reportForm(reported) {
2679
- box.textContent = "";
2680
- var heading = document.createElement("strong");
2681
- heading.textContent = "Report " + reported.name;
2682
- var select = document.createElement("select");
2683
- select.style.cssText = "display:block;width:100%;margin:12px 0;min-height:40px;padding:0 10px;background:#0d0f12;color:#eceef0;border:1px solid #000;box-shadow:inset 0 2px 5px rgba(0,0,0,.65),0 1px 0 rgba(255,255,255,.06);font:inherit";
2684
- REASONS.forEach(function (reason) { var option = document.createElement("option"); option.value = reason[0]; option.textContent = reason[1]; select.appendChild(option); });
2685
- var note = document.createElement("p");
2686
- note.textContent = "Zaisma reviews reports. Nothing about you is sent. They'll also be muted for you.";
2687
- note.style.cssText = "margin:0 0 12px;color:#969ba4;font-size:13px";
2688
- var send = document.createElement("button");
2689
- send.textContent = "Send report";
2690
- send.style.cssText = buttonStyle + ";background:linear-gradient(180deg,#eef0f2 0%,#b9bcc1 46%,#7d8086 100%);box-shadow:inset 1px 1px 0 #fff,inset -1px -1px 0 #000;color:#0b0b0c";
2691
- var back = document.createElement("button");
2692
- back.textContent = "Back";
2693
- back.style.cssText = buttonStyle + ";margin-left:6px";
2694
- back.onclick = function () { render(); };
2695
- send.onclick = function () {
2696
- send.disabled = true;
2697
- room.mute(reported.id);
2698
- room.report(reported.id, select.value).then(function () { render("Thanks. Your report was sent."); }, function (error) { render(error.message); });
2699
- };
2700
- box.append(heading, select, note, send, back);
2701
- }
2702
-
2703
- render();
2704
- document.body.appendChild(wrap);
2705
- wrap.tabIndex = -1;
2706
- wrap.focus();
2707
- }
2708
-
2709
2580
  var rooms = { list: listRooms, quickJoin: quickJoin, linkedRoomCode: roomCodeFromPage(), linkedServerUrl: roomCodeFromPage() ? serverUrlFromPage() : null, isLinkedAsSpectator: watchFromPage() };
2710
2581
  /*
2711
2582
  * Friend requests (zaisma.d.ts: Friends.sendRequest): the Zaisma page asks the player before anything is
@@ -2753,5 +2624,5 @@
2753
2624
  });
2754
2625
 
2755
2626
  var friends = { list: listFriends, setRoom: setOwnRoom, sendRequest: sendFriendRequest };
2756
- window.Zaisma = { sdkVersionNumber: 1, gameSlug: config.game || null, player: player, setUpOffZaisma: setUpOffZaisma, joinRoom: joinRoom, rooms: rooms, server: server, invite: invite, checkChat: checkChat, openPlayersPanel: openPlayersPanel, links: links, items: items, friends: friends, saves: saves, versions: versions, page: zaismaPage };
2627
+ window.Zaisma = { sdkVersionNumber: 1, gameSlug: config.game || null, player: player, setUpOffZaisma: setUpOffZaisma, joinRoom: joinRoom, rooms: rooms, server: server, invite: invite, checkChat: checkChat, links: links, items: items, friends: friends, saves: saves, versions: versions, page: zaismaPage };
2757
2628
  })();
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
@@ -12,3 +12,4 @@ export * from "./pass.ts";
12
12
  export * from "./names.ts";
13
13
  export * from "./chat.ts";
14
14
  export * from "./relay.ts";
15
+ export * from "./doors.ts";
package/src/internal.js CHANGED
@@ -14,3 +14,4 @@ export * from "./pass.js";
14
14
  export * from "./names.js";
15
15
  export * from "./chat.js";
16
16
  export * from "./relay.js";
17
+ export * from "./doors.js";
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
- switch (message.type) {
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 === "probe_answer" && this.answer(spectatorId, message.probeNumber))
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";