cauthy-auth 0.0.0-stage → 0.2.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/react.mjs ADDED
@@ -0,0 +1,98 @@
1
+ // src/react.tsx
2
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
3
+
4
+ // src/permissions.ts
5
+ function matches(entry, resource, action) {
6
+ const i = entry.lastIndexOf(":");
7
+ if (i < 0) return false;
8
+ const r = entry.slice(0, i);
9
+ const a = entry.slice(i + 1);
10
+ return (r === "*" || r === resource) && (a === "*" || a === action);
11
+ }
12
+ function can(organization, resource, action) {
13
+ if (!organization?.permissions) return false;
14
+ if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
15
+ return organization.permissions.some((p) => matches(p, resource, action));
16
+ }
17
+
18
+ // src/react.tsx
19
+ import { jsx } from "react/jsx-runtime";
20
+ var Ctx = createContext(null);
21
+ var AHEAD_MS = 6e4;
22
+ function CauthyProvider({ children, basePath = "/api/auth", initialSession }) {
23
+ const base = basePath.replace(/\/+$/, "");
24
+ const [session, setSession] = useState(initialSession ?? null);
25
+ const [status, setStatus] = useState(
26
+ initialSession === void 0 ? "loading" : initialSession === null ? "unauthenticated" : "authenticated"
27
+ );
28
+ const asking = useRef(null);
29
+ const refresh = useCallback(() => {
30
+ asking.current ??= (async () => {
31
+ try {
32
+ const res = await fetch(`${base}/session`, { credentials: "same-origin", headers: { accept: "application/json" } });
33
+ if (!res.ok) throw new Error(String(res.status));
34
+ const body = await res.json();
35
+ if (body.status !== "expired") {
36
+ setSession(body.session);
37
+ setStatus(body.session ? "authenticated" : "unauthenticated");
38
+ }
39
+ return body.session;
40
+ } catch {
41
+ setStatus((s) => s === "loading" ? "unauthenticated" : s);
42
+ return null;
43
+ } finally {
44
+ asking.current = null;
45
+ }
46
+ })();
47
+ return asking.current;
48
+ }, [base]);
49
+ useEffect(() => {
50
+ if (initialSession === void 0) void refresh();
51
+ }, [initialSession, refresh]);
52
+ useEffect(() => {
53
+ if (!session) return;
54
+ const wait = Math.max(new Date(session.expiresAt).getTime() - Date.now() - AHEAD_MS, 5e3);
55
+ const timer = setTimeout(() => void refresh(), wait);
56
+ return () => clearTimeout(timer);
57
+ }, [session, refresh]);
58
+ useEffect(() => {
59
+ const onVisible = () => {
60
+ if (document.visibilityState === "visible") void refresh();
61
+ };
62
+ document.addEventListener("visibilitychange", onVisible);
63
+ return () => document.removeEventListener("visibilitychange", onVisible);
64
+ }, [refresh]);
65
+ const value = useMemo(
66
+ () => ({
67
+ session,
68
+ status,
69
+ refresh,
70
+ signIn(options = {}) {
71
+ const q = new URLSearchParams({ return_to: options.returnTo ?? window.location.pathname + window.location.search });
72
+ if (options.locale) q.set("locale", options.locale);
73
+ if (options.prompt) q.set("prompt", options.prompt);
74
+ window.location.assign(`${base}/login?${q}`);
75
+ },
76
+ signOut() {
77
+ window.location.assign(`${base}/logout`);
78
+ },
79
+ accountUrl(returnTo) {
80
+ const back = returnTo ?? (typeof window === "undefined" ? "/" : window.location.pathname + window.location.search);
81
+ return `${base}/account?return_to=${encodeURIComponent(back)}`;
82
+ }
83
+ }),
84
+ [session, status, refresh, base]
85
+ );
86
+ return /* @__PURE__ */ jsx(Ctx.Provider, { value, children });
87
+ }
88
+ function useSession() {
89
+ const ctx = useContext(Ctx);
90
+ if (ctx === null) throw new Error("cauthy-auth: useSession needs <CauthyProvider> above it");
91
+ return ctx;
92
+ }
93
+ export {
94
+ CauthyProvider,
95
+ can,
96
+ useSession
97
+ };
98
+ //# sourceMappingURL=react.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react.tsx","../src/permissions.ts"],"sourcesContent":["// React: who is signed in, in the browser.\n//\n// <CauthyProvider> // once, at the root\n// <App />\n// </CauthyProvider>\n//\n// const { session, status, signIn, signOut } = useSession()\n//\n// The browser never holds a token: it asks the application's own server\n// (`/api/auth/session`), which answers who is signed in and their\n// organizations. The session is kept current by asking again a little before\n// it expires and when the window comes back into view.\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\nimport type { PublicSession } from './types'\n\nexport type { JsonValue, PublicSession, SessionOrganization, SessionUser } from './types'\nexport { can } from './permissions'\n\nexport interface SessionContext {\n /** The signed-in person and their organizations, or null. */\n session: PublicSession | null\n status: 'loading' | 'authenticated' | 'unauthenticated'\n /** Sends the browser to sign in, and back to `returnTo` (default: where it is now). */\n signIn(options?: { returnTo?: string; locale?: string; prompt?: 'login' }): void\n /** Signs out of this application and of Cauthy in this browser. */\n signOut(): void\n /** Asks the server again who is signed in. */\n refresh(): Promise<PublicSession | null>\n /** Where the person looks after their account: password, second step, passkeys. */\n accountUrl(returnTo?: string): string\n}\n\nexport interface CauthyProviderProps {\n children: ReactNode\n /** Where the server mounted the routes. Default: `/api/auth`. */\n basePath?: string\n /**\n * The session the server already read while rendering (SSR): the first\n * paint is then right, with no \"loading\". `null` means \"nobody\"; leave it\n * out when the server did not look.\n */\n initialSession?: PublicSession | null\n}\n\nconst Ctx = createContext<SessionContext | null>(null)\n\n/** Ask again this long before the session's token runs out. */\nconst AHEAD_MS = 60_000\n\nexport function CauthyProvider({ children, basePath = '/api/auth', initialSession }: CauthyProviderProps) {\n const base = basePath.replace(/\\/+$/, '')\n const [session, setSession] = useState<PublicSession | null>(initialSession ?? null)\n const [status, setStatus] = useState<SessionContext['status']>(\n initialSession === undefined ? 'loading' : initialSession === null ? 'unauthenticated' : 'authenticated',\n )\n const asking = useRef<Promise<PublicSession | null> | null>(null)\n\n const refresh = useCallback((): Promise<PublicSession | null> => {\n asking.current ??= (async () => {\n try {\n const res = await fetch(`${base}/session`, { credentials: 'same-origin', headers: { accept: 'application/json' } })\n if (!res.ok) throw new Error(String(res.status))\n const body = (await res.json()) as { session: PublicSession | null; status: string }\n // \"expired\" is the server unable to reach Cauthy: what is on screen\n // stays, and the next ask settles it.\n if (body.status !== 'expired') {\n setSession(body.session)\n setStatus(body.session ? 'authenticated' : 'unauthenticated')\n }\n return body.session\n } catch {\n setStatus((s) => (s === 'loading' ? 'unauthenticated' : s))\n return null\n } finally {\n asking.current = null\n }\n })()\n return asking.current\n }, [base])\n\n useEffect(() => {\n if (initialSession === undefined) void refresh()\n }, [initialSession, refresh])\n\n useEffect(() => {\n if (!session) return\n const wait = Math.max(new Date(session.expiresAt).getTime() - Date.now() - AHEAD_MS, 5_000)\n const timer = setTimeout(() => void refresh(), wait)\n return () => clearTimeout(timer)\n }, [session, refresh])\n\n useEffect(() => {\n const onVisible = () => {\n if (document.visibilityState === 'visible') void refresh()\n }\n document.addEventListener('visibilitychange', onVisible)\n return () => document.removeEventListener('visibilitychange', onVisible)\n }, [refresh])\n\n const value = useMemo<SessionContext>(\n () => ({\n session,\n status,\n refresh,\n signIn(options = {}) {\n const q = new URLSearchParams({ return_to: options.returnTo ?? window.location.pathname + window.location.search })\n if (options.locale) q.set('locale', options.locale)\n if (options.prompt) q.set('prompt', options.prompt)\n window.location.assign(`${base}/login?${q}`)\n },\n signOut() {\n window.location.assign(`${base}/logout`)\n },\n accountUrl(returnTo) {\n const back = returnTo ?? (typeof window === 'undefined' ? '/' : window.location.pathname + window.location.search)\n return `${base}/account?return_to=${encodeURIComponent(back)}`\n },\n }),\n [session, status, refresh, base],\n )\n return <Ctx.Provider value={value}>{children}</Ctx.Provider>\n}\n\nexport function useSession(): SessionContext {\n const ctx = useContext(Ctx)\n if (ctx === null) throw new Error('cauthy-auth: useSession needs <CauthyProvider> above it')\n return ctx\n}\n","// What a person may do in an organization.\n//\n// When a system keeps its catalog of roles in Cauthy, each of a person's\n// organizations arrives with what their role — and anything given or denied to\n// them apart from it — lets them do. Checking is a lookup in that list: no\n// call to Cauthy, on the server or in the browser.\n\n/** The part of an organization that says what its member may do. */\nexport interface Permitted {\n /** \"resource:action\" entries the person is allowed; \"*\" is any. Absent when the system keeps no catalog in Cauthy. */\n permissions?: string[]\n /** Entries the person is denied whatever else allows them. */\n denied?: string[]\n}\n\nfunction matches(entry: string, resource: string, action: string): boolean {\n // The resource may itself hold a colon: the action is what follows the last one.\n const i = entry.lastIndexOf(':')\n if (i < 0) return false\n const r = entry.slice(0, i)\n const a = entry.slice(i + 1)\n return (r === '*' || r === resource) && (a === '*' || a === action)\n}\n\n/**\n * Whether the person may do an action on a resource in an organization:\n * allowed by some entry, and denied by none. A denial wins.\n *\n * An organization of a system that keeps no catalog in Cauthy carries no\n * permissions, and the answer is `false`: decide by its `role` instead.\n *\n * if (!can(organization, 'invoicing', 'write')) return forbidden()\n */\nexport function can(organization: Permitted | null | undefined, resource: string, action: string): boolean {\n if (!organization?.permissions) return false\n if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false\n return organization.permissions.some((p) => matches(p, resource, action))\n}\n\n// ── A system's catalog of roles ────────────────────────────────────────────\n\n/** Something a role may do: an action on a resource, in organizations of a kind (\"*\", the default, is every kind). */\nexport interface PolicyPermission {\n kind?: string\n resource: string\n action: string\n}\n\nexport interface PolicyRole {\n role: string\n name?: string\n description?: string\n /** Roles whose permissions this one has as well. */\n inherits?: string[]\n permissions?: PolicyPermission[]\n}\n\nexport interface PushPolicyOptions {\n /** The system whose catalog this is. Default: `CAUTHY_SYSTEM`. */\n system?: string\n roles: PolicyRole[]\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The workspace's slug. Default: `CAUTHY_TENANT`. */\n tenant?: string\n /** A personal access token with `organizations:manage`. Default: `CAUTHY_TOKEN`. */\n token?: string\n fetch?: typeof fetch\n}\n\n/**\n * Gives Cauthy a system's catalog of roles, replacing the one it has. It is\n * the way to load a first catalog from what a system already had; from then on\n * the catalog is Cauthy's, edited in its dashboard, and pushing again would\n * overwrite what was edited there.\n */\nexport async function pushPolicy(o: PushPolicyOptions): Promise<void> {\n const env = (n: string) => (typeof process !== 'undefined' ? process.env?.[n] : undefined)\n const issuer = (o.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n const system = o.system ?? env('CAUTHY_SYSTEM') ?? ''\n const tenant = o.tenant ?? env('CAUTHY_TENANT') ?? ''\n const token = o.token ?? env('CAUTHY_TOKEN') ?? ''\n for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {\n if (v === '') throw new Error(`cauthy-auth: ${name} is required to push a policy`)\n }\n const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {\n method: 'PUT',\n headers: { authorization: `Bearer ${token}`, 'x-tenant-key': tenant, 'content-type': 'application/json' },\n body: JSON.stringify({ roles: o.roles }),\n })\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as { errors?: { message?: string }[] } | null\n throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`)\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,82 @@
1
+ /** How an application looks on Cauthy's hosted pages. Every value is optional. */
2
+ interface CauthyTheme {
3
+ /** The page. Colours are `#rrggbb` or `rgba(r, g, b, a)`. */
4
+ background?: string;
5
+ /** The card, and inputs. */
6
+ surface?: string;
7
+ /** Headings and what is typed. */
8
+ text?: string;
9
+ /** Paragraphs, labels, small print. */
10
+ textMuted?: string;
11
+ border?: string;
12
+ /** The main button, and what is written on it. */
13
+ primary?: string;
14
+ primaryText?: string;
15
+ /** Links, and the focus ring. */
16
+ accent?: string;
17
+ error?: string;
18
+ errorBackground?: string;
19
+ success?: string;
20
+ successBackground?: string;
21
+ /** Corners of cards and inputs, in pixels (0–40). */
22
+ radius?: number;
23
+ /** Corners of buttons, in pixels (0–40). */
24
+ buttonRadius?: number;
25
+ /** A CSS font-family list. Cauthy loads no font files: put a system font after the application's own. */
26
+ fontFamily?: string;
27
+ /** An image shown where "Cauthy" is, over https. */
28
+ logoUrl?: string;
29
+ }
30
+ interface StackmythThemeOptions {
31
+ /**
32
+ * The named theme to read, when the stylesheet keeps several under
33
+ * `[data-theme=…]` (`"vennet"`). Without it only what applies to every
34
+ * page (`:root`) is read.
35
+ */
36
+ theme?: string;
37
+ /** Which of the application's two modes to take. Default: `light`. */
38
+ mode?: 'light' | 'dark';
39
+ logoUrl?: string;
40
+ }
41
+ interface ImportedTheme {
42
+ theme: CauthyTheme;
43
+ /** Tokens that were there and could not be carried over, and why. */
44
+ warnings: string[];
45
+ }
46
+ /** A colour as Cauthy takes it — `#rrggbb` or `rgba(…)` — or null when it is none it can read. */
47
+ declare function toColor(value: string): string | null;
48
+ /**
49
+ * Reads an application's Stackmyth theme out of its stylesheets.
50
+ *
51
+ * Give it the CSS that declares the tokens, in the order the application
52
+ * loads it — Stackmyth's own (`@stackmyth/core/core.vars.css`, or a theme
53
+ * file) and then the application's brand file that re-points them. Tokens
54
+ * that point at the application's own variables are followed. The
55
+ * application's dark mode is left out unless asked for.
56
+ */
57
+ declare function themeFromStackmyth(css: string | string[], options?: StackmythThemeOptions): ImportedTheme;
58
+ /** A theme as Cauthy's API names its values. */
59
+ declare function toWire(theme: CauthyTheme): Record<string, string | number>;
60
+ interface PushThemeOptions {
61
+ /** Cauthy's address. Default: `CAUTHY_ISSUER`. */
62
+ issuer?: string;
63
+ /** The application's client id. Default: `CAUTHY_CLIENT_ID`. */
64
+ clientId?: string;
65
+ /** The workspace's slug. Default: `CAUTHY_TENANT`. */
66
+ tenant?: string;
67
+ /**
68
+ * A personal access token with `oauth_clients:manage`. Default:
69
+ * `CAUTHY_TOKEN`. It belongs in the deploy pipeline, never in the application.
70
+ */
71
+ token?: string;
72
+ theme: CauthyTheme;
73
+ fetch?: typeof fetch;
74
+ }
75
+ /**
76
+ * Gives the application its look on Cauthy's pages. It replaces the theme: a
77
+ * value left out goes back to Cauthy's own. Run it when the brand changes —
78
+ * from a script or the deploy — not on every request.
79
+ */
80
+ declare function pushTheme(o: PushThemeOptions): Promise<void>;
81
+
82
+ export { type CauthyTheme, type ImportedTheme, type PushThemeOptions, type StackmythThemeOptions, pushTheme, themeFromStackmyth, toColor, toWire };
@@ -0,0 +1,82 @@
1
+ /** How an application looks on Cauthy's hosted pages. Every value is optional. */
2
+ interface CauthyTheme {
3
+ /** The page. Colours are `#rrggbb` or `rgba(r, g, b, a)`. */
4
+ background?: string;
5
+ /** The card, and inputs. */
6
+ surface?: string;
7
+ /** Headings and what is typed. */
8
+ text?: string;
9
+ /** Paragraphs, labels, small print. */
10
+ textMuted?: string;
11
+ border?: string;
12
+ /** The main button, and what is written on it. */
13
+ primary?: string;
14
+ primaryText?: string;
15
+ /** Links, and the focus ring. */
16
+ accent?: string;
17
+ error?: string;
18
+ errorBackground?: string;
19
+ success?: string;
20
+ successBackground?: string;
21
+ /** Corners of cards and inputs, in pixels (0–40). */
22
+ radius?: number;
23
+ /** Corners of buttons, in pixels (0–40). */
24
+ buttonRadius?: number;
25
+ /** A CSS font-family list. Cauthy loads no font files: put a system font after the application's own. */
26
+ fontFamily?: string;
27
+ /** An image shown where "Cauthy" is, over https. */
28
+ logoUrl?: string;
29
+ }
30
+ interface StackmythThemeOptions {
31
+ /**
32
+ * The named theme to read, when the stylesheet keeps several under
33
+ * `[data-theme=…]` (`"vennet"`). Without it only what applies to every
34
+ * page (`:root`) is read.
35
+ */
36
+ theme?: string;
37
+ /** Which of the application's two modes to take. Default: `light`. */
38
+ mode?: 'light' | 'dark';
39
+ logoUrl?: string;
40
+ }
41
+ interface ImportedTheme {
42
+ theme: CauthyTheme;
43
+ /** Tokens that were there and could not be carried over, and why. */
44
+ warnings: string[];
45
+ }
46
+ /** A colour as Cauthy takes it — `#rrggbb` or `rgba(…)` — or null when it is none it can read. */
47
+ declare function toColor(value: string): string | null;
48
+ /**
49
+ * Reads an application's Stackmyth theme out of its stylesheets.
50
+ *
51
+ * Give it the CSS that declares the tokens, in the order the application
52
+ * loads it — Stackmyth's own (`@stackmyth/core/core.vars.css`, or a theme
53
+ * file) and then the application's brand file that re-points them. Tokens
54
+ * that point at the application's own variables are followed. The
55
+ * application's dark mode is left out unless asked for.
56
+ */
57
+ declare function themeFromStackmyth(css: string | string[], options?: StackmythThemeOptions): ImportedTheme;
58
+ /** A theme as Cauthy's API names its values. */
59
+ declare function toWire(theme: CauthyTheme): Record<string, string | number>;
60
+ interface PushThemeOptions {
61
+ /** Cauthy's address. Default: `CAUTHY_ISSUER`. */
62
+ issuer?: string;
63
+ /** The application's client id. Default: `CAUTHY_CLIENT_ID`. */
64
+ clientId?: string;
65
+ /** The workspace's slug. Default: `CAUTHY_TENANT`. */
66
+ tenant?: string;
67
+ /**
68
+ * A personal access token with `oauth_clients:manage`. Default:
69
+ * `CAUTHY_TOKEN`. It belongs in the deploy pipeline, never in the application.
70
+ */
71
+ token?: string;
72
+ theme: CauthyTheme;
73
+ fetch?: typeof fetch;
74
+ }
75
+ /**
76
+ * Gives the application its look on Cauthy's pages. It replaces the theme: a
77
+ * value left out goes back to Cauthy's own. Run it when the brand changes —
78
+ * from a script or the deploy — not on every request.
79
+ */
80
+ declare function pushTheme(o: PushThemeOptions): Promise<void>;
81
+
82
+ export { type CauthyTheme, type ImportedTheme, type PushThemeOptions, type StackmythThemeOptions, pushTheme, themeFromStackmyth, toColor, toWire };
package/dist/theme.js ADDED
@@ -0,0 +1,254 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/theme.ts
21
+ var theme_exports = {};
22
+ __export(theme_exports, {
23
+ pushTheme: () => pushTheme,
24
+ themeFromStackmyth: () => themeFromStackmyth,
25
+ toColor: () => toColor,
26
+ toWire: () => toWire
27
+ });
28
+ module.exports = __toCommonJS(theme_exports);
29
+ var MAX_RADIUS = 40;
30
+ function declarations(css) {
31
+ const text = css.replace(/\/\*[\s\S]*?\*\//g, "");
32
+ const out2 = [];
33
+ const within = [];
34
+ let chunk = "";
35
+ let quote = "";
36
+ let depth = 0;
37
+ const flush = () => {
38
+ const m = /^\s*(--[\w-]+)\s*:\s*([\s\S]+?)\s*$/.exec(chunk);
39
+ if (m) out2.push({ name: m[1], value: m[2].replace(/\s+/g, " ").replace(/\s*!important$/, ""), within: [...within] });
40
+ chunk = "";
41
+ };
42
+ for (const ch of text) {
43
+ if (quote) {
44
+ chunk += ch;
45
+ if (ch === quote) quote = "";
46
+ continue;
47
+ }
48
+ if (ch === '"' || ch === "'") {
49
+ quote = ch;
50
+ chunk += ch;
51
+ } else if (ch === "(") {
52
+ depth++;
53
+ chunk += ch;
54
+ } else if (ch === ")") {
55
+ depth = Math.max(0, depth - 1);
56
+ chunk += ch;
57
+ } else if (ch === "{" && depth === 0) {
58
+ within.push(chunk.trim().replace(/\s+/g, " "));
59
+ chunk = "";
60
+ } else if (ch === "}" && depth === 0) {
61
+ flush();
62
+ within.pop();
63
+ } else if (ch === ";" && depth === 0) {
64
+ flush();
65
+ } else {
66
+ chunk += ch;
67
+ }
68
+ }
69
+ return out2;
70
+ }
71
+ var DARK = /prefers-color-scheme:\s*dark|data-(mode|theme|color-scheme)=["']?dark|\.dark\b/i;
72
+ var LIGHT = /prefers-color-scheme:\s*light|data-(mode|theme|color-scheme)=["']?light|\.light\b/i;
73
+ var NAMED = /data-theme=["']?([\w-]+)/i;
74
+ function applies(d, o) {
75
+ for (const scope of d.within) {
76
+ if (o.mode === "light" && DARK.test(scope)) return false;
77
+ if (o.mode === "dark" && LIGHT.test(scope)) return false;
78
+ const named = NAMED.exec(scope);
79
+ if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false;
80
+ if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host/.test(scope)) return false;
81
+ }
82
+ return true;
83
+ }
84
+ function resolve(value, vars, seen = 0) {
85
+ if (seen > 16) return null;
86
+ let unresolved = false;
87
+ let out2 = value;
88
+ for (let guard = 0; guard < 32 && /var\(/.test(out2); guard++) {
89
+ out2 = out2.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/, (_m, name, fallback) => {
90
+ const known = vars.get(name);
91
+ const next = known !== void 0 ? resolve(known, vars, seen + 1) : fallback !== void 0 ? resolve(fallback.trim(), vars, seen + 1) : null;
92
+ if (next === null) unresolved = true;
93
+ return next ?? "";
94
+ });
95
+ if (unresolved) return null;
96
+ }
97
+ return /var\(/.test(out2) ? null : out2.trim();
98
+ }
99
+ var clamp01 = (n) => Math.min(1, Math.max(0, n));
100
+ var hex2 = (n) => Math.round(clamp01(n) * 255).toString(16).padStart(2, "0");
101
+ var gamma = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
102
+ function out(r, g, b, a = 1) {
103
+ if (a >= 1) return `#${hex2(r)}${hex2(g)}${hex2(b)}`;
104
+ return `rgba(${Math.round(clamp01(r) * 255)}, ${Math.round(clamp01(g) * 255)}, ${Math.round(clamp01(b) * 255)}, ${Math.round(clamp01(a) * 1e3) / 1e3})`;
105
+ }
106
+ var alpha = (s) => s === void 0 ? 1 : s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s);
107
+ var KEYWORDS = { white: "#ffffff", black: "#000000" };
108
+ function toColor(value) {
109
+ const v = value.trim().toLowerCase();
110
+ if (KEYWORDS[v]) return KEYWORDS[v];
111
+ if (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(v)) return v;
112
+ let m = /^rgba?\(\s*([\d.]+%?)[\s,]+([\d.]+%?)[\s,]+([\d.]+%?)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(v);
113
+ if (m) {
114
+ const ch = (s) => s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s) / 255;
115
+ return out(ch(m[1]), ch(m[2]), ch(m[3]), alpha(m[4]));
116
+ }
117
+ m = /^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(v);
118
+ if (m) {
119
+ const h = (parseFloat(m[1]) % 360 + 360) % 360;
120
+ const s = parseFloat(m[2]) / 100;
121
+ const l = parseFloat(m[3]) / 100;
122
+ const f = (n) => {
123
+ const k = (n + h / 30) % 12;
124
+ return l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1));
125
+ };
126
+ return out(f(0), f(8), f(4), alpha(m[4]));
127
+ }
128
+ m = /^oklch\(\s*([\d.]+%?)\s+([\d.]+%?)\s+([\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?))?\s*\)$/.exec(v);
129
+ if (m) {
130
+ const L = m[1].endsWith("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
131
+ const C = m[2].endsWith("%") ? parseFloat(m[2]) / 100 * 0.4 : parseFloat(m[2]);
132
+ const H = parseFloat(m[3]) * Math.PI / 180;
133
+ const a = C * Math.cos(H);
134
+ const b = C * Math.sin(H);
135
+ const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3);
136
+ const mm = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3);
137
+ const s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3);
138
+ return out(
139
+ gamma(4.0767416621 * l - 3.3077115913 * mm + 0.2309699292 * s),
140
+ gamma(-1.2684380046 * l + 2.6097574011 * mm - 0.3413193965 * s),
141
+ gamma(-0.0041960863 * l - 0.7034186147 * mm + 1.707614701 * s),
142
+ alpha(m[4])
143
+ );
144
+ }
145
+ return null;
146
+ }
147
+ function toRadius(value) {
148
+ const m = /^([\d.]+)(px|rem|em)?$/.exec(value.trim());
149
+ if (!m) return null;
150
+ const px = parseFloat(m[1]) * (m[2] === "rem" || m[2] === "em" ? 16 : 1);
151
+ return Number.isFinite(px) ? Math.min(MAX_RADIUS, Math.max(0, Math.round(px))) : null;
152
+ }
153
+ function toFont(value) {
154
+ const v = value.trim();
155
+ return /^[A-Za-z0-9 ,'"_-]{1,200}$/.test(v) && (v.match(/"/g)?.length ?? 0) % 2 === 0 && (v.match(/'/g)?.length ?? 0) % 2 === 0 ? v : null;
156
+ }
157
+ var COLORS = [
158
+ ["background", ["--sm-bg-surface", "--sm-surface"]],
159
+ ["surface", ["--sm-bg-surface-raised", "--sm-surface-raised"]],
160
+ ["text", ["--sm-text-primary", "--sm-surface-text"]],
161
+ ["textMuted", ["--sm-text-secondary"]],
162
+ ["border", ["--sm-border-default", "--sm-surface-border"]],
163
+ ["primary", ["--sm-primary", "--sm-bg-interactive"]],
164
+ ["primaryText", ["--sm-primary-text", "--sm-text-inverse"]],
165
+ ["accent", ["--sm-text-brand", "--sm-border-focus", "--sm-accent"]],
166
+ ["error", ["--sm-error-soft-text", "--sm-error"]],
167
+ ["errorBackground", ["--sm-error-soft"]],
168
+ ["success", ["--sm-success-soft-text", "--sm-success"]],
169
+ ["successBackground", ["--sm-success-soft"]]
170
+ ];
171
+ function themeFromStackmyth(css, options = {}) {
172
+ const o = { mode: options.mode ?? "light", theme: options.theme };
173
+ const vars = /* @__PURE__ */ new Map();
174
+ for (const sheet of Array.isArray(css) ? css : [css]) {
175
+ for (const d of declarations(sheet)) if (applies(d, o)) vars.set(d.name, d.value);
176
+ }
177
+ const theme = {};
178
+ const warnings = [];
179
+ const read = (tokens, to, what) => {
180
+ for (const token of tokens) {
181
+ const raw = vars.get(token);
182
+ if (raw === void 0) continue;
183
+ const value = resolve(raw, vars);
184
+ const got = value === null ? null : to(value);
185
+ if (got !== null) return got;
186
+ warnings.push(`${token}: "${raw}" is not ${what} Cauthy can take`);
187
+ }
188
+ return void 0;
189
+ };
190
+ for (const [field, tokens] of COLORS) {
191
+ const c = read(tokens, toColor, "a colour");
192
+ if (c !== void 0) theme[field] = c;
193
+ }
194
+ const radius = read(["--sm-radius-container", "--sm-radius-md"], toRadius, "a length");
195
+ if (radius !== void 0) theme.radius = radius;
196
+ const button = read(["--sm-radius-interactive", "--sm-radius-sm"], toRadius, "a length");
197
+ if (button !== void 0) theme.buttonRadius = button;
198
+ const font = read(["--sm-font-family"], toFont, "a font list");
199
+ if (font !== void 0) theme.fontFamily = font;
200
+ if (options.logoUrl) theme.logoUrl = options.logoUrl;
201
+ if (vars.size === 0) warnings.push("no custom properties were found: is this the stylesheet that declares the --sm-* tokens?");
202
+ else if (![...vars.keys()].some((k) => k.startsWith("--sm-"))) warnings.push("no --sm-* token was found in these stylesheets");
203
+ return { theme, warnings };
204
+ }
205
+ function toWire(theme) {
206
+ const names = {
207
+ background: "background",
208
+ surface: "surface",
209
+ text: "text",
210
+ textMuted: "text_muted",
211
+ border: "border",
212
+ primary: "primary",
213
+ primaryText: "primary_text",
214
+ accent: "accent",
215
+ error: "error",
216
+ errorBackground: "error_background",
217
+ success: "success",
218
+ successBackground: "success_background",
219
+ radius: "radius",
220
+ buttonRadius: "button_radius",
221
+ fontFamily: "font_family",
222
+ logoUrl: "logo_url"
223
+ };
224
+ const wire = {};
225
+ for (const [k, v] of Object.entries(theme)) if (v !== void 0 && v !== "") wire[names[k]] = v;
226
+ return wire;
227
+ }
228
+ async function pushTheme(o) {
229
+ const env = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
230
+ const issuer = (o.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
231
+ const clientId = o.clientId ?? env("CAUTHY_CLIENT_ID") ?? "";
232
+ const tenant = o.tenant ?? env("CAUTHY_TENANT") ?? "";
233
+ const token = o.token ?? env("CAUTHY_TOKEN") ?? "";
234
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
235
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
236
+ }
237
+ const res = await (o.fetch ?? fetch)(`${issuer}/v1/oauth/clients/${encodeURIComponent(clientId)}/theme`, {
238
+ method: "PUT",
239
+ headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
240
+ body: JSON.stringify(toWire(o.theme))
241
+ });
242
+ if (!res.ok) {
243
+ const body = await res.json().catch(() => null);
244
+ throw new Error(`cauthy-auth: Cauthy refused the theme (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
245
+ }
246
+ }
247
+ // Annotate the CommonJS export names for ESM import in node:
248
+ 0 && (module.exports = {
249
+ pushTheme,
250
+ themeFromStackmyth,
251
+ toColor,
252
+ toWire
253
+ });
254
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/theme.ts"],"sourcesContent":["// An application's look, for Cauthy's hosted pages.\n//\n// Cauthy's pages — sign-in, sign-up, the account page — can take the colours,\n// corners, typeface and logo of the application that sends its people there,\n// so signing in reads as a page of the application. A theme is a short list of\n// values, which Cauthy checks one by one; it is never CSS.\n//\n// An application built with Stackmyth already has those values, as its\n// `--sm-*` tokens. `themeFromStackmyth` reads them out of its stylesheets.\n\n/** How an application looks on Cauthy's hosted pages. Every value is optional. */\nexport interface CauthyTheme {\n /** The page. Colours are `#rrggbb` or `rgba(r, g, b, a)`. */\n background?: string\n /** The card, and inputs. */\n surface?: string\n /** Headings and what is typed. */\n text?: string\n /** Paragraphs, labels, small print. */\n textMuted?: string\n border?: string\n /** The main button, and what is written on it. */\n primary?: string\n primaryText?: string\n /** Links, and the focus ring. */\n accent?: string\n error?: string\n errorBackground?: string\n success?: string\n successBackground?: string\n /** Corners of cards and inputs, in pixels (0–40). */\n radius?: number\n /** Corners of buttons, in pixels (0–40). */\n buttonRadius?: number\n /** A CSS font-family list. Cauthy loads no font files: put a system font after the application's own. */\n fontFamily?: string\n /** An image shown where \"Cauthy\" is, over https. */\n logoUrl?: string\n}\n\nexport interface StackmythThemeOptions {\n /**\n * The named theme to read, when the stylesheet keeps several under\n * `[data-theme=…]` (`\"vennet\"`). Without it only what applies to every\n * page (`:root`) is read.\n */\n theme?: string\n /** Which of the application's two modes to take. Default: `light`. */\n mode?: 'light' | 'dark'\n logoUrl?: string\n}\n\nexport interface ImportedTheme {\n theme: CauthyTheme\n /** Tokens that were there and could not be carried over, and why. */\n warnings: string[]\n}\n\nconst MAX_RADIUS = 40\n\n// ── Reading custom properties out of CSS ───────────────────────────────────\n\ninterface Declaration {\n name: string\n value: string\n /** The selectors and at-rules it sits inside, outermost first. */\n within: string[]\n}\n\nfunction declarations(css: string): Declaration[] {\n const text = css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n const out: Declaration[] = []\n const within: string[] = []\n let chunk = ''\n let quote = ''\n let depth = 0 // parentheses, so a `;` inside url(…) does not end anything\n const flush = () => {\n const m = /^\\s*(--[\\w-]+)\\s*:\\s*([\\s\\S]+?)\\s*$/.exec(chunk)\n if (m) out.push({ name: m[1], value: m[2].replace(/\\s+/g, ' ').replace(/\\s*!important$/, ''), within: [...within] })\n chunk = ''\n }\n for (const ch of text) {\n if (quote) {\n chunk += ch\n if (ch === quote) quote = ''\n continue\n }\n if (ch === '\"' || ch === \"'\") {\n quote = ch\n chunk += ch\n } else if (ch === '(') {\n depth++\n chunk += ch\n } else if (ch === ')') {\n depth = Math.max(0, depth - 1)\n chunk += ch\n } else if (ch === '{' && depth === 0) {\n within.push(chunk.trim().replace(/\\s+/g, ' '))\n chunk = ''\n } else if (ch === '}' && depth === 0) {\n flush()\n within.pop()\n } else if (ch === ';' && depth === 0) {\n flush()\n } else {\n chunk += ch\n }\n }\n return out\n}\n\nconst DARK = /prefers-color-scheme:\\s*dark|data-(mode|theme|color-scheme)=[\"']?dark|\\.dark\\b/i\nconst LIGHT = /prefers-color-scheme:\\s*light|data-(mode|theme|color-scheme)=[\"']?light|\\.light\\b/i\nconst NAMED = /data-theme=[\"']?([\\w-]+)/i\n\n/** Whether a declaration applies to the page being themed. */\nfunction applies(d: Declaration, o: Required<Pick<StackmythThemeOptions, 'mode'>> & { theme?: string }): boolean {\n for (const scope of d.within) {\n if (o.mode === 'light' && DARK.test(scope)) return false\n if (o.mode === 'dark' && LIGHT.test(scope)) return false\n const named = NAMED.exec(scope)\n if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false\n // Component-level rules (`.btn { --sm-… }`) are not the page's theme.\n if (!scope.startsWith('@') && !/:root|^html\\b|^body\\b|\\[data-|^\\*$|:host/.test(scope)) return false\n }\n return true\n}\n\n// ── Values ─────────────────────────────────────────────────────────────────\n\nfunction resolve(value: string, vars: Map<string, string>, seen = 0): string | null {\n if (seen > 16) return null\n let unresolved = false\n // Innermost var() first: fallbacks may hold another var().\n let out = value\n for (let guard = 0; guard < 32 && /var\\(/.test(out); guard++) {\n out = out.replace(/var\\(\\s*(--[\\w-]+)\\s*(?:,\\s*([^()]*(?:\\([^()]*\\)[^()]*)*))?\\)/, (_m, name: string, fallback?: string) => {\n const known = vars.get(name)\n const next = known !== undefined ? resolve(known, vars, seen + 1) : fallback !== undefined ? resolve(fallback.trim(), vars, seen + 1) : null\n if (next === null) unresolved = true\n return next ?? ''\n })\n if (unresolved) return null\n }\n return /var\\(/.test(out) ? null : out.trim()\n}\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n))\nconst hex2 = (n: number) => Math.round(clamp01(n) * 255).toString(16).padStart(2, '0')\nconst gamma = (c: number) => (c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055)\n\nfunction out(r: number, g: number, b: number, a = 1): string {\n if (a >= 1) return `#${hex2(r)}${hex2(g)}${hex2(b)}`\n return `rgba(${Math.round(clamp01(r) * 255)}, ${Math.round(clamp01(g) * 255)}, ${Math.round(clamp01(b) * 255)}, ${Math.round(clamp01(a) * 1000) / 1000})`\n}\n\nconst alpha = (s: string | undefined) => (s === undefined ? 1 : s.endsWith('%') ? parseFloat(s) / 100 : parseFloat(s))\n\nconst KEYWORDS: Record<string, string> = { white: '#ffffff', black: '#000000' }\n\n/** A colour as Cauthy takes it — `#rrggbb` or `rgba(…)` — or null when it is none it can read. */\nexport function toColor(value: string): string | null {\n const v = value.trim().toLowerCase()\n if (KEYWORDS[v]) return KEYWORDS[v]\n if (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(v)) return v\n let m = /^rgba?\\(\\s*([\\d.]+%?)[\\s,]+([\\d.]+%?)[\\s,]+([\\d.]+%?)(?:\\s*[,/]\\s*([\\d.]+%?))?\\s*\\)$/.exec(v)\n if (m) {\n const ch = (s: string) => (s.endsWith('%') ? parseFloat(s) / 100 : parseFloat(s) / 255)\n return out(ch(m[1]), ch(m[2]), ch(m[3]), alpha(m[4]))\n }\n m = /^hsla?\\(\\s*([\\d.]+)(?:deg)?[\\s,]+([\\d.]+)%[\\s,]+([\\d.]+)%(?:\\s*[,/]\\s*([\\d.]+%?))?\\s*\\)$/.exec(v)\n if (m) {\n const h = ((parseFloat(m[1]) % 360) + 360) % 360\n const s = parseFloat(m[2]) / 100\n const l = parseFloat(m[3]) / 100\n const f = (n: number) => {\n const k = (n + h / 30) % 12\n return l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1))\n }\n return out(f(0), f(8), f(4), alpha(m[4]))\n }\n m = /^oklch\\(\\s*([\\d.]+%?)\\s+([\\d.]+%?)\\s+([\\d.]+)(?:deg)?(?:\\s*\\/\\s*([\\d.]+%?))?\\s*\\)$/.exec(v)\n if (m) {\n const L = m[1].endsWith('%') ? parseFloat(m[1]) / 100 : parseFloat(m[1])\n const C = m[2].endsWith('%') ? (parseFloat(m[2]) / 100) * 0.4 : parseFloat(m[2])\n const H = (parseFloat(m[3]) * Math.PI) / 180\n const a = C * Math.cos(H)\n const b = C * Math.sin(H)\n const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3)\n const mm = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3)\n const s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3)\n return out(\n gamma(4.0767416621 * l - 3.3077115913 * mm + 0.2309699292 * s),\n gamma(-1.2684380046 * l + 2.6097574011 * mm - 0.3413193965 * s),\n gamma(-0.0041960863 * l - 0.7034186147 * mm + 1.707614701 * s),\n alpha(m[4]),\n )\n }\n return null\n}\n\n/** A length in pixels, within what Cauthy takes, or null. */\nfunction toRadius(value: string): number | null {\n const m = /^([\\d.]+)(px|rem|em)?$/.exec(value.trim())\n if (!m) return null\n const px = parseFloat(m[1]) * (m[2] === 'rem' || m[2] === 'em' ? 16 : 1)\n return Number.isFinite(px) ? Math.min(MAX_RADIUS, Math.max(0, Math.round(px))) : null\n}\n\n/** A font list as Cauthy takes it, or null. */\nfunction toFont(value: string): string | null {\n const v = value.trim()\n return /^[A-Za-z0-9 ,'\"_-]{1,200}$/.test(v) && (v.match(/\"/g)?.length ?? 0) % 2 === 0 && (v.match(/'/g)?.length ?? 0) % 2 === 0 ? v : null\n}\n\n// ── Stackmyth ──────────────────────────────────────────────────────────────\n\n/** Which Stackmyth token each value of a theme is read from, first found wins. */\nconst COLORS: [keyof CauthyTheme, string[]][] = [\n ['background', ['--sm-bg-surface', '--sm-surface']],\n ['surface', ['--sm-bg-surface-raised', '--sm-surface-raised']],\n ['text', ['--sm-text-primary', '--sm-surface-text']],\n ['textMuted', ['--sm-text-secondary']],\n ['border', ['--sm-border-default', '--sm-surface-border']],\n ['primary', ['--sm-primary', '--sm-bg-interactive']],\n ['primaryText', ['--sm-primary-text', '--sm-text-inverse']],\n ['accent', ['--sm-text-brand', '--sm-border-focus', '--sm-accent']],\n ['error', ['--sm-error-soft-text', '--sm-error']],\n ['errorBackground', ['--sm-error-soft']],\n ['success', ['--sm-success-soft-text', '--sm-success']],\n ['successBackground', ['--sm-success-soft']],\n]\n\n/**\n * Reads an application's Stackmyth theme out of its stylesheets.\n *\n * Give it the CSS that declares the tokens, in the order the application\n * loads it — Stackmyth's own (`@stackmyth/core/core.vars.css`, or a theme\n * file) and then the application's brand file that re-points them. Tokens\n * that point at the application's own variables are followed. The\n * application's dark mode is left out unless asked for.\n */\nexport function themeFromStackmyth(css: string | string[], options: StackmythThemeOptions = {}): ImportedTheme {\n const o = { mode: options.mode ?? 'light', theme: options.theme }\n const vars = new Map<string, string>()\n for (const sheet of Array.isArray(css) ? css : [css]) {\n for (const d of declarations(sheet)) if (applies(d, o)) vars.set(d.name, d.value)\n }\n const theme: CauthyTheme = {}\n const warnings: string[] = []\n\n const read = <T>(tokens: string[], to: (v: string) => T | null, what: string): T | undefined => {\n for (const token of tokens) {\n const raw = vars.get(token)\n if (raw === undefined) continue\n const value = resolve(raw, vars)\n const got = value === null ? null : to(value)\n if (got !== null) return got\n warnings.push(`${token}: \"${raw}\" is not ${what} Cauthy can take`)\n }\n return undefined\n }\n\n for (const [field, tokens] of COLORS) {\n const c = read(tokens, toColor, 'a colour')\n if (c !== undefined) (theme as Record<string, unknown>)[field] = c\n }\n const radius = read(['--sm-radius-container', '--sm-radius-md'], toRadius, 'a length')\n if (radius !== undefined) theme.radius = radius\n const button = read(['--sm-radius-interactive', '--sm-radius-sm'], toRadius, 'a length')\n if (button !== undefined) theme.buttonRadius = button\n const font = read(['--sm-font-family'], toFont, 'a font list')\n if (font !== undefined) theme.fontFamily = font\n if (options.logoUrl) theme.logoUrl = options.logoUrl\n\n if (vars.size === 0) warnings.push('no custom properties were found: is this the stylesheet that declares the --sm-* tokens?')\n else if (![...vars.keys()].some((k) => k.startsWith('--sm-'))) warnings.push('no --sm-* token was found in these stylesheets')\n return { theme, warnings }\n}\n\n// ── Sending it to Cauthy ───────────────────────────────────────────────────\n\n/** A theme as Cauthy's API names its values. */\nexport function toWire(theme: CauthyTheme): Record<string, string | number> {\n const names: Record<keyof CauthyTheme, string> = {\n background: 'background', surface: 'surface', text: 'text', textMuted: 'text_muted', border: 'border',\n primary: 'primary', primaryText: 'primary_text', accent: 'accent', error: 'error', errorBackground: 'error_background',\n success: 'success', successBackground: 'success_background', radius: 'radius', buttonRadius: 'button_radius',\n fontFamily: 'font_family', logoUrl: 'logo_url',\n }\n const wire: Record<string, string | number> = {}\n for (const [k, v] of Object.entries(theme)) if (v !== undefined && v !== '') wire[names[k as keyof CauthyTheme]] = v as string | number\n return wire\n}\n\nexport interface PushThemeOptions {\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The application's client id. Default: `CAUTHY_CLIENT_ID`. */\n clientId?: string\n /** The workspace's slug. Default: `CAUTHY_TENANT`. */\n tenant?: string\n /**\n * A personal access token with `oauth_clients:manage`. Default:\n * `CAUTHY_TOKEN`. It belongs in the deploy pipeline, never in the application.\n */\n token?: string\n theme: CauthyTheme\n fetch?: typeof fetch\n}\n\n/**\n * Gives the application its look on Cauthy's pages. It replaces the theme: a\n * value left out goes back to Cauthy's own. Run it when the brand changes —\n * from a script or the deploy — not on every request.\n */\nexport async function pushTheme(o: PushThemeOptions): Promise<void> {\n const env = (n: string) => (typeof process !== 'undefined' ? process.env?.[n] : undefined)\n const issuer = (o.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n const clientId = o.clientId ?? env('CAUTHY_CLIENT_ID') ?? ''\n const tenant = o.tenant ?? env('CAUTHY_TENANT') ?? ''\n const token = o.token ?? env('CAUTHY_TOKEN') ?? ''\n for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {\n if (v === '') throw new Error(`cauthy-auth: ${name} is required to push a theme`)\n }\n const res = await (o.fetch ?? fetch)(`${issuer}/v1/oauth/clients/${encodeURIComponent(clientId)}/theme`, {\n method: 'PUT',\n headers: { authorization: `Bearer ${token}`, 'x-tenant-key': tenant, 'content-type': 'application/json' },\n body: JSON.stringify(toWire(o.theme)),\n })\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as { errors?: { message?: string }[] } | null\n throw new Error(`cauthy-auth: Cauthy refused the theme (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`)\n }\n}\n"],"mappings":"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","names":["out"]}