@thegeem/protocol 0.1.36 → 0.1.38
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/dist/index.cjs +24 -5
- package/dist/index.d.cts +130 -11
- package/dist/index.d.ts +130 -11
- package/dist/index.js +18 -4
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -30,10 +30,15 @@ __export(index_exports, {
|
|
|
30
30
|
MODES: () => MODES,
|
|
31
31
|
MODE_IDS: () => MODE_IDS,
|
|
32
32
|
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
33
|
+
REJECT_REASONS: () => REJECT_REASONS,
|
|
33
34
|
ROOM_ALPHABET: () => ROOM_ALPHABET,
|
|
35
|
+
SUBMISSION_KINDS: () => SUBMISSION_KINDS,
|
|
36
|
+
SUBMISSION_STATUSES: () => SUBMISSION_STATUSES,
|
|
34
37
|
gameIdForPv: () => gameIdForPv,
|
|
35
38
|
gamesForPv: () => gamesForPv,
|
|
36
|
-
modesForGame: () => modesForGame
|
|
39
|
+
modesForGame: () => modesForGame,
|
|
40
|
+
normalizeWesternDigits: () => normalizeWesternDigits,
|
|
41
|
+
sanitizeName: () => sanitizeName
|
|
37
42
|
});
|
|
38
43
|
module.exports = __toCommonJS(index_exports);
|
|
39
44
|
var import_zod = require("zod");
|
|
@@ -75,7 +80,9 @@ var EMOTE_IDS = ["laugh", "wow", "think", "gg"];
|
|
|
75
80
|
var CHARACTER_IDS = ["m", "f"];
|
|
76
81
|
var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
77
82
|
var UNSAFE_CHARS = /[\p{Cc}\u202A-\u202E\u2066-\u2069<>]/gu;
|
|
78
|
-
var
|
|
83
|
+
var normalizeWesternDigits = (s) => s.replace(/[٠-٩۰-۹]/g, (d) => String("\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669".indexOf(d) >= 0 ? "\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669".indexOf(d) : "\u06F0\u06F1\u06F2\u06F3\u06F4\u06F5\u06F6\u06F7\u06F8\u06F9".indexOf(d)));
|
|
84
|
+
var sanitizeName = (s) => normalizeWesternDigits(s.replace(UNSAFE_CHARS, "")).trim();
|
|
85
|
+
var DisplayName = import_zod.z.string().trim().min(1).max(LIMITS.MAX_NAME_LEN).transform(sanitizeName).refine((s) => s.length >= 1, "name is empty after sanitizing");
|
|
79
86
|
var RoomCode = import_zod.z.string().trim().toUpperCase().regex(new RegExp(`^[${ROOM_ALPHABET}]{${LIMITS.ROOM_CODE_LEN}}$`), "bad room code");
|
|
80
87
|
var PlayerId = import_zod.z.string().max(40);
|
|
81
88
|
var HelpType = import_zod.z.enum([
|
|
@@ -89,7 +96,7 @@ var HelpType = import_zod.z.enum([
|
|
|
89
96
|
]);
|
|
90
97
|
var Pv = import_zod.z.number().int().min(1).max(1e6).optional();
|
|
91
98
|
var IdToken = import_zod.z.string().max(4096).optional();
|
|
92
|
-
var Platform = import_zod.z.enum(["web", "ios", "android"]).optional();
|
|
99
|
+
var Platform = import_zod.z.enum(["web", "ios", "android", "tv"]).optional();
|
|
93
100
|
var AppVersion = import_zod.z.string().trim().max(24).regex(/^[0-9]+(\.[0-9]+){0,3}([-+][0-9A-Za-z.-]+)?$/, "bad app version").optional();
|
|
94
101
|
var Avatar = import_zod.z.object({
|
|
95
102
|
emoji: import_zod.z.string().min(1).max(16).transform((s) => s.replace(UNSAFE_CHARS, "")).refine((s) => s.length >= 1, "empty emoji"),
|
|
@@ -216,7 +223,11 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
|
|
|
216
223
|
// trivia is 4-option (0–3)
|
|
217
224
|
import_zod.z.object({ t: import_zod.z.literal("useHelp"), helpType: HelpType }),
|
|
218
225
|
// sabotage a rival (active team only): spend a sabotage help to debuff targetTeam's next question
|
|
219
|
-
|
|
226
|
+
// `player` (gs#80): WHICH member of the target team sits out — required by استريح/restPlayer and
|
|
227
|
+
// ignored by عرجوب/tripLevel, which targets a whole team. Optional on the wire so pre-gs#80 clients
|
|
228
|
+
// are unchanged; the engine refuses to spend a restPlayer card without a valid one, so an old client
|
|
229
|
+
// cannot burn a lifeline by omitting it.
|
|
230
|
+
import_zod.z.object({ t: import_zod.z.literal("sabotage"), helpType: HelpType, targetTeam: import_zod.z.number().int().min(0).max(LIMITS.MAX_PLAYERS - 1), player: import_zod.z.string().max(64).optional() }),
|
|
220
231
|
// 0-based team index
|
|
221
232
|
// moderation
|
|
222
233
|
// Report another player in the room (App Store 1.2 UGC — display names). Available in lobby or
|
|
@@ -260,6 +271,9 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
|
|
|
260
271
|
// Drawer passes on the served word (capped per draw-turn; relay serves a fresh one).
|
|
261
272
|
import_zod.z.object({ t: import_zod.z.literal("skipWord") })
|
|
262
273
|
]);
|
|
274
|
+
var SUBMISSION_STATUSES = ["pending", "accepted", "rejected"];
|
|
275
|
+
var SUBMISSION_KINDS = ["new", "edit"];
|
|
276
|
+
var REJECT_REASONS = ["duplicate", "unclear_answer", "not_family", "inaccurate", "too_narrow", "other"];
|
|
263
277
|
// Annotate the CommonJS export names for ESM import in node:
|
|
264
278
|
0 && (module.exports = {
|
|
265
279
|
CHARACTER_IDS,
|
|
@@ -272,8 +286,13 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
|
|
|
272
286
|
MODES,
|
|
273
287
|
MODE_IDS,
|
|
274
288
|
PROTOCOL_VERSION,
|
|
289
|
+
REJECT_REASONS,
|
|
275
290
|
ROOM_ALPHABET,
|
|
291
|
+
SUBMISSION_KINDS,
|
|
292
|
+
SUBMISSION_STATUSES,
|
|
276
293
|
gameIdForPv,
|
|
277
294
|
gamesForPv,
|
|
278
|
-
modesForGame
|
|
295
|
+
modesForGame,
|
|
296
|
+
normalizeWesternDigits,
|
|
297
|
+
sanitizeName
|
|
279
298
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -103,6 +103,23 @@ declare const CHARACTER_IDS: readonly ["m", "f"];
|
|
|
103
103
|
type CharacterId = (typeof CHARACTER_IDS)[number];
|
|
104
104
|
/** Unambiguous room-code alphabet — no 0/O, 1/I/L. */
|
|
105
105
|
declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
106
|
+
/**
|
|
107
|
+
* Arabic-Indic (٠-٩) and Extended/Persian (۰-۹) digits → Western, D-043 applied to NAMES at the
|
|
108
|
+
* gate (ruling 2026-08-13, the «سالم ١٧» leaderboard sighting). A digit is a GLYPH of a number,
|
|
109
|
+
* not identity — «سالم ١٧» and «سالم 17» are the same name — so normalising at write-time is the
|
|
110
|
+
* same class of edit as the bidi strip above, and every surface on every client agrees on what to
|
|
111
|
+
* call the person. Normalising at RENDER instead would have web and iOS disagreeing about a name.
|
|
112
|
+
* Letters are never touched; this maps exactly twenty codepoints.
|
|
113
|
+
*/
|
|
114
|
+
declare const normalizeWesternDigits: (s: string) => string;
|
|
115
|
+
/**
|
|
116
|
+
* THE definition of "a safe player-supplied string": strip the unsafe class, normalise digits.
|
|
117
|
+
* Exported because it was being hand-copied — the relay's HTTP gates (account PATCH, question
|
|
118
|
+
* submissions) each grew their own copy of this exact body, and one of those copies stored the
|
|
119
|
+
* bidi characters LITERALLY in source, where any formatter or copy-paste could silently change
|
|
120
|
+
* what it matches. A rule that can be edited by accident is not a rule.
|
|
121
|
+
*/
|
|
122
|
+
declare const sanitizeName: (s: string) => string;
|
|
106
123
|
declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
107
124
|
declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
108
125
|
t: z.ZodLiteral<"createRoom">;
|
|
@@ -121,7 +138,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
121
138
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
122
139
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
123
140
|
idToken: z.ZodOptional<z.ZodString>;
|
|
124
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
141
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
125
142
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
126
143
|
}, "strip", z.ZodTypeAny, {
|
|
127
144
|
t: "createRoom";
|
|
@@ -134,7 +151,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
134
151
|
character?: "m" | "f" | undefined;
|
|
135
152
|
pv?: number | undefined;
|
|
136
153
|
idToken?: string | undefined;
|
|
137
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
154
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
138
155
|
appVersion?: string | undefined;
|
|
139
156
|
}, {
|
|
140
157
|
t: "createRoom";
|
|
@@ -147,7 +164,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
147
164
|
character?: "m" | "f" | undefined;
|
|
148
165
|
pv?: number | undefined;
|
|
149
166
|
idToken?: string | undefined;
|
|
150
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
167
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
151
168
|
appVersion?: string | undefined;
|
|
152
169
|
}>, z.ZodObject<{
|
|
153
170
|
t: z.ZodLiteral<"renameHost">;
|
|
@@ -205,7 +222,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
205
222
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
206
223
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
207
224
|
idToken: z.ZodOptional<z.ZodString>;
|
|
208
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
225
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
209
226
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
210
227
|
}, "strip", z.ZodTypeAny, {
|
|
211
228
|
code: string;
|
|
@@ -218,7 +235,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
218
235
|
character?: "m" | "f" | undefined;
|
|
219
236
|
pv?: number | undefined;
|
|
220
237
|
idToken?: string | undefined;
|
|
221
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
238
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
222
239
|
appVersion?: string | undefined;
|
|
223
240
|
}, {
|
|
224
241
|
code: string;
|
|
@@ -231,7 +248,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
231
248
|
character?: "m" | "f" | undefined;
|
|
232
249
|
pv?: number | undefined;
|
|
233
250
|
idToken?: string | undefined;
|
|
234
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
251
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
235
252
|
appVersion?: string | undefined;
|
|
236
253
|
}>, z.ZodObject<{
|
|
237
254
|
t: z.ZodLiteral<"resume">;
|
|
@@ -250,7 +267,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
250
267
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
251
268
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
252
269
|
idToken: z.ZodOptional<z.ZodString>;
|
|
253
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
270
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
254
271
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
255
272
|
}, "strip", z.ZodTypeAny, {
|
|
256
273
|
code: string;
|
|
@@ -263,7 +280,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
263
280
|
character?: "m" | "f" | undefined;
|
|
264
281
|
pv?: number | undefined;
|
|
265
282
|
idToken?: string | undefined;
|
|
266
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
283
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
267
284
|
appVersion?: string | undefined;
|
|
268
285
|
}, {
|
|
269
286
|
code: string;
|
|
@@ -276,7 +293,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
276
293
|
character?: "m" | "f" | undefined;
|
|
277
294
|
pv?: number | undefined;
|
|
278
295
|
idToken?: string | undefined;
|
|
279
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
296
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
280
297
|
appVersion?: string | undefined;
|
|
281
298
|
}>, z.ZodObject<{
|
|
282
299
|
t: z.ZodLiteral<"setPlaying">;
|
|
@@ -505,14 +522,17 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
505
522
|
t: z.ZodLiteral<"sabotage">;
|
|
506
523
|
helpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
507
524
|
targetTeam: z.ZodNumber;
|
|
525
|
+
player: z.ZodOptional<z.ZodString>;
|
|
508
526
|
}, "strip", z.ZodTypeAny, {
|
|
509
527
|
t: "sabotage";
|
|
510
528
|
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
511
529
|
targetTeam: number;
|
|
530
|
+
player?: string | undefined;
|
|
512
531
|
}, {
|
|
513
532
|
t: "sabotage";
|
|
514
533
|
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
515
534
|
targetTeam: number;
|
|
535
|
+
player?: string | undefined;
|
|
516
536
|
}>, z.ZodObject<{
|
|
517
537
|
t: z.ZodLiteral<"report">;
|
|
518
538
|
playerId: z.ZodString;
|
|
@@ -691,6 +711,32 @@ interface LobbyPlayer {
|
|
|
691
711
|
avatar?: AvatarView;
|
|
692
712
|
/** Team index once the game has started; null/absent in the lobby. */
|
|
693
713
|
teamIndex?: number | null;
|
|
714
|
+
/**
|
|
715
|
+
* Equipped avatar-frame slug (e.g. "frame-sadu") — resolves to art via /assets/store/catalog.json
|
|
716
|
+
* (the catalog is the ONLY resolver; nothing about the art is derived from the slug). Read from the
|
|
717
|
+
* account once at room entry, so an equip change shows on the player's next join/resume. Absent =
|
|
718
|
+
* frameless. Earnable-class cosmetics only ever ride the room wire (D-059) — a paid exclusive can
|
|
719
|
+
* never appear here, so there is no receipt check in the broadcast path and no have/have-not row
|
|
720
|
+
* between children on one sofa. (gs#84 contract, D-060.)
|
|
721
|
+
*/
|
|
722
|
+
frame?: string;
|
|
723
|
+
/**
|
|
724
|
+
* This room kicked this exact person before — a HINT for the host, never a gate (the ruling is
|
|
725
|
+
* "label the re-knock, no ban list"). **Present ONLY in the HOST's copy of the frame**: telling a
|
|
726
|
+
* whole room who was once removed is a social leak, not a moderation aid.
|
|
727
|
+
*
|
|
728
|
+
* The same bit also rides `joinRequest` for the immediate ping. This one is the DURABLE carrier —
|
|
729
|
+
* it is RE-READ from the answer stored on the seat (decided once, at join; never recomputed by
|
|
730
|
+
* re-matching, which goes stale) and re-sent on every snapshot, so it survives a host reconnect,
|
|
731
|
+
* where a cached one-shot event would not. A client should prefer this and treat the event as a
|
|
732
|
+
* nudge. MID-GAME there are no lobby frames: there a host who resumes is re-sent the pending
|
|
733
|
+
* `joinRequest`s instead (labels included), so the durable promise holds in both halves.
|
|
734
|
+
*
|
|
735
|
+
* Absent means "no reason to think so", which includes the deliberate miss: an anonymous player who
|
|
736
|
+
* force-quits and rejoins reads as a stranger. Matched only on exact socket/sign-in identity — never
|
|
737
|
+
* a display name or IP, because a wrong label is worse than none.
|
|
738
|
+
*/
|
|
739
|
+
wasKicked?: boolean;
|
|
694
740
|
}
|
|
695
741
|
/**
|
|
696
742
|
* Who is in the room, carried ON the game-state push (gs#71).
|
|
@@ -719,6 +765,9 @@ interface RosterEntry {
|
|
|
719
765
|
isHost?: boolean;
|
|
720
766
|
/** Is this person currently connected? Mirrors `LobbyPlayer.connected`. */
|
|
721
767
|
connected?: boolean;
|
|
768
|
+
/** Equipped avatar-frame slug — mirrors `LobbyPlayer.frame`, because the roster is the MID-GAME
|
|
769
|
+
* carrier (gs#71): a cosmetic with no mid-game carrier silently vanishes for watchers and the TV. */
|
|
770
|
+
frame?: string;
|
|
722
771
|
}
|
|
723
772
|
/** Per-recipient context attached to lobby/state pushes. */
|
|
724
773
|
interface YouContext {
|
|
@@ -786,10 +835,26 @@ interface TeamView {
|
|
|
786
835
|
/** The player on THIS team whose turn it is to answer (rotates each turn); null if the team has
|
|
787
836
|
* no seated players. In controller mode only this player may `answer` on the team's turn. */
|
|
788
837
|
currentPlayerId: string | null;
|
|
838
|
+
/** استريح (gs#80): this team's player who sits out their next question, if a rival named one. */
|
|
839
|
+
restedPlayerId?: string | null;
|
|
789
840
|
/** The seated player's privacy-safe avatar (relay-enriched; engine view omits it). */
|
|
790
841
|
avatar?: AvatarView;
|
|
791
842
|
}
|
|
792
843
|
interface QuestionView {
|
|
844
|
+
/** The category this question came from (gs#94 follow-up, geem-site#54). NOT reveal-gated: the
|
|
845
|
+
* category is already on the board tile the player picked, so it gives nothing away — unlike the
|
|
846
|
+
* id, which is a handle into the bank. It exists so «اقترح سؤال مشابه» can preset the category
|
|
847
|
+
* instead of asking the player to re-pick it; without it a client either asks again or INFERS one,
|
|
848
|
+
* and inferring files somebody's question under the wrong category. Optional: pre-gs#94 servers
|
|
849
|
+
* omit it and clients must cope. */
|
|
850
|
+
categoryId?: string;
|
|
851
|
+
/** The question's id — present ONLY once revealed (gs#90). It exists so a player can say «عدّل
|
|
852
|
+
* السؤال» about a specific question: text is neither unique (template-shaped questions collide on
|
|
853
|
+
* wording) nor stable, so an edit identified only by its text is not reliably applicable. Kept off
|
|
854
|
+
* the wire while the question is LIVE for the same reason `correctAnswerIndex` is — the bank is
|
|
855
|
+
* server-side and an id is a handle into it. Optional on the wire: shipped clients ignore it, and
|
|
856
|
+
* a client that wants it must tolerate its absence pre-reveal. */
|
|
857
|
+
id?: string;
|
|
793
858
|
text: string;
|
|
794
859
|
options: string[];
|
|
795
860
|
hiddenOptionIndices: number[];
|
|
@@ -797,6 +862,14 @@ interface QuestionView {
|
|
|
797
862
|
selectedAnswerIndex: number | null;
|
|
798
863
|
/** Only present once revealed (anti-cheat). Null while the question is live. */
|
|
799
864
|
correctAnswerIndex: number | null;
|
|
865
|
+
/** «سؤال من: …» — community submitter credit (owner ruling 2026-08-14). Present ONLY on
|
|
866
|
+
* questions promoted from community submissions; official questions never carry it. NOT
|
|
867
|
+
* reveal-gated — a name leaks nothing — so place it wherever it reads best. */
|
|
868
|
+
submittedBy?: string;
|
|
869
|
+
/** «معلومة» — optional explanation shown AFTER the reveal. Null while the question is live (it
|
|
870
|
+
* usually names the answer — same anti-cheat gate as correctAnswerIndex) and null when the
|
|
871
|
+
* question simply has none, which is most of the bank: render the card only when non-null. */
|
|
872
|
+
explain?: string | null;
|
|
800
873
|
imageName?: string;
|
|
801
874
|
videoName?: string;
|
|
802
875
|
/** For video questions: seconds into the clip to stop playback (so the answer isn't shown). Absent = play through. */
|
|
@@ -958,6 +1031,7 @@ type ServerMsg = {
|
|
|
958
1031
|
t: 'joinRequest';
|
|
959
1032
|
playerId: string;
|
|
960
1033
|
name: string;
|
|
1034
|
+
wasKicked?: boolean;
|
|
961
1035
|
} | {
|
|
962
1036
|
t: 'joined';
|
|
963
1037
|
youId: string;
|
|
@@ -983,6 +1057,7 @@ type ServerMsg = {
|
|
|
983
1057
|
hostId: string;
|
|
984
1058
|
hostName: string;
|
|
985
1059
|
hostAvatar?: AvatarView;
|
|
1060
|
+
hostFrame?: string;
|
|
986
1061
|
hostPlaying?: boolean;
|
|
987
1062
|
hostPinned?: boolean;
|
|
988
1063
|
players: LobbyPlayer[];
|
|
@@ -994,6 +1069,9 @@ type ServerMsg = {
|
|
|
994
1069
|
tally?: {
|
|
995
1070
|
[playerId: string]: number;
|
|
996
1071
|
};
|
|
1072
|
+
tallyNames?: {
|
|
1073
|
+
[playerId: string]: string;
|
|
1074
|
+
};
|
|
997
1075
|
modeId: ModeId | null;
|
|
998
1076
|
you: YouContext;
|
|
999
1077
|
} | {
|
|
@@ -1040,10 +1118,51 @@ type ServerMsg = {
|
|
|
1040
1118
|
message: string;
|
|
1041
1119
|
level?: 'info' | 'warn' | 'maintenance';
|
|
1042
1120
|
restartInSec?: number;
|
|
1043
|
-
}
|
|
1121
|
+
}
|
|
1122
|
+
/** `reason` (gs#83, additive): today only `'ended'`, and only on `no_room` — the code WAS a room
|
|
1123
|
+
* until recently (stale WhatsApp link, knock after the host closed the night). Absent = "no such
|
|
1124
|
+
* room as far as we know". Lets a client say «انتهى اللعب» instead of «ما في غرفة بهذا الكود»
|
|
1125
|
+
* without guessing. Old clients ignore it. */
|
|
1126
|
+
| {
|
|
1044
1127
|
t: 'error';
|
|
1045
1128
|
code: string;
|
|
1046
1129
|
message: string;
|
|
1130
|
+
reason?: string;
|
|
1047
1131
|
};
|
|
1132
|
+
/** Review states for a community submission. ⚠️ `approved` is NOT here: it is a DEPRECATED alias
|
|
1133
|
+
* written by an older geem-admin, mapped to `accepted` by the relay and permitted by 0031's CHECK
|
|
1134
|
+
* only until that write is switched. It must never become a fourth state. */
|
|
1135
|
+
declare const SUBMISSION_STATUSES: readonly ["pending", "accepted", "rejected"];
|
|
1136
|
+
type SubmissionStatus = (typeof SUBMISSION_STATUSES)[number];
|
|
1137
|
+
/** A brand-new question vs a correction to one already in the bank. */
|
|
1138
|
+
declare const SUBMISSION_KINDS: readonly ["new", "edit"];
|
|
1139
|
+
type SubmissionKind = (typeof SUBMISSION_KINDS)[number];
|
|
1140
|
+
/** Why a submission was turned down. Blame-free by construction — the closest to "you did badly" is
|
|
1141
|
+
* `duplicate`, which is about the BANK, not the writer. Clients translate these and must fall back
|
|
1142
|
+
* to `other` on anything unrecognised, which is exactly what makes adding a seventh survivable for a
|
|
1143
|
+
* build that shipped last month and will never be updated. */
|
|
1144
|
+
declare const REJECT_REASONS: readonly ["duplicate", "unclear_answer", "not_family", "inaccurate", "too_narrow", "other"];
|
|
1145
|
+
type RejectReason = (typeof REJECT_REASONS)[number];
|
|
1146
|
+
/** One row of «أسئلتي». Exists as a named type so the native generator EMITS the three unions above
|
|
1147
|
+
* — quicktype only reaches what a root references, and hand-typed constants are the whole bug. */
|
|
1148
|
+
interface MyQuestionRow {
|
|
1149
|
+
id: string;
|
|
1150
|
+
status: SubmissionStatus;
|
|
1151
|
+
kind: SubmissionKind;
|
|
1152
|
+
text: string;
|
|
1153
|
+
options: string[];
|
|
1154
|
+
correctIndex: number;
|
|
1155
|
+
explain: string | null;
|
|
1156
|
+
createdAt: string;
|
|
1157
|
+
accepted?: {
|
|
1158
|
+
plays: number;
|
|
1159
|
+
firstPlayedAt: string | null;
|
|
1160
|
+
coins: number;
|
|
1161
|
+
};
|
|
1162
|
+
rejected?: {
|
|
1163
|
+
reason: RejectReason;
|
|
1164
|
+
canResubmit: boolean;
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1048
1167
|
|
|
1049
|
-
export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type PackView, type QuestionView, ROOM_ALPHABET, type RosterEntry, type ServerMsg, type SoundName, type StrokePointView, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame };
|
|
1168
|
+
export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, type MyQuestionRow, PROTOCOL_VERSION, type PackView, type QuestionView, REJECT_REASONS, ROOM_ALPHABET, type RejectReason, type RosterEntry, SUBMISSION_KINDS, SUBMISSION_STATUSES, type ServerMsg, type SoundName, type StrokePointView, type SubmissionKind, type SubmissionStatus, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame, normalizeWesternDigits, sanitizeName };
|
package/dist/index.d.ts
CHANGED
|
@@ -103,6 +103,23 @@ declare const CHARACTER_IDS: readonly ["m", "f"];
|
|
|
103
103
|
type CharacterId = (typeof CHARACTER_IDS)[number];
|
|
104
104
|
/** Unambiguous room-code alphabet — no 0/O, 1/I/L. */
|
|
105
105
|
declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
106
|
+
/**
|
|
107
|
+
* Arabic-Indic (٠-٩) and Extended/Persian (۰-۹) digits → Western, D-043 applied to NAMES at the
|
|
108
|
+
* gate (ruling 2026-08-13, the «سالم ١٧» leaderboard sighting). A digit is a GLYPH of a number,
|
|
109
|
+
* not identity — «سالم ١٧» and «سالم 17» are the same name — so normalising at write-time is the
|
|
110
|
+
* same class of edit as the bidi strip above, and every surface on every client agrees on what to
|
|
111
|
+
* call the person. Normalising at RENDER instead would have web and iOS disagreeing about a name.
|
|
112
|
+
* Letters are never touched; this maps exactly twenty codepoints.
|
|
113
|
+
*/
|
|
114
|
+
declare const normalizeWesternDigits: (s: string) => string;
|
|
115
|
+
/**
|
|
116
|
+
* THE definition of "a safe player-supplied string": strip the unsafe class, normalise digits.
|
|
117
|
+
* Exported because it was being hand-copied — the relay's HTTP gates (account PATCH, question
|
|
118
|
+
* submissions) each grew their own copy of this exact body, and one of those copies stored the
|
|
119
|
+
* bidi characters LITERALLY in source, where any formatter or copy-paste could silently change
|
|
120
|
+
* what it matches. A rule that can be edited by accident is not a rule.
|
|
121
|
+
*/
|
|
122
|
+
declare const sanitizeName: (s: string) => string;
|
|
106
123
|
declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
107
124
|
declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
108
125
|
t: z.ZodLiteral<"createRoom">;
|
|
@@ -121,7 +138,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
121
138
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
122
139
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
123
140
|
idToken: z.ZodOptional<z.ZodString>;
|
|
124
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
141
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
125
142
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
126
143
|
}, "strip", z.ZodTypeAny, {
|
|
127
144
|
t: "createRoom";
|
|
@@ -134,7 +151,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
134
151
|
character?: "m" | "f" | undefined;
|
|
135
152
|
pv?: number | undefined;
|
|
136
153
|
idToken?: string | undefined;
|
|
137
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
154
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
138
155
|
appVersion?: string | undefined;
|
|
139
156
|
}, {
|
|
140
157
|
t: "createRoom";
|
|
@@ -147,7 +164,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
147
164
|
character?: "m" | "f" | undefined;
|
|
148
165
|
pv?: number | undefined;
|
|
149
166
|
idToken?: string | undefined;
|
|
150
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
167
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
151
168
|
appVersion?: string | undefined;
|
|
152
169
|
}>, z.ZodObject<{
|
|
153
170
|
t: z.ZodLiteral<"renameHost">;
|
|
@@ -205,7 +222,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
205
222
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
206
223
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
207
224
|
idToken: z.ZodOptional<z.ZodString>;
|
|
208
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
225
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
209
226
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
210
227
|
}, "strip", z.ZodTypeAny, {
|
|
211
228
|
code: string;
|
|
@@ -218,7 +235,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
218
235
|
character?: "m" | "f" | undefined;
|
|
219
236
|
pv?: number | undefined;
|
|
220
237
|
idToken?: string | undefined;
|
|
221
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
238
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
222
239
|
appVersion?: string | undefined;
|
|
223
240
|
}, {
|
|
224
241
|
code: string;
|
|
@@ -231,7 +248,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
231
248
|
character?: "m" | "f" | undefined;
|
|
232
249
|
pv?: number | undefined;
|
|
233
250
|
idToken?: string | undefined;
|
|
234
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
251
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
235
252
|
appVersion?: string | undefined;
|
|
236
253
|
}>, z.ZodObject<{
|
|
237
254
|
t: z.ZodLiteral<"resume">;
|
|
@@ -250,7 +267,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
250
267
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
251
268
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
252
269
|
idToken: z.ZodOptional<z.ZodString>;
|
|
253
|
-
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
270
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
254
271
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
255
272
|
}, "strip", z.ZodTypeAny, {
|
|
256
273
|
code: string;
|
|
@@ -263,7 +280,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
263
280
|
character?: "m" | "f" | undefined;
|
|
264
281
|
pv?: number | undefined;
|
|
265
282
|
idToken?: string | undefined;
|
|
266
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
283
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
267
284
|
appVersion?: string | undefined;
|
|
268
285
|
}, {
|
|
269
286
|
code: string;
|
|
@@ -276,7 +293,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
276
293
|
character?: "m" | "f" | undefined;
|
|
277
294
|
pv?: number | undefined;
|
|
278
295
|
idToken?: string | undefined;
|
|
279
|
-
platform?: "web" | "ios" | "android" | undefined;
|
|
296
|
+
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
280
297
|
appVersion?: string | undefined;
|
|
281
298
|
}>, z.ZodObject<{
|
|
282
299
|
t: z.ZodLiteral<"setPlaying">;
|
|
@@ -505,14 +522,17 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
505
522
|
t: z.ZodLiteral<"sabotage">;
|
|
506
523
|
helpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
507
524
|
targetTeam: z.ZodNumber;
|
|
525
|
+
player: z.ZodOptional<z.ZodString>;
|
|
508
526
|
}, "strip", z.ZodTypeAny, {
|
|
509
527
|
t: "sabotage";
|
|
510
528
|
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
511
529
|
targetTeam: number;
|
|
530
|
+
player?: string | undefined;
|
|
512
531
|
}, {
|
|
513
532
|
t: "sabotage";
|
|
514
533
|
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
515
534
|
targetTeam: number;
|
|
535
|
+
player?: string | undefined;
|
|
516
536
|
}>, z.ZodObject<{
|
|
517
537
|
t: z.ZodLiteral<"report">;
|
|
518
538
|
playerId: z.ZodString;
|
|
@@ -691,6 +711,32 @@ interface LobbyPlayer {
|
|
|
691
711
|
avatar?: AvatarView;
|
|
692
712
|
/** Team index once the game has started; null/absent in the lobby. */
|
|
693
713
|
teamIndex?: number | null;
|
|
714
|
+
/**
|
|
715
|
+
* Equipped avatar-frame slug (e.g. "frame-sadu") — resolves to art via /assets/store/catalog.json
|
|
716
|
+
* (the catalog is the ONLY resolver; nothing about the art is derived from the slug). Read from the
|
|
717
|
+
* account once at room entry, so an equip change shows on the player's next join/resume. Absent =
|
|
718
|
+
* frameless. Earnable-class cosmetics only ever ride the room wire (D-059) — a paid exclusive can
|
|
719
|
+
* never appear here, so there is no receipt check in the broadcast path and no have/have-not row
|
|
720
|
+
* between children on one sofa. (gs#84 contract, D-060.)
|
|
721
|
+
*/
|
|
722
|
+
frame?: string;
|
|
723
|
+
/**
|
|
724
|
+
* This room kicked this exact person before — a HINT for the host, never a gate (the ruling is
|
|
725
|
+
* "label the re-knock, no ban list"). **Present ONLY in the HOST's copy of the frame**: telling a
|
|
726
|
+
* whole room who was once removed is a social leak, not a moderation aid.
|
|
727
|
+
*
|
|
728
|
+
* The same bit also rides `joinRequest` for the immediate ping. This one is the DURABLE carrier —
|
|
729
|
+
* it is RE-READ from the answer stored on the seat (decided once, at join; never recomputed by
|
|
730
|
+
* re-matching, which goes stale) and re-sent on every snapshot, so it survives a host reconnect,
|
|
731
|
+
* where a cached one-shot event would not. A client should prefer this and treat the event as a
|
|
732
|
+
* nudge. MID-GAME there are no lobby frames: there a host who resumes is re-sent the pending
|
|
733
|
+
* `joinRequest`s instead (labels included), so the durable promise holds in both halves.
|
|
734
|
+
*
|
|
735
|
+
* Absent means "no reason to think so", which includes the deliberate miss: an anonymous player who
|
|
736
|
+
* force-quits and rejoins reads as a stranger. Matched only on exact socket/sign-in identity — never
|
|
737
|
+
* a display name or IP, because a wrong label is worse than none.
|
|
738
|
+
*/
|
|
739
|
+
wasKicked?: boolean;
|
|
694
740
|
}
|
|
695
741
|
/**
|
|
696
742
|
* Who is in the room, carried ON the game-state push (gs#71).
|
|
@@ -719,6 +765,9 @@ interface RosterEntry {
|
|
|
719
765
|
isHost?: boolean;
|
|
720
766
|
/** Is this person currently connected? Mirrors `LobbyPlayer.connected`. */
|
|
721
767
|
connected?: boolean;
|
|
768
|
+
/** Equipped avatar-frame slug — mirrors `LobbyPlayer.frame`, because the roster is the MID-GAME
|
|
769
|
+
* carrier (gs#71): a cosmetic with no mid-game carrier silently vanishes for watchers and the TV. */
|
|
770
|
+
frame?: string;
|
|
722
771
|
}
|
|
723
772
|
/** Per-recipient context attached to lobby/state pushes. */
|
|
724
773
|
interface YouContext {
|
|
@@ -786,10 +835,26 @@ interface TeamView {
|
|
|
786
835
|
/** The player on THIS team whose turn it is to answer (rotates each turn); null if the team has
|
|
787
836
|
* no seated players. In controller mode only this player may `answer` on the team's turn. */
|
|
788
837
|
currentPlayerId: string | null;
|
|
838
|
+
/** استريح (gs#80): this team's player who sits out their next question, if a rival named one. */
|
|
839
|
+
restedPlayerId?: string | null;
|
|
789
840
|
/** The seated player's privacy-safe avatar (relay-enriched; engine view omits it). */
|
|
790
841
|
avatar?: AvatarView;
|
|
791
842
|
}
|
|
792
843
|
interface QuestionView {
|
|
844
|
+
/** The category this question came from (gs#94 follow-up, geem-site#54). NOT reveal-gated: the
|
|
845
|
+
* category is already on the board tile the player picked, so it gives nothing away — unlike the
|
|
846
|
+
* id, which is a handle into the bank. It exists so «اقترح سؤال مشابه» can preset the category
|
|
847
|
+
* instead of asking the player to re-pick it; without it a client either asks again or INFERS one,
|
|
848
|
+
* and inferring files somebody's question under the wrong category. Optional: pre-gs#94 servers
|
|
849
|
+
* omit it and clients must cope. */
|
|
850
|
+
categoryId?: string;
|
|
851
|
+
/** The question's id — present ONLY once revealed (gs#90). It exists so a player can say «عدّل
|
|
852
|
+
* السؤال» about a specific question: text is neither unique (template-shaped questions collide on
|
|
853
|
+
* wording) nor stable, so an edit identified only by its text is not reliably applicable. Kept off
|
|
854
|
+
* the wire while the question is LIVE for the same reason `correctAnswerIndex` is — the bank is
|
|
855
|
+
* server-side and an id is a handle into it. Optional on the wire: shipped clients ignore it, and
|
|
856
|
+
* a client that wants it must tolerate its absence pre-reveal. */
|
|
857
|
+
id?: string;
|
|
793
858
|
text: string;
|
|
794
859
|
options: string[];
|
|
795
860
|
hiddenOptionIndices: number[];
|
|
@@ -797,6 +862,14 @@ interface QuestionView {
|
|
|
797
862
|
selectedAnswerIndex: number | null;
|
|
798
863
|
/** Only present once revealed (anti-cheat). Null while the question is live. */
|
|
799
864
|
correctAnswerIndex: number | null;
|
|
865
|
+
/** «سؤال من: …» — community submitter credit (owner ruling 2026-08-14). Present ONLY on
|
|
866
|
+
* questions promoted from community submissions; official questions never carry it. NOT
|
|
867
|
+
* reveal-gated — a name leaks nothing — so place it wherever it reads best. */
|
|
868
|
+
submittedBy?: string;
|
|
869
|
+
/** «معلومة» — optional explanation shown AFTER the reveal. Null while the question is live (it
|
|
870
|
+
* usually names the answer — same anti-cheat gate as correctAnswerIndex) and null when the
|
|
871
|
+
* question simply has none, which is most of the bank: render the card only when non-null. */
|
|
872
|
+
explain?: string | null;
|
|
800
873
|
imageName?: string;
|
|
801
874
|
videoName?: string;
|
|
802
875
|
/** For video questions: seconds into the clip to stop playback (so the answer isn't shown). Absent = play through. */
|
|
@@ -958,6 +1031,7 @@ type ServerMsg = {
|
|
|
958
1031
|
t: 'joinRequest';
|
|
959
1032
|
playerId: string;
|
|
960
1033
|
name: string;
|
|
1034
|
+
wasKicked?: boolean;
|
|
961
1035
|
} | {
|
|
962
1036
|
t: 'joined';
|
|
963
1037
|
youId: string;
|
|
@@ -983,6 +1057,7 @@ type ServerMsg = {
|
|
|
983
1057
|
hostId: string;
|
|
984
1058
|
hostName: string;
|
|
985
1059
|
hostAvatar?: AvatarView;
|
|
1060
|
+
hostFrame?: string;
|
|
986
1061
|
hostPlaying?: boolean;
|
|
987
1062
|
hostPinned?: boolean;
|
|
988
1063
|
players: LobbyPlayer[];
|
|
@@ -994,6 +1069,9 @@ type ServerMsg = {
|
|
|
994
1069
|
tally?: {
|
|
995
1070
|
[playerId: string]: number;
|
|
996
1071
|
};
|
|
1072
|
+
tallyNames?: {
|
|
1073
|
+
[playerId: string]: string;
|
|
1074
|
+
};
|
|
997
1075
|
modeId: ModeId | null;
|
|
998
1076
|
you: YouContext;
|
|
999
1077
|
} | {
|
|
@@ -1040,10 +1118,51 @@ type ServerMsg = {
|
|
|
1040
1118
|
message: string;
|
|
1041
1119
|
level?: 'info' | 'warn' | 'maintenance';
|
|
1042
1120
|
restartInSec?: number;
|
|
1043
|
-
}
|
|
1121
|
+
}
|
|
1122
|
+
/** `reason` (gs#83, additive): today only `'ended'`, and only on `no_room` — the code WAS a room
|
|
1123
|
+
* until recently (stale WhatsApp link, knock after the host closed the night). Absent = "no such
|
|
1124
|
+
* room as far as we know". Lets a client say «انتهى اللعب» instead of «ما في غرفة بهذا الكود»
|
|
1125
|
+
* without guessing. Old clients ignore it. */
|
|
1126
|
+
| {
|
|
1044
1127
|
t: 'error';
|
|
1045
1128
|
code: string;
|
|
1046
1129
|
message: string;
|
|
1130
|
+
reason?: string;
|
|
1047
1131
|
};
|
|
1132
|
+
/** Review states for a community submission. ⚠️ `approved` is NOT here: it is a DEPRECATED alias
|
|
1133
|
+
* written by an older geem-admin, mapped to `accepted` by the relay and permitted by 0031's CHECK
|
|
1134
|
+
* only until that write is switched. It must never become a fourth state. */
|
|
1135
|
+
declare const SUBMISSION_STATUSES: readonly ["pending", "accepted", "rejected"];
|
|
1136
|
+
type SubmissionStatus = (typeof SUBMISSION_STATUSES)[number];
|
|
1137
|
+
/** A brand-new question vs a correction to one already in the bank. */
|
|
1138
|
+
declare const SUBMISSION_KINDS: readonly ["new", "edit"];
|
|
1139
|
+
type SubmissionKind = (typeof SUBMISSION_KINDS)[number];
|
|
1140
|
+
/** Why a submission was turned down. Blame-free by construction — the closest to "you did badly" is
|
|
1141
|
+
* `duplicate`, which is about the BANK, not the writer. Clients translate these and must fall back
|
|
1142
|
+
* to `other` on anything unrecognised, which is exactly what makes adding a seventh survivable for a
|
|
1143
|
+
* build that shipped last month and will never be updated. */
|
|
1144
|
+
declare const REJECT_REASONS: readonly ["duplicate", "unclear_answer", "not_family", "inaccurate", "too_narrow", "other"];
|
|
1145
|
+
type RejectReason = (typeof REJECT_REASONS)[number];
|
|
1146
|
+
/** One row of «أسئلتي». Exists as a named type so the native generator EMITS the three unions above
|
|
1147
|
+
* — quicktype only reaches what a root references, and hand-typed constants are the whole bug. */
|
|
1148
|
+
interface MyQuestionRow {
|
|
1149
|
+
id: string;
|
|
1150
|
+
status: SubmissionStatus;
|
|
1151
|
+
kind: SubmissionKind;
|
|
1152
|
+
text: string;
|
|
1153
|
+
options: string[];
|
|
1154
|
+
correctIndex: number;
|
|
1155
|
+
explain: string | null;
|
|
1156
|
+
createdAt: string;
|
|
1157
|
+
accepted?: {
|
|
1158
|
+
plays: number;
|
|
1159
|
+
firstPlayedAt: string | null;
|
|
1160
|
+
coins: number;
|
|
1161
|
+
};
|
|
1162
|
+
rejected?: {
|
|
1163
|
+
reason: RejectReason;
|
|
1164
|
+
canResubmit: boolean;
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1048
1167
|
|
|
1049
|
-
export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type PackView, type QuestionView, ROOM_ALPHABET, type RosterEntry, type ServerMsg, type SoundName, type StrokePointView, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame };
|
|
1168
|
+
export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, type MyQuestionRow, PROTOCOL_VERSION, type PackView, type QuestionView, REJECT_REASONS, ROOM_ALPHABET, type RejectReason, type RosterEntry, SUBMISSION_KINDS, SUBMISSION_STATUSES, type ServerMsg, type SoundName, type StrokePointView, type SubmissionKind, type SubmissionStatus, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame, normalizeWesternDigits, sanitizeName };
|
package/dist/index.js
CHANGED
|
@@ -38,7 +38,9 @@ var EMOTE_IDS = ["laugh", "wow", "think", "gg"];
|
|
|
38
38
|
var CHARACTER_IDS = ["m", "f"];
|
|
39
39
|
var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
40
40
|
var UNSAFE_CHARS = /[\p{Cc}\u202A-\u202E\u2066-\u2069<>]/gu;
|
|
41
|
-
var
|
|
41
|
+
var normalizeWesternDigits = (s) => s.replace(/[٠-٩۰-۹]/g, (d) => String("\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669".indexOf(d) >= 0 ? "\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669".indexOf(d) : "\u06F0\u06F1\u06F2\u06F3\u06F4\u06F5\u06F6\u06F7\u06F8\u06F9".indexOf(d)));
|
|
42
|
+
var sanitizeName = (s) => normalizeWesternDigits(s.replace(UNSAFE_CHARS, "")).trim();
|
|
43
|
+
var DisplayName = z.string().trim().min(1).max(LIMITS.MAX_NAME_LEN).transform(sanitizeName).refine((s) => s.length >= 1, "name is empty after sanitizing");
|
|
42
44
|
var RoomCode = z.string().trim().toUpperCase().regex(new RegExp(`^[${ROOM_ALPHABET}]{${LIMITS.ROOM_CODE_LEN}}$`), "bad room code");
|
|
43
45
|
var PlayerId = z.string().max(40);
|
|
44
46
|
var HelpType = z.enum([
|
|
@@ -52,7 +54,7 @@ var HelpType = z.enum([
|
|
|
52
54
|
]);
|
|
53
55
|
var Pv = z.number().int().min(1).max(1e6).optional();
|
|
54
56
|
var IdToken = z.string().max(4096).optional();
|
|
55
|
-
var Platform = z.enum(["web", "ios", "android"]).optional();
|
|
57
|
+
var Platform = z.enum(["web", "ios", "android", "tv"]).optional();
|
|
56
58
|
var AppVersion = z.string().trim().max(24).regex(/^[0-9]+(\.[0-9]+){0,3}([-+][0-9A-Za-z.-]+)?$/, "bad app version").optional();
|
|
57
59
|
var Avatar = z.object({
|
|
58
60
|
emoji: z.string().min(1).max(16).transform((s) => s.replace(UNSAFE_CHARS, "")).refine((s) => s.length >= 1, "empty emoji"),
|
|
@@ -179,7 +181,11 @@ var ClientMsg = z.discriminatedUnion("t", [
|
|
|
179
181
|
// trivia is 4-option (0–3)
|
|
180
182
|
z.object({ t: z.literal("useHelp"), helpType: HelpType }),
|
|
181
183
|
// sabotage a rival (active team only): spend a sabotage help to debuff targetTeam's next question
|
|
182
|
-
|
|
184
|
+
// `player` (gs#80): WHICH member of the target team sits out — required by استريح/restPlayer and
|
|
185
|
+
// ignored by عرجوب/tripLevel, which targets a whole team. Optional on the wire so pre-gs#80 clients
|
|
186
|
+
// are unchanged; the engine refuses to spend a restPlayer card without a valid one, so an old client
|
|
187
|
+
// cannot burn a lifeline by omitting it.
|
|
188
|
+
z.object({ t: z.literal("sabotage"), helpType: HelpType, targetTeam: z.number().int().min(0).max(LIMITS.MAX_PLAYERS - 1), player: z.string().max(64).optional() }),
|
|
183
189
|
// 0-based team index
|
|
184
190
|
// moderation
|
|
185
191
|
// Report another player in the room (App Store 1.2 UGC — display names). Available in lobby or
|
|
@@ -223,6 +229,9 @@ var ClientMsg = z.discriminatedUnion("t", [
|
|
|
223
229
|
// Drawer passes on the served word (capped per draw-turn; relay serves a fresh one).
|
|
224
230
|
z.object({ t: z.literal("skipWord") })
|
|
225
231
|
]);
|
|
232
|
+
var SUBMISSION_STATUSES = ["pending", "accepted", "rejected"];
|
|
233
|
+
var SUBMISSION_KINDS = ["new", "edit"];
|
|
234
|
+
var REJECT_REASONS = ["duplicate", "unclear_answer", "not_family", "inaccurate", "too_narrow", "other"];
|
|
226
235
|
export {
|
|
227
236
|
CHARACTER_IDS,
|
|
228
237
|
ClientMsg,
|
|
@@ -234,8 +243,13 @@ export {
|
|
|
234
243
|
MODES,
|
|
235
244
|
MODE_IDS,
|
|
236
245
|
PROTOCOL_VERSION,
|
|
246
|
+
REJECT_REASONS,
|
|
237
247
|
ROOM_ALPHABET,
|
|
248
|
+
SUBMISSION_KINDS,
|
|
249
|
+
SUBMISSION_STATUSES,
|
|
238
250
|
gameIdForPv,
|
|
239
251
|
gamesForPv,
|
|
240
|
-
modesForGame
|
|
252
|
+
modesForGame,
|
|
253
|
+
normalizeWesternDigits,
|
|
254
|
+
sanitizeName
|
|
241
255
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegeem/protocol",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"description": "Geem's wire protocol — shared zod schemas + TypeScript types for talking to the Geem game server over Socket.IO. The source of truth all clients implement against.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://geem.tv",
|