@zaisma/game-server 1.13.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/bin/node-parts.js CHANGED
@@ -289,7 +289,7 @@ export function secretsFromEnvironment() {
289
289
  return Object.freeze(Object.fromEntries(Object.entries(process.env).filter(([, environmentValue]) => typeof environmentValue === "string")));
290
290
  }
291
291
 
292
- const OPTION_NAMES = ["gameServer", "gameSlug", "port", "origins", "issuerUrl", "loadPassKeys", "loadReceiptKeys", "loadProfile", "database", "secrets", "webhookSigningSecret", "roomStateFile", "zaismaKey", "developerApiUrl", "webtransportPort", "certificatePath", "certificateKeyPath"];
292
+ const OPTION_NAMES = ["gameServer", "gameSlug", "port", "origins", "issuerUrl", "loadPassKeys", "loadReceiptKeys", "loadProfile", "database", "secrets", "webhookSigningSecret", "roomStateFile", "zaismaKey", "developerApiUrl", "webtransportPort", "certificatePath", "certificateKeyPath", "identifyPlayer", "outsideGuestsEnabled"];
293
293
 
294
294
  /**
295
295
  * Check what startServer was given: unknown options and options of the wrong kind are a GameServerError,
@@ -325,6 +325,10 @@ export function checkedStartServerOptions(given, issuerUrlDefault) {
325
325
  if (options.roomStateFile !== undefined && options.roomStateFile !== null && !isText(options.roomStateFile)) throw bad("roomStateFile", "a file path", "Give a file path as text, like \"zaisma-room-states.sqlite\", or null to keep no states.");
326
326
  if (options.zaismaKey !== undefined && !isText(options.zaismaKey)) throw bad("zaismaKey", "an API key", "Give an API key with purchase_access write for this game as text, or leave it out to use ZAISMA_KEY.");
327
327
  if (options.developerApiUrl !== undefined && (!isText(options.developerApiUrl) || !URL.canParse(options.developerApiUrl))) throw bad("developerApiUrl", "a URL", 'Give the address of Zaisma\'s developer API, like "https://zaisma.com/api/dev".');
328
+ // Players from outside Zaisma (games played off their Zaisma page): who a playerToken is, and whether players with none come in as guests.
329
+ if (options.identifyPlayer !== undefined && !isFunction(options.identifyPlayer)) throw bad("identifyPlayer", "a function", "Give async (playerToken, { origin }) => ({ id, name }) or null, like signInWithZaismaPlayers({ signInAppIds }) makes.");
330
+ options.outsideGuestsEnabled ??= false;
331
+ if (typeof options.outsideGuestsEnabled !== "boolean") throw bad("outsideGuestsEnabled", "true or false", "Give true to let players with no playerToken in as guests, or false (the default).");
328
332
  // The fast lane (WebTransport): its UDP port and the certificate it holds itself, all three or none.
329
333
  const fastLaneFields = ["webtransportPort", "certificatePath", "certificateKeyPath"];
330
334
  const missingFastLaneFields = fastLaneFields.filter((field) => options[field] === undefined);
package/bin/node.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Server } from "node:http";
2
- import type { GameDatabase, GameServer, PublicKey } from "../src/index.js";
2
+ import type { GameDatabase, GameServer, IdentifyPlayer, PublicKey } from "../src/index.js";
3
3
 
4
4
  /** What startServer takes. Only gameServer and gameSlug are required; the rest have the defaults named here. */
5
5
  export interface StartServerOptions {
@@ -41,6 +41,15 @@ export interface StartServerOptions {
41
41
  certificatePath?: string;
42
42
  /** The fast lane's certificate key, a PEM file (Let's Encrypt's privkey.pem). */
43
43
  certificateKeyPath?: string;
44
+ /**
45
+ * Who a player from outside Zaisma is: a game played off its Zaisma page sends a playerToken in place of a pass
46
+ * (Zaisma.setUpOffZaisma), and this answers { id, name } for it, or null to refuse (bad_player_token). They join
47
+ * with kind "outside_zaisma". signInWithZaismaPlayers({ signInAppIds }) makes one for Sign in with Zaisma; your
48
+ * own checks your game's own accounts. One that throws answers busy. None by default: only Zaisma passes.
49
+ */
50
+ identifyPlayer?: IdentifyPlayer;
51
+ /** Let players from outside Zaisma with no token (playerToken: null) in as guests (guestPlayer): false by default. */
52
+ outsideGuestsEnabled?: boolean;
44
53
  }
45
54
 
46
55
  export interface RunningServer {
package/bin/node.js CHANGED
@@ -7,7 +7,9 @@
7
7
  //
8
8
  // Players connect to wss://your-server/v1/rooms/<game>/<code>, the same address
9
9
  // layout as Zaisma rooms, and prove who they are with Zaisma player passes,
10
- // checked against the keys Zaisma publishes. Put it behind HTTPS (Caddy, nginx,
10
+ // checked against the keys Zaisma publishes. A game played off its Zaisma page sends a playerToken instead
11
+ // (Zaisma.setUpOffZaisma), and identifyPlayer says who that is (signInWithZaismaPlayers for players signed in
12
+ // with Zaisma, or your own accounts); outsideGuestsEnabled lets players with none in as guests. Put it behind HTTPS (Caddy, nginx,
11
13
  // a Cloudflare Tunnel) and add the address to your game's own servers (own_server_urls, or zaisma own-servers).
12
14
  // The game's requests (onRequest) come to POST /v1/requests/<game> on the same port, and its
13
15
  // schedules (onSchedule) run while it's up, at the minutes their crons give in UTC. A Zaisma webhook
@@ -24,7 +26,7 @@ import { createServer } from "node:http";
24
26
  import { fileURLToPath, pathToFileURL } from "node:url";
25
27
  import { resolve } from "node:path";
26
28
  import { readFileSync, realpathSync } from "node:fs";
27
- import { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, ERROR_TEXT, NO_ANSWER_CLOSE_CODE, PASS_ISSUER_URL, PassError, ReceiptError, REMOVED_CLOSE_CODE, REQUEST_ERROR_HTTP_STATUS, REQUEST_LIMITS, ROOM_LIMITS, ROOM_MESSAGE_LIMITS, ROOM_TUNING, WEBHOOK_LIMITS, GameServerError, RoomCore, chatSettingsOf, checkChat, readChatMessage, restoreInfoOf, passesAreFriends, passesHaveBlockBetween, answerRequest, startSchedules, checkRoomData, clientIdle, defineGameServer, errorMessage, newServer, isRoomCode, readRequestBody, relay, requestError, runWebhook, tidyRoomInfo, verifyPass, verifyReceipt, verifyWebhook } from "../src/internal.js";
29
+ import { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, ERROR_TEXT, NO_ANSWER_CLOSE_CODE, PASS_ISSUER_URL, OUTSIDE_ZAISMA_CHAT_SETTINGS, PassError, ReceiptError, REMOVED_CLOSE_CODE, REQUEST_ERROR_HTTP_STATUS, REQUEST_LIMITS, ROOM_LIMITS, ROOM_MESSAGE_LIMITS, ROOM_TUNING, WEBHOOK_LIMITS, GameServerError, RoomCore, chatSettingsOf, checkChat, readChatMessage, restoreInfoOf, passesAreFriends, passesHaveBlockBetween, answerRequest, startSchedules, checkRoomData, checkedOutsidePlayer, clientIdle, defineGameServer, errorMessage, guestPlayer, newServer, isPlayerToken, isRoomCode, readRequestBody, relay, requestError, runWebhook, tidyRoomInfo, verifyPass, verifyReceipt, verifyWebhook } from "../src/internal.js";
28
30
  import { OutsideCallError } from "../src/outside-calls.js";
29
31
  import { startFastLane } from "./fast-lane.js";
30
32
  import { checkedStartServerOptions, localDatabase, localRoomStates, passKeysFrom, profilesFrom, receiptKeysFrom, secretsFromEnvironment } from "./node-parts.js";
@@ -74,11 +76,15 @@ const CATCH_UP_MS = 50;
74
76
  * the ZAISMA_KEY environment variable) and developerApiUrl (Zaisma's developer API; by default https://zaisma.com/api/dev).
75
77
  * webtransportPort (a UDP port), certificatePath and certificateKeyPath (the server's certificate and its key,
76
78
  * PEM files), all three or none, also serve the fast lane (WebTransport) beside the WebSocket.
79
+ * identifyPlayer (async (playerToken, { origin }) => ({ id, name }) or null; none by default) says who a player from
80
+ * outside Zaisma is by the playerToken their game sent (signInWithZaismaPlayers makes one for Sign in with Zaisma),
81
+ * and outsideGuestsEnabled (false by default) lets players with no token (playerToken: null) in as guests (guestPlayer).
82
+ * Both join with kind "outside_zaisma".
77
83
  * Answers { close(), httpServer, fastLaneReady }: close() saves every room first and finishes once the server
78
84
  * has stopped; fastLaneReady settles once the fast lane listens (at once without one), or rejects saying why it can't.
79
85
  */
80
86
  export function startServer(options) {
81
- const { gameServer, gameSlug, port: portNumber, origins, issuerUrl, loadPassKeys = passKeysFrom(`${issuerUrl}/.well-known/jwks.json`), loadReceiptKeys = receiptKeysFrom(`${issuerUrl}/.well-known/receipt-jwks.json`), loadProfile = profilesFrom(issuerUrl), database = localDatabase("zaisma-database.sqlite"), secrets = secretsFromEnvironment(), webhookSigningSecret = process.env.ZAISMA_WEBHOOK_SIGNING_SECRET, roomStateFile = "zaisma-room-states.sqlite", zaismaKey = process.env.ZAISMA_KEY, developerApiUrl = "https://zaisma.com/api/dev", webtransportPort, certificatePath, certificateKeyPath } = checkedStartServerOptions(options, PASS_ISSUER_URL);
87
+ const { gameServer, gameSlug, port: portNumber, origins, issuerUrl, loadPassKeys = passKeysFrom(`${issuerUrl}/.well-known/jwks.json`), loadReceiptKeys = receiptKeysFrom(`${issuerUrl}/.well-known/receipt-jwks.json`), loadProfile = profilesFrom(issuerUrl), database = localDatabase("zaisma-database.sqlite"), secrets = secretsFromEnvironment(), webhookSigningSecret = process.env.ZAISMA_WEBHOOK_SIGNING_SECRET, roomStateFile = "zaisma-room-states.sqlite", zaismaKey = process.env.ZAISMA_KEY, developerApiUrl = "https://zaisma.com/api/dev", webtransportPort, certificatePath, certificateKeyPath, identifyPlayer, outsideGuestsEnabled } = checkedStartServerOptions(options, PASS_ISSUER_URL);
82
88
  const definition = defineGameServer(gameServer);
83
89
  // Exactly once needs Zaisma's record of what's handled: without a key, purchases would come twice, so it doesn't start.
84
90
  if ((definition.onPurchase || definition.onRefund) && !zaismaKey) {
@@ -125,7 +131,7 @@ export function startServer(options) {
125
131
  * account's name from its public profile, and the pass's claims, for blocks between players. Null
126
132
  * for an account deleted since; PassError for a bad pass.
127
133
  */
128
- async function playerFrom(pass) {
134
+ async function passPlayer(pass) {
129
135
  const claims = await verifyPass(pass, await loadPassKeys(), { gameSlug, issuerUrl }).catch(async (error) => {
130
136
  // A new key may have been published since we cached them.
131
137
  if (error instanceof PassError && error.code === "unknown_key") return verifyPass(pass, await loadPassKeys(true), { gameSlug, issuerUrl });
@@ -139,8 +145,43 @@ export function startServer(options) {
139
145
  return name ? { player: { id: claims.sub, name, kind: claims.kind === "account" ? "account" : "guest", ...owned }, claims } : null;
140
146
  }
141
147
 
142
- // Requests from the game to onRequest, outside any room: POST /v1/requests/<game> with { pass, request },
143
- // sent as text/plain JSON so browsers need no preflight. Answers { answer } or { error: { code, message } }.
148
+ /**
149
+ * Who a hello or request is from: by a Zaisma pass, or, from a game played off Zaisma, by the playerToken it
150
+ * sent, as identifyPlayer says, or as a guest when there's none and outsideGuestsEnabled. Answers { player, claims }
151
+ * (claims null for a player from outside Zaisma, who has no pass), or { code } when they can't come in.
152
+ */
153
+ async function playerFrom({ pass, playerToken, origin }) {
154
+ if (typeof pass === "string") {
155
+ try {
156
+ return (await passPlayer(pass)) ?? { code: "bad_pass" };
157
+ } catch (error) {
158
+ if (error instanceof PassError) return { code: "bad_pass" };
159
+ // Zaisma couldn't be reached for the account's name: the player tries again, nothing crashes.
160
+ console.error("Couldn't check a player's pass:", error);
161
+ return { code: "busy" };
162
+ }
163
+ }
164
+ const outside = (player) => ({ player: { ...player, kind: "outside_zaisma" }, claims: null });
165
+ if (playerToken === null) return outsideGuestsEnabled ? outside(guestPlayer()) : { code: "bad_player_token", message: "This server takes players signed in, not guests: the game gives a playerToken." };
166
+ if (!identifyPlayer) return { code: "bad_player_token", message: "This server takes players from Zaisma's pages only." };
167
+ let identified;
168
+ try {
169
+ identified = await identifyPlayer(playerToken, { origin: origin ?? null });
170
+ } catch (error) {
171
+ console.error("Game server error in identifyPlayer:", error);
172
+ return { code: "busy" };
173
+ }
174
+ if (identified === null) return { code: "bad_player_token" };
175
+ const checked = checkedOutsidePlayer(identified);
176
+ if ("mistake" in checked) {
177
+ console.error(`Game server error in identifyPlayer: ${checked.mistake}`);
178
+ return { code: "bad_player_token" };
179
+ }
180
+ return outside(checked.player);
181
+ }
182
+
183
+ // Requests from the game to onRequest, outside any room: POST /v1/requests/<game> with { pass, request }, or
184
+ // { playerToken, request } from a game played off Zaisma, sent as text/plain JSON so browsers need no preflight. Answers { answer } or { error: { code, message } }.
144
185
  function answerHttp(request, response) {
145
186
  if (request.url === "/v1/webhooks") return answerWebhook(request, response);
146
187
  const origin = request.headers.origin;
@@ -171,16 +212,10 @@ export function startServer(options) {
171
212
  request.on("end", async () => {
172
213
  try {
173
214
  if (isTooBig) return fail("too_big");
174
- const body = readRequestBody(text);
215
+ const body = readRequestBody(text, true);
175
216
  if ("code" in body) return fail(body.code);
176
- let checked;
177
- try {
178
- checked = await playerFrom(body.pass);
179
- } catch (error) {
180
- if (error instanceof PassError) return fail("bad_pass");
181
- throw error;
182
- }
183
- if (!checked) return fail("bad_pass");
217
+ const checked = await playerFrom({ ...body, origin });
218
+ if (checked.code) return fail(checked.code, checked.message);
184
219
  const outcome = await answerRequest(definition, checked.player, body.request, server);
185
220
  if ("answer" in outcome) return reply(200, { answer: outcome.answer });
186
221
  if (outcome.code === "game_server_error") console.error("Game server error in onRequest:", outcome.failure);
@@ -512,8 +547,10 @@ export function startServer(options) {
512
547
  if (!started) return;
513
548
  let check = { isAllowed: true, reason: null, message: null, hasSwearing: false, hasSlur: false };
514
549
  if (started.receiverPlayerIds.length) {
515
- const receiverPasses = started.receiverPlayerIds.map((id) => room.passTexts.get(id)).filter((pass) => typeof pass === "string");
516
- check = await checkChat({ text: started.text, senderPass: room.passTexts.get(playerId) ?? "", receiverPasses, isAdultFilterEnabled: definition.adultChatFilterEnabled === true });
550
+ // A player from outside Zaisma goes as null: Zaisma stops their chat with under-18 accounts, and doesn't hear chat among them alone.
551
+ const passOf = (id) => (room.core.players.get(id)?.kind === "outside_zaisma" ? null : (room.passTexts.get(id) ?? ""));
552
+ const receiverPasses = started.receiverPlayerIds.map(passOf);
553
+ check = await checkChat({ text: started.text, senderPass: passOf(playerId), receiverPasses, isAdultFilterEnabled: definition.adultChatFilterEnabled === true });
517
554
  }
518
555
  if (check.isAllowed) room.core.deliverChat(playerId, chat.chatNumber, started.receiverPlayerIds, { text: started.text, hasSwearing: check.hasSwearing, hasSlur: check.hasSlur });
519
556
  else room.core.stopChat(playerId, chat.chatNumber, check.reason ?? "unchecked", check.message ?? "");
@@ -621,13 +658,17 @@ export function startServer(options) {
621
658
  return;
622
659
  }
623
660
  let pass;
661
+ let playerToken;
624
662
  let made = {};
625
663
  // Come to watch, not to play (joinRoom({ isSpectator: true })).
626
664
  let isSpectator = false;
627
665
  try {
628
666
  const hello = JSON.parse(text ?? "");
629
- if (hello.type !== "hello" || typeof hello.pass !== "string") return refuse("no_hello");
630
- pass = hello.pass;
667
+ // A Zaisma pass, or from a game played off Zaisma, its playerToken (null for a guest).
668
+ const isOutside = hello.pass === undefined && isPlayerToken(hello.playerToken);
669
+ if (hello.type !== "hello" || (typeof hello.pass !== "string" && !isOutside)) return refuse("no_hello");
670
+ if (isOutside) playerToken = hello.playerToken;
671
+ else pass = hello.pass;
631
672
  isSpectator = hello.isSpectator === true;
632
673
  if (hello.room && typeof hello.room === "object") made = { ...(hello.room.isListed === true ? { isListed: true } : {}), ...(hello.room.friendJoinsEnabled === false ? { friendJoinsEnabled: false } : {}), ...tidyRoomInfo(hello.room) };
633
674
  made.clientIdle = clientIdle(hello);
@@ -637,16 +678,8 @@ export function startServer(options) {
637
678
  } catch {
638
679
  return refuse("no_hello");
639
680
  }
640
- let checked;
641
- try {
642
- checked = await playerFrom(pass);
643
- } catch (error) {
644
- if (error instanceof PassError) return refuse("bad_pass");
645
- // Zaisma couldn't be reached for the account's name: the player tries again, nothing crashes.
646
- console.error("Couldn't check a player's pass:", error);
647
- return refuse("busy");
648
- }
649
- if (!checked) return refuse("bad_pass");
681
+ const checked = await playerFrom({ pass, playerToken, origin });
682
+ if (checked.code) return refuse(checked.code, checked.message);
650
683
  const { player, claims } = checked;
651
684
  clearTimeout(helloTimer);
652
685
  // A spectator watches a match: one with nobody in it isn't running, and a spectator never starts one.
@@ -663,8 +696,14 @@ export function startServer(options) {
663
696
  // Blocks between accounts come in their passes: this player's and those of everyone the room remembers.
664
697
  // Spectators send nothing, so nothing of theirs reaches a player to block.
665
698
  // Who the player may chat with, from their pass: the room keeps it, and the game never sees it. Spectators send nothing, chat included.
666
- room.core.setChatSettings(player.id, isSpectator ? null : chatSettingsOf(claims));
667
- if (!isSpectator) {
699
+ // A player from outside Zaisma has no pass: no account blocks or friends, and chat as OUTSIDE_ZAISMA_CHAT_SETTINGS says.
700
+ room.core.setChatSettings(player.id, isSpectator ? null : claims ? chatSettingsOf(claims) : OUTSIDE_ZAISMA_CHAT_SETTINGS);
701
+ if (!isSpectator && !claims) {
702
+ room.passes.delete(player.id);
703
+ room.passTexts.delete(player.id);
704
+ room.core.setAccountBlocks(player.id, []);
705
+ room.core.setFriends(player.id, []);
706
+ } else if (!isSpectator) {
668
707
  room.passes.set(player.id, claims);
669
708
  room.passTexts.set(player.id, pass);
670
709
  for (const id of room.passes.keys()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaisma/game-server",
3
- "version": "1.13.0",
3
+ "version": "1.14.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
@@ -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" });
@@ -1488,8 +1526,11 @@ declare namespace Zaisma {
1488
1526
  awayReason: "idle" | "hidden" | "lost" | null;
1489
1527
  }
1490
1528
 
1491
- /** A signed-in Zaisma account, or a guest. More values may come; treat an unknown one as "guest". */
1492
- 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";
1493
1534
 
1494
1535
  /** What anyone sees of an account on its profile, zaisma.com/@username ({@link player.getProfile}). */
1495
1536
  interface PlayerProfile {
@@ -1627,6 +1668,7 @@ declare namespace Zaisma {
1627
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.
1628
1669
  * - game_unavailable: the game can't be played right now.
1629
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.
1630
1672
  * - bad_origin: the connection didn't come from the game's own page.
1631
1673
  * - too_big: a message over 16 KB, or joinData over 4 KB.
1632
1674
  * - too_fast: more messages than the room takes (about 30 a second).
@@ -1642,6 +1684,8 @@ declare namespace Zaisma {
1642
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.
1643
1685
  *
1644
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".
1645
1689
  */
1646
1690
  type RoomErrorCode =
1647
1691
  | "room_full"
@@ -1654,6 +1698,7 @@ declare namespace Zaisma {
1654
1698
  | "game_not_open_to_player"
1655
1699
  | "game_unavailable"
1656
1700
  | "bad_pass"
1701
+ | "bad_player_token"
1657
1702
  | "bad_origin"
1658
1703
  | "too_big"
1659
1704
  | "too_fast"
@@ -1679,6 +1724,7 @@ declare namespace Zaisma {
1679
1724
  * - bad_request: not plain JSON with the player's pass.
1680
1725
  * - too_big: a request over 16 KB.
1681
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.
1682
1728
  * - account_needed: the player is a guest, and the game needs a Zaisma account for multiplayer or the whole game.
1683
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.
1684
1730
  * - game_unavailable: the game can't be played right now.
@@ -1688,11 +1734,14 @@ declare namespace Zaisma {
1688
1734
  * - slow_down: too many requests from this address; try again in a minute.
1689
1735
  * - bad_origin: the request didn't come from the game's own page.
1690
1736
  * - busy: Zaisma couldn't answer right now; try again soon.
1737
+ *
1738
+ * More values may come; treat an unknown one as "request_failed".
1691
1739
  */
1692
1740
  type RequestErrorCode =
1693
1741
  | "bad_request"
1694
1742
  | "too_big"
1695
1743
  | "bad_pass"
1744
+ | "bad_player_token"
1696
1745
  | "account_needed"
1697
1746
  | "game_not_open_to_player"
1698
1747
  | "game_unavailable"
@@ -1711,7 +1760,8 @@ declare namespace Zaisma {
1711
1760
  * - zaisma_rooms_not_used: the game doesn't use Zaisma rooms.
1712
1761
  * - bad_report: a report whose reason isn't a {@link ReportReason}, or whose player ID isn't one.
1713
1762
  * - bad_origin: the call didn't come from the game's own page.
1714
- * - 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.
1715
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}).
1716
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}).
1717
1767
  * - cloud_saves_off: the game has cloud saves off, so it has no saves on players' accounts ({@link saves}).
@@ -1752,6 +1802,7 @@ declare namespace Zaisma {
1752
1802
  | "bad_report"
1753
1803
  | "bad_origin"
1754
1804
  | "not_on_zaisma"
1805
+ | "bad_game_slug"
1755
1806
  | "sign_in_declined"
1756
1807
  | "account_offer_too_soon"
1757
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"];
@@ -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.
@@ -1527,7 +1572,9 @@
1527
1572
 
1528
1573
  function connect(roomCode, joinOptions, isRetry) {
1529
1574
  var serverUrl = joinOptions.serverUrl ? String(joinOptions.serverUrl).replace(/\/+$/, "") : null;
1530
- 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;
1531
1578
  var idleTimeoutSec = joinOptions.idleTimeoutSec;
1532
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 + "."));
1533
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\"."));
@@ -1539,12 +1586,14 @@
1539
1586
  : serverUrl
1540
1587
  ? Promise.resolve(randomRoomCode())
1541
1588
  : post("/v1/rooms", { gameSlug: config.game }).then(function (answer) { return answer.roomCode; });
1542
- 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) {
1543
1591
  var base = (serverUrl || config.rooms).replace(/^http/, "ws");
1544
- var path = "/v1/rooms/" + config.game + "/" + ready[0];
1592
+ var path = "/v1/rooms/" + (isOffZaisma ? offZaisma.gameSlug : config.game) + "/" + ready[0];
1545
1593
  var room = new Room({
1546
1594
  roomCode: ready[0],
1547
- pass: ready[1].pass,
1595
+ pass: isOffZaisma ? null : ready[1].pass,
1596
+ isOffZaisma: isOffZaisma,
1548
1597
  url: base + path,
1549
1598
  // The fast lane reaches only the game's own secure servers: Zaisma's rooms take the WebSocket, whatever transport says.
1550
1599
  fastLaneUrl: joinOptions.transport === "webtransport" && serverUrl && OWN_SERVER_URL.test(serverUrl) ? serverUrl.replace(/^wss:/, "https:") + path : null,
@@ -1566,8 +1615,8 @@
1566
1615
  room._ready = { resolve: resolve, reject: reject };
1567
1616
  room._connect();
1568
1617
  }).catch(function (error) {
1569
- // A pass this tab kept from before Zaisma changed its keys: get a new one and try once more.
1570
- 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;
1571
1620
  try { sessionStorage.removeItem(passKey); } catch (storageError) { /* fine */ }
1572
1621
  return connect(room.roomCode, joinOptions, true);
1573
1622
  });
@@ -1770,13 +1819,15 @@
1770
1819
 
1771
1820
  /**
1772
1821
  * A request to the game's own game server, its onRequest, outside any room: what it answered
1773
- * (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.
1774
1824
  */
1775
1825
  function serverRequest(request, requestOptions, isRetry) {
1776
1826
  var refused = unknownOption(requestOptions, REQUEST_OPTIONS, "Zaisma.server.request");
1777
1827
  if (refused) return Promise.reject(refused);
1778
1828
  var serverUrl = requestOptions && requestOptions.serverUrl ? String(requestOptions.serverUrl).replace(/\/+$/, "") : null;
1779
- 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);
1780
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\"."));
1781
1832
  var requestText;
1782
1833
  try {
@@ -1787,13 +1838,17 @@
1787
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\" }."));
1788
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."));
1789
1840
  var base = (serverUrl || config.rooms).replace(/^ws/, "http");
1790
- return getPass().then(function (mine) {
1791
- 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);
1792
1847
  }).then(function (answer) {
1793
1848
  return answer.answer;
1794
1849
  }, function (error) {
1795
- // A pass this tab kept from before Zaisma changed its keys: get a new one and try once more.
1796
- 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;
1797
1852
  try { sessionStorage.removeItem(passKey); } catch (storageError) { /* fine */ }
1798
1853
  return serverRequest(request, requestOptions, true);
1799
1854
  });
@@ -2698,5 +2753,5 @@
2698
2753
  });
2699
2754
 
2700
2755
  var friends = { list: listFriends, setRoom: setOwnRoom, sendRequest: sendFriendRequest };
2701
- 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 };
2702
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
  }