@zaisma/game-server 1.22.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bench/glass.js +25098 -24583
- package/bin/node.js +2 -2
- package/package.json +1 -1
- package/sdk/zaisma.d.ts +4 -0
- package/sdk/zaisma.js +64 -4
- package/src/doors.d.ts +210 -0
- package/src/doors.js +281 -0
- package/src/internal.d.ts +1 -0
- package/src/internal.js +1 -0
- package/src/requests.d.ts +1 -1
- package/src/requests.js +2 -0
- package/src/room.d.ts +101 -1
- package/src/room.js +159 -6
package/bin/node.js
CHANGED
|
@@ -26,7 +26,7 @@ import { createServer } from "node:http";
|
|
|
26
26
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27
27
|
import { resolve } from "node:path";
|
|
28
28
|
import { readFileSync, realpathSync } from "node:fs";
|
|
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";
|
|
29
|
+
import { roomMessageProblem, 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";
|
|
30
30
|
import { OutsideCallError } from "../src/outside-calls.js";
|
|
31
31
|
import { startFastLane } from "./fast-lane.js";
|
|
32
32
|
import { checkedStartServerOptions, localDatabase, localRoomStates, passKeysFrom, profilesFrom, receiptKeysFrom, secretsFromEnvironment } from "./node-parts.js";
|
|
@@ -666,7 +666,7 @@ export function startServer(options) {
|
|
|
666
666
|
const hello = JSON.parse(text ?? "");
|
|
667
667
|
// A Zaisma pass, or from a game played off Zaisma, its playerToken (null for a guest).
|
|
668
668
|
const isOutside = hello.pass === undefined && isPlayerToken(hello.playerToken);
|
|
669
|
-
if (hello.type !== "hello" || (typeof hello.pass !== "string" && !isOutside)) return refuse("no_hello");
|
|
669
|
+
if (hello.type !== "hello" || (typeof hello.pass !== "string" && !isOutside) || roomMessageProblem("hello", hello)) return refuse("no_hello");
|
|
670
670
|
if (isOutside) playerToken = hello.playerToken;
|
|
671
671
|
else pass = hello.pass;
|
|
672
672
|
isSpectator = hello.isSpectator === true;
|
package/package.json
CHANGED
package/sdk/zaisma.d.ts
CHANGED
|
@@ -1653,6 +1653,7 @@ declare namespace Zaisma {
|
|
|
1653
1653
|
* - ended: the match is over.
|
|
1654
1654
|
* - kept_out: the player was removed for the rest of the match.
|
|
1655
1655
|
* - refused: the game server's canJoin turned them away.
|
|
1656
|
+
* - rented_share_used: this player is on a rented-server network, and the share of the game's hosting such traffic gets is used. Zaisma's page tells them to turn off their VPN or sign in. Everyone else plays on.
|
|
1656
1657
|
* - hosting_paused: the game's hosting on Zaisma is paused this month (its free allowance, or its developer's budget or credit, is used); new rooms open again when the month resets (UTC) or its developer adds credit, and a running match takes no new players and ends within the hour.
|
|
1657
1658
|
* - account_needed: the player is a guest, and the game needs a Zaisma account for multiplayer or the whole game.
|
|
1658
1659
|
* - 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,6 +1685,7 @@ declare namespace Zaisma {
|
|
|
1684
1685
|
| "kept_out"
|
|
1685
1686
|
| "refused"
|
|
1686
1687
|
| "hosting_paused"
|
|
1688
|
+
| "rented_share_used"
|
|
1687
1689
|
| "account_needed"
|
|
1688
1690
|
| "game_not_open_to_player"
|
|
1689
1691
|
| "game_unavailable"
|
|
@@ -1720,6 +1722,7 @@ declare namespace Zaisma {
|
|
|
1720
1722
|
* - game_unavailable: the game can't be played right now.
|
|
1721
1723
|
* - no_request_handler: the game's server has no onRequest.
|
|
1722
1724
|
* - game_server_error: onRequest threw, answered something that isn't plain JSON or over 1 MB, or took over 10 seconds.
|
|
1725
|
+
* - rented_share_used: this player is on a rented-server network, and the share of the game's hosting such traffic gets is used. Zaisma's page tells them to turn off their VPN or sign in. Everyone else plays on.
|
|
1723
1726
|
* - hosting_paused: the game's hosting on Zaisma is paused this month; requests work again when the month resets (UTC) or its developer adds credit.
|
|
1724
1727
|
* - slow_down: too many requests from this address; try again in a minute.
|
|
1725
1728
|
* - bad_origin: the request didn't come from the game's own page.
|
|
@@ -1738,6 +1741,7 @@ declare namespace Zaisma {
|
|
|
1738
1741
|
| "no_request_handler"
|
|
1739
1742
|
| "game_server_error"
|
|
1740
1743
|
| "hosting_paused"
|
|
1744
|
+
| "rented_share_used"
|
|
1741
1745
|
| "slow_down"
|
|
1742
1746
|
| "bad_origin"
|
|
1743
1747
|
| "busy";
|
package/sdk/zaisma.js
CHANGED
|
@@ -179,6 +179,7 @@
|
|
|
179
179
|
return response.json().then(function (answer) {
|
|
180
180
|
if (response.ok) return answer;
|
|
181
181
|
var error = answer && answer.error;
|
|
182
|
+
if (error && error.code === "rented_share_used") tellRentedShareUsed();
|
|
182
183
|
if (error && error.code) throw codedError(error.code, error.message);
|
|
183
184
|
requestFailed();
|
|
184
185
|
}, requestFailed);
|
|
@@ -218,10 +219,52 @@
|
|
|
218
219
|
return keyPromise;
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
/**
|
|
223
|
+
* What a Zaisma room or request hears about where this player plays from on the meter: the word /v1/pass gave beside the pass, which
|
|
224
|
+
* this SDK can't read, and two things an honest browser can say. Nothing that tells this person from anyone else. Only rooms read them.
|
|
225
|
+
*/
|
|
226
|
+
var keptPlayShare = null;
|
|
227
|
+
var isInputSeen = false;
|
|
228
|
+
var graphicsChipAnswer = null;
|
|
229
|
+
function hasGraphicsChip() {
|
|
230
|
+
if (graphicsChipAnswer !== null) return graphicsChipAnswer;
|
|
231
|
+
graphicsChipAnswer = false;
|
|
232
|
+
try {
|
|
233
|
+
var webgl = document.createElement("canvas").getContext("webgl2") || document.createElement("canvas").getContext("webgl");
|
|
234
|
+
if (webgl) {
|
|
235
|
+
var rendererInfo = webgl.getExtension("WEBGL_debug_renderer_info");
|
|
236
|
+
var renderer = rendererInfo ? String(webgl.getParameter(rendererInfo.UNMASKED_RENDERER_WEBGL)) : "";
|
|
237
|
+
graphicsChipAnswer = !/swiftshader|llvmpipe|software|basic render/i.test(renderer);
|
|
238
|
+
var losing = webgl.getExtension("WEBGL_lose_context");
|
|
239
|
+
if (losing) losing.loseContext();
|
|
240
|
+
}
|
|
241
|
+
} catch (error) {
|
|
242
|
+
graphicsChipAnswer = true; // a browser that won't say counts as one that has one
|
|
243
|
+
}
|
|
244
|
+
return graphicsChipAnswer;
|
|
245
|
+
}
|
|
246
|
+
function playShareFacts() {
|
|
247
|
+
return { playShare: keptPlayShare || undefined, hasGraphicsChip: hasGraphicsChip(), isInputSeen: isInputSeen };
|
|
248
|
+
}
|
|
249
|
+
function firstInput() {
|
|
250
|
+
if (isInputSeen) return;
|
|
251
|
+
isInputSeen = true;
|
|
252
|
+
// A room told of no graphics chip and no input hears of the input now, without waiting for the game to send anything.
|
|
253
|
+
if (graphicsChipAnswer === false) joined.forEach(function (room) { if (room._ws && room._ws.readyState === 1) room._say({ type: "here" }); });
|
|
254
|
+
}
|
|
255
|
+
var FIRST_INPUTS = ["keydown", "pointerdown", "wheel", "touchstart"];
|
|
256
|
+
FIRST_INPUTS.forEach(function (type) { addEventListener(type, firstInput, { capture: true, passive: true }); });
|
|
257
|
+
addEventListener("gamepadconnected", firstInput);
|
|
258
|
+
/** Zaisma's game page says why a player on a rented network is stopped, with Sign in: only Zaisma can do that step. */
|
|
259
|
+
function tellRentedShareUsed() {
|
|
260
|
+
if (config.site && window.parent !== window) window.parent.postMessage({ type: "zaisma:rented-share-used" }, config.site);
|
|
261
|
+
}
|
|
262
|
+
|
|
221
263
|
/** The pass this tab kept, while it has 10 minutes or more left, else null. */
|
|
222
264
|
function keptPass() {
|
|
223
265
|
try {
|
|
224
266
|
var kept = JSON.parse(sessionStorage.getItem(passKey) || "null");
|
|
267
|
+
keptPlayShare = sessionStorage.getItem(passKey + ":share");
|
|
225
268
|
return kept && Date.parse(kept.expiresAt) - Date.now() > 10 * 60 * 1000 ? kept : null;
|
|
226
269
|
} catch (error) {
|
|
227
270
|
return null; // storage off: fetch a new pass
|
|
@@ -243,7 +286,11 @@
|
|
|
243
286
|
if (kept && kept.kind === "guest" && given.playerKind === "guest") return kept;
|
|
244
287
|
return post("/v1/pass", { gameSlug: config.game, roomKey: given.key }).then(function (answer) {
|
|
245
288
|
var mine = { id: answer.player.id, name: answer.player.name, kind: answer.player.kind, pass: answer.pass, expiresAt: answer.expiresAt };
|
|
246
|
-
|
|
289
|
+
keptPlayShare = answer.playShare || null;
|
|
290
|
+
try {
|
|
291
|
+
sessionStorage.setItem(passKey, JSON.stringify(mine));
|
|
292
|
+
if (keptPlayShare) sessionStorage.setItem(passKey + ":share", keptPlayShare);
|
|
293
|
+
} catch (error) { /* fine */ }
|
|
247
294
|
return mine;
|
|
248
295
|
});
|
|
249
296
|
}, function (error) {
|
|
@@ -696,7 +743,7 @@
|
|
|
696
743
|
};
|
|
697
744
|
|
|
698
745
|
// Codes that mean this room won't take the player, so joining fails instead of trying again.
|
|
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"];
|
|
746
|
+
var REFUSED = ["room_full", "room_closed", "ended", "bad_pass", "bad_player_token", "too_big", "game_unavailable", "kept_out", "refused", "hosting_paused", "rented_share_used", "account_needed", "game_not_open_to_player", "blocked_player_here", "other_version", "spectators_full", "room_not_running", "already_playing", "bad_origin"];
|
|
700
747
|
|
|
701
748
|
/**
|
|
702
749
|
* Whether a Zaisma room on another version (other_version) has the page reload the game at it: on the
|
|
@@ -730,7 +777,15 @@
|
|
|
730
777
|
room._probeIntervalSec = null;
|
|
731
778
|
var hello = { type: "hello", protocolVersionNumber: PROTOCOL_VERSION_NUMBER, room: room._made, joinData: room._joinData, idleTimeoutSec: room._clientIdle.idleTimeoutSec, idleCheckEnabled: room._clientIdle.idleCheckEnabled };
|
|
732
779
|
if (room._isOffZaisma) hello.playerToken = offZaisma.playerToken;
|
|
733
|
-
else
|
|
780
|
+
else {
|
|
781
|
+
hello.pass = room._pass;
|
|
782
|
+
if (!room._serverUrl) {
|
|
783
|
+
var told = playShareFacts();
|
|
784
|
+
hello.playShare = told.playShare;
|
|
785
|
+
hello.hasGraphicsChip = told.hasGraphicsChip;
|
|
786
|
+
hello.isInputSeen = told.isInputSeen;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
734
789
|
// quickJoin's first try at a room: rather another room than one with someone this player blocked.
|
|
735
790
|
if (room._avoidsBlockedPlayers) hello.avoidBlockedPlayers = true;
|
|
736
791
|
// Any quickJoin: Zaisma learns the game joins players by itself, for its Rooms tab.
|
|
@@ -860,6 +915,7 @@
|
|
|
860
915
|
window.parent.postMessage(room._spectating({ type: "zaisma:reload", code: room.roomCode, versionId: message.versionId }), config.site);
|
|
861
916
|
} else if (message.type === "error") {
|
|
862
917
|
room._emit("error", { code: message.code, message: message.message });
|
|
918
|
+
if (message.code === "rented_share_used") tellRentedShareUsed();
|
|
863
919
|
if (room._ready && REFUSED.indexOf(message.code) !== -1) {
|
|
864
920
|
room._done = true;
|
|
865
921
|
room._ready.reject(codedError(message.code, message.message));
|
|
@@ -1840,7 +1896,11 @@
|
|
|
1840
1896
|
var base = (serverUrl || config.rooms).replace(/^ws/, "http");
|
|
1841
1897
|
var isOffZaisma = !config.rooms;
|
|
1842
1898
|
// 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) {
|
|
1899
|
+
var sender = isOffZaisma ? Promise.resolve({ playerToken: offZaisma.playerToken }) : getPass().then(function (mine) {
|
|
1900
|
+
if (serverUrl) return { pass: mine.pass };
|
|
1901
|
+
var told = playShareFacts();
|
|
1902
|
+
return { pass: mine.pass, playShare: told.playShare, hasGraphicsChip: told.hasGraphicsChip, isInputSeen: told.isInputSeen };
|
|
1903
|
+
});
|
|
1844
1904
|
return sender.then(function (who) {
|
|
1845
1905
|
who.request = JSON.parse(requestText);
|
|
1846
1906
|
return fetch(base + "/v1/requests/" + (isOffZaisma ? offZaisma.gameSlug : config.game), { method: "POST", headers: { "Content-Type": "text/plain" }, body: JSON.stringify(who) }).then(answered, requestFailed);
|
package/src/doors.d.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Door contracts (docs/DOORS.md). Every way into Zaisma, of every kind, states who may call it, what it may
|
|
3
|
+
* reach and what it trusts from other doors, and goes through checkDoor before its own code runs. Here, in the
|
|
4
|
+
* game server package's internal part, so a room on a developer's own server goes through the same check;
|
|
5
|
+
* never in the package's published index.
|
|
6
|
+
*/
|
|
7
|
+
/** Callers the door's guard proves itself, from what it read of the request or the room. */
|
|
8
|
+
export type DoorCallerProvenByGuard =
|
|
9
|
+
/** Guests too. A door that shows a signed-in person more trusts `session` as well. */
|
|
10
|
+
"anyone"
|
|
11
|
+
/** A session that works. */
|
|
12
|
+
| "signed_in"
|
|
13
|
+
/** A session with a finished account, and no developer ban unless the door keeps it open to them. */
|
|
14
|
+
| "developer"
|
|
15
|
+
/** Cloudflare Access to /admin. */
|
|
16
|
+
| "admin"
|
|
17
|
+
/** Anyone in the room, spectators too. */
|
|
18
|
+
| "room_member"
|
|
19
|
+
/** A player in the room, not a spectator. */
|
|
20
|
+
| "room_player"
|
|
21
|
+
/** The room's host. */
|
|
22
|
+
| "room_host";
|
|
23
|
+
/** Callers a named function proves, because the proof takes the door's own data (a body's signature, a token's row). */
|
|
24
|
+
export type DoorCallerProvenByFunction =
|
|
25
|
+
/** The API router: an API key's access, or a session from Zaisma's own pages. */
|
|
26
|
+
"api_caller"
|
|
27
|
+
/** A tester or preview link's token in the address. */
|
|
28
|
+
| "link_holder"
|
|
29
|
+
/** A Sign in with Zaisma code or access token. */
|
|
30
|
+
| "token_holder"
|
|
31
|
+
/** Stripe or GitHub, by the signature on the body. */
|
|
32
|
+
| "signed_sender"
|
|
33
|
+
/** A game's own page, by the request's Origin. */
|
|
34
|
+
| "game_page"
|
|
35
|
+
/** A signed player pass. */
|
|
36
|
+
| "pass_holder";
|
|
37
|
+
export type DoorCaller = DoorCallerProvenByGuard | DoorCallerProvenByFunction;
|
|
38
|
+
/**
|
|
39
|
+
* What a door may touch. The first seven are the API's own words (ApiRoute's reaches, apps/site/src/developer.ts):
|
|
40
|
+
* `account`, `player_save`, `game`, `webhook`, `sign_in_app` and `forum_topic` are the caller's own or what the caller may read,
|
|
41
|
+
* and `nothing_private` is what anyone may see. The `any_` ones and `zaisma_settings` are only ever an admin's.
|
|
42
|
+
*/
|
|
43
|
+
export type DoorReach = "account" | "player_save" | "game" | "webhook" | "sign_in_app" | "forum_topic" | "nothing_private" | "any_game" | "any_account" | "zaisma_settings" | "room" | "purchase";
|
|
44
|
+
/** What a door takes from another door, as the security map names them (scripts/security-map/doors.mjs). */
|
|
45
|
+
export type DoorTrust = "session" | "api_key" | "oauth_code" | "oauth_access" | "tester_link" | "preview_link" | "player_pass" | "admin_access" | "player_id" | "stripe_signature" | "github_signature" | "game_origin" | "purchase_receipt" | "passkey" | "email_code";
|
|
46
|
+
/** The function that proves a caller; a test checks it exists. */
|
|
47
|
+
export interface DoorProof {
|
|
48
|
+
source_path: string;
|
|
49
|
+
symbol_name: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* What a door takes, as the small part of JSON Schema Zaisma's schemas use. Only the helpers below make one, and each
|
|
53
|
+
* takes its bound, so "accepts anything" can't be stated. Only a game's own data goes by size alone, at the caps it has.
|
|
54
|
+
*/
|
|
55
|
+
export type DoorShape = Readonly<Record<string, unknown>> & {
|
|
56
|
+
readonly [SHAPE_MARK]: true;
|
|
57
|
+
};
|
|
58
|
+
declare const SHAPE_MARK: unique symbol;
|
|
59
|
+
export declare const text: (maximumCharacterCount: number, pattern?: string) => DoorShape;
|
|
60
|
+
export declare const oneOfWords: (...words: readonly string[]) => DoorShape;
|
|
61
|
+
export declare const integer: (minimumNumber?: number, maximumNumber?: number) => DoorShape;
|
|
62
|
+
export declare const realNumber: (minimumNumber: number, maximumNumber: number) => DoorShape;
|
|
63
|
+
export declare const yesOrNo: DoorShape;
|
|
64
|
+
export declare const anyOf: (...shapes: readonly DoorShape[]) => DoorShape;
|
|
65
|
+
export declare const orNothing: (shape: DoorShape) => DoorShape;
|
|
66
|
+
export declare const listOf: (items: DoorShape, maximumItemCount: number, minimumItemCount?: number) => DoorShape;
|
|
67
|
+
/** A closed object: these fields and no others, the required ones named. */
|
|
68
|
+
export declare const fieldsOf: (properties: Readonly<Record<string, DoorShape>>, required?: readonly string[]) => DoorShape;
|
|
69
|
+
/** A game's own data, of any shape: its size only, in characters of its JSON. */
|
|
70
|
+
export declare const gameData: (maximumSizeBytes: number) => DoorShape;
|
|
71
|
+
/** A door's body, when it takes one. maximumSizeBytes is checked before any of it is read; raw bytes go by size alone, and the door reads them itself. */
|
|
72
|
+
export type DoorBody = {
|
|
73
|
+
kind: "json";
|
|
74
|
+
fields: Readonly<Record<string, DoorShape>>;
|
|
75
|
+
required?: readonly string[];
|
|
76
|
+
maximumSizeBytes: number;
|
|
77
|
+
isOptional?: true;
|
|
78
|
+
} | {
|
|
79
|
+
kind: "form";
|
|
80
|
+
fields: Readonly<Record<string, DoorShape>>;
|
|
81
|
+
fileNames?: readonly string[];
|
|
82
|
+
maximumSizeBytes: number;
|
|
83
|
+
} | {
|
|
84
|
+
kind: "bytes";
|
|
85
|
+
maximumSizeBytes: number;
|
|
86
|
+
};
|
|
87
|
+
/** What a door takes: its query's names and its body. A query's values arrive as text: integer and boolean shapes read theirs. */
|
|
88
|
+
export interface DoorTakes {
|
|
89
|
+
query: Readonly<Record<string, DoorShape>>;
|
|
90
|
+
body: DoorBody | null;
|
|
91
|
+
}
|
|
92
|
+
/** Nothing: a door with no query it reads and no body. */
|
|
93
|
+
export declare const TAKES_NOTHING: DoorTakes;
|
|
94
|
+
/** What a door answers: a JSON schema, checked on live requests and cut to what it declares, or a kind with nothing to cut. */
|
|
95
|
+
export type DoorAnswer = {
|
|
96
|
+
kind: "json";
|
|
97
|
+
schema: Readonly<Record<string, unknown>>;
|
|
98
|
+
} | {
|
|
99
|
+
kind: "page" | "empty" | "redirect" | "file" | "bytes";
|
|
100
|
+
};
|
|
101
|
+
export declare const ANSWERS_PAGE: DoorAnswer;
|
|
102
|
+
export declare const ANSWERS_EMPTY: DoorAnswer;
|
|
103
|
+
export declare const ANSWERS_REDIRECT: DoorAnswer;
|
|
104
|
+
export declare const ANSWERS_FILE: DoorAnswer;
|
|
105
|
+
export declare const ANSWERS_BYTES: DoorAnswer;
|
|
106
|
+
export declare const answersJson: (schema: Readonly<Record<string, unknown>>) => DoorAnswer;
|
|
107
|
+
/** A door's limit in calls a minute, from this short list. Each is a rate limit binding in the Workers that use it: LIMIT_<LEVEL>. */
|
|
108
|
+
export declare const DOOR_LIMIT_LEVELS: {
|
|
109
|
+
readonly very_rare: 2;
|
|
110
|
+
readonly guess: 5;
|
|
111
|
+
readonly rare: 10;
|
|
112
|
+
readonly steady: 30;
|
|
113
|
+
readonly busy: 120;
|
|
114
|
+
readonly building: 1000;
|
|
115
|
+
};
|
|
116
|
+
export type DoorLimitLevel = keyof typeof DOOR_LIMIT_LEVELS;
|
|
117
|
+
/** Why a door has no limit of its own: from this list only, and the daily review reads every door that gives one. */
|
|
118
|
+
export declare const DOOR_NO_LIMIT_REASONS: {
|
|
119
|
+
readonly answers_from_memory: "It answers fixed bytes from memory and reads nothing.";
|
|
120
|
+
readonly cached_file: "It serves a file Cloudflare caches, with no database behind it and no egress fee.";
|
|
121
|
+
readonly admin_only: "Cloudflare Access stands in front of it, and only Zaisma's own people get through.";
|
|
122
|
+
readonly signed_by_sender: "It reads nothing until the sender's signature on the body checks out.";
|
|
123
|
+
readonly counted_by_its_call: "All it does is make calls that have their own limit.";
|
|
124
|
+
readonly room_token_bucket: "A room's message bucket takes a token for each message, per player, before any door runs.";
|
|
125
|
+
};
|
|
126
|
+
export type DoorNoLimitReason = keyof typeof DOOR_NO_LIMIT_REASONS;
|
|
127
|
+
/**
|
|
128
|
+
* How much a door can be used. A limit counts the player, the account or the game wherever Zaisma knows it, and an
|
|
129
|
+
* address only before it knows, at the level a whole building clears, so 25 players on one school's wifi play like
|
|
130
|
+
* anyone. group shares one counter between doors; with none, each door counts on its own.
|
|
131
|
+
*/
|
|
132
|
+
export type DoorLimit = {
|
|
133
|
+
level: DoorLimitLevel;
|
|
134
|
+
counts: "player" | "account" | "game";
|
|
135
|
+
group?: string;
|
|
136
|
+
} | {
|
|
137
|
+
level: "building";
|
|
138
|
+
counts: "address";
|
|
139
|
+
group?: string;
|
|
140
|
+
} | {
|
|
141
|
+
whyNoLimit: DoorNoLimitReason;
|
|
142
|
+
};
|
|
143
|
+
export interface DoorContractFields {
|
|
144
|
+
reaches: DoorReach;
|
|
145
|
+
trusts: readonly DoorTrust[];
|
|
146
|
+
takes: DoorTakes;
|
|
147
|
+
answers: DoorAnswer;
|
|
148
|
+
limit: DoorLimit;
|
|
149
|
+
}
|
|
150
|
+
/** A caller a function proves can't be stated without naming the function, so it doesn't build. */
|
|
151
|
+
export type DoorContract = DoorContractFields & ({
|
|
152
|
+
whoMayCall: DoorCallerProvenByGuard;
|
|
153
|
+
} | {
|
|
154
|
+
whoMayCall: DoorCallerProvenByFunction;
|
|
155
|
+
provenBy: DoorProof;
|
|
156
|
+
});
|
|
157
|
+
/** A Worker's rate limit bindings, by level. A self-hosted room has none, and a missing one lets the call through. */
|
|
158
|
+
/** A rate limit binding, as Workers gives one. */
|
|
159
|
+
export interface DoorLimitBinding {
|
|
160
|
+
limit(options: {
|
|
161
|
+
key: string;
|
|
162
|
+
}): Promise<object>;
|
|
163
|
+
}
|
|
164
|
+
export type DoorLimitBindings = {
|
|
165
|
+
[Level in DoorLimitLevel as `LIMIT_${Uppercase<Level>}`]?: DoorLimitBinding;
|
|
166
|
+
};
|
|
167
|
+
/** Whether a call goes past a level's limit: door's own counter, for one subject. Keys never leave Cloudflare's counter. */
|
|
168
|
+
export declare function isPastLevel(env: DoorLimitBindings, level: DoorLimitLevel, counterName: string, subject: string): Promise<boolean>;
|
|
169
|
+
/** Whether a call goes past a door's limit. doorName names its counter unless its limit shares a group. */
|
|
170
|
+
export declare function isPastDoorLimit(env: DoorLimitBindings, limit: DoorLimit, doorName: string, subject: string): Promise<boolean>;
|
|
171
|
+
type Schema = Readonly<Record<string, unknown>>;
|
|
172
|
+
/** Where a value doesn't match its shape or schema, as "path: problem" lines; [] when it fits. Answers and inputs both come here. */
|
|
173
|
+
export declare function schemaProblems(schema: Schema, given: unknown, path?: string): string[];
|
|
174
|
+
/** Where a value's types don't match a schema; null reads as left out, and what a call behind it allows, names included, stays the call's to say, with its own fix. */
|
|
175
|
+
export declare function typeProblems(schema: Schema, given: unknown, path?: string): string[];
|
|
176
|
+
/**
|
|
177
|
+
* An answer with every field its schema didn't declare taken out, and the paths it took (a list's index as []): the
|
|
178
|
+
* caller still gets the answer. Nothing else changes, and an answer with nothing to cut comes back as the same value.
|
|
179
|
+
* Only closed objects cut, so a game's own rows and arguments pass as they are.
|
|
180
|
+
*/
|
|
181
|
+
export declare function cutUndeclared(schema: Schema, given: unknown, path?: string): {
|
|
182
|
+
kept: unknown;
|
|
183
|
+
cutPaths: string[];
|
|
184
|
+
};
|
|
185
|
+
/** The first problem with a query, as [name, what's wrong], or null. isOpenToOtherNames: a page ignores names it doesn't read, since share links pick some up. */
|
|
186
|
+
export declare function queryProblem(fields: Readonly<Record<string, DoorShape>>, query: URLSearchParams, isOpenToOtherNames?: boolean): {
|
|
187
|
+
field: string;
|
|
188
|
+
problem: string;
|
|
189
|
+
} | null;
|
|
190
|
+
/** The first problem with a JSON body's fields: closed, each field its shape, the required ones there. */
|
|
191
|
+
export declare function bodyProblem(fields: Readonly<Record<string, DoorShape>>, required: readonly string[], body: Readonly<Record<string, unknown>>): {
|
|
192
|
+
field: string;
|
|
193
|
+
problem: string;
|
|
194
|
+
} | null;
|
|
195
|
+
/** The first problem with a form's fields: its names closed, each value text, or a list where it may repeat. A form's text boxes send what they send, so its numbers are the door's to read. */
|
|
196
|
+
export declare function formProblem(fields: Readonly<Record<string, DoorShape>>, fileNames: readonly string[], form: {
|
|
197
|
+
keys(): IterableIterator<string>;
|
|
198
|
+
getAll(name: string): unknown[];
|
|
199
|
+
}): {
|
|
200
|
+
field: string;
|
|
201
|
+
problem: string;
|
|
202
|
+
} | null;
|
|
203
|
+
/**
|
|
204
|
+
* The one check every door goes through. `proven` is who the door's guard found the caller to be; a caller a
|
|
205
|
+
* named function proves passes here, and that function refuses them, before anything else of the door runs.
|
|
206
|
+
*/
|
|
207
|
+
export declare function checkDoor(contract: DoorContract, proven: readonly DoorCaller[]): boolean;
|
|
208
|
+
/** Where a door's path parts come from: `:name` parts, each a [^/]+ unless named here, and a final `*` for the rest. */
|
|
209
|
+
export declare function doorPathPattern(path: string, partPatterns?: Readonly<Record<string, string>>): RegExp;
|
|
210
|
+
export {};
|