@zaisma/game-server 1.12.0 → 1.14.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/sdk/zaisma.d.ts CHANGED
@@ -26,7 +26,9 @@
26
26
  *
27
27
  * Player passes, for rooms and your own servers, need the game played on its zaisma.com page, which
28
28
  * checks the browser on Play: joining fails with `error.code` "unchecked" when it couldn't, or when the
29
- * game was opened anywhere else, and "not_on_zaisma" when the game isn't hosted on Zaisma at all.
29
+ * game was opened anywhere else. A game played off Zaisma (exported, on another site, wrapped for a store)
30
+ * plays online against your own game server only: call {@link Zaisma.setUpOffZaisma} first, and give
31
+ * joinRoom and server.request a serverUrl; without them they fail with "not_on_zaisma".
30
32
  *
31
33
  * @example
32
34
  * const room = await Zaisma.joinRoom(); // joins ?room=CODE if the link has one, else makes a room
@@ -140,8 +142,9 @@ declare namespace Zaisma {
140
142
  * refused (the game server's canJoin said no; the message is its refusalMessage), hosting_paused (the
141
143
  * game's hosting is paused this month: its free allowance or its developer's budget or credit is used). No player pass: unchecked, or account_needed for a
142
144
  * guest when the game needs accounts (see Zaisma.player.getPass()). The
143
- * call is set up wrong: unknown_option, bad_room_code, bad_server_url, bad_transport, not_on_zaisma (the game isn't hosted
144
- * on Zaisma, serverUrl or not; serverUrl points a game hosted on Zaisma at your own server). Or connection_closed (the connection closed first, say the game left or the player
145
+ * call is set up wrong: unknown_option, bad_room_code, bad_server_url, bad_transport, not_on_zaisma (the game is played
146
+ * off Zaisma without a serverUrl, or before {@link setUpOffZaisma}). Off Zaisma, bad_player_token: your server
147
+ * didn't take the playerToken (get the player a new one and call setUpOffZaisma again). Or connection_closed (the connection closed first, say the game left or the player
145
148
  * joined from another tab) and request_failed (Zaisma didn't answer). {@link ErrorCode} has them all.
146
149
  *
147
150
  * @example
@@ -154,6 +157,40 @@ declare namespace Zaisma {
154
157
  */
155
158
  function joinRoom(joinOptions?: JoinOptions): Promise<Room>;
156
159
 
160
+ /**
161
+ * Set up a game played off Zaisma (exported, on another site, wrapped for a store) to play online against
162
+ * your own game server. Off Zaisma there's no Zaisma pass, so joinRoom and server.request with a serverUrl
163
+ * send the playerToken in its place, and your server says who the player is (startServer's identifyPlayer):
164
+ * a Sign in with Zaisma ID token (same player ID and name as on Zaisma), your game's own token, or null for
165
+ * a guest when your server lets guests in (outsideGuestsEnabled). They join with kind "outside_zaisma",
166
+ * in the same rooms as players from your Zaisma page.
167
+ *
168
+ * On a Zaisma page it changes nothing and resolves { isOffZaisma: false }, so the same code runs in both
169
+ * places. Call it again to replace the token (a new sign-in, or signing out); rooms use the newest one when
170
+ * they reconnect. Off Zaisma there's no Zaisma bar, saves, buy sheet, friends or room lists.
171
+ *
172
+ * Fails with error.code unknown_option, bad_game_slug (not a game slug) and bad_player_token (not text up to 8 KB, or null).
173
+ *
174
+ * @example
175
+ * await Zaisma.setUpOffZaisma({ gameSlug: "star-raiders", playerToken: idToken });
176
+ * const room = await Zaisma.joinRoom({ serverUrl: "wss://play.example.com" });
177
+ */
178
+ function setUpOffZaisma(setUpOptions: SetUpOffZaismaOptions): Promise<OffZaismaSetup>;
179
+
180
+ /** Options for {@link setUpOffZaisma}. */
181
+ interface SetUpOffZaismaOptions {
182
+ /** Your game's slug, as its Zaisma link names it, like "star-raiders": the address your server's rooms and requests take. */
183
+ gameSlug: string;
184
+ /** The player's token for your own server, text up to 8 KB: a Sign in with Zaisma ID token or your game's own. Null or left out for a guest. */
185
+ playerToken?: string | null;
186
+ }
187
+
188
+ /** What {@link setUpOffZaisma} answers. */
189
+ interface OffZaismaSetup {
190
+ /** False on a Zaisma page (nothing changed); true elsewhere, where joins and requests now carry the playerToken. */
191
+ isOffZaisma: boolean;
192
+ }
193
+
157
194
  /** Room browsing: the game's listed rooms, which show on its Zaisma page and to the game itself. */
158
195
  const rooms: Rooms;
159
196
 
@@ -982,8 +1019,9 @@ declare namespace Zaisma {
982
1019
  * seconds; you see why with the game server's errors), hosting_paused (the game's hosting
983
1020
  * on Zaisma is paused this month: its free allowance or its developer's budget or credit is used), too_big, bad_request (not plain JSON), slow_down
984
1021
  * (over about 120 a minute from one address), bad_pass, account_needed, unchecked,
985
- * game_unavailable, bad_origin, busy, bad_server_url, not_on_zaisma (the game isn't hosted
986
- * on Zaisma, serverUrl or not), unknown_option and request_failed.
1022
+ * game_unavailable, bad_origin, busy, bad_server_url, not_on_zaisma (the game is played off
1023
+ * Zaisma without a serverUrl, or before {@link Zaisma.setUpOffZaisma}), bad_player_token (off Zaisma,
1024
+ * your server didn't take the playerToken), unknown_option and request_failed.
987
1025
  *
988
1026
  * @example
989
1027
  * const top = await Zaisma.server.request({ type: "leaderboard" });
@@ -1347,6 +1385,15 @@ declare namespace Zaisma {
1347
1385
  * room.on("versionUpdate", ({ appliesAt }) => showBanner(`Updating at ${new Date(appliesAt).toLocaleTimeString()}`));
1348
1386
  */
1349
1387
  versionUpdate: (versionUpdate: VersionUpdate) => void;
1388
+ /**
1389
+ * A room message reached this room and its game server has no onRoomMessage, so every player's game hears
1390
+ * it: from another room of the game (senderKind "room", fromRoomCode), its game server outside rooms
1391
+ * ("server") or the developer (the API and MCP, "developer"), like an announcement or a live event starting.
1392
+ * Zaisma shows nothing: your game decides. A game server with onRoomMessage takes it instead.
1393
+ * @example
1394
+ * room.on("roomMessage", ({ message }) => { if (message.kind === "blood_moon") startBloodMoon(); });
1395
+ */
1396
+ roomMessage?: (roomMessage: RoomMessage) => void;
1350
1397
  /**
1351
1398
  * Another player's chat message reached this player ({@link Room.chat}). Messages from players this
1352
1399
  * player muted or blocked never come. hasSwearing and hasSlur flag what Zaisma allows: hide or blur
@@ -1438,6 +1485,18 @@ declare namespace Zaisma {
1438
1485
  answerTimeoutSec: number;
1439
1486
  }
1440
1487
 
1488
+ /** A room message, as the room's "roomMessage" event hands it over. */
1489
+ interface RoomMessage {
1490
+ /** The message as sent: plain JSON, up to 16 KB. */
1491
+ message: unknown;
1492
+ /** Who sent it: another room's game server, the game server outside rooms, or the developer. */
1493
+ senderKind: "room" | "server" | "developer";
1494
+ /** The code of the room that sent it, or null unless senderKind is "room". */
1495
+ fromRoomCode: string | null;
1496
+ /** When it was sent, an ISO time. */
1497
+ sentAt: string;
1498
+ }
1499
+
1441
1500
  /** A match moving to a version published "now" (the room's "versionUpdate" event). */
1442
1501
  interface VersionUpdate {
1443
1502
  /** The version it moves to, like "version_k2v9q4x7m1p3w8r5t6y0". */
@@ -1467,8 +1526,11 @@ declare namespace Zaisma {
1467
1526
  awayReason: "idle" | "hidden" | "lost" | null;
1468
1527
  }
1469
1528
 
1470
- /** A signed-in Zaisma account, or a guest. More values may come; treat an unknown one as "guest". */
1471
- type PlayerKind = "guest" | "account";
1529
+ /**
1530
+ * A signed-in Zaisma account, a guest, or a player your own server let in from outside Zaisma
1531
+ * (outside_zaisma: a game played off Zaisma, {@link setUpOffZaisma}). More values may come; treat an unknown one as "guest".
1532
+ */
1533
+ type PlayerKind = "guest" | "account" | "outside_zaisma";
1472
1534
 
1473
1535
  /** What anyone sees of an account on its profile, zaisma.com/@username ({@link player.getProfile}). */
1474
1536
  interface PlayerProfile {
@@ -1606,6 +1668,7 @@ declare namespace Zaisma {
1606
1668
  * - game_not_open_to_player: the account is younger than the game's minimum age (set on the dashboard); the game never learns its age.
1607
1669
  * - game_unavailable: the game can't be played right now.
1608
1670
  * - bad_pass: the player pass wasn't accepted (the SDK gets a new one and tries once more).
1671
+ * - bad_player_token: off Zaisma, your own server didn't take the playerToken ({@link setUpOffZaisma}); the game gets the player a new one.
1609
1672
  * - bad_origin: the connection didn't come from the game's own page.
1610
1673
  * - too_big: a message over 16 KB, or joinData over 4 KB.
1611
1674
  * - too_fast: more messages than the room takes (about 30 a second).
@@ -1621,6 +1684,8 @@ declare namespace Zaisma {
1621
1684
  * - chat_not_allowed, chat_empty, chat_too_long, chat_too_fast: {@link Room.chat} refused (they reject its promise, not the "error" event); chat_not_here: the room's server doesn't take chat.
1622
1685
  *
1623
1686
  * More codes may come within v1: handle one you don't know like an error you can show (its message).
1687
+ *
1688
+ * More values may come; treat an unknown one as "request_failed".
1624
1689
  */
1625
1690
  type RoomErrorCode =
1626
1691
  | "room_full"
@@ -1633,6 +1698,7 @@ declare namespace Zaisma {
1633
1698
  | "game_not_open_to_player"
1634
1699
  | "game_unavailable"
1635
1700
  | "bad_pass"
1701
+ | "bad_player_token"
1636
1702
  | "bad_origin"
1637
1703
  | "too_big"
1638
1704
  | "too_fast"
@@ -1658,6 +1724,7 @@ declare namespace Zaisma {
1658
1724
  * - bad_request: not plain JSON with the player's pass.
1659
1725
  * - too_big: a request over 16 KB.
1660
1726
  * - bad_pass: the player pass wasn't accepted (the SDK gets a new one and tries once more).
1727
+ * - bad_player_token: off Zaisma, your own server didn't take the playerToken ({@link setUpOffZaisma}); the game gets the player a new one.
1661
1728
  * - account_needed: the player is a guest, and the game needs a Zaisma account for multiplayer or the whole game.
1662
1729
  * - game_not_open_to_player: the account is younger than the game's minimum age (set on the dashboard); the game never learns its age.
1663
1730
  * - game_unavailable: the game can't be played right now.
@@ -1667,11 +1734,14 @@ declare namespace Zaisma {
1667
1734
  * - slow_down: too many requests from this address; try again in a minute.
1668
1735
  * - bad_origin: the request didn't come from the game's own page.
1669
1736
  * - busy: Zaisma couldn't answer right now; try again soon.
1737
+ *
1738
+ * More values may come; treat an unknown one as "request_failed".
1670
1739
  */
1671
1740
  type RequestErrorCode =
1672
1741
  | "bad_request"
1673
1742
  | "too_big"
1674
1743
  | "bad_pass"
1744
+ | "bad_player_token"
1675
1745
  | "account_needed"
1676
1746
  | "game_not_open_to_player"
1677
1747
  | "game_unavailable"
@@ -1690,7 +1760,8 @@ declare namespace Zaisma {
1690
1760
  * - zaisma_rooms_not_used: the game doesn't use Zaisma rooms.
1691
1761
  * - bad_report: a report whose reason isn't a {@link ReportReason}, or whose player ID isn't one.
1692
1762
  * - bad_origin: the call didn't come from the game's own page.
1693
- * - not_on_zaisma: the game isn't hosted on Zaisma, so it has no player passes, sign-in or room lists, and joinRoom needs a serverUrl.
1763
+ * - not_on_zaisma: the game is played off Zaisma, so it has no player passes, sign-in or room lists; joinRoom and server.request need {@link setUpOffZaisma} and a serverUrl.
1764
+ * - bad_game_slug: {@link setUpOffZaisma}'s gameSlug isn't a game slug: 1 to 40 lowercase letters, digits or hyphens.
1694
1765
  * - sign_in_declined: the player chose Not now when the game asked them to sign in ({@link player.signIn}) or showed the "Keep your progress" card ({@link player.offerAccount}).
1695
1766
  * - account_offer_too_soon: the "Keep your progress" card already showed this visit, or the player chose Not now in the last 3 days ({@link player.offerAccount}).
1696
1767
  * - cloud_saves_off: the game has cloud saves off, so it has no saves on players' accounts ({@link saves}).
@@ -1731,6 +1802,7 @@ declare namespace Zaisma {
1731
1802
  | "bad_report"
1732
1803
  | "bad_origin"
1733
1804
  | "not_on_zaisma"
1805
+ | "bad_game_slug"
1734
1806
  | "sign_in_declined"
1735
1807
  | "account_offer_too_soon"
1736
1808
  | "cloud_saves_off"
package/sdk/zaisma.js CHANGED
@@ -6,8 +6,11 @@
6
6
  * together; test/node/sdk-types.test.ts checks that they match.
7
7
  *
8
8
  * On Zaisma, the games Worker defines __ZAISMA__ before this file with the rooms address, the game and
9
- * the site. A game that isn't hosted on Zaisma gets "not_on_zaisma" for online play, serverUrl or not; serverUrl points a
10
- * game hosted on Zaisma at your own server.
9
+ * the site; serverUrl points a game played there at your own server. Played anywhere else (exported, on
10
+ * another site, wrapped for a store), a game plays online against your own server only: after
11
+ * Zaisma.setUpOffZaisma({ gameSlug, playerToken }), joinRoom and server.request with a serverUrl send the
12
+ * playerToken in place of a Zaisma pass, and your server says who that is (its identifyPlayer). Saves, the buy
13
+ * sheet, friends and room lists stay on Zaisma, and so does the Zaisma bar.
11
14
  */
12
15
  (function () {
13
16
  "use strict";
@@ -18,6 +21,7 @@
18
21
  var OWN_SERVER_URL = /^(wss|https):\/\/[^/]/;
19
22
 
20
23
  // What each call takes. Anything else is refused, so a misspelled or old option never goes quietly unused.
24
+ var SET_UP_OFF_ZAISMA_OPTIONS = ["gameSlug", "playerToken"];
21
25
  var JOIN_OPTIONS = ["roomCode", "serverUrl", "transport", "isListed", "friendJoinsEnabled", "name", "labels", "joinData", "idleTimeoutSec", "idleCheckEnabled", "isSpectator"];
22
26
  // quickJoin finds Zaisma rooms, so it takes no serverUrl.
23
27
  var QUICK_JOIN_OPTIONS = ["isListed", "friendJoinsEnabled", "name", "labels", "joinData", "idleTimeoutSec", "idleCheckEnabled", "region"];
@@ -33,7 +37,7 @@
33
37
  /** A fast lane that isn't up in this long gives way to the WebSocket, for the rest of the room's life. */
34
38
  var FAST_LANE_CONNECT_TIMEOUT_MS = 3000;
35
39
  var ROOM_INFO_FIELDS = ["name", "labels"];
36
- var ROOM_EVENTS = ["message", "bytes", "state", "players", "connectionStatus", "ended", "ending", "endingCancelled", "kicked", "settings", "move", "moved", "idle", "away", "back", "muted", "error", "versionUpdate", "chat"];
40
+ var ROOM_EVENTS = ["message", "bytes", "state", "players", "connectionStatus", "ended", "ending", "endingCancelled", "kicked", "settings", "move", "moved", "idle", "away", "back", "muted", "error", "versionUpdate", "chat", "roomMessage"];
37
41
  /** Chat (room.chat): the longest message, the lines a room keeps in memory for reports, and how many of them a report carries. */
38
42
  var MAXIMUM_CHAT_CHARACTER_COUNT = 2000;
39
43
  var KEPT_CHAT_LINE_COUNT = 50;
@@ -76,6 +80,13 @@
76
80
  var DEFAULT_SMOOTHING_DELAY_MS = 100;
77
81
  var KEPT_VIEWS_DURATION_MS = 1000;
78
82
 
83
+ /** A game's slug, as its Zaisma link names it. */
84
+ var GAME_SLUG = /^[a-z0-9-]{1,40}$/;
85
+ /** The longest playerToken a game off Zaisma sends its own server, as UTF-8. */
86
+ var MAXIMUM_PLAYER_TOKEN_SIZE_BYTES = 8 * 1024;
87
+ /** Off Zaisma, after Zaisma.setUpOffZaisma: { gameSlug, playerToken } for joins and requests to the game's own server. */
88
+ var offZaisma = null;
89
+
79
90
  /** An Error with a code, as the SDK's errors all have. */
80
91
  function codedError(code, message) {
81
92
  var error = new Error(message);
@@ -88,6 +99,36 @@
88
99
  return codedError("not_on_zaisma", message);
89
100
  }
90
101
 
102
+ /**
103
+ * Off Zaisma, the game's own server can't take a Zaisma pass: what to do instead, or null when it can go ahead
104
+ * (on Zaisma, or set up off it). serverUrl: the call's own server, which off Zaisma it needs.
105
+ */
106
+ function offZaismaRefusal(serverUrl, call) {
107
+ if (config.rooms) return null;
108
+ if (!serverUrl) return notOnZaisma("Zaisma's rooms serve games played on Zaisma; off Zaisma, give serverUrl, the address of your own game server, to " + call + ".");
109
+ if (!offZaisma) return notOnZaisma("Off Zaisma, call Zaisma.setUpOffZaisma({ gameSlug, playerToken }) first: your own server knows the player by the playerToken, and serverUrl points " + call + " at it.");
110
+ return null;
111
+ }
112
+
113
+ /**
114
+ * Set up a game played off Zaisma (zaisma.d.ts: Zaisma.setUpOffZaisma). On a Zaisma page it changes nothing,
115
+ * so the same code runs in both places; elsewhere it keeps the game's slug and its player's token for joins
116
+ * and requests to the game's own server. Called again, it replaces the token (a new sign-in, or signing out).
117
+ */
118
+ function setUpOffZaisma(setUpOptions) {
119
+ var refused = unknownOption(setUpOptions, SET_UP_OFF_ZAISMA_OPTIONS, "Zaisma.setUpOffZaisma");
120
+ if (refused) return Promise.reject(refused);
121
+ if (config.rooms) return Promise.resolve({ isOffZaisma: false });
122
+ setUpOptions = setUpOptions || {};
123
+ var gameSlug = setUpOptions.gameSlug;
124
+ if (typeof gameSlug !== "string" || !GAME_SLUG.test(gameSlug)) return Promise.reject(codedError("bad_game_slug", "gameSlug is your game's slug, as its Zaisma link names it: 1 to 40 lowercase letters, digits or hyphens, like \"my-game\"."));
125
+ var playerToken = setUpOptions.playerToken === undefined ? null : setUpOptions.playerToken;
126
+ var isToken = typeof playerToken === "string" && playerToken.length > 0 && new TextEncoder().encode(playerToken).length <= MAXIMUM_PLAYER_TOKEN_SIZE_BYTES;
127
+ if (playerToken !== null && !isToken) return Promise.reject(codedError("bad_player_token", "playerToken is the player's token for your own server, as text up to 8 KB (a Sign in with Zaisma ID token, or your game's own), or null for a guest."));
128
+ offZaisma = { gameSlug: gameSlug, playerToken: playerToken };
129
+ return Promise.resolve({ isOffZaisma: true });
130
+ }
131
+
91
132
  /** The error for options a call doesn't take, naming the ones it does, or null when they're all fine. */
92
133
  function unknownOption(given, valid, call) {
93
134
  if (given === undefined || given === null) return null;
@@ -435,6 +476,8 @@
435
476
  // What this connection uses now: "webtransport" once the fast lane opens, else "websocket".
436
477
  this.transport = "websocket";
437
478
  this._pass = setup.pass;
479
+ // Off Zaisma: the hello carries the playerToken Zaisma.setUpOffZaisma holds now, so a reconnect uses a refreshed one.
480
+ this._isOffZaisma = setup.isOffZaisma === true;
438
481
  this._made = { isListed: this.isListed, friendJoinsEnabled: setup.friendJoinsEnabled === false ? false : undefined, name: setup.name, labels: setup.labels };
439
482
  this._joinData = setup.joinData === undefined ? null : setup.joinData;
440
483
  this._serverUrl = setup.serverUrl;
@@ -653,7 +696,7 @@
653
696
  };
654
697
 
655
698
  // Codes that mean this room won't take the player, so joining fails instead of trying again.
656
- var REFUSED = ["room_full", "room_closed", "ended", "bad_pass", "too_big", "game_unavailable", "kept_out", "refused", "hosting_paused", "account_needed", "game_not_open_to_player", "blocked_player_here", "other_version", "spectators_full", "room_not_running", "already_playing", "bad_origin"];
699
+ var REFUSED = ["room_full", "room_closed", "ended", "bad_pass", "bad_player_token", "too_big", "game_unavailable", "kept_out", "refused", "hosting_paused", "account_needed", "game_not_open_to_player", "blocked_player_here", "other_version", "spectators_full", "room_not_running", "already_playing", "bad_origin"];
657
700
 
658
701
  /**
659
702
  * Whether a Zaisma room on another version (other_version) has the page reload the game at it: on the
@@ -685,7 +728,9 @@
685
728
  room.transport = isFastLane ? "webtransport" : "websocket";
686
729
  room._pingAtMs = room._pongAtMs = room._heardAtMs = Date.now();
687
730
  room._probeIntervalSec = null;
688
- var hello = { type: "hello", protocolVersionNumber: PROTOCOL_VERSION_NUMBER, pass: room._pass, room: room._made, joinData: room._joinData, idleTimeoutSec: room._clientIdle.idleTimeoutSec, idleCheckEnabled: room._clientIdle.idleCheckEnabled };
731
+ var hello = { type: "hello", protocolVersionNumber: PROTOCOL_VERSION_NUMBER, room: room._made, joinData: room._joinData, idleTimeoutSec: room._clientIdle.idleTimeoutSec, idleCheckEnabled: room._clientIdle.idleCheckEnabled };
732
+ if (room._isOffZaisma) hello.playerToken = offZaisma.playerToken;
733
+ else hello.pass = room._pass;
689
734
  // quickJoin's first try at a room: rather another room than one with someone this player blocked.
690
735
  if (room._avoidsBlockedPlayers) hello.avoidBlockedPlayers = true;
691
736
  // Any quickJoin: Zaisma learns the game joins players by itself, for its Rooms tab.
@@ -795,6 +840,10 @@
795
840
  } else if (message.type === "ending_cancelled") {
796
841
  room.endsAt = null;
797
842
  room._emit("endingCancelled");
843
+ } else if (message.type === "room_message") {
844
+ // A room message for a room whose game server has no onRoomMessage: another room, the game server or the developer sent it.
845
+ var roomMessage = message.roomMessage || {};
846
+ room._emit("roomMessage", { message: roomMessage.message, senderKind: roomMessage.senderKind, fromRoomCode: roomMessage.fromRoomCode || null, sentAt: roomMessage.sentAt });
798
847
  } else if (message.type === "version_update") {
799
848
  var versionUpdate = { versionId: message.versionId, appliesAt: message.appliesAt, isReloading: message.isReloading === true };
800
849
  if (message.step === "now") room._isReloadingForVersion = versionUpdate.isReloading;
@@ -1523,7 +1572,9 @@
1523
1572
 
1524
1573
  function connect(roomCode, joinOptions, isRetry) {
1525
1574
  var serverUrl = joinOptions.serverUrl ? String(joinOptions.serverUrl).replace(/\/+$/, "") : null;
1526
- if (!serverUrl && !config.rooms) return Promise.reject(notOnZaisma("Player passes come from Zaisma, so rooms work only in games hosted on Zaisma, in Zaisma rooms or on your own server (serverUrl)."));
1575
+ var offRefusal = offZaismaRefusal(serverUrl, "Zaisma.joinRoom");
1576
+ if (offRefusal) return Promise.reject(offRefusal);
1577
+ var isOffZaisma = !config.rooms;
1527
1578
  var idleTimeoutSec = joinOptions.idleTimeoutSec;
1528
1579
  if (idleTimeoutSec !== undefined && !(typeof idleTimeoutSec === "number" && idleTimeoutSec >= MINIMUM_IDLE_TIMEOUT_SEC && idleTimeoutSec <= MAXIMUM_IDLE_TIMEOUT_SEC)) return Promise.reject(codedError("bad_idle_timeout", "idleTimeoutSec runs from " + MINIMUM_IDLE_TIMEOUT_SEC + " to " + MAXIMUM_IDLE_TIMEOUT_SEC + "."));
1529
1580
  if (serverUrl && !SERVER_URL.test(serverUrl)) return Promise.reject(codedError("bad_server_url", "serverUrl takes a full address starting with wss://, ws://, https:// or http://, like \"wss://play.example.com\"."));
@@ -1535,12 +1586,14 @@
1535
1586
  : serverUrl
1536
1587
  ? Promise.resolve(randomRoomCode())
1537
1588
  : post("/v1/rooms", { gameSlug: config.game }).then(function (answer) { return answer.roomCode; });
1538
- return Promise.all([roomCodeReady, getPass()]).then(function (ready) {
1589
+ // Off Zaisma there's no pass: the hello carries the playerToken instead.
1590
+ return Promise.all([roomCodeReady, isOffZaisma ? null : getPass()]).then(function (ready) {
1539
1591
  var base = (serverUrl || config.rooms).replace(/^http/, "ws");
1540
- var path = "/v1/rooms/" + config.game + "/" + ready[0];
1592
+ var path = "/v1/rooms/" + (isOffZaisma ? offZaisma.gameSlug : config.game) + "/" + ready[0];
1541
1593
  var room = new Room({
1542
1594
  roomCode: ready[0],
1543
- pass: ready[1].pass,
1595
+ pass: isOffZaisma ? null : ready[1].pass,
1596
+ isOffZaisma: isOffZaisma,
1544
1597
  url: base + path,
1545
1598
  // The fast lane reaches only the game's own secure servers: Zaisma's rooms take the WebSocket, whatever transport says.
1546
1599
  fastLaneUrl: joinOptions.transport === "webtransport" && serverUrl && OWN_SERVER_URL.test(serverUrl) ? serverUrl.replace(/^wss:/, "https:") + path : null,
@@ -1562,8 +1615,8 @@
1562
1615
  room._ready = { resolve: resolve, reject: reject };
1563
1616
  room._connect();
1564
1617
  }).catch(function (error) {
1565
- // A pass this tab kept from before Zaisma changed its keys: get a new one and try once more.
1566
- if (error.code !== "bad_pass" || isRetry) throw error;
1618
+ // A pass this tab kept from before Zaisma changed its keys: get a new one and try once more. Off Zaisma, only the game can get a new token.
1619
+ if (error.code !== "bad_pass" || isRetry || isOffZaisma) throw error;
1567
1620
  try { sessionStorage.removeItem(passKey); } catch (storageError) { /* fine */ }
1568
1621
  return connect(room.roomCode, joinOptions, true);
1569
1622
  });
@@ -1766,13 +1819,15 @@
1766
1819
 
1767
1820
  /**
1768
1821
  * A request to the game's own game server, its onRequest, outside any room: what it answered
1769
- * (zaisma.d.ts: Zaisma.server.request). The player's pass goes with it, so the server knows who's asking.
1822
+ * (zaisma.d.ts: Zaisma.server.request). The player's pass goes with it, so the server knows who's asking;
1823
+ * off Zaisma, the playerToken Zaisma.setUpOffZaisma holds.
1770
1824
  */
1771
1825
  function serverRequest(request, requestOptions, isRetry) {
1772
1826
  var refused = unknownOption(requestOptions, REQUEST_OPTIONS, "Zaisma.server.request");
1773
1827
  if (refused) return Promise.reject(refused);
1774
1828
  var serverUrl = requestOptions && requestOptions.serverUrl ? String(requestOptions.serverUrl).replace(/\/+$/, "") : null;
1775
- if (!serverUrl && !config.rooms) return Promise.reject(notOnZaisma("Player passes come from Zaisma, so Zaisma.server.request works only in games hosted on Zaisma, to Zaisma's servers or to your own (serverUrl)."));
1829
+ var offRefusal = offZaismaRefusal(serverUrl, "Zaisma.server.request");
1830
+ if (offRefusal) return Promise.reject(offRefusal);
1776
1831
  if (serverUrl && !SERVER_URL.test(serverUrl)) return Promise.reject(codedError("bad_server_url", "serverUrl takes a full address starting with https://, http://, wss:// or ws://, like \"https://play.example.com\"."));
1777
1832
  var requestText;
1778
1833
  try {
@@ -1783,13 +1838,17 @@
1783
1838
  if (requestText === undefined) return Promise.reject(codedError("bad_request", "A request is plain JSON: numbers, text, true or false, null, lists and plain objects, like { type: \"buy\", itemId: \"sword\" }."));
1784
1839
  if (new TextEncoder().encode(requestText).length > MAXIMUM_REQUEST_SIZE_BYTES) return Promise.reject(codedError("too_big", "Requests are up to 16 KB as JSON."));
1785
1840
  var base = (serverUrl || config.rooms).replace(/^ws/, "http");
1786
- return getPass().then(function (mine) {
1787
- return fetch(base + "/v1/requests/" + config.game, { method: "POST", headers: { "Content-Type": "text/plain" }, body: JSON.stringify({ pass: mine.pass, request: JSON.parse(requestText) }) }).then(answered, requestFailed);
1841
+ var isOffZaisma = !config.rooms;
1842
+ // Off Zaisma there's no pass: the playerToken goes in its place.
1843
+ var sender = isOffZaisma ? Promise.resolve({ playerToken: offZaisma.playerToken }) : getPass().then(function (mine) { return { pass: mine.pass }; });
1844
+ return sender.then(function (who) {
1845
+ who.request = JSON.parse(requestText);
1846
+ return fetch(base + "/v1/requests/" + (isOffZaisma ? offZaisma.gameSlug : config.game), { method: "POST", headers: { "Content-Type": "text/plain" }, body: JSON.stringify(who) }).then(answered, requestFailed);
1788
1847
  }).then(function (answer) {
1789
1848
  return answer.answer;
1790
1849
  }, function (error) {
1791
- // A pass this tab kept from before Zaisma changed its keys: get a new one and try once more.
1792
- if (error.code !== "bad_pass" || isRetry) throw error;
1850
+ // A pass this tab kept from before Zaisma changed its keys: get a new one and try once more. Off Zaisma, only the game can get a new token.
1851
+ if (error.code !== "bad_pass" || isRetry || isOffZaisma) throw error;
1793
1852
  try { sessionStorage.removeItem(passKey); } catch (storageError) { /* fine */ }
1794
1853
  return serverRequest(request, requestOptions, true);
1795
1854
  });
@@ -2694,5 +2753,5 @@
2694
2753
  });
2695
2754
 
2696
2755
  var friends = { list: listFriends, setRoom: setOwnRoom, sendRequest: sendFriendRequest };
2697
- window.Zaisma = { sdkVersionNumber: 1, gameSlug: config.game || null, player: player, joinRoom: joinRoom, rooms: rooms, server: server, invite: invite, checkChat: checkChat, openPlayersPanel: openPlayersPanel, links: links, items: items, friends: friends, saves: saves, versions: versions, page: zaismaPage };
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 };
2698
2757
  })();
package/src/chat.d.ts CHANGED
@@ -9,6 +9,11 @@
9
9
  * checkChat sends the players' passes (as your server received them) and the sender's last few messages to
10
10
  * Zaisma (POST <issuer>/v1/chat/check), so a phone number sent a few digits at a time is caught too.
11
11
  *
12
+ * A player your server let in from outside Zaisma (kind outside_zaisma) has no pass: give null in their place.
13
+ * Zaisma counts them as strangers of unknown age, so a message between one and an under-18 account is stopped
14
+ * (not_allowed_to_chat); between them and adults it's checked as between adults. A message among players from
15
+ * outside Zaisma only is allowed without asking Zaisma: none of its players is in it, so your own rules apply.
16
+ *
12
17
  * It's for your own server only. In Zaisma rooms the room filters the chat it delivers itself, so a game
13
18
  * server running there never sees passes, and checkChat throws a ChatCheckError (own_server_only).
14
19
  */
@@ -36,10 +41,10 @@ export interface ChatCheck {
36
41
  export interface ChatCheckOptions {
37
42
  /** The message. */
38
43
  text: string;
39
- /** The sender's player pass, the signed token as your server received it. */
40
- senderPass: string;
41
- /** The passes of everyone it would reach, up to 50. */
42
- receiverPasses: string[];
44
+ /** The sender's player pass, the signed token as your server received it; null for a player from outside Zaisma. */
45
+ senderPass: string | null;
46
+ /** The passes of everyone it would reach, up to 50; null for each player from outside Zaisma. */
47
+ receiverPasses: (string | null)[];
43
48
  /** Check chat between adults too, as for under-18s; each message Zaisma's AI looks at counts on your hosting meter. False by default. */
44
49
  isAdultFilterEnabled?: boolean;
45
50
  }
package/src/chat.js CHANGED
@@ -9,6 +9,11 @@
9
9
  * checkChat sends the players' passes (as your server received them) and the sender's last few messages to
10
10
  * Zaisma (POST <issuer>/v1/chat/check), so a phone number sent a few digits at a time is caught too.
11
11
  *
12
+ * A player your server let in from outside Zaisma (kind outside_zaisma) has no pass: give null in their place.
13
+ * Zaisma counts them as strangers of unknown age, so a message between one and an under-18 account is stopped
14
+ * (not_allowed_to_chat); between them and adults it's checked as between adults. A message among players from
15
+ * outside Zaisma only is allowed without asking Zaisma: none of its players is in it, so your own rules apply.
16
+ *
12
17
  * It's for your own server only. In Zaisma rooms the room filters the chat it delivers itself, so a game
13
18
  * server running there never sees passes, and checkChat throws a ChatCheckError (own_server_only).
14
19
  */
@@ -84,15 +89,26 @@ function rememberText(senderPlayerId, text, nowMs) {
84
89
  export async function checkChat({ text, senderPass, receiverPasses }) {
85
90
  // Read apart from the destructuring, which is v1's signature as it's locked (test/contracts).
86
91
  const isAdultFilterEnabled = arguments[0]?.isAdultFilterEnabled === true;
87
- const claims = readClaims(String(senderPass ?? ""));
88
- if (typeof claims.sub !== "string" || !isZaismaIssuer(claims.iss) || !Array.isArray(receiverPasses))
92
+ if (!Array.isArray(receiverPasses) || !receiverPasses.every((pass) => pass === null || typeof pass === "string") || !(senderPass === null || typeof senderPass === "string"))
89
93
  return UNCHECKED;
90
94
  const message = String(text ?? "");
91
95
  if (message.length > CHAT_CHECK_LIMITS.maximumMessageCharacterCount)
92
96
  return TOO_LONG;
97
+ // Among players from outside Zaisma only: no Zaisma player is in it, so the server's own rules apply, and Zaisma never hears of it.
98
+ const firstPass = [senderPass, ...receiverPasses].find((pass) => typeof pass === "string");
99
+ if (firstPass === undefined)
100
+ return { isAllowed: true, reason: null, message: null, hasSwearing: false, hasSlur: false };
101
+ // Where to ask comes from a pass, the sender's when they have one: they all come from the same issuer for the same game.
102
+ const claims = readClaims(firstPass);
103
+ const senderClaims = senderPass === null ? null : readClaims(senderPass);
104
+ if (typeof claims.sub !== "string" || !isZaismaIssuer(claims.iss) || (senderClaims && typeof senderClaims.sub !== "string"))
105
+ return UNCHECKED;
93
106
  const nowMs = Date.now();
94
- const recentTexts = (recentBySender.get(claims.sub) ?? []).filter((sent) => nowMs - sent.sentAtMs < CHAT_CHECK_LIMITS.recentTextDurationMs).map((sent) => sent.text);
95
- rememberText(claims.sub, message, nowMs);
107
+ // A sender from outside Zaisma has no player ID Zaisma knows, so their last messages aren't sent along.
108
+ const senderPlayerId = senderClaims ? senderClaims.sub : null;
109
+ const recentTexts = senderPlayerId ? (recentBySender.get(senderPlayerId) ?? []).filter((sent) => nowMs - sent.sentAtMs < CHAT_CHECK_LIMITS.recentTextDurationMs).map((sent) => sent.text) : [];
110
+ if (senderPlayerId)
111
+ rememberText(senderPlayerId, message, nowMs);
96
112
  try {
97
113
  const response = await fetch(`${claims.iss}/v1/chat/check`, {
98
114
  method: "POST",
package/src/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, GAME_SERVER_OPTIONS, GameServerError, NO_ANSWER_CLOSE_CODE, REMOVED_CLOSE_CODE, ROOM_LIMITS, SEAT_GONE_CLOSE_CODE, VERSION_UPDATE_CLOSE_CODE, defineGameServer, type Audience, type AwayInfo, type AwayMode, type AwayReason, type BroadcastOptions, type Delivery, type GameServer, type GameServerErrorCode, type HandlerResult, type IdleCheck, type JoinAnswer, type ChatAnswer, type ChatInfo, type JoinInfo, type KickOptions, type LeaveInfo, type LeaveReason, type MoveOptions, type MoveResult, type Player, type Purchase, type Refusal, type ReserveOptions, type RestoreInfo, type Room, type RoomErrorCode, type RoomInfo, type SendOptions, type VersionUpdate, } from "./room.ts";
1
+ export { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, GAME_SERVER_OPTIONS, GameServerError, NO_ANSWER_CLOSE_CODE, REMOVED_CLOSE_CODE, ROOM_LIMITS, ROOM_MESSAGE_LIMITS, ROOM_MESSAGE_SENDER_KINDS, SEAT_GONE_CLOSE_CODE, VERSION_UPDATE_CLOSE_CODE, defineGameServer, type Audience, type AwayInfo, type AwayMode, type AwayReason, type BroadcastOptions, type Delivery, type GameServer, type GameServerErrorCode, type HandlerResult, type IdleCheck, type JoinAnswer, type ChatAnswer, type ChatInfo, type JoinInfo, type KickOptions, type LiveRoom, type LeaveInfo, type LeaveReason, type MoveOptions, type MoveResult, type Player, type Purchase, type Refusal, type ReserveOptions, type RestoreInfo, type Room, type RoomErrorCode, type RoomInfo, type RoomMessage, type RoomMessageSenderKind, type RoomPlayer, type RoomSelection, type SendOptions, type VersionUpdate, } from "./room.ts";
2
2
  export { REQUEST_LIMITS, type RequestErrorCode, type Server } from "./requests.ts";
3
3
  export { SCHEDULE_LIMITS, type Schedule, type ScheduledRun } from "./schedules.ts";
4
4
  export { WEBHOOK_LIMITS, verifyWebhook, type WebhookError, type WebhookErrorCode, type WebhookEvent, type WebhookEventKind, type WebhookHeaders } from "./webhooks.ts";
@@ -7,4 +7,5 @@ export { OUTSIDE_CALL_LIMITS, OutsideCallError, type OutsideCallErrorCode } from
7
7
  export { PASS_ISSUER_URL, PASS_TOKEN_TYPE, PASS_VERSION_NUMBER, PassError, RECEIPT_ISSUER_URL, RECEIPT_TOKEN_TYPE, RECEIPT_VERSION_NUMBER, ReceiptError, canChat, passesAreFriends, passesHaveBlockBetween, verifyPass, verifyReceipt, type PassClaims, type PassErrorCode, type PassExpectations, type PlayerKind, type PublicKey, type ReceiptClaims, type ReceiptErrorCode, type ReceiptExpectations, type WhoCanChat } from "./pass.ts";
8
8
  export { CHAT_CHECK_LIMITS, CHAT_ROOM_LIMITS, ChatCheckError, checkChat, type ChatCheck, type ChatCheckOptions, type ChatCheckReason } from "./chat.ts";
9
9
  export { guestName, isRoomCode } from "./names.ts";
10
+ export { guestPlayer, signInWithZaismaPlayers, type IdentifyPlayer, type OutsidePlayer, type SignInWithZaismaPlayersOptions } from "./outside-players.ts";
10
11
  export { relay } from "./relay.ts";
package/src/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // What @zaisma/game-server gives developers, named one by one: from v1 it only grows (test/contracts).
2
2
  // Zaisma's own parts of the package, never locked, are in ./internal.
3
- export { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, GAME_SERVER_OPTIONS, GameServerError, NO_ANSWER_CLOSE_CODE, REMOVED_CLOSE_CODE, ROOM_LIMITS, SEAT_GONE_CLOSE_CODE, VERSION_UPDATE_CLOSE_CODE, defineGameServer, } from "./room.js";
3
+ export { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, GAME_SERVER_OPTIONS, GameServerError, NO_ANSWER_CLOSE_CODE, REMOVED_CLOSE_CODE, ROOM_LIMITS, ROOM_MESSAGE_LIMITS, ROOM_MESSAGE_SENDER_KINDS, SEAT_GONE_CLOSE_CODE, VERSION_UPDATE_CLOSE_CODE, defineGameServer, } from "./room.js";
4
4
  export { REQUEST_LIMITS } from "./requests.js";
5
5
  export { SCHEDULE_LIMITS } from "./schedules.js";
6
6
  export { WEBHOOK_LIMITS, verifyWebhook } from "./webhooks.js";
@@ -9,4 +9,5 @@ export { OUTSIDE_CALL_LIMITS, OutsideCallError } from "./outside-calls.js";
9
9
  export { PASS_ISSUER_URL, PASS_TOKEN_TYPE, PASS_VERSION_NUMBER, PassError, RECEIPT_ISSUER_URL, RECEIPT_TOKEN_TYPE, RECEIPT_VERSION_NUMBER, ReceiptError, canChat, passesAreFriends, passesHaveBlockBetween, verifyPass, verifyReceipt } from "./pass.js";
10
10
  export { CHAT_CHECK_LIMITS, CHAT_ROOM_LIMITS, ChatCheckError, checkChat } from "./chat.js";
11
11
  export { guestName, isRoomCode } from "./names.js";
12
+ export { guestPlayer, signInWithZaismaPlayers } from "./outside-players.js";
12
13
  export { relay } from "./relay.js";
package/src/internal.d.ts CHANGED
@@ -5,6 +5,7 @@ export * from "./database.ts";
5
5
  export * from "./dump.ts";
6
6
  export * from "./requests.ts";
7
7
  export * from "./outside-calls.ts";
8
+ export * from "./outside-players.ts";
8
9
  export * from "./schedules.ts";
9
10
  export * from "./webhooks.ts";
10
11
  export * from "./pass.ts";
package/src/internal.js CHANGED
@@ -7,6 +7,7 @@ export * from "./database.js";
7
7
  export * from "./dump.js";
8
8
  export * from "./requests.js";
9
9
  export * from "./outside-calls.js";
10
+ export * from "./outside-players.js";
10
11
  export * from "./schedules.js";
11
12
  export * from "./webhooks.js";
12
13
  export * from "./pass.js";
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Players from outside Zaisma, for a game server on your own server: a game played anywhere but its Zaisma
3
+ * page (exported, on another site, wrapped for a store) has no Zaisma pass, so its game sends a playerToken
4
+ * instead (Zaisma.setUpOffZaisma) and your server says who that is with startServer's identifyPlayer. Such a
5
+ * player joins with kind "outside_zaisma", in the same rooms as players from the Zaisma page.
6
+ *
7
+ * signInWithZaismaPlayers makes an identifyPlayer for players signed in with Zaisma (Sign in with Zaisma's ID
8
+ * tokens): the same player ID and name as on Zaisma. guestPlayer is who a player with no token is, when
9
+ * startServer's outsideGuestsEnabled lets them in: a temporary ID and a generated name, like Zaisma's guests.
10
+ */
11
+ import { type PublicKey } from "./pass.ts";
12
+ /** Who a player from outside Zaisma is, as identifyPlayer answers: an ID your server keeps for them, and the name others see. */
13
+ export interface OutsidePlayer {
14
+ /** 1 to 100 characters: a Zaisma player ID for a player signed in with Zaisma, or your own accounts' IDs. */
15
+ id: string;
16
+ /** 1 to 40 characters, as other players see it. */
17
+ name: string;
18
+ }
19
+ /** startServer's identifyPlayer: who a playerToken is, or null when it isn't one your server takes. */
20
+ export type IdentifyPlayer = (playerToken: string, connection: {
21
+ origin: string | null;
22
+ }) => Promise<OutsidePlayer | null>;
23
+ export interface SignInWithZaismaPlayersOptions {
24
+ /** Your sign-in apps' IDs (sign_in_app_…), 1 to 20: an ID token for any other app is refused. */
25
+ signInAppIds: string[];
26
+ /** Where Sign in with Zaisma runs, the ID tokens' iss: https://zaisma.com unless a local preview of Zaisma. */
27
+ siteUrl?: string;
28
+ /** async (isRefreshing) => the ID token keys; by default from <siteUrl>/.well-known/jwks.json, fetched again once on a key it doesn't know. */
29
+ loadKeys?: (isRefreshing: boolean) => Promise<PublicKey[]>;
30
+ /** async (playerId) => an account's public profile or null, for a token with no name (signed in without the profile scope). */
31
+ loadProfile?: (playerId: string) => Promise<{
32
+ name: string;
33
+ } | null>;
34
+ }
35
+ export declare const OUTSIDE_PLAYER_LIMITS: {
36
+ readonly maximumIdCharacterCount: 100;
37
+ readonly maximumSignInAppCount: 20;
38
+ /** How far an ID token's exp may be past, for clocks that differ a little. */
39
+ readonly clockLeewaySec: 60;
40
+ /** How long the keys and profiles are kept before they're fetched again. */
41
+ readonly keysKeptMs: 3600000;
42
+ readonly profileKeptMs: 300000;
43
+ /** A refresh for a key ID it doesn't know waits this long after the last fetch, so made-up key IDs can't make it fetch on every token. */
44
+ readonly minimumRefreshIntervalMs: 30000;
45
+ readonly maximumProfileCount: 10000;
46
+ };
47
+ /**
48
+ * An identifyPlayer for players signed in with Zaisma: the game signs them in with Sign in with Zaisma and
49
+ * sends the ID token as its playerToken. The token is checked like any OpenID Connect ID token: RS256 only,
50
+ * signed by Zaisma's published key, its iss the siteUrl, its aud one of signInAppIds, not expired. The player
51
+ * is { id, name }: the account's Zaisma player ID, the same as on Zaisma, and its name from the token (or its
52
+ * public profile when the token has none). Answers null for any token that doesn't check out, and for an
53
+ * account deleted since; throws only when Zaisma's keys or profiles can't be reached (the runner answers busy).
54
+ *
55
+ * startServer({ gameServer, gameSlug, identifyPlayer: signInWithZaismaPlayers({ signInAppIds: ["sign_in_app_8f3k2m"] }) });
56
+ */
57
+ export declare function signInWithZaismaPlayers(options: SignInWithZaismaPlayersOptions): IdentifyPlayer;
58
+ /** A player with no token, when startServer's outsideGuestsEnabled lets them in: a temporary player ID and a generated name, as Zaisma's guests have. */
59
+ export declare function guestPlayer(): OutsidePlayer;
60
+ /** What identifyPlayer answered, checked for the runner: the player, with their name tidied, or what was wrong with it. */
61
+ export declare function checkedOutsidePlayer(given: unknown): {
62
+ player: OutsidePlayer;
63
+ } | {
64
+ mistake: string;
65
+ };