@thegeem/protocol 0.1.37 → 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 CHANGED
@@ -30,11 +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
39
  modesForGame: () => modesForGame,
37
- normalizeWesternDigits: () => normalizeWesternDigits
40
+ normalizeWesternDigits: () => normalizeWesternDigits,
41
+ sanitizeName: () => sanitizeName
38
42
  });
39
43
  module.exports = __toCommonJS(index_exports);
40
44
  var import_zod = require("zod");
@@ -77,7 +81,8 @@ var CHARACTER_IDS = ["m", "f"];
77
81
  var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
78
82
  var UNSAFE_CHARS = /[\p{Cc}\u202A-\u202E\u2066-\u2069<>]/gu;
79
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)));
80
- var DisplayName = import_zod.z.string().trim().min(1).max(LIMITS.MAX_NAME_LEN).transform((s) => normalizeWesternDigits(s.replace(UNSAFE_CHARS, "")).trim()).refine((s) => s.length >= 1, "name is empty after sanitizing");
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");
81
86
  var RoomCode = import_zod.z.string().trim().toUpperCase().regex(new RegExp(`^[${ROOM_ALPHABET}]{${LIMITS.ROOM_CODE_LEN}}$`), "bad room code");
82
87
  var PlayerId = import_zod.z.string().max(40);
83
88
  var HelpType = import_zod.z.enum([
@@ -266,6 +271,9 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
266
271
  // Drawer passes on the served word (capped per draw-turn; relay serves a fresh one).
267
272
  import_zod.z.object({ t: import_zod.z.literal("skipWord") })
268
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"];
269
277
  // Annotate the CommonJS export names for ESM import in node:
270
278
  0 && (module.exports = {
271
279
  CHARACTER_IDS,
@@ -278,9 +286,13 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
278
286
  MODES,
279
287
  MODE_IDS,
280
288
  PROTOCOL_VERSION,
289
+ REJECT_REASONS,
281
290
  ROOM_ALPHABET,
291
+ SUBMISSION_KINDS,
292
+ SUBMISSION_STATUSES,
282
293
  gameIdForPv,
283
294
  gamesForPv,
284
295
  modesForGame,
285
- normalizeWesternDigits
296
+ normalizeWesternDigits,
297
+ sanitizeName
286
298
  });
package/dist/index.d.cts CHANGED
@@ -112,6 +112,14 @@ declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
112
112
  * Letters are never touched; this maps exactly twenty codepoints.
113
113
  */
114
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;
115
123
  declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
116
124
  declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
117
125
  t: z.ZodLiteral<"createRoom">;
@@ -833,6 +841,20 @@ interface TeamView {
833
841
  avatar?: AvatarView;
834
842
  }
835
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;
836
858
  text: string;
837
859
  options: string[];
838
860
  hiddenOptionIndices: number[];
@@ -840,6 +862,10 @@ interface QuestionView {
840
862
  selectedAnswerIndex: number | null;
841
863
  /** Only present once revealed (anti-cheat). Null while the question is live. */
842
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;
843
869
  /** «معلومة» — optional explanation shown AFTER the reveal. Null while the question is live (it
844
870
  * usually names the answer — same anti-cheat gate as correctAnswerIndex) and null when the
845
871
  * question simply has none, which is most of the bank: render the card only when non-null. */
@@ -1103,5 +1129,40 @@ type ServerMsg = {
1103
1129
  message: string;
1104
1130
  reason?: string;
1105
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
+ }
1106
1167
 
1107
- 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, normalizeWesternDigits };
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
@@ -112,6 +112,14 @@ declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
112
112
  * Letters are never touched; this maps exactly twenty codepoints.
113
113
  */
114
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;
115
123
  declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
116
124
  declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
117
125
  t: z.ZodLiteral<"createRoom">;
@@ -833,6 +841,20 @@ interface TeamView {
833
841
  avatar?: AvatarView;
834
842
  }
835
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;
836
858
  text: string;
837
859
  options: string[];
838
860
  hiddenOptionIndices: number[];
@@ -840,6 +862,10 @@ interface QuestionView {
840
862
  selectedAnswerIndex: number | null;
841
863
  /** Only present once revealed (anti-cheat). Null while the question is live. */
842
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;
843
869
  /** «معلومة» — optional explanation shown AFTER the reveal. Null while the question is live (it
844
870
  * usually names the answer — same anti-cheat gate as correctAnswerIndex) and null when the
845
871
  * question simply has none, which is most of the bank: render the card only when non-null. */
@@ -1103,5 +1129,40 @@ type ServerMsg = {
1103
1129
  message: string;
1104
1130
  reason?: string;
1105
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
+ }
1106
1167
 
1107
- 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, normalizeWesternDigits };
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
@@ -39,7 +39,8 @@ var CHARACTER_IDS = ["m", "f"];
39
39
  var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
40
40
  var UNSAFE_CHARS = /[\p{Cc}\u202A-\u202E\u2066-\u2069<>]/gu;
41
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 DisplayName = z.string().trim().min(1).max(LIMITS.MAX_NAME_LEN).transform((s) => normalizeWesternDigits(s.replace(UNSAFE_CHARS, "")).trim()).refine((s) => s.length >= 1, "name is empty after sanitizing");
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");
43
44
  var RoomCode = z.string().trim().toUpperCase().regex(new RegExp(`^[${ROOM_ALPHABET}]{${LIMITS.ROOM_CODE_LEN}}$`), "bad room code");
44
45
  var PlayerId = z.string().max(40);
45
46
  var HelpType = z.enum([
@@ -228,6 +229,9 @@ var ClientMsg = z.discriminatedUnion("t", [
228
229
  // Drawer passes on the served word (capped per draw-turn; relay serves a fresh one).
229
230
  z.object({ t: z.literal("skipWord") })
230
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"];
231
235
  export {
232
236
  CHARACTER_IDS,
233
237
  ClientMsg,
@@ -239,9 +243,13 @@ export {
239
243
  MODES,
240
244
  MODE_IDS,
241
245
  PROTOCOL_VERSION,
246
+ REJECT_REASONS,
242
247
  ROOM_ALPHABET,
248
+ SUBMISSION_KINDS,
249
+ SUBMISSION_STATUSES,
243
250
  gameIdForPv,
244
251
  gamesForPv,
245
252
  modesForGame,
246
- normalizeWesternDigits
253
+ normalizeWesternDigits,
254
+ sanitizeName
247
255
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegeem/protocol",
3
- "version": "0.1.37",
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",