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.
Files changed (72) hide show
  1. package/README.md +68 -3
  2. package/dist/{cauthy-BCTCqubt.d.ts → cauthy-Bn3iv50y.d.ts} +1 -1
  3. package/dist/{cauthy-hswMoS7B.d.mts → cauthy-C29HCvag.d.mts} +1 -1
  4. package/dist/cli.js +490 -25
  5. package/dist/client.d.mts +236 -0
  6. package/dist/client.d.ts +236 -0
  7. package/dist/client.js +331 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/client.mjs +297 -0
  10. package/dist/client.mjs.map +1 -0
  11. package/dist/express.d.mts +24 -0
  12. package/dist/express.d.ts +24 -0
  13. package/dist/express.js +102 -0
  14. package/dist/express.js.map +1 -0
  15. package/dist/express.mjs +76 -0
  16. package/dist/express.mjs.map +1 -0
  17. package/dist/fastify.d.mts +2 -2
  18. package/dist/fastify.d.ts +2 -2
  19. package/dist/fastify.js +3 -1
  20. package/dist/fastify.js.map +1 -1
  21. package/dist/fastify.mjs +3 -1
  22. package/dist/fastify.mjs.map +1 -1
  23. package/dist/hono.d.mts +23 -0
  24. package/dist/hono.d.ts +23 -0
  25. package/dist/hono.js +50 -0
  26. package/dist/hono.js.map +1 -0
  27. package/dist/hono.mjs +24 -0
  28. package/dist/hono.mjs.map +1 -0
  29. package/dist/index.d.mts +44 -6
  30. package/dist/index.d.ts +44 -6
  31. package/dist/index.js +668 -37
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +656 -37
  34. package/dist/index.mjs.map +1 -1
  35. package/dist/next.d.mts +24 -0
  36. package/dist/next.d.ts +24 -0
  37. package/dist/next.js +77 -0
  38. package/dist/next.js.map +1 -0
  39. package/dist/next.mjs +52 -0
  40. package/dist/next.mjs.map +1 -0
  41. package/dist/react.d.mts +546 -7
  42. package/dist/react.d.ts +546 -7
  43. package/dist/react.js +925 -14
  44. package/dist/react.js.map +1 -1
  45. package/dist/react.mjs +913 -10
  46. package/dist/react.mjs.map +1 -1
  47. package/dist/styles.css +271 -0
  48. package/dist/svelte.d.mts +26 -0
  49. package/dist/svelte.d.ts +26 -0
  50. package/dist/svelte.js +402 -0
  51. package/dist/svelte.js.map +1 -0
  52. package/dist/svelte.mjs +373 -0
  53. package/dist/svelte.mjs.map +1 -0
  54. package/dist/{system-DvcQjORr.d.ts → system-DhRr_WxR.d.ts} +139 -1
  55. package/dist/{system-CR8p4BjB.d.mts → system-ZD_Wa8Bm.d.mts} +139 -1
  56. package/dist/theme.d.mts +56 -1
  57. package/dist/theme.d.ts +56 -1
  58. package/dist/theme.js +181 -5
  59. package/dist/theme.js.map +1 -1
  60. package/dist/theme.mjs +176 -4
  61. package/dist/theme.mjs.map +1 -1
  62. package/dist/validate-B_iC-4y8.d.mts +19 -0
  63. package/dist/validate-B_iC-4y8.d.ts +19 -0
  64. package/dist/vue.d.mts +28 -0
  65. package/dist/vue.d.ts +28 -0
  66. package/dist/vue.js +420 -0
  67. package/dist/vue.js.map +1 -0
  68. package/dist/vue.mjs +391 -0
  69. package/dist/vue.mjs.map +1 -0
  70. package/package.json +68 -7
  71. package/dist/permissions-B-a3eN0h.d.mts +0 -52
  72. 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 env = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
16
- const issuer = (o.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
17
- const tenant = o.tenant ?? env("CAUTHY_TENANT") ?? "";
18
- const token = o.token ?? env("CAUTHY_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(env) {
51
- return Object.entries(env).map(([k, v]) => `${k}=${v}`).join("\n") + "\n";
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 env = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
57
- const issuer = (o.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
58
- const system = o.system ?? env("CAUTHY_SYSTEM") ?? "";
59
- const tenant = o.tenant ?? env("CAUTHY_TENANT") ?? "";
60
- const token = o.token ?? env("CAUTHY_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 env = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
288
- const issuer = (o.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
289
- const clientId = o.clientId ?? env("CAUTHY_CLIENT_ID") ?? "";
290
- const tenant = o.tenant ?? env("CAUTHY_TENANT") ?? "";
291
- const token = o.token ?? env("CAUTHY_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 --file <roles.json> [--system <name>] [--push]
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 Stackmyth theme and gives it to Cauthy's hosted pages.
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 declares --sm-* tokens. Repeat it, in the
442
- order the application loads them: Stackmyth's own first,
443
- the application's brand file after.
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 = themeFromStackmyth(sheets, { theme, mode, logoUrl });
501
- for (const w of imported.warnings) process.stderr.write(`warning: ${w}
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");