@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.
- package/LICENSE +662 -0
- package/dist/analytics.d.ts +21 -0
- package/dist/analytics.js +21 -0
- package/dist/avatar-helpers.d.ts +46 -0
- package/dist/avatar-helpers.js +69 -0
- package/dist/avatars.d.ts +6 -0
- package/dist/avatars.js +138 -0
- package/dist/badges.d.ts +126 -0
- package/dist/badges.js +667 -0
- package/dist/constants/auth.constants.d.ts +26 -0
- package/dist/constants/auth.constants.js +94 -0
- package/dist/constants/feature-flags.constants.d.ts +138 -0
- package/dist/constants/feature-flags.constants.js +119 -0
- package/dist/constants/font.constants.d.ts +9 -0
- package/dist/constants/font.constants.js +14 -0
- package/dist/constants/lobby.constants.d.ts +2 -0
- package/dist/constants/lobby.constants.js +438 -0
- package/dist/constants/patterns.constants.d.ts +4 -0
- package/dist/constants/patterns.constants.js +4 -0
- package/dist/constants/theme.constants.d.ts +24 -0
- package/dist/constants/theme.constants.js +69 -0
- package/dist/env.d.ts +3 -0
- package/dist/env.js +5 -0
- package/dist/font.d.ts +4 -0
- package/dist/font.js +8 -0
- package/dist/history-range.d.ts +13 -0
- package/dist/history-range.js +29 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +26 -0
- package/dist/lobby.d.ts +16 -0
- package/dist/lobby.js +50 -0
- package/dist/manage-questions.d.ts +52 -0
- package/dist/manage-questions.js +26 -0
- package/dist/math.d.ts +27 -0
- package/dist/math.js +30 -0
- package/dist/murmurhash.d.ts +9 -0
- package/dist/murmurhash.js +52 -0
- package/dist/pricing.d.ts +32 -0
- package/dist/pricing.js +32 -0
- package/dist/profile-tabs.d.ts +8 -0
- package/dist/profile-tabs.js +18 -0
- package/dist/quiz-routes.d.ts +28 -0
- package/dist/quiz-routes.js +65 -0
- package/dist/sanitize.d.ts +38 -0
- package/dist/sanitize.js +109 -0
- package/dist/schemas/question.schema.d.ts +1 -0
- package/dist/schemas/question.schema.js +1 -0
- package/dist/solo-entitlements.d.ts +28 -0
- package/dist/solo-entitlements.js +56 -0
- package/dist/theme.d.ts +39 -0
- package/dist/theme.js +139 -0
- package/dist/types/auth.d.ts +143 -0
- package/dist/types/auth.js +28 -0
- package/dist/utils.d.ts +50 -0
- package/dist/utils.js +133 -0
- 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
|
+
};
|