@orakl/protocol 0.11.0 → 0.13.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/dist/emotes.d.ts CHANGED
@@ -1,5 +1,73 @@
1
- export declare const EMOTE_SPRITES: Record<string, {
2
- x: number;
3
- y: number;
1
+ import { Schema } from "effect";
2
+ /** Most emotes a Player's set holds. */
3
+ export declare const MAX_EMOTE_SET = 5;
4
+ /** An emote id on the wire: a seeded name (e.g. `heart`) or a generated id. */
5
+ export declare const EmoteIdSchema: Schema.filter<typeof Schema.String>;
6
+ /** A Player's chosen emotes, in their order. */
7
+ export declare const EmoteSetSchema: Schema.mutable<Schema.filter<Schema.Array$<Schema.filter<typeof Schema.String>>>>;
8
+ /** One live emote in the public gallery. */
9
+ export declare const EmoteSchema: Schema.Struct<{
10
+ id: typeof Schema.String;
11
+ title: typeof Schema.String;
12
+ description: typeof Schema.String;
13
+ /** Public, content-hashed image path; may be animated. */
14
+ src: typeof Schema.String;
15
+ /** First frame, shown when the viewer asks for reduced motion. */
16
+ stillSrc: typeof Schema.String;
17
+ }>;
18
+ export type Emote = typeof EmoteSchema.Type;
19
+ /** One pack in the public gallery: its live emotes in the Admin's order. */
20
+ export declare const EmotePackSchema: Schema.Struct<{
21
+ id: typeof Schema.String;
22
+ title: typeof Schema.String;
23
+ description: typeof Schema.String;
24
+ /** The pack whose first emotes a Player who chose nothing gets. */
25
+ isDefault: typeof Schema.Boolean;
26
+ emotes: Schema.mutable<Schema.Array$<Schema.Struct<{
27
+ id: typeof Schema.String;
28
+ title: typeof Schema.String;
29
+ description: typeof Schema.String;
30
+ /** Public, content-hashed image path; may be animated. */
31
+ src: typeof Schema.String;
32
+ /** First frame, shown when the viewer asks for reduced motion. */
33
+ stillSrc: typeof Schema.String;
34
+ }>>>;
35
+ }>;
36
+ export type EmotePack = typeof EmotePackSchema.Type;
37
+ /** Body of `GET /api/emotes`: every pack with live emotes, in pack order. */
38
+ export declare const EmoteGallerySchema: Schema.mutable<Schema.Array$<Schema.Struct<{
39
+ id: typeof Schema.String;
40
+ title: typeof Schema.String;
41
+ description: typeof Schema.String;
42
+ /** The pack whose first emotes a Player who chose nothing gets. */
43
+ isDefault: typeof Schema.Boolean;
44
+ emotes: Schema.mutable<Schema.Array$<Schema.Struct<{
45
+ id: typeof Schema.String;
46
+ title: typeof Schema.String;
47
+ description: typeof Schema.String;
48
+ /** Public, content-hashed image path; may be animated. */
49
+ src: typeof Schema.String;
50
+ /** First frame, shown when the viewer asks for reduced motion. */
51
+ stillSrc: typeof Schema.String;
52
+ }>>>;
53
+ }>>>;
54
+ /** Body of `GET /api/pages/admin/emotes`: every pack and emote, retired ones included. */
55
+ export declare const ManageEmotesPageSchema: Schema.Struct<{
56
+ packs: Schema.mutable<Schema.Array$<Schema.Struct<{
57
+ id: typeof Schema.String;
58
+ title: typeof Schema.String;
59
+ description: typeof Schema.String;
60
+ sortOrder: typeof Schema.Number;
61
+ isDefault: typeof Schema.Boolean;
62
+ emotes: Schema.mutable<Schema.Array$<Schema.Struct<{
63
+ sortOrder: typeof Schema.Number;
64
+ /** Hidden from the gallery and pickers; a Game in progress keeps showing it. */
65
+ retired: typeof Schema.Boolean;
66
+ id: typeof Schema.String;
67
+ title: typeof Schema.String;
68
+ description: typeof Schema.String;
69
+ src: typeof Schema.String;
70
+ stillSrc: typeof Schema.String;
71
+ }>>>;
72
+ }>>>;
4
73
  }>;
5
- export declare const SELECTABLE_EMOTE_IDS: readonly ["alert", "anger", "bars", "cash", "circle", "cloud", "cross", "dots1", "dots2", "dots3", "drop", "drops", "exclamation", "exclamations", "faceAngry", "faceHappy", "faceSad", "heart", "heartBroken", "hearts", "idea", "laugh", "music", "question", "sleep", "sleeps", "star", "stars", "swirl"];
package/dist/emotes.js CHANGED
@@ -1,62 +1,45 @@
1
- export const EMOTE_SPRITES = {
2
- alert: { x: 64, y: 64 },
3
- anger: { x: 64, y: 48 },
4
- bars: { x: 64, y: 32 },
5
- cash: { x: 64, y: 16 },
6
- circle: { x: 64, y: 0 },
7
- cloud: { x: 48, y: 80 },
8
- cross: { x: 48, y: 64 },
9
- dots1: { x: 48, y: 48 },
10
- dots2: { x: 48, y: 32 },
11
- dots3: { x: 48, y: 16 },
12
- drop: { x: 48, y: 0 },
13
- drops: { x: 32, y: 80 },
14
- exclamation: { x: 32, y: 64 },
15
- exclamations: { x: 32, y: 48 },
16
- faceAngry: { x: 64, y: 80 },
17
- faceHappy: { x: 32, y: 16 },
18
- faceSad: { x: 32, y: 0 },
19
- heart: { x: 16, y: 80 },
20
- heartBroken: { x: 16, y: 64 },
21
- hearts: { x: 16, y: 48 },
22
- idea: { x: 16, y: 32 },
23
- laugh: { x: 16, y: 16 },
24
- music: { x: 16, y: 0 },
25
- question: { x: 0, y: 80 },
26
- sleep: { x: 0, y: 64 },
27
- sleeps: { x: 0, y: 48 },
28
- star: { x: 0, y: 32 },
29
- stars: { x: 0, y: 16 },
30
- swirl: { x: 0, y: 0 },
31
- };
32
- export const SELECTABLE_EMOTE_IDS = [
33
- "alert",
34
- "anger",
35
- "bars",
36
- "cash",
37
- "circle",
38
- "cloud",
39
- "cross",
40
- "dots1",
41
- "dots2",
42
- "dots3",
43
- "drop",
44
- "drops",
45
- "exclamation",
46
- "exclamations",
47
- "faceAngry",
48
- "faceHappy",
49
- "faceSad",
50
- "heart",
51
- "heartBroken",
52
- "hearts",
53
- "idea",
54
- "laugh",
55
- "music",
56
- "question",
57
- "sleep",
58
- "sleeps",
59
- "star",
60
- "stars",
61
- "swirl",
62
- ];
1
+ import { Schema } from "effect";
2
+ /** Most emotes a Player's set holds. */
3
+ export const MAX_EMOTE_SET = 5;
4
+ /** An emote id on the wire: a seeded name (e.g. `heart`) or a generated id. */
5
+ export const EmoteIdSchema = Schema.String.pipe(Schema.pattern(/^[A-Za-z0-9_-]{1,64}$/));
6
+ /** A Player's chosen emotes, in their order. */
7
+ export const EmoteSetSchema = Schema.mutable(Schema.Array(EmoteIdSchema).pipe(Schema.maxItems(MAX_EMOTE_SET)));
8
+ /** One live emote in the public gallery. */
9
+ export const EmoteSchema = Schema.Struct({
10
+ id: Schema.String,
11
+ title: Schema.String,
12
+ description: Schema.String,
13
+ /** Public, content-hashed image path; may be animated. */
14
+ src: Schema.String,
15
+ /** First frame, shown when the viewer asks for reduced motion. */
16
+ stillSrc: Schema.String,
17
+ });
18
+ /** One pack in the public gallery: its live emotes in the Admin's order. */
19
+ export const EmotePackSchema = Schema.Struct({
20
+ id: Schema.String,
21
+ title: Schema.String,
22
+ description: Schema.String,
23
+ /** The pack whose first emotes a Player who chose nothing gets. */
24
+ isDefault: Schema.Boolean,
25
+ emotes: Schema.mutable(Schema.Array(EmoteSchema)),
26
+ });
27
+ /** Body of `GET /api/emotes`: every pack with live emotes, in pack order. */
28
+ export const EmoteGallerySchema = Schema.mutable(Schema.Array(EmotePackSchema));
29
+ const ManagedEmoteSchema = Schema.Struct({
30
+ ...EmoteSchema.fields,
31
+ sortOrder: Schema.Number,
32
+ /** Hidden from the gallery and pickers; a Game in progress keeps showing it. */
33
+ retired: Schema.Boolean,
34
+ });
35
+ /** Body of `GET /api/pages/admin/emotes`: every pack and emote, retired ones included. */
36
+ export const ManageEmotesPageSchema = Schema.Struct({
37
+ packs: Schema.mutable(Schema.Array(Schema.Struct({
38
+ id: Schema.String,
39
+ title: Schema.String,
40
+ description: Schema.String,
41
+ sortOrder: Schema.Number,
42
+ isDefault: Schema.Boolean,
43
+ emotes: Schema.mutable(Schema.Array(ManagedEmoteSchema)),
44
+ }))),
45
+ });
@@ -1 +1 @@
1
- export type ErrorCode = "no_active_lobby" | "no_active_game" | "device_blocked" | "nickname_taken" | "nickname_locked" | "device_in_lobby" | "player_not_found" | "player_not_pending" | "intermission_invalid_phase" | "not_in_intermission" | "category_name_exists" | "category_not_found" | "category_has_questions" | "tag_name_exists" | "tag_not_found" | "tag_has_questions" | "tag_in_season" | "category_in_season" | "season_name_exists" | "season_not_found" | "user_not_found" | "curation_request_pending" | "curator_self_join" | "already_flagged" | "question_not_found" | "invalid_answer" | "game_in_progress" | "already_answered" | "observer_forbidden" | "no_questions" | "season_out_of_season" | "no_categories" | "too_few_players" | "hosting_closed" | "not_answered" | "flag_not_toggleable" | "solo_invalid_phase" | "solo_invalid_answer" | "solo_nonce_mismatch" | "solo_answer_expired" | "solo_guest_limit" | "solo_no_questions" | "solo_season_out_of_season" | "solo_resume_denied" | "solo_auth_failed";
1
+ export type ErrorCode = "no_active_lobby" | "no_active_game" | "device_blocked" | "nickname_taken" | "nickname_locked" | "device_in_lobby" | "player_not_found" | "player_not_pending" | "intermission_invalid_phase" | "not_in_intermission" | "category_name_exists" | "category_not_found" | "category_has_questions" | "tag_name_exists" | "tag_not_found" | "tag_has_questions" | "tag_in_season" | "category_in_season" | "season_name_exists" | "season_not_found" | "user_not_found" | "curation_request_pending" | "curator_self_join" | "already_flagged" | "question_not_found" | "invalid_answer" | "game_in_progress" | "already_answered" | "observer_forbidden" | "no_questions" | "season_out_of_season" | "no_categories" | "too_few_players" | "hosting_closed" | "not_answered" | "emote_not_in_set" | "flag_not_toggleable" | "solo_invalid_phase" | "solo_invalid_answer" | "solo_nonce_mismatch" | "solo_answer_expired" | "solo_guest_limit" | "solo_no_questions" | "solo_season_out_of_season" | "solo_resume_denied" | "solo_auth_failed";
package/dist/game.d.ts CHANGED
@@ -19,6 +19,8 @@ export interface Player {
19
19
  id: string;
20
20
  nickname: string;
21
21
  avatar: string;
22
+ /** The Player's resolved emote set, in their order; the only ids they may send. */
23
+ emotes: string[];
22
24
  score: number;
23
25
  ready: boolean;
24
26
  status: PlayerStatus;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type { BadgeAward, BadgeId, BadgeMode, BadgeRunParams, } from "./badges.ts";
2
- export { EMOTE_SPRITES, SELECTABLE_EMOTE_IDS } from "./emotes.ts";
2
+ export { type Emote, EmoteGallerySchema, EmoteIdSchema, type EmotePack, EmotePackSchema, EmoteSchema, EmoteSetSchema, MAX_EMOTE_SET, ManageEmotesPageSchema, } from "./emotes.ts";
3
3
  export type { ErrorCode } from "./error-codes.ts";
4
4
  export { type AdvanceMode, type Difficulty, type DifficultyFilter, GAME, type QuestionsPerRound, type ScoringMode, type TimerDuration, } from "./game.settings.ts";
5
5
  export type { AccessMode, ActiveGamePhase, GamePhase, GameState, ImageMatchingQuizQuestion, Player, PlayerRole, PlayerStatus, QuizAnswer, QuizConfig, QuizQuestion, StandardQuizQuestion, } from "./game.ts";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { EMOTE_SPRITES, SELECTABLE_EMOTE_IDS } from "./emotes.js";
1
+ export { EmoteGallerySchema, EmoteIdSchema, EmotePackSchema, EmoteSchema, EmoteSetSchema, MAX_EMOTE_SET, ManageEmotesPageSchema, } from "./emotes.js";
2
2
  export { GAME, } from "./game.settings.js";
3
3
  export * from "./pages/app.schema.js";
4
4
  export * from "./pages/auth.schema.js";
@@ -19,6 +19,8 @@ export declare const ProfilePageSchema: Schema.Struct<{
19
19
  role: Schema.Literal<["anonymous", "display", "member", "curator", "admin"]>;
20
20
  powers: Schema.mutable<Schema.Array$<Schema.Schema<"can-access-mimic" | "can-send-feedback" | "can-import-quizzes" | "can-manage-features" | "can-add-questions" | "can-manage-categories" | "can-moderate-questions" | "can-publish-questions" | "is-power-user" | "can-manage-avatars" | "can-apply-for-curation" | "can-1v1", "can-access-mimic" | "can-send-feedback" | "can-import-quizzes" | "can-manage-features" | "can-add-questions" | "can-manage-categories" | "can-moderate-questions" | "can-publish-questions" | "is-power-user" | "can-manage-avatars" | "can-apply-for-curation" | "can-1v1", never>>>;
21
21
  avatar: typeof Schema.String;
22
+ /** Saved emote set, in order. */
23
+ emoteIds: Schema.optional<Schema.mutable<Schema.filter<Schema.Array$<Schema.filter<typeof Schema.String>>>>>;
22
24
  emailVerified: typeof Schema.Boolean;
23
25
  }>;
24
26
  theme: Schema.Struct<{
@@ -621,12 +623,7 @@ export declare const CuratorHistoryEntryPageSchema: Schema.Struct<{
621
623
  readonly questionText?: string | undefined;
622
624
  readonly mediaUrl?: string | undefined;
623
625
  readonly mediaType?: "audio" | "video" | "image" | undefined;
624
- readonly mediaCredit?: {
625
- readonly licence: "cc0" | "public-domain" | "cc-by" | "cc-by-sa" | "own";
626
- readonly author?: string | undefined;
627
- readonly sourceUrl?: string | undefined;
628
- readonly licenceUrl?: string | undefined;
629
- } | undefined;
626
+ readonly mediaCredit?: unknown;
630
627
  }, never>>>>;
631
628
  }>;
632
629
  presetLimit: typeof Schema.Number;
@@ -1,5 +1,6 @@
1
1
  import { Schema } from "effect";
2
- import { MediaCreditSchema, MediaTypeSchema, StoredQuestionSourceSchema, } from "../question.schema.js";
2
+ import { EmoteSetSchema } from "../emotes.js";
3
+ import { MediaTypeSchema, StoredMediaCreditSchema, StoredQuestionSourceSchema, } from "../question.schema.js";
3
4
  import { ThemeModeSchema, UserPowerSchema, UserRoleSchema, } from "./common.schema.js";
4
5
  /** Page-load response bodies for `src/api/routes/pages.ts`. */
5
6
  /** A JSON array, decoded mutable — Effect's `Schema.Array` types its `.Type`
@@ -55,6 +56,8 @@ export const ProfilePageSchema = Schema.Struct({
55
56
  role: UserRoleSchema,
56
57
  powers: Arr(UserPowerSchema),
57
58
  avatar: Schema.String,
59
+ /** Saved emote set, in order. */
60
+ emoteIds: Schema.optional(EmoteSetSchema),
58
61
  emailVerified: Schema.Boolean,
59
62
  }),
60
63
  theme: Schema.Struct({ mode: ThemeModeSchema }),
@@ -338,7 +341,7 @@ const QuestionRecapEntrySchema = Schema.Struct({
338
341
  questionText: Schema.optional(Schema.String),
339
342
  mediaUrl: Schema.optional(Schema.String),
340
343
  mediaType: Schema.optional(MediaTypeSchema),
341
- mediaCredit: Schema.optional(MediaCreditSchema),
344
+ mediaCredit: Schema.optional(StoredMediaCreditSchema),
342
345
  source: Schema.optional(StoredQuestionSourceSchema),
343
346
  postAnswerNote: Schema.optional(Schema.String),
344
347
  answered: Schema.Number,
@@ -19,12 +19,17 @@ export declare const GameAnswerRecordSchema: Schema.Struct<{
19
19
  correctAnswerText: Schema.optional<typeof Schema.String>;
20
20
  mediaUrl: Schema.optional<typeof Schema.String>;
21
21
  mediaType: Schema.optional<Schema.Literal<["image", "audio", "video"]>>;
22
- mediaCredit: Schema.optional<Schema.filter<Schema.Struct<{
23
- licence: Schema.Literal<["cc0", "public-domain", "cc-by", "cc-by-sa", "own"]>;
24
- author: Schema.optional<Schema.filter<typeof Schema.String>>;
25
- sourceUrl: Schema.optional<Schema.filter<Schema.filter<typeof Schema.String>>>;
26
- licenceUrl: Schema.optional<Schema.filter<Schema.filter<typeof Schema.String>>>;
27
- }>>>;
22
+ mediaCredit: Schema.optional<Schema.transform<typeof Schema.Unknown, Schema.UndefinedOr<Schema.SchemaClass<{
23
+ readonly licence: "cc0" | "public-domain" | "cc-by" | "cc-by-sa" | "own";
24
+ readonly author?: string | undefined;
25
+ readonly sourceUrl?: string | undefined;
26
+ readonly licenceUrl?: string | undefined;
27
+ }, {
28
+ readonly licence: "cc0" | "public-domain" | "cc-by" | "cc-by-sa" | "own";
29
+ readonly author?: string | undefined;
30
+ readonly sourceUrl?: string | undefined;
31
+ readonly licenceUrl?: string | undefined;
32
+ }, never>>>>;
28
33
  source: Schema.optional<Schema.transform<typeof Schema.Unknown, Schema.UndefinedOr<Schema.SchemaClass<{
29
34
  readonly url: string;
30
35
  readonly label?: string | undefined;
@@ -1,5 +1,5 @@
1
1
  import { Schema } from "effect";
2
- import { MediaCreditSchema, MediaTypeSchema, StoredQuestionSourceSchema, } from "../question.schema.js";
2
+ import { MediaTypeSchema, StoredMediaCreditSchema, StoredQuestionSourceSchema, } from "../question.schema.js";
3
3
  /** Schemas shared by more than one page-load body. */
4
4
  export const UserRoleSchema = Schema.Literal("anonymous", "display", "member", "curator", "admin");
5
5
  export const UserPowerSchema = Schema.Literal("can-access-mimic", "can-send-feedback", "can-import-quizzes", "can-manage-features", "can-add-questions", "can-manage-categories", "can-moderate-questions", "can-publish-questions", "is-power-user", "can-manage-avatars", "can-apply-for-curation", "can-1v1");
@@ -20,7 +20,7 @@ export const GameAnswerRecordSchema = Schema.Struct({
20
20
  correctAnswerText: Schema.optional(Schema.String),
21
21
  mediaUrl: Schema.optional(Schema.String),
22
22
  mediaType: Schema.optional(MediaTypeSchema),
23
- mediaCredit: Schema.optional(MediaCreditSchema),
23
+ mediaCredit: Schema.optional(StoredMediaCreditSchema),
24
24
  source: Schema.optional(StoredQuestionSourceSchema),
25
25
  postAnswerNote: Schema.optional(Schema.String),
26
26
  });
@@ -241,12 +241,17 @@ export declare const GameReviewPageSchema: Schema.Struct<{
241
241
  correctAnswerText: Schema.optional<typeof Schema.String>;
242
242
  mediaUrl: Schema.optional<typeof Schema.String>;
243
243
  mediaType: Schema.optional<Schema.Literal<["image", "audio", "video"]>>;
244
- mediaCredit: Schema.optional<Schema.filter<Schema.Struct<{
245
- licence: Schema.Literal<["cc0", "public-domain", "cc-by", "cc-by-sa", "own"]>;
246
- author: Schema.optional<Schema.filter<typeof Schema.String>>;
247
- sourceUrl: Schema.optional<Schema.filter<Schema.filter<typeof Schema.String>>>;
248
- licenceUrl: Schema.optional<Schema.filter<Schema.filter<typeof Schema.String>>>;
249
- }>>>;
244
+ mediaCredit: Schema.optional<Schema.transform<typeof Schema.Unknown, Schema.UndefinedOr<Schema.SchemaClass<{
245
+ readonly licence: "cc0" | "public-domain" | "cc-by" | "cc-by-sa" | "own";
246
+ readonly author?: string | undefined;
247
+ readonly sourceUrl?: string | undefined;
248
+ readonly licenceUrl?: string | undefined;
249
+ }, {
250
+ readonly licence: "cc0" | "public-domain" | "cc-by" | "cc-by-sa" | "own";
251
+ readonly author?: string | undefined;
252
+ readonly sourceUrl?: string | undefined;
253
+ readonly licenceUrl?: string | undefined;
254
+ }, never>>>>;
250
255
  source: Schema.optional<Schema.transform<typeof Schema.Unknown, Schema.UndefinedOr<Schema.SchemaClass<{
251
256
  readonly url: string;
252
257
  readonly label?: string | undefined;
@@ -302,12 +307,17 @@ export declare const SoloReviewPageSchema: Schema.Struct<{
302
307
  questionId: Schema.NullOr<typeof Schema.String>;
303
308
  mediaUrl: Schema.NullOr<typeof Schema.String>;
304
309
  mediaType: Schema.NullOr<Schema.Literal<["image", "audio", "video"]>>;
305
- mediaCredit: Schema.optional<Schema.NullOr<Schema.filter<Schema.Struct<{
306
- licence: Schema.Literal<["cc0", "public-domain", "cc-by", "cc-by-sa", "own"]>;
307
- author: Schema.optional<Schema.filter<typeof Schema.String>>;
308
- sourceUrl: Schema.optional<Schema.filter<Schema.filter<typeof Schema.String>>>;
309
- licenceUrl: Schema.optional<Schema.filter<Schema.filter<typeof Schema.String>>>;
310
- }>>>>;
310
+ mediaCredit: Schema.optional<Schema.Union<[typeof Schema.Null, Schema.transform<typeof Schema.Unknown, Schema.UndefinedOr<Schema.SchemaClass<{
311
+ readonly licence: "cc0" | "public-domain" | "cc-by" | "cc-by-sa" | "own";
312
+ readonly author?: string | undefined;
313
+ readonly sourceUrl?: string | undefined;
314
+ readonly licenceUrl?: string | undefined;
315
+ }, {
316
+ readonly licence: "cc0" | "public-domain" | "cc-by" | "cc-by-sa" | "own";
317
+ readonly author?: string | undefined;
318
+ readonly sourceUrl?: string | undefined;
319
+ readonly licenceUrl?: string | undefined;
320
+ }, never>>>]>>;
311
321
  source: Schema.Union<[typeof Schema.Null, Schema.transform<typeof Schema.Unknown, Schema.UndefinedOr<Schema.SchemaClass<{
312
322
  readonly url: string;
313
323
  readonly label?: string | undefined;
@@ -1,5 +1,5 @@
1
1
  import { Schema } from "effect";
2
- import { MediaCreditSchema, MediaTypeSchema, StoredQuestionSourceSchema, } from "../question.schema.js";
2
+ import { MediaTypeSchema, StoredMediaCreditSchema, StoredQuestionSourceSchema, } from "../question.schema.js";
3
3
  import { GameAnswerRecordSchema, PlayerRoleSchema, UserPowerSchema, UserRoleSchema, } from "./common.schema.js";
4
4
  /** Page-load response bodies for `src/api/routes/pages-history.ts and admin-feature-flags.ts`. */
5
5
  /** Achievement badge id (mirrors `BadgeId` in `src/lib/badges.ts`). */
@@ -208,7 +208,8 @@ const SoloRunReviewEntrySchema = Schema.Struct({
208
208
  mediaUrl: Schema.NullOr(Schema.String),
209
209
  mediaType: Schema.NullOr(MediaTypeSchema),
210
210
  // Optional: a review served before credits existed has no key.
211
- mediaCredit: Schema.optional(Schema.NullOr(MediaCreditSchema)),
211
+ // Null first: the stored-credit transform accepts any value, null included.
212
+ mediaCredit: Schema.optional(Schema.Union(Schema.Null, StoredMediaCreditSchema)),
212
213
  // Null first: the stored-source transform accepts any value, null included.
213
214
  source: Schema.Union(Schema.Null, StoredQuestionSourceSchema),
214
215
  postAnswerNote: Schema.NullOr(Schema.String),
@@ -92,6 +92,22 @@ export declare const QuizContextPageSchema: Schema.Struct<{
92
92
  src: typeof Schema.String;
93
93
  }>>>;
94
94
  emotesEnabled: typeof Schema.Boolean;
95
+ /** The emote gallery the setup picker offers and the play page draws from. */
96
+ emotePacks: Schema.mutable<Schema.Array$<Schema.Struct<{
97
+ id: typeof Schema.String;
98
+ title: typeof Schema.String;
99
+ description: typeof Schema.String;
100
+ isDefault: typeof Schema.Boolean;
101
+ emotes: Schema.mutable<Schema.Array$<Schema.Struct<{
102
+ id: typeof Schema.String;
103
+ title: typeof Schema.String;
104
+ description: typeof Schema.String;
105
+ src: typeof Schema.String;
106
+ stillSrc: typeof Schema.String;
107
+ }>>>;
108
+ }>>>;
109
+ /** The signed-in Player's saved emote set; absent for a guest. */
110
+ profileEmoteIds: Schema.optional<Schema.mutable<Schema.filter<Schema.Array$<Schema.filter<typeof Schema.String>>>>>;
95
111
  lobby: Schema.NullOr<Schema.Struct<{
96
112
  quizName: typeof Schema.String;
97
113
  description: typeof Schema.String;
@@ -104,6 +120,7 @@ export declare const QuizContextPageSchema: Schema.Struct<{
104
120
  id: typeof Schema.String;
105
121
  nickname: typeof Schema.String;
106
122
  avatar: typeof Schema.String;
123
+ emotes: Schema.mutable<Schema.Array$<typeof Schema.String>>;
107
124
  score: typeof Schema.Number;
108
125
  status: Schema.Literal<["pending", "active", "disconnected"]>;
109
126
  role: Schema.Literal<["player", "observer"]>;
@@ -135,12 +152,17 @@ export declare const QuizResultsPageSchema: Schema.Struct<{
135
152
  correctAnswerText: Schema.optional<typeof Schema.String>;
136
153
  mediaUrl: Schema.optional<typeof Schema.String>;
137
154
  mediaType: Schema.optional<Schema.Literal<["image", "audio", "video"]>>;
138
- mediaCredit: Schema.optional<Schema.filter<Schema.Struct<{
139
- licence: Schema.Literal<["cc0", "public-domain", "cc-by", "cc-by-sa", "own"]>;
140
- author: Schema.optional<Schema.filter<typeof Schema.String>>;
141
- sourceUrl: Schema.optional<Schema.filter<Schema.filter<typeof Schema.String>>>;
142
- licenceUrl: Schema.optional<Schema.filter<Schema.filter<typeof Schema.String>>>;
143
- }>>>;
155
+ mediaCredit: Schema.optional<Schema.transform<typeof Schema.Unknown, Schema.UndefinedOr<Schema.SchemaClass<{
156
+ readonly licence: "cc0" | "public-domain" | "cc-by" | "cc-by-sa" | "own";
157
+ readonly author?: string | undefined;
158
+ readonly sourceUrl?: string | undefined;
159
+ readonly licenceUrl?: string | undefined;
160
+ }, {
161
+ readonly licence: "cc0" | "public-domain" | "cc-by" | "cc-by-sa" | "own";
162
+ readonly author?: string | undefined;
163
+ readonly sourceUrl?: string | undefined;
164
+ readonly licenceUrl?: string | undefined;
165
+ }, never>>>>;
144
166
  source: Schema.optional<Schema.transform<typeof Schema.Unknown, Schema.UndefinedOr<Schema.SchemaClass<{
145
167
  readonly url: string;
146
168
  readonly label?: string | undefined;
@@ -1,4 +1,5 @@
1
1
  import { Schema } from "effect";
2
+ import { EmotePackSchema, EmoteSetSchema } from "../emotes.js";
2
3
  import { GAME } from "../game.settings.js";
3
4
  import { GameAnswerRecordSchema, PlayerRoleSchema, SkyPhaseSchema, ThemeModeSchema, UserPowerSchema, UserRoleSchema, } from "./common.schema.js";
4
5
  /** Page-load response bodies for `src/api/routes/layout.ts and quiz.ts`. */
@@ -93,6 +94,7 @@ const LobbySchema = Schema.Struct({
93
94
  id: Schema.String,
94
95
  nickname: Schema.String,
95
96
  avatar: Schema.String,
97
+ emotes: Schema.mutable(Schema.Array(Schema.String)),
96
98
  score: Schema.Number,
97
99
  status: PlayerStatusSchema,
98
100
  role: PlayerRoleSchema,
@@ -110,6 +112,10 @@ export const QuizContextPageSchema = Schema.Struct({
110
112
  profileAvatarId: Schema.String,
111
113
  avatars: Schema.mutable(Schema.Array(AvatarSchema)),
112
114
  emotesEnabled: Schema.Boolean,
115
+ /** The emote gallery the setup picker offers and the play page draws from. */
116
+ emotePacks: Schema.mutable(Schema.Array(EmotePackSchema)),
117
+ /** The signed-in Player's saved emote set; absent for a guest. */
118
+ profileEmoteIds: Schema.optional(EmoteSetSchema),
113
119
  lobby: Schema.NullOr(LobbySchema),
114
120
  // Replayed game WS frames, opaque here — applyStateMessages reads their
115
121
  // fields by message `type` on the client, so decoding preserves every key
@@ -24,6 +24,22 @@ export declare const MediaCreditSchema: Schema.filter<Schema.Struct<{
24
24
  licenceUrl: Schema.optional<Schema.filter<Schema.filter<typeof Schema.String>>>;
25
25
  }>>;
26
26
  export type MediaCredit = typeof MediaCreditSchema.Type;
27
+ /**
28
+ * Read-side credit: a stored value that fails `MediaCreditSchema` (a rule
29
+ * tightened after it was written, a wrong shape) decodes to undefined, so one
30
+ * bad credit never fails a whole page.
31
+ */
32
+ export declare const StoredMediaCreditSchema: Schema.transform<typeof Schema.Unknown, Schema.UndefinedOr<Schema.SchemaClass<{
33
+ readonly licence: "cc0" | "public-domain" | "cc-by" | "cc-by-sa" | "own";
34
+ readonly author?: string | undefined;
35
+ readonly sourceUrl?: string | undefined;
36
+ readonly licenceUrl?: string | undefined;
37
+ }, {
38
+ readonly licence: "cc0" | "public-domain" | "cc-by" | "cc-by-sa" | "own";
39
+ readonly author?: string | undefined;
40
+ readonly sourceUrl?: string | undefined;
41
+ readonly licenceUrl?: string | undefined;
42
+ }, never>>>;
27
43
  export declare const MediaSchema: Schema.Struct<{
28
44
  type: Schema.Literal<["image", "audio", "video"]>;
29
45
  url: typeof Schema.String;
@@ -57,6 +57,17 @@ export const MediaCreditSchema = Schema.Struct({
57
57
  }).pipe(Schema.filter((c) => !ATTRIBUTION_LICENCES.includes(c.licence) ||
58
58
  Boolean(c.author?.trim()) ||
59
59
  "Author is required for CC BY and CC BY-SA"));
60
+ const isMediaCredit = Schema.is(MediaCreditSchema);
61
+ /**
62
+ * Read-side credit: a stored value that fails `MediaCreditSchema` (a rule
63
+ * tightened after it was written, a wrong shape) decodes to undefined, so one
64
+ * bad credit never fails a whole page.
65
+ */
66
+ export const StoredMediaCreditSchema = Schema.transform(Schema.Unknown, Schema.UndefinedOr(Schema.typeSchema(MediaCreditSchema)), {
67
+ strict: true,
68
+ decode: (credit) => (isMediaCredit(credit) ? credit : undefined),
69
+ encode: (credit) => credit,
70
+ });
60
71
  export const MediaSchema = Schema.Struct({
61
72
  type: MediaTypeSchema,
62
73
  url: Schema.String,
@@ -8,7 +8,7 @@ export declare const ClientMessageSchema: Schema.Union<[Schema.Struct<{
8
8
  playerId: Schema.optional<Schema.filter<typeof Schema.NonEmptyString>>;
9
9
  }>, Schema.Struct<{
10
10
  type: Schema.Literal<["player:emote"]>;
11
- emoteId: Schema.Literal<["alert", "anger", "bars", "cash", "circle", "cloud", "cross", "dots1", "dots2", "dots3", "drop", "drops", "exclamation", "exclamations", "faceAngry", "faceHappy", "faceSad", "heart", "heartBroken", "hearts", "idea", "laugh", "music", "question", "sleep", "sleeps", "star", "stars", "swirl"]>;
11
+ emoteId: Schema.filter<typeof Schema.String>;
12
12
  offsetX: Schema.filter<Schema.filter<typeof Schema.Number>>;
13
13
  offsetY: Schema.filter<Schema.filter<typeof Schema.Number>>;
14
14
  }>, Schema.Struct<{
@@ -1,5 +1,5 @@
1
1
  import { Schema } from "effect";
2
- import { SELECTABLE_EMOTE_IDS } from "./emotes.js";
2
+ import { EmoteIdSchema } from "./emotes.js";
3
3
  import { GAME } from "./game.settings.js";
4
4
  import { DifficultySchema, MediaCreditSchema, MediaTypeSchema, } from "./question.schema.js";
5
5
  import { OptionalScoringModeSchema } from "./scoring.schema.js";
@@ -31,7 +31,8 @@ const PlayerAnswerMessageSchema = Schema.Struct({
31
31
  });
32
32
  const PlayerEmoteMessageSchema = Schema.Struct({
33
33
  type: Schema.Literal("player:emote"),
34
- emoteId: Schema.Literal(...SELECTABLE_EMOTE_IDS),
34
+ // Any well-formed id; the server refuses one outside the sender's set.
35
+ emoteId: EmoteIdSchema,
35
36
  // Broadcast to every player and used directly as CSS left%/top% (0-1) —
36
37
  // bounded, not just finite, or an attacker-controlled value corrupts the
37
38
  // shared emote overlay for the whole lobby.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orakl/protocol",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Orakl wire contract: WebSocket frames and API response schemas.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {