@orakl/shared 0.1.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.
Files changed (56) hide show
  1. package/LICENSE +662 -0
  2. package/dist/analytics.d.ts +21 -0
  3. package/dist/analytics.js +21 -0
  4. package/dist/avatar-helpers.d.ts +46 -0
  5. package/dist/avatar-helpers.js +69 -0
  6. package/dist/avatars.d.ts +6 -0
  7. package/dist/avatars.js +138 -0
  8. package/dist/badges.d.ts +126 -0
  9. package/dist/badges.js +667 -0
  10. package/dist/constants/auth.constants.d.ts +26 -0
  11. package/dist/constants/auth.constants.js +94 -0
  12. package/dist/constants/feature-flags.constants.d.ts +138 -0
  13. package/dist/constants/feature-flags.constants.js +119 -0
  14. package/dist/constants/font.constants.d.ts +9 -0
  15. package/dist/constants/font.constants.js +14 -0
  16. package/dist/constants/lobby.constants.d.ts +2 -0
  17. package/dist/constants/lobby.constants.js +438 -0
  18. package/dist/constants/patterns.constants.d.ts +4 -0
  19. package/dist/constants/patterns.constants.js +4 -0
  20. package/dist/constants/theme.constants.d.ts +24 -0
  21. package/dist/constants/theme.constants.js +69 -0
  22. package/dist/env.d.ts +3 -0
  23. package/dist/env.js +5 -0
  24. package/dist/font.d.ts +4 -0
  25. package/dist/font.js +8 -0
  26. package/dist/history-range.d.ts +13 -0
  27. package/dist/history-range.js +29 -0
  28. package/dist/index.d.ts +23 -0
  29. package/dist/index.js +26 -0
  30. package/dist/lobby.d.ts +16 -0
  31. package/dist/lobby.js +50 -0
  32. package/dist/manage-questions.d.ts +52 -0
  33. package/dist/manage-questions.js +26 -0
  34. package/dist/math.d.ts +27 -0
  35. package/dist/math.js +30 -0
  36. package/dist/murmurhash.d.ts +9 -0
  37. package/dist/murmurhash.js +52 -0
  38. package/dist/pricing.d.ts +32 -0
  39. package/dist/pricing.js +32 -0
  40. package/dist/profile-tabs.d.ts +8 -0
  41. package/dist/profile-tabs.js +18 -0
  42. package/dist/quiz-routes.d.ts +28 -0
  43. package/dist/quiz-routes.js +65 -0
  44. package/dist/sanitize.d.ts +38 -0
  45. package/dist/sanitize.js +109 -0
  46. package/dist/schemas/question.schema.d.ts +1 -0
  47. package/dist/schemas/question.schema.js +1 -0
  48. package/dist/solo-entitlements.d.ts +28 -0
  49. package/dist/solo-entitlements.js +56 -0
  50. package/dist/theme.d.ts +39 -0
  51. package/dist/theme.js +139 -0
  52. package/dist/types/auth.d.ts +143 -0
  53. package/dist/types/auth.js +28 -0
  54. package/dist/utils.d.ts +50 -0
  55. package/dist/utils.js +133 -0
  56. package/package.json +45 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Keyed by `UserPower`, so a name missing from it — or a typo in one — is a
3
+ * compile error rather than a power nothing reads. Two things live here per
4
+ * name, because both must exist for a power to be usable:
5
+ *
6
+ * - `shape` — whether the name stands at a gate or only steers the UI. Read by
7
+ * the copy guard in `tests/role-ladder.test.ts`, so a gating power cannot
8
+ * inherit the "reaches nothing" line by copy-paste.
9
+ * - `description` — what `/admin/users` prints under the toggle. Written for
10
+ * the admin deciding whether to grant it: what the holder gets, and any flag
11
+ * or role that applies on top. One or two short sentences.
12
+ */
13
+ const POWERS = {
14
+ "can-access-mimic": {
15
+ shape: "gate",
16
+ description: "Opens the Mimic fixture routes in production, under /mimic. They return 404 to everyone else.",
17
+ },
18
+ "can-send-feedback": {
19
+ shape: "gate",
20
+ description: "Shows the feedback widget on every page and accepts what it sends. The FEEDBACK_WIDGET flag applies on top.",
21
+ },
22
+ "can-import-quizzes": {
23
+ shape: "gate",
24
+ description: "Opens question import at /manage/import, and the duplicate check it runs. The ENABLE_IMPORTS flag applies on top.",
25
+ },
26
+ "can-manage-features": {
27
+ shape: "gate",
28
+ description: "Opens the feature-flag dashboard at /admin/feature-flags. The admin role is needed as well.",
29
+ },
30
+ "can-add-questions": {
31
+ shape: "gate",
32
+ description: "Opens question authoring at /curator/questions, and the image upload and inline category create behind it. The curator role is needed as well.",
33
+ },
34
+ "can-manage-categories": {
35
+ shape: "gate",
36
+ description: "Opens category management at /manage/categories.",
37
+ },
38
+ "can-moderate-questions": {
39
+ shape: "gate",
40
+ description: "Opens the review queue for player-reported questions, at /manage/flagged-questions.",
41
+ },
42
+ "can-publish-questions": {
43
+ shape: "gate",
44
+ description: "Opens the authored-questions dashboard at /manage/questions and promotes questions into the shared bank, where every curator's games draw them, and withdraws them again.",
45
+ },
46
+ "is-power-user": {
47
+ shape: "display",
48
+ description: "Hides every field-description info icon, for this account only. Reaches no page and no data.",
49
+ },
50
+ "can-manage-avatars": {
51
+ shape: "gate",
52
+ description: "Opens avatar management at /manage/avatars, and the avatar sync behind it.",
53
+ },
54
+ "can-apply-for-curation": {
55
+ shape: "gate",
56
+ description: "Opens the curator application on /profile and its endpoint. The member role is needed as well.",
57
+ },
58
+ "can-1v1": {
59
+ shape: "gate",
60
+ description: "Lets a curator start a game with fewer than two other players.",
61
+ },
62
+ };
63
+ /**
64
+ * The runtime list of `UserPower`, which is what `/admin/users` offers and what
65
+ * the request schema accepts. Derived from `POWERS`, so it cannot drift from
66
+ * the union. See `UserPower` for what each one reaches.
67
+ */
68
+ export const ALL_POWERS = Object.keys(POWERS);
69
+ export const powerShape = (power) => POWERS[power].shape;
70
+ /** One line for `/admin/users` to print under the toggle. */
71
+ export const powerDescription = (power) => POWERS[power].description;
72
+ /**
73
+ * Rank per role, for `hasRole`. `anonymous` and `display` share rank 0 —
74
+ * neither is an account, so every "is this signed in" gate compares against
75
+ * `member` rather than naming a role.
76
+ */
77
+ export const ROLE_HIERARCHY = {
78
+ anonymous: 0,
79
+ display: 0,
80
+ member: 1,
81
+ curator: 2,
82
+ admin: 3,
83
+ };
84
+ export const ANON_USER = {
85
+ id: "anon",
86
+ email: null,
87
+ nickname: null,
88
+ avatar: null,
89
+ role: "anonymous",
90
+ powers: [],
91
+ lobbyCode: null,
92
+ emailVerified: false,
93
+ sid: null,
94
+ };
@@ -0,0 +1,138 @@
1
+ import type { UserPower, UserRole } from "../types/auth.ts";
2
+ type FeatureFlagRule = {
3
+ percentageOfUsers?: number;
4
+ userRoles?: UserRole[];
5
+ } & ({
6
+ percentageOfUsers: number;
7
+ } | {
8
+ userRoles: UserRole[];
9
+ });
10
+ /**
11
+ * Which mechanism governs a flag (map #859, decision 1):
12
+ * - `build` — resolved at build time; disabled features leave the bundle
13
+ * - `db` — one live on/off for everyone, toggled from the admin dashboard
14
+ * - `role` — live on/off plus role targeting
15
+ *
16
+ * A `build` flag is XOR with this registry (decision 5): it lives as a
17
+ * `__FEATURE_*__` define in `build-flags.mjs`, never as an entry here, so the
18
+ * tier exists for the dashboard to render build flags as read-only status.
19
+ */
20
+ export type FlagTier = "build" | "db" | "role";
21
+ /**
22
+ * A flag's code-side description. The registry is the source of truth for which
23
+ * flags exist; the `feature_flags` table carries live overrides on top.
24
+ *
25
+ * `roles` pairs only with a boolean default — a rule array carries its own role
26
+ * restrictions, so the two forms are kept mutually exclusive.
27
+ */
28
+ /** The tiers a registry entry may carry — `build` is XOR with this registry. */
29
+ type RuntimeFlagTier = Exclude<FlagTier, "build">;
30
+ export type FlagDescriptor = {
31
+ tier: RuntimeFlagTier;
32
+ /** Code default. */
33
+ default: boolean;
34
+ description: string;
35
+ /** Roles the flag is limited to while its default is `true`. */
36
+ roles?: UserRole[];
37
+ /**
38
+ * A power this flag composes with (map #859, decision 3). `canUseFeature`
39
+ * requires both; the flag alone never grants the capability.
40
+ */
41
+ power?: UserPower;
42
+ } | {
43
+ tier: RuntimeFlagTier;
44
+ /**
45
+ * Code-only default: a rule list evaluated per user (percentage rollout
46
+ * and/or role restrictions, which live inside the rules). It answers per
47
+ * user rather than globally, so the dashboard renders it read-only —
48
+ * a database override would replace the rule, not adjust it. Change the
49
+ * rules here, or pin the whole flag with `FEATURE_FLAG_<NAME>`.
50
+ *
51
+ * No registered flag uses this form today; the evaluator and its tests
52
+ * stay in place for the next rollout that needs one.
53
+ */
54
+ default: FeatureFlagRule[];
55
+ description: string;
56
+ roles?: never;
57
+ /** As above — a code rule can compose with a power just the same. */
58
+ power?: UserPower;
59
+ };
60
+ /**
61
+ * Feature flag registry — source of truth for which flags exist, their tier,
62
+ * default, description and optional role targeting.
63
+ *
64
+ * Resolution order stays env → DB → code:
65
+ * set FEATURE_FLAG_<NAME>=true/false to override any flag per environment.
66
+ */
67
+ /** Shown by both import surfaces, so a copy edit cannot reach only one. */
68
+ export declare const IMPORTS_REFUSED = "Importing questions is not available to you.";
69
+ export declare const FLAG_REGISTRY: {
70
+ FEEDBACK_WIDGET: {
71
+ tier: "db";
72
+ default: false;
73
+ description: string;
74
+ };
75
+ ENABLE_IMPORTS: {
76
+ tier: "db";
77
+ default: true;
78
+ description: string;
79
+ power: "can-import-quizzes";
80
+ };
81
+ PLAYER_HISTORY: {
82
+ tier: "db";
83
+ default: false;
84
+ description: string;
85
+ };
86
+ CURATOR_HISTORY: {
87
+ tier: "db";
88
+ default: false;
89
+ description: string;
90
+ };
91
+ SOLO_LEADERBOARDS: {
92
+ tier: "db";
93
+ default: false;
94
+ description: string;
95
+ };
96
+ QUIZ_SCORING_MODES: {
97
+ tier: "db";
98
+ default: false;
99
+ description: string;
100
+ };
101
+ QUIZ_ACCESS_MODES: {
102
+ tier: "db";
103
+ default: false;
104
+ description: string;
105
+ };
106
+ QUIZ_MAX_PLAYERS: {
107
+ tier: "db";
108
+ default: false;
109
+ description: string;
110
+ };
111
+ QUIZ_PRESETS: {
112
+ tier: "db";
113
+ default: false;
114
+ description: string;
115
+ };
116
+ IMAGE_QUIZ_CATEGORY: {
117
+ tier: "db";
118
+ default: false;
119
+ description: string;
120
+ };
121
+ BADGE_DISPLAY: {
122
+ tier: "db";
123
+ default: false;
124
+ description: string;
125
+ };
126
+ ROLE_SELECTION: {
127
+ tier: "db";
128
+ default: false;
129
+ description: string;
130
+ };
131
+ };
132
+ export type FeatureFlagName = keyof typeof FLAG_REGISTRY;
133
+ /**
134
+ * Code defaults keyed by flag name, derived from the registry. Kept as the
135
+ * shape `evaluateFeatureFlag` consumes.
136
+ */
137
+ export declare const FEATURE_FLAGS: { [K in FeatureFlagName]: (typeof FLAG_REGISTRY)[K]["default"]; };
138
+ export {};
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Feature flag registry — source of truth for which flags exist, their tier,
3
+ * default, description and optional role targeting.
4
+ *
5
+ * Resolution order stays env → DB → code:
6
+ * set FEATURE_FLAG_<NAME>=true/false to override any flag per environment.
7
+ */
8
+ /** Shown by both import surfaces, so a copy edit cannot reach only one. */
9
+ export const IMPORTS_REFUSED = "Importing questions is not available to you.";
10
+ export const FLAG_REGISTRY = {
11
+ FEEDBACK_WIDGET: {
12
+ tier: "db",
13
+ default: false,
14
+ description: "Click-to-annotate feedback widget (local dev + PR previews)",
15
+ },
16
+ // Curator question import (map #859, decision 3). Composed with the
17
+ // `can-import-quizzes` power — the flag is the global switch, the power says
18
+ // who. Defaults on so the cutover takes nobody's access away. Turning it off
19
+ // stops imports for everyone, power-holders included, but takes up to the
20
+ // 5-minute resolution TTL to reach every process (decision 4) — it is not an
21
+ // instant kill switch.
22
+ ENABLE_IMPORTS: {
23
+ tier: "db",
24
+ default: true,
25
+ description: "Curator question import",
26
+ power: "can-import-quizzes",
27
+ },
28
+ // Player history (map #859, decision 18). `CONTEXT.md` names Player history
29
+ // and Game history as separate things, so they carry separate flags. Off
30
+ // hides every route into a player's own record and refuses the pages
31
+ // themselves; the runs, stats and badges behind them are untouched, and
32
+ // account export still returns them — export is a data right, not a feature
33
+ // (decision 19).
34
+ PLAYER_HISTORY: {
35
+ tier: "db",
36
+ default: false,
37
+ description: "Player history — a player's own runs, stats and badges",
38
+ },
39
+ // Game history (map #859, decision 18) — the curator's record of nights they
40
+ // ran. Off hides the History link and refuses both the pages and the
41
+ // endpoints behind them. The second save-as-preset entry lives on a session
42
+ // page and goes with it; the create form keeps its own (decision 23).
43
+ CURATOR_HISTORY: {
44
+ tier: "db",
45
+ default: false,
46
+ description: "Game history — a curator's record of the nights they ran",
47
+ },
48
+ // The persistent solo boards (map #859, decision 22): the rankings page, its
49
+ // endpoint and the share card. The end-of-game ceremony standings are how a
50
+ // Game ends rather than a leaderboard feature, and are not gated here.
51
+ SOLO_LEADERBOARDS: {
52
+ tier: "db",
53
+ default: false,
54
+ description: "Oracle rankings — the persistent solo leaderboards",
55
+ },
56
+ // The four curator create-form options (map #859, decisions 13, 16, 21).
57
+ // Each removes one control from the form and pins its value on the server:
58
+ // the form is reachable without the page, so the hidden control is never
59
+ // the guard. A value submitted for a gated field is ignored rather than
60
+ // refused — the curator was not offered the choice, so there is no mistake
61
+ // to report back.
62
+ QUIZ_SCORING_MODES: {
63
+ tier: "db",
64
+ default: false,
65
+ description: "Scoring picker on the create form; off pins classic scoring",
66
+ },
67
+ QUIZ_ACCESS_MODES: {
68
+ tier: "db",
69
+ default: false,
70
+ description: "Access picker on the create form; off pins every lobby open",
71
+ },
72
+ QUIZ_MAX_PLAYERS: {
73
+ tier: "db",
74
+ default: false,
75
+ description: "Max-players field on the create form; off leaves lobbies uncapped",
76
+ },
77
+ QUIZ_PRESETS: {
78
+ tier: "db",
79
+ default: false,
80
+ description: "Saved quizzes — save, list and apply. Stored rows survive while off",
81
+ },
82
+ // Interim release measure (map #859, decision 10): hides the seeded
83
+ // "Image Quiz" category from the surfaces that pick a category to play or
84
+ // author into, while the image-upload model redesign waits in Fog. Defaults
85
+ // off — hiding it is the point — and never touches the question data, so
86
+ // turning it back on restores the category as it was.
87
+ IMAGE_QUIZ_CATEGORY: {
88
+ tier: "db",
89
+ default: false,
90
+ description: "Image Quiz category in category pickers",
91
+ },
92
+ // Badge display (map #928, decision 1). Off hides badges on every surface —
93
+ // multiplayer and solo results Accolades, game and solo history detail, the
94
+ // aggregate tally — while computation and persistence stay untouched.
95
+ BADGE_DISPLAY: {
96
+ tier: "db",
97
+ default: false,
98
+ description: "Badge display — Accolades on results and badges in history",
99
+ },
100
+ // Role selection (map #928, decisions 2, 8, 9). Off hides the setup-page
101
+ // role picker (every joiner is a player), the mid-tournament roles page and
102
+ // its "New quiz" trigger (the curator returns players to the lobby
103
+ // directly instead), and the observer tiles/badges on results and the
104
+ // curator display. Server-side observer answer/emote refusals stay live
105
+ // regardless.
106
+ ROLE_SELECTION: {
107
+ tier: "db",
108
+ default: false,
109
+ description: "Role selection — the observer/player picker between games",
110
+ },
111
+ };
112
+ /**
113
+ * Code defaults keyed by flag name, derived from the registry. Kept as the
114
+ * shape `evaluateFeatureFlag` consumes.
115
+ */
116
+ export const FEATURE_FLAGS = Object.fromEntries(Object.entries(FLAG_REGISTRY).map(([name, { default: value }]) => [
117
+ name,
118
+ value,
119
+ ]));
@@ -0,0 +1,9 @@
1
+ import type { FontPreference } from "../font.ts";
2
+ export declare const FONT_COOKIE = "orakl-font";
3
+ export declare const DEFAULT_FONT: FontPreference;
4
+ /** All selectable typeface preferences. */
5
+ export declare const FONT_PREFERENCES: FontPreference[];
6
+ /** Human-readable labels. */
7
+ export declare const FONT_LABELS: Record<FontPreference, string>;
8
+ /** Short descriptions for the picker UI. */
9
+ export declare const FONT_DESCRIPTIONS: Record<FontPreference, string>;
@@ -0,0 +1,14 @@
1
+ export const FONT_COOKIE = "orakl-font";
2
+ export const DEFAULT_FONT = "default";
3
+ /** All selectable typeface preferences. */
4
+ export const FONT_PREFERENCES = ["default", "dyslexic"];
5
+ /** Human-readable labels. */
6
+ export const FONT_LABELS = {
7
+ default: "Default",
8
+ dyslexic: "Dyslexia-friendly",
9
+ };
10
+ /** Short descriptions for the picker UI. */
11
+ export const FONT_DESCRIPTIONS = {
12
+ default: "Albertus Nova and Geist",
13
+ dyslexic: "OpenDyslexic, weighted to anchor letters",
14
+ };
@@ -0,0 +1,2 @@
1
+ export declare const ADJECTIVES: string[];
2
+ export declare const NOUNS: string[];