cauthy-auth 0.5.2 → 0.8.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/README.md +68 -3
- package/dist/{cauthy-BCTCqubt.d.ts → cauthy-Bn3iv50y.d.ts} +1 -1
- package/dist/{cauthy-hswMoS7B.d.mts → cauthy-C29HCvag.d.mts} +1 -1
- package/dist/cli.js +490 -25
- package/dist/client.d.mts +236 -0
- package/dist/client.d.ts +236 -0
- package/dist/client.js +331 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +297 -0
- package/dist/client.mjs.map +1 -0
- package/dist/express.d.mts +24 -0
- package/dist/express.d.ts +24 -0
- package/dist/express.js +102 -0
- package/dist/express.js.map +1 -0
- package/dist/express.mjs +76 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +2 -2
- package/dist/fastify.d.ts +2 -2
- package/dist/fastify.js +3 -1
- package/dist/fastify.js.map +1 -1
- package/dist/fastify.mjs +3 -1
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.d.mts +23 -0
- package/dist/hono.d.ts +23 -0
- package/dist/hono.js +50 -0
- package/dist/hono.js.map +1 -0
- package/dist/hono.mjs +24 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +44 -6
- package/dist/index.d.ts +44 -6
- package/dist/index.js +668 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +656 -37
- package/dist/index.mjs.map +1 -1
- package/dist/next.d.mts +24 -0
- package/dist/next.d.ts +24 -0
- package/dist/next.js +77 -0
- package/dist/next.js.map +1 -0
- package/dist/next.mjs +52 -0
- package/dist/next.mjs.map +1 -0
- package/dist/react.d.mts +546 -7
- package/dist/react.d.ts +546 -7
- package/dist/react.js +925 -14
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +913 -10
- package/dist/react.mjs.map +1 -1
- package/dist/styles.css +271 -0
- package/dist/svelte.d.mts +26 -0
- package/dist/svelte.d.ts +26 -0
- package/dist/svelte.js +402 -0
- package/dist/svelte.js.map +1 -0
- package/dist/svelte.mjs +373 -0
- package/dist/svelte.mjs.map +1 -0
- package/dist/{system-DvcQjORr.d.ts → system-DhRr_WxR.d.ts} +139 -1
- package/dist/{system-CR8p4BjB.d.mts → system-ZD_Wa8Bm.d.mts} +139 -1
- package/dist/theme.d.mts +56 -1
- package/dist/theme.d.ts +56 -1
- package/dist/theme.js +181 -5
- package/dist/theme.js.map +1 -1
- package/dist/theme.mjs +176 -4
- package/dist/theme.mjs.map +1 -1
- package/dist/validate-B_iC-4y8.d.mts +19 -0
- package/dist/validate-B_iC-4y8.d.ts +19 -0
- package/dist/vue.d.mts +28 -0
- package/dist/vue.d.ts +28 -0
- package/dist/vue.js +420 -0
- package/dist/vue.js.map +1 -0
- package/dist/vue.mjs +391 -0
- package/dist/vue.mjs.map +1 -0
- package/package.json +68 -7
- package/dist/permissions-B-a3eN0h.d.mts +0 -52
- package/dist/permissions-B-a3eN0h.d.ts +0 -52
package/dist/cli.js
CHANGED
|
@@ -12,10 +12,10 @@ function sessionSecret() {
|
|
|
12
12
|
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
13
13
|
}
|
|
14
14
|
async function connectSystem(o) {
|
|
15
|
-
const
|
|
16
|
-
const issuer = (o.issuer ??
|
|
17
|
-
const tenant = o.tenant ??
|
|
18
|
-
const token = o.token ??
|
|
15
|
+
const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
16
|
+
const issuer = (o.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
17
|
+
const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
|
|
18
|
+
const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
|
|
19
19
|
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
20
20
|
if (v === "") throw new Error(`cauthy-auth: ${name} is required to connect a system`);
|
|
21
21
|
}
|
|
@@ -47,17 +47,67 @@ async function connectSystem(o) {
|
|
|
47
47
|
env: { ...d.env, CAUTHY_SECRET: sessionSecret() }
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
function envFile(
|
|
51
|
-
return Object.entries(
|
|
50
|
+
function envFile(env2) {
|
|
51
|
+
return Object.entries(env2).map(([k, v]) => `${k}=${v}`).join("\n") + "\n";
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// src/permissions.ts
|
|
55
|
+
var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
56
|
+
function policySource(system, roles) {
|
|
57
|
+
const text = (v) => JSON.stringify(v);
|
|
58
|
+
const key = (k) => IDENT.test(k) ? k : text(k);
|
|
59
|
+
const one = (r) => {
|
|
60
|
+
const parts = [`role: ${text(r.role)}`];
|
|
61
|
+
if (r.name) parts.push(`name: ${text(r.name)}`);
|
|
62
|
+
if (r.description) parts.push(`description: ${text(r.description)}`);
|
|
63
|
+
if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
|
|
64
|
+
if (r.permissions?.length) {
|
|
65
|
+
const list = r.permissions.map((p) => ` { ${[...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [], `resource: ${text(p.resource)}`, `action: ${text(p.action)}`].join(", ")} },`);
|
|
66
|
+
parts.push(`permissions: [
|
|
67
|
+
${list.join("\n")}
|
|
68
|
+
]`);
|
|
69
|
+
}
|
|
70
|
+
return ` {
|
|
71
|
+
${parts.join(",\n ")},
|
|
72
|
+
},`;
|
|
73
|
+
};
|
|
74
|
+
return `// The catalog of roles of ${text(system)}, as Cauthy has it.
|
|
75
|
+
// Written by \`cauthy-auth policy --pull\`: run it again after the catalog
|
|
76
|
+
// changes, and do not edit this file by hand.
|
|
77
|
+
|
|
78
|
+
import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
|
|
79
|
+
|
|
80
|
+
export const policy = definePolicy([
|
|
81
|
+
${roles.map(one).join("\n")}
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
export const { can, hasRole } = policy
|
|
85
|
+
export type Role = RoleOf<typeof policy>
|
|
86
|
+
export type Permission = PermissionOf<typeof policy>
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
async function pullPolicy(o = {}) {
|
|
90
|
+
const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
91
|
+
const issuer = (o.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
92
|
+
const system = o.system ?? env2("CAUTHY_SYSTEM") ?? "";
|
|
93
|
+
const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
|
|
94
|
+
const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
|
|
95
|
+
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
96
|
+
if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
|
|
97
|
+
}
|
|
98
|
+
const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
|
|
99
|
+
headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, accept: "application/json" }
|
|
100
|
+
});
|
|
101
|
+
const body = await res.json().catch(() => null);
|
|
102
|
+
if (!res.ok) throw new Error(`cauthy-auth: Cauthy refused to give the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
|
|
103
|
+
return body?.data?.roles ?? [];
|
|
104
|
+
}
|
|
55
105
|
async function pushPolicy(o) {
|
|
56
|
-
const
|
|
57
|
-
const issuer = (o.issuer ??
|
|
58
|
-
const system = o.system ??
|
|
59
|
-
const tenant = o.tenant ??
|
|
60
|
-
const token = o.token ??
|
|
106
|
+
const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
107
|
+
const issuer = (o.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
108
|
+
const system = o.system ?? env2("CAUTHY_SYSTEM") ?? "";
|
|
109
|
+
const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
|
|
110
|
+
const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
|
|
61
111
|
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
62
112
|
if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
|
|
63
113
|
}
|
|
@@ -72,6 +122,9 @@ async function pushPolicy(o) {
|
|
|
72
122
|
}
|
|
73
123
|
}
|
|
74
124
|
|
|
125
|
+
// src/system.ts
|
|
126
|
+
var import_jose = require("jose");
|
|
127
|
+
|
|
75
128
|
// src/theme.ts
|
|
76
129
|
var MAX_RADIUS = 40;
|
|
77
130
|
function declarations(css) {
|
|
@@ -124,10 +177,92 @@ function applies(d, o) {
|
|
|
124
177
|
if (o.mode === "dark" && LIGHT.test(scope)) return false;
|
|
125
178
|
const named = NAMED.exec(scope);
|
|
126
179
|
if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false;
|
|
127
|
-
if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host/.test(scope)) return false;
|
|
180
|
+
if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host|^\.(dark|light)\b/.test(scope)) return false;
|
|
128
181
|
}
|
|
129
182
|
return true;
|
|
130
183
|
}
|
|
184
|
+
var TOKEN_CHARS = /^[A-Za-z0-9 #%.,()/'"_+*-]+$/;
|
|
185
|
+
var TOKEN_FUNCS = /* @__PURE__ */ new Set([
|
|
186
|
+
"var",
|
|
187
|
+
"calc",
|
|
188
|
+
"min",
|
|
189
|
+
"max",
|
|
190
|
+
"clamp",
|
|
191
|
+
"rgb",
|
|
192
|
+
"rgba",
|
|
193
|
+
"hsl",
|
|
194
|
+
"hsla",
|
|
195
|
+
"hwb",
|
|
196
|
+
"lab",
|
|
197
|
+
"lch",
|
|
198
|
+
"oklab",
|
|
199
|
+
"oklch",
|
|
200
|
+
"color",
|
|
201
|
+
"color-mix",
|
|
202
|
+
"light-dark",
|
|
203
|
+
"cubic-bezier",
|
|
204
|
+
"steps",
|
|
205
|
+
"linear",
|
|
206
|
+
"linear-gradient",
|
|
207
|
+
"radial-gradient",
|
|
208
|
+
"conic-gradient",
|
|
209
|
+
"repeating-linear-gradient",
|
|
210
|
+
"repeating-radial-gradient",
|
|
211
|
+
"drop-shadow",
|
|
212
|
+
"blur",
|
|
213
|
+
"translate",
|
|
214
|
+
"translatex",
|
|
215
|
+
"translatey",
|
|
216
|
+
"scale",
|
|
217
|
+
"rotate"
|
|
218
|
+
]);
|
|
219
|
+
function tokenValue(value) {
|
|
220
|
+
const v = value.replace(/\s+/g, " ").trim();
|
|
221
|
+
if (v === "" || v.length > 400 || !TOKEN_CHARS.test(v)) return null;
|
|
222
|
+
for (const m of v.matchAll(/([A-Za-z-]+)\(/g)) if (!TOKEN_FUNCS.has(m[1].toLowerCase())) return null;
|
|
223
|
+
if (/^\(|[ ,(]\(/.test(v)) return null;
|
|
224
|
+
return v;
|
|
225
|
+
}
|
|
226
|
+
var THEME_SCOPE = /^(?:\s*(?::root|html|body|\[data-(?:theme|mode|color-scheme)(?:=["']?[\w-]+["']?)?\])\s*,?)+$/i;
|
|
227
|
+
function tokensFromStackmyth(css, options = {}) {
|
|
228
|
+
const mode = options.mode ?? "light";
|
|
229
|
+
const all = (Array.isArray(css) ? css : [css]).flatMap(declarations);
|
|
230
|
+
const named = options.theme ?? all.map((d) => d.within.map((w) => NAMED.exec(w)?.[1]).find((n) => n && !/^(dark|light)$/i.test(n))).find(Boolean);
|
|
231
|
+
const vars = /* @__PURE__ */ new Map();
|
|
232
|
+
for (const d of all) {
|
|
233
|
+
if (!applies(d, { mode, theme: named })) continue;
|
|
234
|
+
if (!d.within.every((w) => w.startsWith("@") || THEME_SCOPE.test(w))) continue;
|
|
235
|
+
vars.set(d.name, d.value);
|
|
236
|
+
}
|
|
237
|
+
const inline = (value, depth = 0) => {
|
|
238
|
+
if (depth > 16) return null;
|
|
239
|
+
let failed = false;
|
|
240
|
+
let out2 = value;
|
|
241
|
+
for (let guard = 0; guard < 64; guard++) {
|
|
242
|
+
const next = out2.replace(/var\(\s*(--(?!sm-)[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/, (_m, name, fallback) => {
|
|
243
|
+
const known = vars.get(name);
|
|
244
|
+
const got = known !== void 0 ? inline(known, depth + 1) : fallback !== void 0 ? inline(fallback.trim(), depth + 1) : null;
|
|
245
|
+
if (got === null) failed = true;
|
|
246
|
+
return got ?? "";
|
|
247
|
+
});
|
|
248
|
+
if (failed) return null;
|
|
249
|
+
if (next === out2) break;
|
|
250
|
+
out2 = next;
|
|
251
|
+
}
|
|
252
|
+
return out2;
|
|
253
|
+
};
|
|
254
|
+
const tokens = {};
|
|
255
|
+
const warnings = [];
|
|
256
|
+
for (const [name, raw] of vars) {
|
|
257
|
+
if (!/^--sm-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) continue;
|
|
258
|
+
const written = inline(raw);
|
|
259
|
+
const value = written === null ? null : tokenValue(written);
|
|
260
|
+
if (value === null) warnings.push(`${name}: "${raw.length > 60 ? raw.slice(0, 57) + "\u2026" : raw}" is not a value Cauthy takes`);
|
|
261
|
+
else tokens[name] = value;
|
|
262
|
+
}
|
|
263
|
+
if (Object.keys(tokens).length === 0) warnings.push("no --sm-* token was found in these stylesheets");
|
|
264
|
+
return { tokens, warnings };
|
|
265
|
+
}
|
|
131
266
|
function resolve(value, vars, seen = 0) {
|
|
132
267
|
if (seen > 16) return null;
|
|
133
268
|
let unresolved = false;
|
|
@@ -249,6 +384,63 @@ function themeFromStackmyth(css, options = {}) {
|
|
|
249
384
|
else if (![...vars.keys()].some((k) => k.startsWith("--sm-"))) warnings.push("no --sm-* token was found in these stylesheets");
|
|
250
385
|
return { theme, warnings };
|
|
251
386
|
}
|
|
387
|
+
var RADII = ["radius", "buttonRadius"];
|
|
388
|
+
var SHADCN = [
|
|
389
|
+
["background", ["--background", "--color-background"]],
|
|
390
|
+
["surface", ["--card", "--color-card", "--popover", "--color-popover"]],
|
|
391
|
+
["text", ["--foreground", "--color-foreground"]],
|
|
392
|
+
["textMuted", ["--muted-foreground", "--color-muted-foreground"]],
|
|
393
|
+
["border", ["--border", "--color-border"]],
|
|
394
|
+
["inputBorder", ["--input", "--color-input"]],
|
|
395
|
+
["primary", ["--primary", "--color-primary"]],
|
|
396
|
+
["primaryText", ["--primary-foreground", "--color-primary-foreground"]],
|
|
397
|
+
["accent", ["--ring", "--color-ring", "--primary", "--color-primary"]],
|
|
398
|
+
["error", ["--destructive", "--color-destructive"]],
|
|
399
|
+
["radius", ["--radius"]],
|
|
400
|
+
["buttonRadius", ["--radius"]],
|
|
401
|
+
["fontFamily", ["--font-sans", "--font-family"]]
|
|
402
|
+
];
|
|
403
|
+
function shadcnColor(value) {
|
|
404
|
+
return /^[\d.]+(?:deg)?\s+[\d.]+%\s+[\d.]+%(?:\s*\/\s*[\d.]+%?)?$/.test(value.trim()) ? toColor(`hsl(${value.trim()})`) : toColor(value);
|
|
405
|
+
}
|
|
406
|
+
function pageVariables(css, mode, theme) {
|
|
407
|
+
const vars = /* @__PURE__ */ new Map();
|
|
408
|
+
for (const sheet of Array.isArray(css) ? css : [css]) {
|
|
409
|
+
for (const d of declarations(sheet)) if (applies(d, { mode, theme })) vars.set(d.name, d.value);
|
|
410
|
+
}
|
|
411
|
+
return vars;
|
|
412
|
+
}
|
|
413
|
+
function themeFromCss(css, options = {}) {
|
|
414
|
+
const mode = options.mode ?? "light";
|
|
415
|
+
const vars = pageVariables(css, mode, options.theme);
|
|
416
|
+
const names = [...vars.keys()];
|
|
417
|
+
let format;
|
|
418
|
+
if (options.format && options.format !== "auto") format = options.format;
|
|
419
|
+
else if (options.map && Object.keys(options.map).length > 0) format = "custom";
|
|
420
|
+
else if (names.some((n) => n.startsWith("--sm-"))) format = "stackmyth";
|
|
421
|
+
else if (names.some((n) => /^--(color-)?(primary|background|foreground)$/.test(n))) format = "shadcn";
|
|
422
|
+
else format = "custom";
|
|
423
|
+
if (format === "stackmyth") return { ...themeFromStackmyth(css, { mode, theme: options.theme }), format };
|
|
424
|
+
const spec = format === "shadcn" ? SHADCN : Object.entries(options.map ?? {}).filter(([, v]) => v).map(([k, v]) => [k, [String(v).startsWith("--") ? String(v) : `--${v}`]]);
|
|
425
|
+
const theme = {};
|
|
426
|
+
const warnings = [];
|
|
427
|
+
for (const [field, tokens] of spec) {
|
|
428
|
+
for (const token of tokens) {
|
|
429
|
+
const raw = vars.get(token);
|
|
430
|
+
if (raw === void 0) continue;
|
|
431
|
+
const value = resolve(raw, vars);
|
|
432
|
+
const got = value === null ? null : RADII.includes(field) ? toRadius(value) : field === "fontFamily" ? toFont(value) : format === "shadcn" ? shadcnColor(value) : toColor(value);
|
|
433
|
+
if (got !== null) {
|
|
434
|
+
theme[field] = got;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
warnings.push(`${token}: "${raw.length > 60 ? raw.slice(0, 57) + "\u2026" : raw}" is not ${RADII.includes(field) ? "a length" : field === "fontFamily" ? "a font list" : "a colour"} Cauthy can take`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (vars.size === 0) warnings.push("no custom properties were found in these stylesheets");
|
|
441
|
+
else if (format === "custom" && spec.length === 0) warnings.push("these stylesheets use names of their own: say which variable each value is read from");
|
|
442
|
+
return { theme, warnings, format };
|
|
443
|
+
}
|
|
252
444
|
function toWire(theme) {
|
|
253
445
|
const names = {
|
|
254
446
|
background: "background",
|
|
@@ -274,7 +466,8 @@ function toWire(theme) {
|
|
|
274
466
|
panelIconUrl: "panel_icon_url",
|
|
275
467
|
texts: "texts",
|
|
276
468
|
inputBackground: "input_background",
|
|
277
|
-
inputBorder: "input_border"
|
|
469
|
+
inputBorder: "input_border",
|
|
470
|
+
tokens: "tokens"
|
|
278
471
|
};
|
|
279
472
|
const wire = {};
|
|
280
473
|
for (const [k, v] of Object.entries(theme)) {
|
|
@@ -283,12 +476,44 @@ function toWire(theme) {
|
|
|
283
476
|
}
|
|
284
477
|
return wire;
|
|
285
478
|
}
|
|
479
|
+
function fromWire(wire) {
|
|
480
|
+
const names = {
|
|
481
|
+
background: "background",
|
|
482
|
+
surface: "surface",
|
|
483
|
+
text: "text",
|
|
484
|
+
text_muted: "textMuted",
|
|
485
|
+
border: "border",
|
|
486
|
+
primary: "primary",
|
|
487
|
+
primary_text: "primaryText",
|
|
488
|
+
accent: "accent",
|
|
489
|
+
error: "error",
|
|
490
|
+
error_background: "errorBackground",
|
|
491
|
+
success: "success",
|
|
492
|
+
success_background: "successBackground",
|
|
493
|
+
radius: "radius",
|
|
494
|
+
button_radius: "buttonRadius",
|
|
495
|
+
font_family: "fontFamily",
|
|
496
|
+
logo_url: "logoUrl",
|
|
497
|
+
fonts: "fonts",
|
|
498
|
+
layout: "layout",
|
|
499
|
+
panel_colors: "panelColors",
|
|
500
|
+
panel_text: "panelText",
|
|
501
|
+
panel_icon_url: "panelIconUrl",
|
|
502
|
+
texts: "texts",
|
|
503
|
+
input_background: "inputBackground",
|
|
504
|
+
input_border: "inputBorder",
|
|
505
|
+
tokens: "tokens"
|
|
506
|
+
};
|
|
507
|
+
const theme = {};
|
|
508
|
+
for (const [k, v] of Object.entries(wire ?? {})) if (names[k] && v !== void 0 && v !== null && v !== "") theme[names[k]] = v;
|
|
509
|
+
return theme;
|
|
510
|
+
}
|
|
286
511
|
async function pushTheme(o) {
|
|
287
|
-
const
|
|
288
|
-
const issuer = (o.issuer ??
|
|
289
|
-
const clientId = o.clientId ??
|
|
290
|
-
const tenant = o.tenant ??
|
|
291
|
-
const token = o.token ??
|
|
512
|
+
const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
513
|
+
const issuer = (o.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
514
|
+
const clientId = o.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
|
|
515
|
+
const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
|
|
516
|
+
const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
|
|
292
517
|
for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
|
|
293
518
|
if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
|
|
294
519
|
}
|
|
@@ -303,6 +528,184 @@ async function pushTheme(o) {
|
|
|
303
528
|
}
|
|
304
529
|
}
|
|
305
530
|
|
|
531
|
+
// src/types.ts
|
|
532
|
+
var CauthyConfigError = class extends Error {
|
|
533
|
+
constructor(message) {
|
|
534
|
+
super(`cauthy-auth: ${message}`);
|
|
535
|
+
this.name = "CauthyConfigError";
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/system.ts
|
|
540
|
+
var ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
541
|
+
var SKEW_MS = 3e4;
|
|
542
|
+
var CauthyApiError = class extends Error {
|
|
543
|
+
constructor(status, code, message) {
|
|
544
|
+
super(`cauthy-auth: ${message}`);
|
|
545
|
+
this.status = status;
|
|
546
|
+
this.code = code;
|
|
547
|
+
this.name = "CauthyApiError";
|
|
548
|
+
}
|
|
549
|
+
status;
|
|
550
|
+
code;
|
|
551
|
+
};
|
|
552
|
+
function env(name) {
|
|
553
|
+
return typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
554
|
+
}
|
|
555
|
+
function toOrganization(o) {
|
|
556
|
+
return {
|
|
557
|
+
id: o.id,
|
|
558
|
+
system: o.system,
|
|
559
|
+
name: o.name,
|
|
560
|
+
kind: o.kind,
|
|
561
|
+
externalId: o.external_id,
|
|
562
|
+
status: o.status,
|
|
563
|
+
metadata: o.metadata ?? {},
|
|
564
|
+
members: o.members,
|
|
565
|
+
createdAt: o.created_at,
|
|
566
|
+
updatedAt: o.updated_at
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function toMember(m) {
|
|
570
|
+
return { userId: m.user_id, email: m.email, name: m.name, status: m.status, role: m.role, ...m.invited === void 0 ? {} : { invited: m.invited } };
|
|
571
|
+
}
|
|
572
|
+
function asJwk(k) {
|
|
573
|
+
if (typeof k.privateKey !== "string") return k.privateKey;
|
|
574
|
+
return k.privateKey.includes("BEGIN") ? void 0 : JSON.parse(k.privateKey);
|
|
575
|
+
}
|
|
576
|
+
async function loadKey(k) {
|
|
577
|
+
const jwk = asJwk(k);
|
|
578
|
+
return jwk ? (0, import_jose.importJWK)(jwk, "RS256") : (0, import_jose.importPKCS8)(k.privateKey, "RS256", { extractable: true });
|
|
579
|
+
}
|
|
580
|
+
function createSystem(given = {}) {
|
|
581
|
+
const issuer = (given.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
|
|
582
|
+
const clientId = given.clientId ?? env("CAUTHY_CLIENT_ID") ?? "";
|
|
583
|
+
const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET") ?? "";
|
|
584
|
+
const envKey = env("CAUTHY_SYSTEM_KEY");
|
|
585
|
+
const key = given.key ?? (envKey ? { privateKey: envKey, keyId: env("CAUTHY_SYSTEM_KEY_ID") ?? "" } : void 0);
|
|
586
|
+
const doFetch = (...args) => (given.fetch ?? fetch)(...args);
|
|
587
|
+
const tokenEndpoint = `${issuer}/v1/oauth/token`;
|
|
588
|
+
function configured() {
|
|
589
|
+
if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address");
|
|
590
|
+
if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
|
|
591
|
+
if (key ? key.keyId === "" : clientSecret === "") {
|
|
592
|
+
throw new CauthyConfigError(key ? "the key needs its id (CAUTHY_SYSTEM_KEY_ID)" : "to act on its own the application needs its secret (CAUTHY_CLIENT_SECRET) or a key (CAUTHY_SYSTEM_KEY)");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
let imported;
|
|
596
|
+
const signingKey = () => imported ??= loadKey(key);
|
|
597
|
+
async function fail2(res, what) {
|
|
598
|
+
const body = await res.json().catch(() => null);
|
|
599
|
+
const first = body?.errors?.[0];
|
|
600
|
+
throw new CauthyApiError(res.status, first?.code ?? "error", `Cauthy refused ${what} (${res.status}): ${first?.message ?? res.statusText}`);
|
|
601
|
+
}
|
|
602
|
+
let held;
|
|
603
|
+
let asking;
|
|
604
|
+
async function ask() {
|
|
605
|
+
configured();
|
|
606
|
+
const form = new URLSearchParams({ grant_type: "client_credentials", client_id: clientId });
|
|
607
|
+
if (key) {
|
|
608
|
+
const assertion = await new import_jose.SignJWT({}).setProtectedHeader({ alg: "RS256", kid: key.keyId }).setIssuer(clientId).setSubject(clientId).setAudience(tokenEndpoint).setJti(crypto.randomUUID()).setIssuedAt().setExpirationTime("2m").sign(await signingKey());
|
|
609
|
+
form.set("client_assertion_type", ASSERTION_TYPE);
|
|
610
|
+
form.set("client_assertion", assertion);
|
|
611
|
+
} else {
|
|
612
|
+
form.set("client_secret", clientSecret);
|
|
613
|
+
}
|
|
614
|
+
const res = await doFetch(tokenEndpoint, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form });
|
|
615
|
+
if (!res.ok) await fail2(res, "the application's own token");
|
|
616
|
+
const t = await res.json();
|
|
617
|
+
held = { value: t.access_token, until: Date.now() + t.expires_in * 1e3 - SKEW_MS };
|
|
618
|
+
return t.access_token;
|
|
619
|
+
}
|
|
620
|
+
function token() {
|
|
621
|
+
if (held && Date.now() < held.until) return Promise.resolve(held.value);
|
|
622
|
+
return asking ??= ask().finally(() => {
|
|
623
|
+
asking = void 0;
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
async function call(method, path, body) {
|
|
627
|
+
const send = async () => doFetch(`${issuer}${path}`, {
|
|
628
|
+
method,
|
|
629
|
+
headers: { authorization: `Bearer ${await token()}`, ...body === void 0 ? {} : { "content-type": "application/json" } },
|
|
630
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
631
|
+
});
|
|
632
|
+
let res = await send();
|
|
633
|
+
if (res.status === 401 && held) {
|
|
634
|
+
held = void 0;
|
|
635
|
+
res = await send();
|
|
636
|
+
}
|
|
637
|
+
if (!res.ok) await fail2(res, `${method} ${path}`);
|
|
638
|
+
return res.status === 204 ? void 0 : res.json();
|
|
639
|
+
}
|
|
640
|
+
const data = async (method, path, body) => (await call(method, path, body))?.data;
|
|
641
|
+
const id = encodeURIComponent;
|
|
642
|
+
return {
|
|
643
|
+
token,
|
|
644
|
+
request: data,
|
|
645
|
+
organizations: {
|
|
646
|
+
async list(query = {}) {
|
|
647
|
+
const q = new URLSearchParams();
|
|
648
|
+
for (const [k, v] of Object.entries(query)) if (v !== void 0) q.set(k, String(v));
|
|
649
|
+
const out2 = await call("GET", `/v1/organizations${q.size ? `?${q}` : ""}`);
|
|
650
|
+
return { items: out2.data.map(toOrganization), total: out2.total_count };
|
|
651
|
+
},
|
|
652
|
+
async create(input) {
|
|
653
|
+
return toOrganization(await data(
|
|
654
|
+
"POST",
|
|
655
|
+
"/v1/organizations",
|
|
656
|
+
{ name: input.name, kind: input.kind, external_id: input.externalId, metadata: input.metadata }
|
|
657
|
+
));
|
|
658
|
+
},
|
|
659
|
+
async get(orgId) {
|
|
660
|
+
return toOrganization(await data("GET", `/v1/organizations/${id(orgId)}`));
|
|
661
|
+
},
|
|
662
|
+
async update(orgId, patch) {
|
|
663
|
+
return toOrganization(await data("PATCH", `/v1/organizations/${id(orgId)}`, patch));
|
|
664
|
+
},
|
|
665
|
+
async remove(orgId) {
|
|
666
|
+
await call("DELETE", `/v1/organizations/${id(orgId)}`);
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
members: {
|
|
670
|
+
async list(orgId) {
|
|
671
|
+
return (await data("GET", `/v1/organizations/${id(orgId)}/members`)).map(toMember);
|
|
672
|
+
},
|
|
673
|
+
async add(orgId, m) {
|
|
674
|
+
return toMember(await data(
|
|
675
|
+
"POST",
|
|
676
|
+
`/v1/organizations/${id(orgId)}/members`,
|
|
677
|
+
{ role: m.role, user_id: m.userId, email: m.email, name: m.name, locale: m.locale, client_id: clientId }
|
|
678
|
+
));
|
|
679
|
+
},
|
|
680
|
+
async setRole(orgId, userId, role) {
|
|
681
|
+
return toMember(await data("PUT", `/v1/organizations/${id(orgId)}/members/${id(userId)}`, { role }));
|
|
682
|
+
},
|
|
683
|
+
async remove(orgId, userId) {
|
|
684
|
+
await call("DELETE", `/v1/organizations/${id(orgId)}/members/${id(userId)}`);
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
async invite(person) {
|
|
688
|
+
const u = await data("POST", "/v1/users", { ...person, client_id: clientId });
|
|
689
|
+
return { id: u.id, email: u.email };
|
|
690
|
+
},
|
|
691
|
+
theme: {
|
|
692
|
+
async get(of = clientId) {
|
|
693
|
+
return fromWire(await data("GET", `/v1/oauth/clients/${id(of)}/theme`));
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
policy: {
|
|
697
|
+
async get(system) {
|
|
698
|
+
return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
|
|
699
|
+
}
|
|
700
|
+
},
|
|
701
|
+
async publicJwks() {
|
|
702
|
+
if (!key) return { keys: [] };
|
|
703
|
+
const { kty, n, e } = asJwk(key) ?? await (0, import_jose.exportJWK)(await signingKey());
|
|
704
|
+
return { keys: [{ kty, n, e, kid: key.keyId, alg: "RS256", use: "sig" }] };
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
306
709
|
// src/cli.ts
|
|
307
710
|
var INIT_HELP = `cauthy-auth init --system <name> --app-url <address> [options]
|
|
308
711
|
|
|
@@ -375,7 +778,23 @@ Then, in the application:
|
|
|
375
778
|
and serve auth.handler(request) under ${(given["--callback-path"] ?? "/api/auth/callback").replace(/\/callback$/, "")}/*.
|
|
376
779
|
`);
|
|
377
780
|
}
|
|
378
|
-
var POLICY_HELP = `cauthy-auth policy --
|
|
781
|
+
var POLICY_HELP = `cauthy-auth policy --pull [--out <file.ts>] [--system <name>]
|
|
782
|
+
cauthy-auth policy --file <roles.json> [--system <name>] [--push]
|
|
783
|
+
|
|
784
|
+
--pull writes the catalog Cauthy has as a TypeScript module, so the
|
|
785
|
+
application's roles and permissions are typed from it:
|
|
786
|
+
|
|
787
|
+
import { can, hasRole } from './policy'
|
|
788
|
+
can(organization, 'ledger:write')
|
|
789
|
+
|
|
790
|
+
--pull Read the catalog from Cauthy.
|
|
791
|
+
--out <file> Write the module there. Without it, it is printed.
|
|
792
|
+
|
|
793
|
+
To pull: CAUTHY_ISSUER, CAUTHY_SYSTEM, and either the application's own
|
|
794
|
+
identity (CAUTHY_CLIENT_ID with CAUTHY_CLIENT_SECRET) or CAUTHY_TENANT with
|
|
795
|
+
CAUTHY_TOKEN (a personal access token with organizations:manage).
|
|
796
|
+
|
|
797
|
+
--file does the opposite, once:
|
|
379
798
|
|
|
380
799
|
Loads a system's catalog of roles into Cauthy from a file:
|
|
381
800
|
|
|
@@ -402,16 +821,34 @@ async function policy(argv) {
|
|
|
402
821
|
let file;
|
|
403
822
|
let system;
|
|
404
823
|
let push = false;
|
|
824
|
+
let pull = false;
|
|
825
|
+
let out2;
|
|
405
826
|
for (let i = 0; i < argv.length; i++) {
|
|
406
827
|
const arg = argv[i];
|
|
407
828
|
const value = () => argv[++i] ?? fail(`${arg} needs a value`);
|
|
408
829
|
if (arg === "--file") file = value();
|
|
409
830
|
else if (arg === "--system") system = value();
|
|
410
831
|
else if (arg === "--push") push = true;
|
|
832
|
+
else if (arg === "--pull") pull = true;
|
|
833
|
+
else if (arg === "--out") out2 = value();
|
|
411
834
|
else fail(`unknown option ${arg}
|
|
412
835
|
|
|
413
836
|
${POLICY_HELP}`);
|
|
414
837
|
}
|
|
838
|
+
if (pull) {
|
|
839
|
+
const of = system ?? process.env.CAUTHY_SYSTEM ?? fail("name the system with --system or CAUTHY_SYSTEM");
|
|
840
|
+
const read = process.env.CAUTHY_TOKEN ? await pullPolicy({ system: of }) : await createSystem().policy.get(of);
|
|
841
|
+
if (read.length === 0) fail(`Cauthy has no catalog of roles for "${of}"`);
|
|
842
|
+
const source = policySource(of, read);
|
|
843
|
+
if (out2) {
|
|
844
|
+
(0, import_node_fs.writeFileSync)(out2, source);
|
|
845
|
+
process.stdout.write(`${read.length} roles written to ${out2}: ${read.map((r) => r.role).join(", ")}
|
|
846
|
+
`);
|
|
847
|
+
} else {
|
|
848
|
+
process.stdout.write(source);
|
|
849
|
+
}
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
415
852
|
if (!file) fail("give the catalog with --file\n\n" + POLICY_HELP);
|
|
416
853
|
let roles;
|
|
417
854
|
try {
|
|
@@ -436,11 +873,20 @@ var HELP = `cauthy-auth init \u2026 connect an application to Cauthy (cauthy
|
|
|
436
873
|
cauthy-auth policy \u2026 load a system's catalog of roles (cauthy-auth policy --help)
|
|
437
874
|
cauthy-auth theme [options]
|
|
438
875
|
|
|
439
|
-
Reads the application's
|
|
876
|
+
Reads the application's look out of its stylesheets and gives it to Cauthy:
|
|
877
|
+
its hosted pages, and cauthy-auth's ready-made forms. The dashboard (Sign-in
|
|
878
|
+
Page Look) does the same with a preview.
|
|
440
879
|
|
|
441
|
-
--css <file> A stylesheet that
|
|
442
|
-
order the application loads them
|
|
443
|
-
|
|
880
|
+
--css <file> A stylesheet that sets the application's colours. Repeat it,
|
|
881
|
+
in the order the application loads them.
|
|
882
|
+
--format <name> auto (default), shadcn (shadcn/ui and Tailwind), stackmyth,
|
|
883
|
+
or custom with --map.
|
|
884
|
+
--map <v>=<var> For custom: which variable a value is read from, e.g.
|
|
885
|
+
--map primary=--brand --map radius=--corner. Values:
|
|
886
|
+
primary, primaryText, accent, background, surface, text,
|
|
887
|
+
textMuted, border, inputBackground, inputBorder, error,
|
|
888
|
+
errorBackground, success, successBackground, radius,
|
|
889
|
+
buttonRadius, fontFamily.
|
|
444
890
|
--theme <name> The named theme to read, for a sheet with [data-theme=\u2026].
|
|
445
891
|
--mode <mode> light (default) or dark.
|
|
446
892
|
--logo <url> An https image shown where "Cauthy" is.
|
|
@@ -468,6 +914,8 @@ async function main(argv) {
|
|
|
468
914
|
let logoUrl;
|
|
469
915
|
let push = false;
|
|
470
916
|
let clear = false;
|
|
917
|
+
let format = "auto";
|
|
918
|
+
const map = {};
|
|
471
919
|
for (let i = 1; i < argv.length; i++) {
|
|
472
920
|
const arg = argv[i];
|
|
473
921
|
const value = () => argv[++i] ?? fail(`${arg} needs a value`);
|
|
@@ -477,6 +925,14 @@ async function main(argv) {
|
|
|
477
925
|
const m = value();
|
|
478
926
|
if (m !== "light" && m !== "dark") fail("--mode is light or dark");
|
|
479
927
|
mode = m;
|
|
928
|
+
} else if (arg === "--format") {
|
|
929
|
+
const f = value();
|
|
930
|
+
if (!["auto", "shadcn", "stackmyth", "custom"].includes(f)) fail("--format is auto, shadcn, stackmyth or custom");
|
|
931
|
+
format = f;
|
|
932
|
+
} else if (arg === "--map") {
|
|
933
|
+
const m = /^(\w+)=(.+)$/.exec(value());
|
|
934
|
+
if (!m) fail("--map is <value>=<variable>, e.g. primary=--brand");
|
|
935
|
+
map[m[1]] = m[2];
|
|
480
936
|
} else if (arg === "--logo") logoUrl = value();
|
|
481
937
|
else if (arg === "--push") push = true;
|
|
482
938
|
else if (arg === "--clear") clear = true;
|
|
@@ -497,8 +953,17 @@ ${HELP}`);
|
|
|
497
953
|
return fail(`cannot read ${f}`);
|
|
498
954
|
}
|
|
499
955
|
});
|
|
500
|
-
const imported =
|
|
501
|
-
|
|
956
|
+
const imported = themeFromCss(sheets, { format, theme, mode, map: Object.keys(map).length > 0 ? map : void 0 });
|
|
957
|
+
if (logoUrl) imported.theme.logoUrl = logoUrl;
|
|
958
|
+
const warnings = [...imported.warnings];
|
|
959
|
+
if (imported.format === "stackmyth") {
|
|
960
|
+
const read = tokensFromStackmyth(sheets, { theme, mode });
|
|
961
|
+
if (Object.keys(read.tokens).length > 0) imported.theme.tokens = read.tokens;
|
|
962
|
+
warnings.push(...read.warnings);
|
|
963
|
+
}
|
|
964
|
+
process.stderr.write(`read as: ${imported.format}
|
|
965
|
+
`);
|
|
966
|
+
for (const w of warnings) process.stderr.write(`warning: ${w}
|
|
502
967
|
`);
|
|
503
968
|
if (Object.keys(imported.theme).length === 0) fail("no theme could be read from these stylesheets");
|
|
504
969
|
process.stdout.write(JSON.stringify(toWire(imported.theme), null, 2) + "\n");
|