@zaisma/game-server 1.5.0 → 1.7.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/local-play.js CHANGED
@@ -1215,6 +1215,12 @@ export async function startLocalPlay(givenOptions) {
1215
1215
  }
1216
1216
  return notLocal();
1217
1217
  }
1218
+ // Zaisma's chat check, as the runner's rooms ask it for room.chat (checkChat): the bench lets every message through,
1219
+ // nothing flagged, the way /v1/filter stands in below. Zaisma's own filter runs once the game is on Zaisma.
1220
+ if (request.method === "POST" && path === "/v1/chat/check") {
1221
+ await readBody();
1222
+ return reply(200, { isAllowed: true, reason: null, message: null, hasSwearing: false, hasSlur: false });
1223
+ }
1218
1224
  if (request.method === "POST" && ["/v1/pass", "/v1/rooms", "/v1/report", "/v1/filter", "/v1/friends", "/v1/friends/room", "/v1/items/unhandled", "/v1/items/handled"].includes(path)) {
1219
1225
  if (!isGameOrigin(origin)) return fail(403, "bad_origin", "Not allowed from here.");
1220
1226
  const body = await readBody();
@@ -1225,7 +1231,7 @@ export async function startLocalPlay(givenOptions) {
1225
1231
  return reply(200, await passFor(given.as, given.playerNumber));
1226
1232
  }
1227
1233
  if (path === "/v1/rooms") return reply(200, { roomCode: roomCode() });
1228
- if (path === "/v1/filter") return reply(200, { isAllowed: true, reason: null, message: null, hasSwearing: false, text: String(body.text ?? ""), is_stand_in: true });
1234
+ if (path === "/v1/filter") return reply(200, { isAllowed: true, reason: null, message: null, hasSwearing: false, hasSlur: false, text: String(body.text ?? ""), is_stand_in: true });
1229
1235
  if (path === "/v1/report") {
1230
1236
  log(`Report (not sent anywhere): ${JSON.stringify({ roomCode: body.roomCode, playerId: body.playerId, playerName: body.playerName, reason: body.reason, details: body.details })}`);
1231
1237
  return reply(200, { isReported: true });
package/bin/node.js CHANGED
@@ -22,7 +22,7 @@ import { createServer } from "node:http";
22
22
  import { fileURLToPath, pathToFileURL } from "node:url";
23
23
  import { resolve } from "node:path";
24
24
  import { realpathSync } from "node:fs";
25
- 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_TUNING, WEBHOOK_LIMITS, GameServerError, RoomCore, restoreInfoOf, passesAreFriends, passesHaveBlockBetween, answerRequest, startSchedules, checkRoomData, clientIdle, defineGameServer, errorMessage, isRoomCode, readRequestBody, relay, requestError, runWebhook, tidyRoomInfo, verifyPass, verifyReceipt, verifyWebhook } from "../src/internal.js";
25
+ 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_TUNING, WEBHOOK_LIMITS, GameServerError, RoomCore, chatSettingsOf, checkChat, readChatMessage, restoreInfoOf, passesAreFriends, passesHaveBlockBetween, answerRequest, startSchedules, checkRoomData, clientIdle, defineGameServer, errorMessage, isRoomCode, readRequestBody, relay, requestError, runWebhook, tidyRoomInfo, verifyPass, verifyReceipt, verifyWebhook } from "../src/internal.js";
26
26
  import { OutsideCallError } from "../src/outside-calls.js";
27
27
  import { checkedStartServerOptions, localDatabase, localRoomStates, passKeysFrom, profilesFrom, receiptKeysFrom, secretsFromEnvironment } from "./node-parts.js";
28
28
 
@@ -290,10 +290,11 @@ export function startServer(options) {
290
290
  saved ? { state: saved.state, room: saved.savedRoom, ...(definition.ticksPerSec || saved.matchPlayers?.length ? { restoreInfo: restoreInfoOf(saved.savedAtMs, Date.now(), null, saved.isSavedEmpty), lostPlayers: saved.matchPlayers } : {}) } : { start },
291
291
  );
292
292
  if (saved) core.savedAtMs = saved.savedAtMs;
293
- // passes: each player's checked pass, newest, for the blocks and friendships between them (passesHaveBlockBetween, passesAreFriends).
293
+ // passes: each player's checked pass, newest, for the blocks and friendships between them (passesHaveBlockBetween, passesAreFriends);
294
+ // passTexts: the same passes as the players sent them, for Zaisma's chat filter (checkChat).
294
295
  // savedStateKey: what the room last saved (its state and its match's players), so an unchanged one isn't written again.
295
296
  // seatedAtMs: when each player came into this room on their own, for the game's one-room rule (the oldest seat closes first).
296
- room = { core, sockets, passes: new Map(), seatedAtMs: new Map(), timer: null, probeTimer: null, waiting: false, savedStateKey: saved ? stateKeyOf(saved.stateJson, saved.matchPlayers, saved.savedRoom.isKept === true) : null, hasReportedStateSize: false };
297
+ room = { core, sockets, passes: new Map(), passTexts: new Map(), seatedAtMs: new Map(), timer: null, probeTimer: null, waiting: false, savedStateKey: saved ? stateKeyOf(saved.stateJson, saved.matchPlayers, saved.savedRoom.isKept === true) : null, hasReportedStateSize: false };
297
298
  rooms.set(code, room);
298
299
  return room;
299
300
  }
@@ -402,6 +403,24 @@ export function startServer(options) {
402
403
  }
403
404
  }
404
405
 
406
+ /**
407
+ * A player's chat message (room.chat): the room checks it and asks the game server's onChat (startChat),
408
+ * Zaisma's filter looks (checkChat, with the passes of those who may read it), and it goes to them
409
+ * (deliverChat) or the sender hears why not (stopChat). Everyone it may reach is in the sender's chat
410
+ * group, so the first CHAT_CHECK_LIMITS.maximumReceiverCount of them stand for the rest.
411
+ */
412
+ async function chatIn(room, playerId, chat) {
413
+ const started = await room.core.startChat(playerId, chat);
414
+ if (!started) return;
415
+ let check = { isAllowed: true, reason: null, message: null, hasSwearing: false, hasSlur: false };
416
+ if (started.receiverPlayerIds.length) {
417
+ const receiverPasses = started.receiverPlayerIds.map((id) => room.passTexts.get(id)).filter((pass) => typeof pass === "string");
418
+ check = await checkChat({ text: started.text, senderPass: room.passTexts.get(playerId) ?? "", receiverPasses, isAdultFilterEnabled: definition.adultChatFilterEnabled === true });
419
+ }
420
+ if (check.isAllowed) room.core.deliverChat(playerId, chat.chatNumber, started.receiverPlayerIds, { text: started.text, hasSwearing: check.hasSwearing, hasSlur: check.hasSlur });
421
+ else room.core.stopChat(playerId, chat.chatNumber, check.reason ?? "unchecked", check.message ?? "");
422
+ }
423
+
405
424
  /** Keep a room for players on their way, even if it empties meanwhile. */
406
425
  function wait(code, room, durationMs) {
407
426
  room.waiting = true;
@@ -489,6 +508,9 @@ export function startServer(options) {
489
508
  if (text === "ping") return ws.send("pong");
490
509
  if (playerId) {
491
510
  const room = rooms.get(code);
511
+ // Chat goes through Zaisma's filter (checkChat) on its way, so the room takes it before the game's messages.
512
+ const chat = text === null ? null : readChatMessage(text);
513
+ if (chat && room) return chatIn(room, playerId, chat);
492
514
  // Bytes (a binary frame) go to the game server's onBytes, under the same limits as messages.
493
515
  const errorCode = text === null ? room?.core.receiveBytes(playerId, binaryBytes(received)) : room?.core.receive(playerId, text);
494
516
  if (errorCode) ws.send(errorMessage(errorCode));
@@ -537,9 +559,16 @@ export function startServer(options) {
537
559
  room.sockets.set(player.id, ws);
538
560
  // Blocks between accounts come in their passes: this player's and those of everyone the room remembers.
539
561
  // Spectators send nothing, so nothing of theirs reaches a player to block.
562
+ // Who the player may chat with, from their pass: the room keeps it, and the game never sees it. Spectators send nothing, chat included.
563
+ room.core.setChatSettings(player.id, isSpectator ? null : chatSettingsOf(claims));
540
564
  if (!isSpectator) {
541
565
  room.passes.set(player.id, claims);
542
- for (const id of room.passes.keys()) if (!room.core.known.has(id) && id !== player.id) room.passes.delete(id);
566
+ room.passTexts.set(player.id, pass);
567
+ for (const id of room.passes.keys()) {
568
+ if (room.core.known.has(id) || id === player.id) continue;
569
+ room.passes.delete(id);
570
+ room.passTexts.delete(id);
571
+ }
543
572
  room.core.setAccountBlocks(player.id, [...room.passes].filter(([id, other]) => id !== player.id && passesHaveBlockBetween(claims, other)).map(([id]) => id));
544
573
  room.core.setFriends(player.id, [...room.passes].filter(([id, other]) => id !== player.id && passesAreFriends(claims, other)).map(([id]) => id));
545
574
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaisma/game-server",
3
- "version": "1.5.0",
3
+ "version": "1.7.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
@@ -51,8 +51,9 @@ declare namespace Zaisma {
51
51
  /**
52
52
  * This tab's player, with their player pass: the signed-in Zaisma account, with the same player ID in
53
53
  * every game, or a guest. joinRoom gets one by itself; call this to let the player into a server of
54
- * your own. Kept for the tab, so a guest who reloads stays the same guest, and renewed when it has
55
- * under 10 minutes left or the player signed in or out. Fails with error.code "unchecked" when the
54
+ * your own. A guest's is kept for the tab, so a guest who reloads stays the same guest, and renewed when
55
+ * it has under 10 minutes left or the player signs in; an account gets a new one each time the game
56
+ * loads, for whoever is signed in then. Fails with error.code "unchecked" when the
56
57
  * game isn't played on its zaisma.com page, "not_on_zaisma" when it isn't hosted on Zaisma, and
57
58
  * "account_needed" for a guest when the game needs an account for multiplayer or the whole game
58
59
  * (its account_requirement setting): ask them to sign in with Zaisma.player.signIn().
@@ -176,10 +177,10 @@ declare namespace Zaisma {
176
177
  /**
177
178
  * Zaisma's chat check: the same check Zaisma runs on its side on every chat message to or from an
178
179
  * under-18 account. It stops contact details in any disguise, moving to another app or meeting up,
179
- * personal details, slurs and sexual content, and answers with the words to show the sender. Swearing
180
- * is flagged (hasSwearing) but allowed: blocking it is your game's call. Use it to show the sender why
181
- * at once; Zaisma checks again when the message is delivered, so it can't be skipped. Checks the whole
182
- * text, up to 2000 characters; a longer one is refused (too_long), never cut.
180
+ * personal details and sexual content, and answers with the words to show the sender. Swearing and
181
+ * slurs are flagged (hasSwearing, hasSlur) but allowed: blocking them is your game's call. For chat
182
+ * between players use {@link Room.chat}, which runs this check on its way; this one is for other text
183
+ * players type. Checks the whole text, up to 2000 characters; a longer one is refused (too_long), never cut.
183
184
  *
184
185
  * @example
185
186
  * const check = await Zaisma.checkChat(typedMessage);
@@ -997,6 +998,11 @@ declare namespace Zaisma {
997
998
  readonly name: string | null;
998
999
  /** The room's labels, like { Mode: "Classic", Map: "Docks" }. */
999
1000
  readonly labels: Readonly<Record<string, string>>;
1001
+ /**
1002
+ * Whether this player can chat at all ({@link Room.chat}): a Zaisma account whose chat setting isn't
1003
+ * nobody, playing (not watching). False for guests: hide your chat box, and offer preset phrases or emotes.
1004
+ */
1005
+ readonly canChat: boolean;
1000
1006
  /**
1001
1007
  * When Zaisma will end this match, an ISO time, or null: set once the game's hosting pauses for the
1002
1008
  * month, an hour after the pause (the "ending" event). Never set in a room on your own server.
@@ -1133,8 +1139,31 @@ declare namespace Zaisma {
1133
1139
  * player changed their mind. A block never removes anyone from the room.
1134
1140
  */
1135
1141
  block(playerId: string): Promise<boolean>;
1136
- /** Report a player to Zaisma, with what happened in the player's own words if they gave any (details). Nothing about the reporting player is sent. */
1142
+ /**
1143
+ * Report a player to Zaisma, with what happened in the player's own words if they gave any (details).
1144
+ * The last chat lines (up to 20) they sent this player, or this player sent them, go with it from this
1145
+ * screen; nothing else about the reporting player is sent.
1146
+ */
1137
1147
  report(playerId: string, reason: ReportReason, details?: string): Promise<ReportAnswer>;
1148
+ /**
1149
+ * Send a chat message: through the room to the players in it (or chatOptions.toPlayerIds of them) who
1150
+ * may chat with this player, on Zaisma's rooms or your own server's runner. It reaches only players in
1151
+ * this player's chat group whose settings let them chat with this player, never anyone with a block
1152
+ * between them, and never spectators. Your game server's onChat may stop it or send it to fewer
1153
+ * players first; then Zaisma's chat filter checks it on its way (messages to and from under-18s
1154
+ * always, between adults when your game server turns it on), and Zaisma keeps none of it. Draw the
1155
+ * chat yourself: the "chat" event brings the others' messages; show this player's own when it resolves.
1156
+ *
1157
+ * Resolves { sentAt } once it went, even when nobody could read it (the sender is never told who
1158
+ * can't). Rejects with error.code chat_stopped (error.reason says why, like contact_details or
1159
+ * stopped_by_game, and error.message is what to show the player), chat_not_allowed (a guest, or chat
1160
+ * set to nobody: check {@link Room.canChat} first), chat_empty, chat_too_long (over 2000 characters),
1161
+ * chat_too_fast (over 20 in 10 seconds) or connection_closed.
1162
+ *
1163
+ * @example
1164
+ * room.chat(input.value).then(({ sentAt }) => addLine(room.me.name, input.value), (error) => showError(error.message));
1165
+ */
1166
+ chat(text: string, chatOptions?: ChatOptions): Promise<ChatSent>;
1138
1167
 
1139
1168
  /**
1140
1169
  * Listen for an event. Listening is optional: without "move" and "back" listeners, the SDK follows
@@ -1241,8 +1270,53 @@ declare namespace Zaisma {
1241
1270
  * room.on("versionUpdate", ({ appliesAt }) => showBanner(`Updating at ${new Date(appliesAt).toLocaleTimeString()}`));
1242
1271
  */
1243
1272
  versionUpdate: (versionUpdate: VersionUpdate) => void;
1273
+ /**
1274
+ * Another player's chat message reached this player ({@link Room.chat}). Messages from players this
1275
+ * player muted or blocked never come. hasSwearing and hasSlur flag what Zaisma allows: hide or blur
1276
+ * them if your game doesn't. (Optional here only so games written for v1's events still fit.)
1277
+ */
1278
+ chat?: (chatMessage: ChatMessage) => void;
1279
+ }
1280
+
1281
+ /** What {@link Room.chat} takes. */
1282
+ interface ChatOptions {
1283
+ /** Send it to only these players in the room (like a team), from room.players. All players when left out. */
1284
+ toPlayerIds?: string[];
1244
1285
  }
1245
1286
 
1287
+ /** {@link Room.chat}'s answer once the message went. */
1288
+ interface ChatSent {
1289
+ /** When the room sent it, as an ISO time. */
1290
+ sentAt: string;
1291
+ }
1292
+
1293
+ /** A chat message another player sent ({@link Room.chat}), as the "chat" event brings it. */
1294
+ interface ChatMessage {
1295
+ fromPlayerId: string;
1296
+ text: string;
1297
+ /** It swears: Zaisma allows it; hide it if your game doesn't. */
1298
+ hasSwearing: boolean;
1299
+ /** It has a slur: Zaisma allows it too; your game and its moderators decide. */
1300
+ hasSlur: boolean;
1301
+ /** When the room sent it, as an ISO time. */
1302
+ sentAt: string;
1303
+ }
1304
+
1305
+ /**
1306
+ * The codes {@link Room.chat} rejects with: chat_stopped (the game server's onChat or Zaisma's filter
1307
+ * stopped it: error.reason and error.message say why), chat_not_allowed, chat_empty, chat_too_long,
1308
+ * chat_too_fast, bad_player_id, busy (try again in a moment), ended, spectator_cannot_send and
1309
+ * connection_closed. More codes may come within v1: show the error's message.
1310
+ */
1311
+ type ChatErrorCode = "chat_stopped" | "chat_not_allowed" | "chat_empty" | "chat_too_long" | "chat_too_fast" | "chat_not_here" | "bad_player_id" | "busy" | "ended" | "spectator_cannot_send" | "connection_closed";
1312
+
1313
+ /**
1314
+ * Why {@link Room.chat} was stopped (error.reason with chat_stopped): "stopped_by_game" (your game
1315
+ * server's onChat), the filter's {@link ChatCheckReason}s, "chat_paused" (the sender's chat is paused
1316
+ * for now) or "not_allowed_to_chat". More values may come.
1317
+ */
1318
+ type ChatStoppedReason = "stopped_by_game" | ChatCheckReason | "chat_paused" | "not_allowed_to_chat";
1319
+
1246
1320
  /** What {@link Room.smoothedState} takes. */
1247
1321
  interface SmoothingOptions {
1248
1322
  /** How far behind the room's clock to draw, in milliseconds, from 0 to 1000. By default 100. */
@@ -1467,6 +1541,7 @@ declare namespace Zaisma {
1467
1541
  * - already_playing: a player in the room tried to watch it too.
1468
1542
  * - spectator_cannot_send: a spectator tried to send to the room ({@link Room.send}, {@link Room.sendBytes}).
1469
1543
  * - send_queue_full: the SDK's own: more than 60 reliable sends waited while the room wasn't reachable, so the oldest were dropped. Send less while connectionStatus isn't open, or use latest_only for updates that replace each other.
1544
+ * - 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.
1470
1545
  *
1471
1546
  * More codes may come within v1: handle one you don't know like an error you can show (its message).
1472
1547
  */
@@ -1494,7 +1569,12 @@ declare namespace Zaisma {
1494
1569
  | "spectators_full"
1495
1570
  | "already_playing"
1496
1571
  | "spectator_cannot_send"
1497
- | "send_queue_full";
1572
+ | "send_queue_full"
1573
+ | "chat_not_here"
1574
+ | "chat_not_allowed"
1575
+ | "chat_empty"
1576
+ | "chat_too_long"
1577
+ | "chat_too_fast";
1498
1578
 
1499
1579
  /**
1500
1580
  * The codes a request's errors have ({@link Server.request}), from Zaisma or the Node runner alike:
@@ -1551,6 +1631,7 @@ declare namespace Zaisma {
1551
1631
  * - bad_idle_timeout: an idleTimeoutSec that isn't from 30 to 86400.
1552
1632
  * - request_failed: Zaisma didn't answer (no connection, or a problem on Zaisma's side); try again.
1553
1633
  * - no_game: {@link Items.buy} named a game that isn't open to play here.
1634
+ * - chat_stopped, chat_not_allowed, chat_empty, chat_too_long, chat_too_fast, chat_not_here: {@link Room.chat} didn't send ({@link ChatErrorCode}).
1554
1635
  * - And a request's ({@link RequestErrorCode}).
1555
1636
  *
1556
1637
  * More values may come; treat an unknown one as "request_failed".
@@ -1558,6 +1639,12 @@ declare namespace Zaisma {
1558
1639
  type ErrorCode =
1559
1640
  | RoomErrorCode
1560
1641
  | RequestErrorCode
1642
+ | "chat_stopped"
1643
+ | "chat_not_allowed"
1644
+ | "chat_empty"
1645
+ | "chat_too_long"
1646
+ | "chat_too_fast"
1647
+ | "chat_not_here"
1561
1648
  | "unchecked"
1562
1649
  | "unknown_option"
1563
1650
  | "unknown_event"
@@ -1636,6 +1723,8 @@ declare namespace Zaisma {
1636
1723
  message: string | null;
1637
1724
  /** Whether it swears. Zaisma allows swearing; block it yourself if your game doesn't. */
1638
1725
  hasSwearing: boolean;
1726
+ /** Whether it has a slur. Zaisma allows it too; your game and its moderators decide. */
1727
+ hasSlur: boolean;
1639
1728
  /** The text tidied up: hidden characters and extra spaces gone, line breaks kept (empty when it's too_long). */
1640
1729
  text: string;
1641
1730
  }
@@ -1643,10 +1732,11 @@ declare namespace Zaisma {
1643
1732
  /**
1644
1733
  * Why {@link checkChat} stopped a message: "contact_details" (a link, handle, email, phone number or
1645
1734
  * contact app in any disguise, or moving to another app or meeting up), "personal_details" (where
1646
- * someone lives, their school, their full name, photos), "slur", "sexual", "unchecked" (Zaisma couldn't
1647
- * check it: try again in a moment) or "too_long" (over 2000 characters). The game server's
1648
- * ChatCheckReason has two more, "chat_paused" and "not_allowed_to_chat", which only a room can give.
1649
- * More values may come; treat an unknown one as a stop: show message.
1735
+ * someone lives, their school, their full name, photos), "sexual" or "too_long" (over 2000 characters).
1736
+ * "slur" and "unchecked" are v1's and no longer given: slurs are flagged (hasSlur), and what the AI can't
1737
+ * look at goes after the rules. The game server's ChatCheckReason has two more, "chat_paused" and
1738
+ * "not_allowed_to_chat", which only a room can give. More values may come; treat an unknown one as a
1739
+ * stop: show message.
1650
1740
  */
1651
1741
  type ChatCheckReason = "contact_details" | "personal_details" | "slur" | "sexual" | "unchecked" | "too_long";
1652
1742
 
package/sdk/zaisma.js CHANGED
@@ -29,7 +29,11 @@
29
29
  var SMOOTHING_OPTIONS = ["delayMs"];
30
30
  var DELIVERIES = ["reliable", "latest_only"];
31
31
  var ROOM_INFO_FIELDS = ["name", "labels"];
32
- var ROOM_EVENTS = ["message", "bytes", "state", "players", "connectionStatus", "ended", "ending", "kicked", "settings", "move", "moved", "idle", "away", "back", "muted", "error", "versionUpdate"];
32
+ var ROOM_EVENTS = ["message", "bytes", "state", "players", "connectionStatus", "ended", "ending", "kicked", "settings", "move", "moved", "idle", "away", "back", "muted", "error", "versionUpdate", "chat"];
33
+ /** Chat (room.chat): the longest message, the lines a room keeps in memory for reports, and how many of them a report carries. */
34
+ var MAXIMUM_CHAT_CHARACTER_COUNT = 2000;
35
+ var KEPT_CHAT_LINE_COUNT = 50;
36
+ var REPORTED_CHAT_LINE_COUNT = 20;
33
37
  var SAVES_EVENTS = ["conflict", "developerChange"];
34
38
  var ITEMS_EVENTS = ["purchase", "refund", "sheetOpen", "sheetClose"];
35
39
  var VERSIONS_EVENTS = ["update"];
@@ -186,9 +190,11 @@
186
190
  if (!config.rooms) return Promise.reject(notOnZaisma("Player passes come from Zaisma, so they work only in games played on Zaisma."));
187
191
  if (!passPromise) {
188
192
  passPromise = roomKey().then(function (given) {
189
- // The kept pass, unless the player signed in or out since it was made.
193
+ // A guest keeps their pass, so a reload is the same guest. An account gets a new one with each page's key: the key is for
194
+ // whoever is signed in now (another account than the kept pass's, maybe), and its pass brings their chat settings, friends
195
+ // and blocks as they are now; the account's player ID is the same either way.
190
196
  var kept = keptPass();
191
- if (kept && kept.kind === given.playerKind) return kept;
197
+ if (kept && kept.kind === "guest" && given.playerKind === "guest") return kept;
192
198
  return post("/v1/pass", { gameSlug: config.game, roomKey: given.key }).then(function (answer) {
193
199
  var mine = { id: answer.player.id, name: answer.player.name, kind: answer.player.kind, pass: answer.pass, expiresAt: answer.expiresAt };
194
200
  try { sessionStorage.setItem(passKey, JSON.stringify(mine)); } catch (error) { /* fine */ }
@@ -457,6 +463,12 @@
457
463
  this._flushTimer = null;
458
464
  this._isQueueFullSaid = false;
459
465
  this._isWelcomed = false;
466
+ // Chat (room.chat): whether this player can chat at all (from welcome), the messages waiting for the
467
+ // room's answer by their chatNumber, and the room's last chat lines, in memory only, for reports.
468
+ this.canChat = false;
469
+ this._chatNumber = 0;
470
+ this._chats = new Map();
471
+ this._chatLines = [];
460
472
  }
461
473
  Room.prototype = Object.create(Emitter.prototype);
462
474
 
@@ -557,6 +569,7 @@
557
569
  room._checks = message.awayChecks || { idleTimeoutSec: null, hiddenTimeoutSec: null };
558
570
  room._probeIntervalSec = typeof message.probeIntervalSec === "number" ? message.probeIntervalSec : null;
559
571
  room.spectatorCount = typeof message.spectatorCount === "number" ? message.spectatorCount : 0;
572
+ room.canChat = message.canChat === true && !room.isSpectator;
560
573
  room._away = room._gone = room._idleSent = room._coming = room._offered = room._isReloadingForVersion = false;
561
574
  room.awayReason = null;
562
575
  room._isQueueFullSaid = false;
@@ -606,6 +619,8 @@
606
619
  room._mutedChanged();
607
620
  } else if (message.type === "message") {
608
621
  room._emit("message", message.data, typeof message.fromPlayerId === "string" ? message.fromPlayerId : null);
622
+ } else if (message.type === "chat" || message.type === "chat_sent" || message.type === "chat_stopped" || (message.type === "error" && typeof message.chatNumber === "number")) {
623
+ room._chatAnswer(message);
609
624
  } else if (message.type === "state") {
610
625
  room._takeState(message);
611
626
  } else if (message.type === "room") {
@@ -661,6 +676,7 @@
661
676
  if (room._done || event.code === 4000 || event.code === 4001) {
662
677
  room._done = true;
663
678
  room._queue = [];
679
+ room._failChats();
664
680
  room._setConnectionStatus("closed");
665
681
  if (room._ready) {
666
682
  room._ready.reject(codedError("connection_closed", "The connection closed before the room let the player in" + (event.reason ? ": " + event.reason : "") + "."));
@@ -1253,16 +1269,85 @@
1253
1269
  });
1254
1270
  return blockAsk;
1255
1271
  }
1256
- /** Report a player to Zaisma. reason: real_life_threat, personal_details, following_after_block or sexual (Zaisma looks), or harassment, cheating or other (passed to the game's developer). Nothing about you is sent. */
1272
+ /**
1273
+ * Report a player to Zaisma. reason: real_life_threat, personal_details, following_after_block or sexual (Zaisma looks), or harassment, cheating or other (passed to the game's developer).
1274
+ * The last chat lines they sent you, or you sent them, go with it (up to 20); nothing else about you is sent.
1275
+ */
1257
1276
  Room.prototype.report = function (playerId, reason, details) {
1258
1277
  var reported = this.players.filter(function (each) { return each.id === playerId; })[0];
1259
- return post("/v1/report", { gameSlug: config.game, roomCode: this.roomCode, playerId: playerId, playerName: reported ? reported.name : "", reason: reason, details: details || "" });
1278
+ var me = this.me ? this.me.id : null;
1279
+ var chatMessages = this._chatLines.filter(function (line) {
1280
+ return line.fromPlayerId === playerId || (line.fromPlayerId === me && (!line.toPlayerIds || line.toPlayerIds.indexOf(playerId) !== -1));
1281
+ }).slice(-REPORTED_CHAT_LINE_COUNT).map(function (line) { return { fromPlayerId: line.fromPlayerId, text: line.text, sentAt: line.sentAt }; });
1282
+ return post("/v1/report", { gameSlug: config.game, roomCode: this.roomCode, playerId: playerId, playerName: reported ? reported.name : "", reason: reason, details: details || "", chatMessages: chatMessages });
1283
+ };
1284
+
1285
+ /**
1286
+ * Send a chat message (zaisma.d.ts: room.chat): through the room, to the players who may chat with this
1287
+ * one, after the game server's onChat and Zaisma's chat filter. Resolves { sentAt } once it went, even to
1288
+ * nobody; rejects with a coded error when it didn't.
1289
+ */
1290
+ Room.prototype.chat = function (text, chatOptions) {
1291
+ var toPlayerIds = chatOptions && chatOptions.toPlayerIds;
1292
+ if (!this.canChat) return Promise.reject(codedError("chat_not_allowed", "You can't chat here."));
1293
+ if (typeof text !== "string" || !text.trim()) return Promise.reject(codedError("chat_empty", "Type a message first."));
1294
+ if (text.length > MAXIMUM_CHAT_CHARACTER_COUNT) return Promise.reject(codedError("chat_too_long", "That message is too long to send."));
1295
+ if (toPlayerIds !== undefined && !(Array.isArray(toPlayerIds) && toPlayerIds.every(function (id) { return typeof id === "string"; }))) {
1296
+ return Promise.reject(codedError("bad_player_id", "chatOptions.toPlayerIds takes a list of player IDs from room.players."));
1297
+ }
1298
+ if (this._done) return Promise.reject(codedError("connection_closed", "This room is closed."));
1299
+ var room = this;
1300
+ var chatNumber = ++this._chatNumber;
1301
+ var payload = { type: "chat", chatNumber: chatNumber, text: text };
1302
+ if (toPlayerIds) payload.toPlayerIds = toPlayerIds.slice();
1303
+ return new Promise(function (resolve, reject) {
1304
+ room._chats.set(chatNumber, { text: text, toPlayerIds: payload.toPlayerIds || null, resolve: resolve, reject: reject });
1305
+ // Waits in order with the other sends while the connection is back.
1306
+ room._sendReliable(JSON.stringify(payload));
1307
+ });
1308
+ };
1309
+ /** A chat line, kept in memory for reports: the last KEPT_CHAT_LINE_COUNT. */
1310
+ Room.prototype._keepChatLine = function (line) {
1311
+ this._chatLines.push(line);
1312
+ if (this._chatLines.length > KEPT_CHAT_LINE_COUNT) this._chatLines.shift();
1313
+ };
1314
+ /** What the room said about chat: someone's message, or its answer on one of this player's. */
1315
+ Room.prototype._chatAnswer = function (message) {
1316
+ if (message.type === "chat") {
1317
+ if (typeof message.fromPlayerId !== "string" || typeof message.text !== "string") return;
1318
+ // Muted and blocked players' chat never reaches the game.
1319
+ if (this.isMuted(message.fromPlayerId)) return;
1320
+ var chat = { fromPlayerId: message.fromPlayerId, text: message.text, hasSwearing: message.hasSwearing === true, hasSlur: message.hasSlur === true, sentAt: typeof message.sentAt === "string" ? message.sentAt : new Date().toISOString() };
1321
+ this._keepChatLine({ fromPlayerId: chat.fromPlayerId, text: chat.text, sentAt: chat.sentAt, toPlayerIds: null });
1322
+ this._emit("chat", chat);
1323
+ return;
1324
+ }
1325
+ var waiting = this._chats.get(message.chatNumber);
1326
+ if (!waiting) return;
1327
+ this._chats.delete(message.chatNumber);
1328
+ if (message.type === "chat_sent") {
1329
+ this._keepChatLine({ fromPlayerId: this.me ? this.me.id : null, text: waiting.text, sentAt: message.sentAt, toPlayerIds: waiting.toPlayerIds });
1330
+ waiting.resolve({ sentAt: message.sentAt });
1331
+ } else if (message.type === "chat_stopped") {
1332
+ var stopped = codedError("chat_stopped", typeof message.message === "string" && message.message ? message.message : "Your message wasn't sent.");
1333
+ stopped.reason = typeof message.reason === "string" ? message.reason : null;
1334
+ waiting.reject(stopped);
1335
+ } else {
1336
+ waiting.reject(codedError(message.code, message.message));
1337
+ }
1338
+ };
1339
+ /** Chat messages still waiting when the room closes don't go. */
1340
+ Room.prototype._failChats = function () {
1341
+ var waiting = Array.from(this._chats.values());
1342
+ this._chats.clear();
1343
+ waiting.forEach(function (chat) { chat.reject(codedError("connection_closed", "The room closed before the message went.")); });
1260
1344
  };
1261
1345
 
1262
1346
  Room.prototype.leave = function () {
1263
1347
  if (!this._done) left(this);
1264
1348
  this._done = true;
1265
1349
  this._queue = [];
1350
+ this._failChats();
1266
1351
  if (this._ws) this._ws.close(1000, "Left");
1267
1352
  };
1268
1353
 
package/src/chat.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Zaisma's chat filter for your own server (docs/DECISIONS.md, "The text filter catches what matters and
3
3
  * leaves the rest alone"). Every chat message to or from an under-18 account is checked on Zaisma's side:
4
- * contact details in any disguise, moving to another app or meeting up, personal details, slurs and
5
- * sexual content are stopped. Swearing is only flagged (hasSwearing): blocking it is your call. The answer
6
- * is one verdict for the whole message, never per receiver, so it never says who's under 18.
4
+ * contact details in any disguise, moving to another app or meeting up, personal details and sexual
5
+ * content are stopped. Swearing and slurs are only flagged (hasSwearing, hasSlur): blocking them is your
6
+ * call. Between adults it only flags, unless you turn the filter on for them too (isAdultFilterEnabled).
7
+ * The answer is one verdict for the whole message, never per receiver, so it never says who's under 18.
7
8
  *
8
9
  * checkChat sends the players' passes (as your server received them) and the sender's last few messages to
9
10
  * Zaisma (POST <issuer>/v1/chat/check), so a phone number sent a few digits at a time is caught too.
@@ -11,14 +12,17 @@
11
12
  * It's for your own server only. In Zaisma rooms the room filters the chat it delivers itself, so a game
12
13
  * server running there never sees passes, and checkChat throws a ChatCheckError (own_server_only).
13
14
  */
14
- /** Why a message was stopped. The same reasons as Zaisma.checkChat, plus chat_paused and not_allowed_to_chat. */
15
+ /**
16
+ * Why a message was stopped. The same reasons as Zaisma.checkChat, plus chat_paused, not_allowed_to_chat and
17
+ * unchecked (Zaisma couldn't be reached). slur is v1's and no longer given: slurs are flagged (hasSlur), never stopped.
18
+ */
15
19
  export type ChatCheckReason = "contact_details" | "personal_details" | "slur" | "sexual" | "chat_paused" | "unchecked" | "too_long" | "not_allowed_to_chat";
16
20
  export interface ChatCheck {
17
21
  /** Whether to deliver it. */
18
22
  isAllowed: boolean;
19
23
  /**
20
- * Why not; null when it's allowed. chat_paused: the sender's chat is paused or removed. unchecked: Zaisma
21
- * couldn't check it, so don't deliver it (try again in a moment). too_long: over 2000 characters.
24
+ * Why not; null when it's allowed. chat_paused: the sender's chat is paused or removed. unchecked: your
25
+ * server couldn't reach Zaisma, so don't deliver it (try again in a moment). too_long: over 2000 characters.
22
26
  * not_allowed_to_chat: canChat says the sender and a receiver may not chat (Zaisma checks it too).
23
27
  */
24
28
  reason: ChatCheckReason | null;
@@ -26,6 +30,8 @@ export interface ChatCheck {
26
30
  message: string | null;
27
31
  /** Whether it swears. Zaisma allows it; block it yourself if your game doesn't. */
28
32
  hasSwearing: boolean;
33
+ /** Whether it has a slur. Zaisma allows it too, so your game and its moderators decide. */
34
+ hasSlur: boolean;
29
35
  }
30
36
  export interface ChatCheckOptions {
31
37
  /** The message. */
@@ -34,7 +40,15 @@ export interface ChatCheckOptions {
34
40
  senderPass: string;
35
41
  /** The passes of everyone it would reach, up to 50. */
36
42
  receiverPasses: string[];
43
+ /** 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
+ isAdultFilterEnabled?: boolean;
37
45
  }
46
+ /** Chat in rooms (room.chat), for both Zaisma's rooms and your own server's runner: past any human's typing, so it only stops floods. */
47
+ export declare const CHAT_ROOM_LIMITS: {
48
+ /** Chat messages one player may send in any chatWindowDurationMs; past it, chat_too_fast. */
49
+ readonly maximumChatMessageCount: 20;
50
+ readonly chatWindowDurationMs: 10000;
51
+ };
38
52
  export declare const CHAT_CHECK_LIMITS: {
39
53
  /** The longest message checked, in characters: the whole text is checked, and a longer one is refused (too_long), never cut. */
40
54
  readonly maximumMessageCharacterCount: 2000;
@@ -56,8 +70,9 @@ export declare class ChatCheckError extends Error {
56
70
  /**
57
71
  * Check one chat message with Zaisma's filter. Call it on every chat message your server delivers, after
58
72
  * canChat says the players may chat, and deliver it only when isAllowed (Zaisma applies canChat's rule
59
- * too, so a message to anyone who may not chat is stopped). It's free. When Zaisma can't be
60
- * reached it answers unchecked: don't deliver the message.
73
+ * too, so a message to anyone who may not chat is stopped). It's free, except the checks of chat between
74
+ * adults when isAdultFilterEnabled. When Zaisma can't be reached it answers unchecked: don't deliver the message.
75
+ * The runner (zaisma-game-server) does all this for room.chat in its rooms.
61
76
  *
62
77
  * const check = await checkChat({ text, senderPass, receiverPasses: [receiverPass] });
63
78
  * if (!check.isAllowed || check.hasSwearing) return reply(sender, check.message ?? "No swearing here.");
package/src/chat.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Zaisma's chat filter for your own server (docs/DECISIONS.md, "The text filter catches what matters and
3
3
  * leaves the rest alone"). Every chat message to or from an under-18 account is checked on Zaisma's side:
4
- * contact details in any disguise, moving to another app or meeting up, personal details, slurs and
5
- * sexual content are stopped. Swearing is only flagged (hasSwearing): blocking it is your call. The answer
6
- * is one verdict for the whole message, never per receiver, so it never says who's under 18.
4
+ * contact details in any disguise, moving to another app or meeting up, personal details and sexual
5
+ * content are stopped. Swearing and slurs are only flagged (hasSwearing, hasSlur): blocking them is your
6
+ * call. Between adults it only flags, unless you turn the filter on for them too (isAdultFilterEnabled).
7
+ * The answer is one verdict for the whole message, never per receiver, so it never says who's under 18.
7
8
  *
8
9
  * checkChat sends the players' passes (as your server received them) and the sender's last few messages to
9
10
  * Zaisma (POST <issuer>/v1/chat/check), so a phone number sent a few digits at a time is caught too.
@@ -12,6 +13,12 @@
12
13
  * server running there never sees passes, and checkChat throws a ChatCheckError (own_server_only).
13
14
  */
14
15
  import { OutsideCallError } from "./outside-calls.js";
16
+ /** Chat in rooms (room.chat), for both Zaisma's rooms and your own server's runner: past any human's typing, so it only stops floods. */
17
+ export const CHAT_ROOM_LIMITS = {
18
+ /** Chat messages one player may send in any chatWindowDurationMs; past it, chat_too_fast. */
19
+ maximumChatMessageCount: 20,
20
+ chatWindowDurationMs: 10_000,
21
+ };
15
22
  export const CHAT_CHECK_LIMITS = {
16
23
  /** The longest message checked, in characters: the whole text is checked, and a longer one is refused (too_long), never cut. */
17
24
  maximumMessageCharacterCount: 2000,
@@ -34,9 +41,9 @@ export class ChatCheckError extends Error {
34
41
  this.fix = fix;
35
42
  }
36
43
  }
37
- const UNCHECKED = { isAllowed: false, reason: "unchecked", message: "This message couldn't be checked. Try again in a moment.", hasSwearing: false };
38
- const TOO_LONG = { isAllowed: false, reason: "too_long", message: "That message is too long to send.", hasSwearing: false };
39
- const REASONS = ["contact_details", "personal_details", "slur", "sexual", "chat_paused", "unchecked", "too_long", "not_allowed_to_chat"];
44
+ const UNCHECKED = { isAllowed: false, reason: "unchecked", message: "This message couldn't be checked. Try again in a moment.", hasSwearing: false, hasSlur: false };
45
+ const TOO_LONG = { isAllowed: false, reason: "too_long", message: "That message is too long to send.", hasSwearing: false, hasSlur: false };
46
+ const REASONS = ["contact_details", "personal_details", "sexual", "chat_paused", "unchecked", "too_long", "not_allowed_to_chat"];
40
47
  /** Each sender's last messages, by the sender's pass's player ID: only in this process's memory. */
41
48
  const recentBySender = new Map();
42
49
  /** The claims of a pass, unchecked: Zaisma checks the signature. Only to find whose it is and where it's from. */
@@ -67,13 +74,16 @@ function rememberText(senderPlayerId, text, nowMs) {
67
74
  /**
68
75
  * Check one chat message with Zaisma's filter. Call it on every chat message your server delivers, after
69
76
  * canChat says the players may chat, and deliver it only when isAllowed (Zaisma applies canChat's rule
70
- * too, so a message to anyone who may not chat is stopped). It's free. When Zaisma can't be
71
- * reached it answers unchecked: don't deliver the message.
77
+ * too, so a message to anyone who may not chat is stopped). It's free, except the checks of chat between
78
+ * adults when isAdultFilterEnabled. When Zaisma can't be reached it answers unchecked: don't deliver the message.
79
+ * The runner (zaisma-game-server) does all this for room.chat in its rooms.
72
80
  *
73
81
  * const check = await checkChat({ text, senderPass, receiverPasses: [receiverPass] });
74
82
  * if (!check.isAllowed || check.hasSwearing) return reply(sender, check.message ?? "No swearing here.");
75
83
  */
76
84
  export async function checkChat({ text, senderPass, receiverPasses }) {
85
+ // Read apart from the destructuring, which is v1's signature as it's locked (test/contracts).
86
+ const isAdultFilterEnabled = arguments[0]?.isAdultFilterEnabled === true;
77
87
  const claims = readClaims(String(senderPass ?? ""));
78
88
  if (typeof claims.sub !== "string" || !isZaismaIssuer(claims.iss) || !Array.isArray(receiverPasses))
79
89
  return UNCHECKED;
@@ -87,7 +97,7 @@ export async function checkChat({ text, senderPass, receiverPasses }) {
87
97
  const response = await fetch(`${claims.iss}/v1/chat/check`, {
88
98
  method: "POST",
89
99
  headers: { "Content-Type": "application/json" },
90
- body: JSON.stringify({ gameSlug: claims.aud, senderPass, receiverPasses: receiverPasses.slice(0, CHAT_CHECK_LIMITS.maximumReceiverCount), text: message, recentTexts }),
100
+ body: JSON.stringify({ gameSlug: claims.aud, senderPass, receiverPasses: receiverPasses.slice(0, CHAT_CHECK_LIMITS.maximumReceiverCount), text: message, recentTexts, isAdultFilterEnabled }),
91
101
  signal: AbortSignal.timeout(CHAT_CHECK_LIMITS.checkTimeoutMs),
92
102
  });
93
103
  if (!response.ok)
@@ -96,12 +106,12 @@ export async function checkChat({ text, senderPass, receiverPasses }) {
96
106
  if (typeof answer.isAllowed !== "boolean")
97
107
  return UNCHECKED;
98
108
  const reason = typeof answer.reason === "string" && REASONS.includes(answer.reason) ? answer.reason : null;
99
- return { isAllowed: answer.isAllowed, reason, message: typeof answer.message === "string" ? answer.message : null, hasSwearing: answer.hasSwearing === true };
109
+ return { isAllowed: answer.isAllowed, reason, message: typeof answer.message === "string" ? answer.message : null, hasSwearing: answer.hasSwearing === true, hasSlur: answer.hasSlur === true };
100
110
  }
101
111
  catch (error) {
102
112
  // In Zaisma rooms the gateway refuses the call to Zaisma's own address: checkChat is for a game server on your own server.
103
113
  if (error instanceof OutsideCallError && ["unreachable_host", "not_https", "host_not_allowed"].includes(error.code)) {
104
- throw new ChatCheckError("checkChat is for a game server on your own server. In Zaisma rooms the room runs Zaisma's chat filter on the chat it delivers.", "Send chat through Zaisma's chat hosting in rooms once it's out; until then, check what a player types in the game with Zaisma.checkChat, or offer preset phrases and emotes.");
114
+ throw new ChatCheckError("checkChat is for a game server on your own server. In Zaisma rooms the room runs Zaisma's chat filter on the chat it delivers.", "In Zaisma rooms, send chat with room.chat in the game; the room filters it.");
105
115
  }
106
116
  return UNCHECKED;
107
117
  }
package/src/index.d.ts CHANGED
@@ -1,10 +1,10 @@
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 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, 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";
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";
5
5
  export { DATABASE_LIMITS, DatabaseError, type DatabaseErrorCode, type DatabaseRow, type DatabaseStatement, type DatabaseValue, type GameDatabase } from "./database.ts";
6
6
  export { OUTSIDE_CALL_LIMITS, OutsideCallError, type OutsideCallErrorCode } from "./outside-calls.ts";
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
- export { CHAT_CHECK_LIMITS, ChatCheckError, checkChat, type ChatCheck, type ChatCheckOptions, type ChatCheckReason } from "./chat.ts";
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
10
  export { relay } from "./relay.ts";