cauthy-auth 0.5.1 → 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 +492 -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 +672 -37
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +660 -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 +59 -1
  57. package/dist/theme.d.ts +59 -1
  58. package/dist/theme.js +185 -5
  59. package/dist/theme.js.map +1 -1
  60. package/dist/theme.mjs +180 -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/index.js CHANGED
@@ -22,14 +22,26 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  CauthyApiError: () => CauthyApiError,
24
24
  CauthyConfigError: () => CauthyConfigError,
25
+ CauthyTokenError: () => CauthyTokenError,
25
26
  CauthyWebhookError: () => CauthyWebhookError,
27
+ MIN_PASSWORD: () => MIN_PASSWORD,
26
28
  can: () => can,
29
+ checkCode: () => checkCode,
30
+ checkEmail: () => checkEmail,
31
+ checkName: () => checkName,
32
+ checkNewPassword: () => checkNewPassword,
33
+ checkPassword: () => checkPassword,
27
34
  connectSystem: () => connectSystem,
28
35
  createCauthy: () => createCauthy,
29
36
  createSystem: () => createSystem,
37
+ definePolicy: () => definePolicy,
38
+ policySource: () => policySource,
39
+ pullPolicy: () => pullPolicy,
30
40
  pushPolicy: () => pushPolicy,
31
41
  pushTheme: () => pushTheme,
32
42
  themeFromStackmyth: () => themeFromStackmyth,
43
+ tokensFromStackmyth: () => tokensFromStackmyth,
44
+ verifyToken: () => verifyToken,
33
45
  verifyWebhook: () => verifyWebhook
34
46
  });
35
47
  module.exports = __toCommonJS(src_exports);
@@ -145,10 +157,92 @@ function applies(d, o) {
145
157
  if (o.mode === "dark" && LIGHT.test(scope)) return false;
146
158
  const named = NAMED.exec(scope);
147
159
  if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false;
148
- if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host/.test(scope)) return false;
160
+ if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host|^\.(dark|light)\b/.test(scope)) return false;
149
161
  }
150
162
  return true;
151
163
  }
164
+ var TOKEN_CHARS = /^[A-Za-z0-9 #%.,()/'"_+*-]+$/;
165
+ var TOKEN_FUNCS = /* @__PURE__ */ new Set([
166
+ "var",
167
+ "calc",
168
+ "min",
169
+ "max",
170
+ "clamp",
171
+ "rgb",
172
+ "rgba",
173
+ "hsl",
174
+ "hsla",
175
+ "hwb",
176
+ "lab",
177
+ "lch",
178
+ "oklab",
179
+ "oklch",
180
+ "color",
181
+ "color-mix",
182
+ "light-dark",
183
+ "cubic-bezier",
184
+ "steps",
185
+ "linear",
186
+ "linear-gradient",
187
+ "radial-gradient",
188
+ "conic-gradient",
189
+ "repeating-linear-gradient",
190
+ "repeating-radial-gradient",
191
+ "drop-shadow",
192
+ "blur",
193
+ "translate",
194
+ "translatex",
195
+ "translatey",
196
+ "scale",
197
+ "rotate"
198
+ ]);
199
+ function tokenValue(value) {
200
+ const v = value.replace(/\s+/g, " ").trim();
201
+ if (v === "" || v.length > 400 || !TOKEN_CHARS.test(v)) return null;
202
+ for (const m of v.matchAll(/([A-Za-z-]+)\(/g)) if (!TOKEN_FUNCS.has(m[1].toLowerCase())) return null;
203
+ if (/^\(|[ ,(]\(/.test(v)) return null;
204
+ return v;
205
+ }
206
+ var THEME_SCOPE = /^(?:\s*(?::root|html|body|\[data-(?:theme|mode|color-scheme)(?:=["']?[\w-]+["']?)?\])\s*,?)+$/i;
207
+ function tokensFromStackmyth(css, options = {}) {
208
+ const mode = options.mode ?? "light";
209
+ const all = (Array.isArray(css) ? css : [css]).flatMap(declarations);
210
+ 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);
211
+ const vars = /* @__PURE__ */ new Map();
212
+ for (const d of all) {
213
+ if (!applies(d, { mode, theme: named })) continue;
214
+ if (!d.within.every((w) => w.startsWith("@") || THEME_SCOPE.test(w))) continue;
215
+ vars.set(d.name, d.value);
216
+ }
217
+ const inline = (value, depth = 0) => {
218
+ if (depth > 16) return null;
219
+ let failed = false;
220
+ let out2 = value;
221
+ for (let guard = 0; guard < 64; guard++) {
222
+ const next = out2.replace(/var\(\s*(--(?!sm-)[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/, (_m, name, fallback) => {
223
+ const known = vars.get(name);
224
+ const got = known !== void 0 ? inline(known, depth + 1) : fallback !== void 0 ? inline(fallback.trim(), depth + 1) : null;
225
+ if (got === null) failed = true;
226
+ return got ?? "";
227
+ });
228
+ if (failed) return null;
229
+ if (next === out2) break;
230
+ out2 = next;
231
+ }
232
+ return out2;
233
+ };
234
+ const tokens = {};
235
+ const warnings = [];
236
+ for (const [name, raw] of vars) {
237
+ if (!/^--sm-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) continue;
238
+ const written = inline(raw);
239
+ const value = written === null ? null : tokenValue(written);
240
+ if (value === null) warnings.push(`${name}: "${raw.length > 60 ? raw.slice(0, 57) + "\u2026" : raw}" is not a value Cauthy takes`);
241
+ else tokens[name] = value;
242
+ }
243
+ if (Object.keys(tokens).length === 0) warnings.push("no --sm-* token was found in these stylesheets");
244
+ return { tokens, warnings };
245
+ }
152
246
  function resolve(value, vars, seen = 0) {
153
247
  if (seen > 16) return null;
154
248
  let unresolved = false;
@@ -293,7 +387,10 @@ function toWire(theme) {
293
387
  panelColors: "panel_colors",
294
388
  panelText: "panel_text",
295
389
  panelIconUrl: "panel_icon_url",
296
- texts: "texts"
390
+ texts: "texts",
391
+ inputBackground: "input_background",
392
+ inputBorder: "input_border",
393
+ tokens: "tokens"
297
394
  };
298
395
  const wire = {};
299
396
  for (const [k, v] of Object.entries(theme)) {
@@ -325,18 +422,21 @@ function fromWire(wire) {
325
422
  panel_colors: "panelColors",
326
423
  panel_text: "panelText",
327
424
  panel_icon_url: "panelIconUrl",
328
- texts: "texts"
425
+ texts: "texts",
426
+ input_background: "inputBackground",
427
+ input_border: "inputBorder",
428
+ tokens: "tokens"
329
429
  };
330
430
  const theme = {};
331
431
  for (const [k, v] of Object.entries(wire ?? {})) if (names[k] && v !== void 0 && v !== null && v !== "") theme[names[k]] = v;
332
432
  return theme;
333
433
  }
334
434
  async function pushTheme(o) {
335
- const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
336
- const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
337
- const clientId = o.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
338
- const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
339
- const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
435
+ const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
436
+ const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
437
+ const clientId = o.clientId ?? env4("CAUTHY_CLIENT_ID") ?? "";
438
+ const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
439
+ const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
340
440
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
341
441
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
342
442
  }
@@ -516,6 +616,11 @@ function createSystem(given = {}) {
516
616
  return fromWire(await data("GET", `/v1/oauth/clients/${id(of)}/theme`));
517
617
  }
518
618
  },
619
+ policy: {
620
+ async get(system) {
621
+ return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
622
+ }
623
+ },
519
624
  async publicJwks() {
520
625
  if (!key) return { keys: [] };
521
626
  const { kty, n, e } = asJwk(key) ?? await (0, import_jose2.exportJWK)(await signingKey());
@@ -524,6 +629,45 @@ function createSystem(given = {}) {
524
629
  };
525
630
  }
526
631
 
632
+ // src/validate.ts
633
+ var MIN_PASSWORD = 8;
634
+ var MAX_PASSWORD = 256;
635
+ var MAX_NAME = 120;
636
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
637
+ function checkEmail(value) {
638
+ const v = typeof value === "string" ? value.trim() : "";
639
+ if (v === "") return { field: "email", code: "required", message: "Enter your email address." };
640
+ if (v.length > 254 || !EMAIL.test(v)) return { field: "email", code: "invalid_email", message: "That does not look like an email address." };
641
+ return null;
642
+ }
643
+ function checkPassword(value) {
644
+ if (typeof value !== "string" || value === "") return { field: "password", code: "required", message: "Enter your password." };
645
+ if (value.length > MAX_PASSWORD) return { field: "password", code: "too_long", message: "That password is too long." };
646
+ return null;
647
+ }
648
+ function checkNewPassword(value) {
649
+ const basic = checkPassword(value);
650
+ if (basic) return basic;
651
+ if (value.length < MIN_PASSWORD) {
652
+ return { field: "password", code: "too_short", message: `Use at least ${MIN_PASSWORD} characters.` };
653
+ }
654
+ return null;
655
+ }
656
+ function checkName(value) {
657
+ if (value === void 0 || value === null || value === "") return null;
658
+ if (typeof value !== "string" || value.trim().length > MAX_NAME) return { field: "name", code: "too_long", message: `Use at most ${MAX_NAME} characters.` };
659
+ return null;
660
+ }
661
+ function checkCode(value) {
662
+ const v = typeof value === "string" ? value.trim() : "";
663
+ if (v === "") return { field: "code", code: "required", message: "Enter the code." };
664
+ if (v.length > 40) return { field: "code", code: "too_long", message: "That is not a code." };
665
+ return null;
666
+ }
667
+ function firstProblem(...checks) {
668
+ return checks.find((c) => c !== null) ?? null;
669
+ }
670
+
527
671
  // src/webhook.ts
528
672
  var import_jose3 = require("jose");
529
673
  var CauthyWebhookError = class extends Error {
@@ -545,8 +689,8 @@ function keysOf(issuer) {
545
689
  }
546
690
  var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
547
691
  async function verifyWebhook(request, options = {}) {
548
- const env3 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
549
- const issuer = (options.issuer ?? env3 ?? "").replace(/\/+$/, "");
692
+ const env4 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
693
+ const issuer = (options.issuer ?? env4 ?? "").replace(/\/+$/, "");
550
694
  if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
551
695
  const signature = request.headers.get("x-cauthy-signature-jwt");
552
696
  if (!signature) throw new CauthyWebhookError("no signature");
@@ -597,9 +741,19 @@ async function verifyWebhook(request, options = {}) {
597
741
 
598
742
  // src/cauthy.ts
599
743
  var SESSION_DAYS = 30;
600
- var FLOW_SECONDS = 600;
744
+ var FLOW_SECONDS = 3600;
745
+ var AGAIN_SECONDS = 120;
601
746
  var SKEW_SECONDS = 30;
602
747
  var ORGANIZATIONS_TTL_MS = 6e4;
748
+ var LOOK_TTL_MS = 6e4;
749
+ var PROVIDER_ID = /^(custom:)?[a-z0-9_-]{1,40}$/;
750
+ function providerIn(segment) {
751
+ try {
752
+ return decodeURIComponent(segment);
753
+ } catch {
754
+ return "";
755
+ }
756
+ }
603
757
  function env2(name) {
604
758
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
605
759
  }
@@ -632,6 +786,7 @@ function createCauthy(given = {}) {
632
786
  const clientSecret = given.clientSecret ?? env2("CAUTHY_CLIENT_SECRET");
633
787
  const secret = given.secret ?? env2("CAUTHY_SECRET") ?? "";
634
788
  const system = given.system ?? env2("CAUTHY_SYSTEM") ?? "";
789
+ const tenant = given.tenant ?? env2("CAUTHY_TENANT") ?? "";
635
790
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
636
791
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
637
792
  if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
@@ -640,18 +795,26 @@ function createCauthy(given = {}) {
640
795
  const afterSignOut = localPath(given.afterSignOut, "/");
641
796
  const cookieName = given.cookieName ?? "cauthy_session";
642
797
  const flowCookie = `${cookieName}_flow`;
798
+ const againCookie = `${cookieName}_again`;
799
+ const stepCookie = `${cookieName}_step`;
643
800
  const doFetch = (...args) => (given.fetch ?? fetch)(...args);
644
801
  const key = keyOf(secret);
802
+ const pages = {
803
+ signIn: given.pages?.signIn ? localPath(given.pages.signIn, "/") : void 0,
804
+ resetPassword: given.pages?.resetPassword ? localPath(given.pages.resetPassword, "/") : void 0,
805
+ verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0
806
+ };
807
+ const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch });
645
808
  const originOf = (request) => (given.baseURL ?? new URL(request.url).origin).replace(/\/+$/, "");
646
809
  const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
647
810
  async function token(form) {
648
- const body = new URLSearchParams({ client_id: clientId, ...form });
649
- if (clientSecret) body.set("client_secret", clientSecret);
811
+ const body2 = new URLSearchParams({ client_id: clientId, ...form });
812
+ if (clientSecret) body2.set("client_secret", clientSecret);
650
813
  try {
651
814
  const res = await doFetch(`${issuer}/v1/oauth/token`, {
652
815
  method: "POST",
653
816
  headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
654
- body
817
+ body: body2
655
818
  });
656
819
  if (res.ok) return { ok: true, body: await res.json() };
657
820
  return { ok: false, ended: res.status >= 400 && res.status < 500 };
@@ -681,12 +844,12 @@ function createCauthy(given = {}) {
681
844
  return null;
682
845
  }
683
846
  }
684
- async function stored(body) {
685
- if (typeof body?.access_token !== "string" || typeof body?.refresh_token !== "string") return null;
686
- const who = await userinfo(body.access_token);
847
+ async function stored(body2) {
848
+ if (typeof body2?.access_token !== "string" || typeof body2?.refresh_token !== "string") return null;
849
+ const who = await userinfo(body2.access_token);
687
850
  if (who === null || who.u.id === "") return null;
688
- const lifetime = typeof body.expires_in === "number" && body.expires_in > 0 ? body.expires_in : 300;
689
- return { at: body.access_token, rt: body.refresh_token, exp: Math.floor(Date.now() / 1e3) + lifetime, ...who };
851
+ const lifetime = typeof body2.expires_in === "number" && body2.expires_in > 0 ? body2.expires_in : 300;
852
+ return { at: body2.access_token, rt: body2.refresh_token, exp: Math.floor(Date.now() / 1e3) + lifetime, ...who };
690
853
  }
691
854
  function toSession(s) {
692
855
  return { user: s.u, tenantId: s.tid, roles: s.roles, accessToken: s.at, expiresAt: new Date(s.exp * 1e3).toISOString() };
@@ -760,8 +923,8 @@ function createCauthy(given = {}) {
760
923
  const url = `${issuer}/v1/oauth/organizations${system ? `?system=${encodeURIComponent(system)}` : ""}`;
761
924
  const res = await doFetch(url, { headers: { authorization: `Bearer ${session.accessToken}` } });
762
925
  if (!res.ok) throw new Error(`cauthy-auth: reading organizations: ${res.status}`);
763
- const body = await res.json();
764
- const value = (body.data ?? []).map(
926
+ const body2 = await res.json();
927
+ const value = (body2.data ?? []).map(
765
928
  (o) => ({
766
929
  id: String(o.id),
767
930
  system: String(o.system ?? ""),
@@ -806,20 +969,28 @@ function createCauthy(given = {}) {
806
969
  if (prompt === "login" || prompt === "none") q.set("prompt", prompt);
807
970
  const locale = url.searchParams.get("locale") ?? url.searchParams.get("ui_locales");
808
971
  if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set("ui_locales", locale);
809
- return redirect(`${issuer}/v1/oauth/authorize?${q}`, [
972
+ const passkey = url.searchParams.get("method") === "passkey" ? "#passkey" : "";
973
+ return redirect(`${issuer}/v1/oauth/authorize?${q}${passkey}`, [
810
974
  writeCookie(flowCookie, await seal(flow, await key), attrs(request, FLOW_SECONDS))
811
975
  ]);
812
976
  }
813
977
  function failed(request, reason) {
814
978
  const h = new Headers({ "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
815
979
  h.append("set-cookie", writeCookie(flowCookie, "", attrs(request, 0)));
980
+ h.append("set-cookie", writeCookie(againCookie, "", attrs(request, 0)));
816
981
  return new Response(`Sign-in could not be completed (${reason}). Go back and try again.`, { status: 400, headers: h });
817
982
  }
818
983
  async function callback(request, url) {
819
984
  const had = readCookies(request.headers.get("cookie"));
820
985
  const sealed = had.get(flowCookie);
821
986
  const flow = sealed ? await unseal(sealed, await key) : null;
822
- if (flow === null || url.searchParams.get("state") !== flow.state) return failed(request, "state_mismatch");
987
+ if (flow === null || url.searchParams.get("state") !== flow.state) {
988
+ if (had.get(againCookie) === void 0) {
989
+ const back = flow?.returnTo ?? afterSignIn;
990
+ return redirect(`${basePath}/login?return_to=${encodeURIComponent(back)}`, [writeCookie(againCookie, "1", attrs(request, AGAIN_SECONDS))]);
991
+ }
992
+ return failed(request, "state_mismatch");
993
+ }
823
994
  const error = url.searchParams.get("error");
824
995
  if (error !== null) {
825
996
  if (error === "login_required") return redirect(flow.returnTo, [writeCookie(flowCookie, "", attrs(request, 0))]);
@@ -835,28 +1006,332 @@ function createCauthy(given = {}) {
835
1006
  });
836
1007
  const s = res.ok ? await stored(res.body) : null;
837
1008
  if (s === null) return failed(request, "exchange_failed");
838
- return redirect(flow.returnTo, [...await write(request, s, had), writeCookie(flowCookie, "", attrs(request, 0))]);
1009
+ return redirect(flow.returnTo, [...await write(request, s, had), writeCookie(flowCookie, "", attrs(request, 0)), writeCookie(againCookie, "", attrs(request, 0))]);
839
1010
  }
840
1011
  async function logout(request) {
841
1012
  const { had, value } = await read(request);
842
1013
  if (value !== null) {
843
- const body = new URLSearchParams({ client_id: clientId, token: value.rt });
844
- if (clientSecret) body.set("client_secret", clientSecret);
1014
+ const body2 = new URLSearchParams({ client_id: clientId, token: value.rt });
1015
+ if (clientSecret) body2.set("client_secret", clientSecret);
845
1016
  await doFetch(`${issuer}/v1/oauth/revoke`, {
846
1017
  method: "POST",
847
1018
  headers: { "content-type": "application/x-www-form-urlencoded" },
848
- body
1019
+ body: body2
849
1020
  }).catch(() => void 0);
850
1021
  orgCache.delete(value.at);
851
1022
  }
852
1023
  const q = new URLSearchParams({ client_id: clientId, post_logout_redirect_uri: `${originOf(request)}${afterSignOut}` });
853
1024
  return redirect(`${issuer}/v1/oauth/logout?${q}`, clearParts(cookieName, attrs(request, 0), had));
854
1025
  }
1026
+ const STEP_SECONDS = 600;
1027
+ const problem = (status2, code, message, field, headers) => json({ error: { code, message, ...field ? { field } : {} } }, status2, headers);
1028
+ const invalid = (p) => problem(400, p.code, p.message, p.field);
1029
+ function ownRequest(request) {
1030
+ const origin = request.headers.get("origin");
1031
+ if (origin !== null) return origin === originOf(request) || origin === new URL(request.url).origin;
1032
+ return (request.headers.get("sec-fetch-site") ?? "same-origin") === "same-origin";
1033
+ }
1034
+ async function body(request) {
1035
+ if (!(request.headers.get("content-type") ?? "").includes("application/json")) return null;
1036
+ try {
1037
+ const b = await request.json();
1038
+ return typeof b === "object" && b !== null && !Array.isArray(b) ? b : null;
1039
+ } catch {
1040
+ return null;
1041
+ }
1042
+ }
1043
+ const clientIp = given.clientIp ?? ((request) => request.headers.get("x-forwarded-for")?.split(",").pop()?.trim() || void 0);
1044
+ let noProofUntil = 0;
1045
+ async function passedOn(request) {
1046
+ const ip = clientIp(request);
1047
+ if (!ip || Date.now() < noProofUntil) return {};
1048
+ try {
1049
+ return { "x-cauthy-client-ip": ip, "x-cauthy-system-token": await ownSystem.token() };
1050
+ } catch {
1051
+ noProofUntil = Date.now() + 5 * 6e4;
1052
+ return {};
1053
+ }
1054
+ }
1055
+ async function ask(request, method, path, payload, page, bearer2) {
1056
+ try {
1057
+ const res = await doFetch(`${issuer}${path}`, {
1058
+ method,
1059
+ headers: {
1060
+ "x-tenant-key": tenant,
1061
+ accept: "application/json",
1062
+ // A link Cauthy mails opens the application's page for it when it
1063
+ // has one; otherwise Cauthy's own. Either way in the reader's language.
1064
+ ...page ? { "x-cauthy-link-to": `${originOf(request)}${page}`, "x-cauthy-client-id": clientId } : { "x-cauthy-links": "hosted" },
1065
+ ...await passedOn(request),
1066
+ ...bearer2 ? { authorization: `Bearer ${bearer2}` } : {},
1067
+ ...request.headers.get("accept-language") ? { "accept-language": request.headers.get("accept-language") } : {},
1068
+ ...request.headers.get("user-agent") ? { "user-agent": request.headers.get("user-agent") } : {},
1069
+ ...payload === void 0 ? {} : { "content-type": "application/json" }
1070
+ },
1071
+ body: payload === void 0 ? void 0 : JSON.stringify(payload)
1072
+ });
1073
+ const b = res.status === 204 || res.status === 202 ? null : await res.json().catch(() => null);
1074
+ if (res.ok) return { status: res.status, data: b?.data ?? b };
1075
+ const first = b?.errors?.[0];
1076
+ return { status: res.status, data: null, error: { code: String(first?.code ?? "error"), message: String(first?.message ?? "The request could not be completed.") } };
1077
+ } catch {
1078
+ return { status: 503, data: null, error: { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." } };
1079
+ }
1080
+ }
1081
+ const accountOf = (d) => ({
1082
+ id: String(d?.id ?? ""),
1083
+ email: String(d?.email ?? ""),
1084
+ name: String(d?.name ?? ""),
1085
+ emailVerified: d?.email_verified === true,
1086
+ hasPassword: d?.has_password === true,
1087
+ mfaEnabled: d?.mfa_enabled === true
1088
+ });
1089
+ async function signedInAs(request) {
1090
+ const r = await resolve2(request);
1091
+ const headers = new Headers();
1092
+ for (const c of r.cookies) headers.append("set-cookie", c);
1093
+ if (r.session === null) return { response: problem(401, "unauthenticated", "Sign in first.", void 0, headers) };
1094
+ return { token: r.session.accessToken, kept: r.cookies.length === 0 ? (await read(request)).value : null, headers };
1095
+ }
1096
+ async function signedIn(request, tokens, extra = []) {
1097
+ const s = await stored(tokens);
1098
+ if (s === null) return problem(502, "unavailable", "Sign-in is unavailable right now. Try again in a moment.");
1099
+ const h = new Headers();
1100
+ for (const c of [...await write(request, s, readCookies(request.headers.get("cookie"))), ...extra]) h.append("set-cookie", c);
1101
+ return json({ status: "signed_in", session: await toPublic(toSession(s)) }, 200, h);
1102
+ }
1103
+ let lookCache = null;
1104
+ async function ownRoute(request, url, route) {
1105
+ const post = request.method === "POST";
1106
+ const own = ["/sign-in/email", "/sign-in/mfa", "/sign-up/email", "/forgot-password", "/reset-password", "/verify-email", "/sign-in/magic-link", "/sign-in/magic-link/verify", "/sign-in/email-code", "/me", "/me/password", "/me/mfa/setup", "/me/mfa/verify", "/me/mfa/disable", "/me/mfa/recovery-codes", "/me/sessions", "/me/sessions/end", "/sign-out", "/methods", "/look"].includes(route) || route.startsWith("/sign-in/social/") || route.startsWith("/callback/");
1107
+ if (!own) return null;
1108
+ if (tenant === "") {
1109
+ return problem(500, "not_configured", "cauthy-auth: tenant (CAUTHY_TENANT) is required for the application's own sign-in screens");
1110
+ }
1111
+ if (route === "/look" && !post) {
1112
+ if (lookCache === null || lookCache.until < Date.now()) {
1113
+ const res = await ask(request, "GET", `/v1/auth/look/${encodeURIComponent(clientId)}`);
1114
+ lookCache = { until: Date.now() + LOOK_TTL_MS, look: res.error ? {} : res.data ?? {} };
1115
+ }
1116
+ return json(lookCache.look);
1117
+ }
1118
+ if (route === "/methods" && !post) {
1119
+ const [providers, settings] = await Promise.all([ask(request, "GET", "/v1/auth/social/providers"), ask(request, "GET", "/v1/auth/settings")]);
1120
+ const list = Array.isArray(providers.data) ? providers.data : [];
1121
+ return json({
1122
+ providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? "") })).filter((p) => p.id !== ""),
1123
+ signUp: settings.data?.allow_signup !== false,
1124
+ // A sign-in link needs a page of the application to open.
1125
+ magicLink: settings.data?.allow_magic_link === true && pages.signIn !== void 0,
1126
+ // Asked for on Cauthy's page: the person goes there for that step and comes back.
1127
+ passkey: true
1128
+ });
1129
+ }
1130
+ if (route.startsWith("/sign-in/social/") && !post) {
1131
+ const provider = providerIn(route.slice("/sign-in/social/".length));
1132
+ if (!PROVIDER_ID.test(provider)) return new Response("Not found", { status: 404 });
1133
+ const returnTo = localPath(url.searchParams.get("return_to"), afterSignIn);
1134
+ const callback2 = `${basePath}/callback/${provider}`;
1135
+ const started = await ask(request, "GET", `/v1/auth/social/${provider}?redirect_uri=${encodeURIComponent(`${originOf(request)}${callback2}`)}`, void 0, callback2);
1136
+ if (started.error || typeof started.data?.redirect_url !== "string") {
1137
+ return redirect(`${returnTo}${returnTo.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(started.error?.code ?? "provider_unavailable")}`);
1138
+ }
1139
+ const step = { kind: "social", token: String(started.data.state ?? ""), provider, returnTo };
1140
+ return redirect(started.data.redirect_url, [writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS))]);
1141
+ }
1142
+ if (route.startsWith("/callback/") && !post) {
1143
+ const provider = providerIn(route.slice("/callback/".length));
1144
+ const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
1145
+ const step = sealed ? await unseal(sealed, await key) : null;
1146
+ const clear = writeCookie(stepCookie, "", attrs(request, 0));
1147
+ const back = step?.returnTo ?? afterSignIn;
1148
+ const failedTo = (code2) => redirect(`${back}${back.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code2)}`, [clear]);
1149
+ if (step === null || step.kind !== "social" || step.provider !== provider || url.searchParams.get("state") !== step.token) return failedTo("state_mismatch");
1150
+ const code = url.searchParams.get("code");
1151
+ if (!code) return failedTo(url.searchParams.get("error") === "access_denied" ? "cancelled" : "provider_refused");
1152
+ const q = new URLSearchParams({ code, state: step.token, redirect_uri: `${originOf(request)}${basePath}/callback/${provider}` });
1153
+ const done = await ask(request, "GET", `/v1/auth/social/${provider}/callback?${q}`);
1154
+ if (done.error) return failedTo(done.error.code);
1155
+ const s = await stored(done.data);
1156
+ if (s === null) return failedTo("unavailable");
1157
+ return redirect(back, [...await write(request, s, readCookies(request.headers.get("cookie"))), clear]);
1158
+ }
1159
+ if ((route === "/me" || route === "/me/sessions") && !post) {
1160
+ const mine = await signedInAs(request);
1161
+ if ("response" in mine) return mine.response;
1162
+ const res = await ask(request, "GET", route === "/me" ? "/v1/auth/me" : "/v1/auth/me/sessions", void 0, void 0, mine.token);
1163
+ if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
1164
+ if (route === "/me/sessions") {
1165
+ const list = Array.isArray(res.data) ? res.data : [];
1166
+ return json({ sessions: list.map((x) => ({ device: String(x.device ?? ""), ip: String(x.ip ?? ""), createdAt: String(x.created_at ?? ""), expiresAt: String(x.expires_at ?? "") })) }, 200, mine.headers);
1167
+ }
1168
+ return json({ account: accountOf(res.data) }, 200, mine.headers);
1169
+ }
1170
+ if (!post) return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
1171
+ if (!ownRequest(request)) return problem(403, "cross_site", "This request did not come from this application.");
1172
+ if (route === "/sign-out") {
1173
+ const { had, value } = await read(request);
1174
+ if (value !== null) {
1175
+ const form = new URLSearchParams({ client_id: clientId, token: value.rt });
1176
+ if (clientSecret) form.set("client_secret", clientSecret);
1177
+ await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }).catch(() => void 0);
1178
+ orgCache.delete(value.at);
1179
+ }
1180
+ const h = new Headers();
1181
+ for (const c of clearParts(cookieName, attrs(request, 0), had)) h.append("set-cookie", c);
1182
+ return json({ status: "signed_out" }, 200, h);
1183
+ }
1184
+ const b = await body(request);
1185
+ if (b === null) return problem(400, "bad_request", "Send the form as JSON.");
1186
+ if (route === "/sign-in/email") {
1187
+ const bad = firstProblem(checkEmail(b.email), checkPassword(b.password));
1188
+ if (bad) return invalid(bad);
1189
+ const res = await ask(request, "POST", "/v1/auth/login", { email: String(b.email).trim(), password: b.password });
1190
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1191
+ if (res.data?.mfa_required === true) {
1192
+ const step = { kind: "mfa", token: String(res.data.mfa_token ?? ""), returnTo: afterSignIn };
1193
+ return json({ status: "mfa_required" }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
1194
+ }
1195
+ return signedIn(request, res.data);
1196
+ }
1197
+ if (route === "/sign-in/mfa") {
1198
+ const bad = checkCode(b.code);
1199
+ if (bad) return invalid(bad);
1200
+ const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
1201
+ const step = sealed ? await unseal(sealed, await key) : null;
1202
+ if (step === null || step.kind !== "mfa") return problem(400, "step_expired", "Sign in again: the code was asked for too long ago.");
1203
+ const res = await ask(request, "POST", "/v1/auth/mfa/challenge", { mfa_token: step.token, code: String(b.code).trim() });
1204
+ if (res.error) return problem(res.status, res.error.code, res.error.message, "code");
1205
+ return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))]);
1206
+ }
1207
+ if (route === "/sign-up/email") {
1208
+ const bad = firstProblem(checkName(b.name), checkEmail(b.email), checkNewPassword(b.password));
1209
+ if (bad) return invalid(bad);
1210
+ const email = String(b.email).trim();
1211
+ const made = await ask(request, "POST", "/v1/auth/register", { email, password: b.password, display_name: typeof b.name === "string" ? b.name.trim() : "" }, pages.verifyEmail);
1212
+ if (made.error) return problem(made.status, made.error.code, made.error.message);
1213
+ if (made.data?.verification_required === true) return json({ status: "verification_required", email });
1214
+ const res = await ask(request, "POST", "/v1/auth/login", { email, password: b.password });
1215
+ if (res.error || res.data?.mfa_required === true) return json({ status: "verification_required", email });
1216
+ return signedIn(request, res.data);
1217
+ }
1218
+ if (route === "/forgot-password") {
1219
+ const bad = checkEmail(b.email);
1220
+ if (bad) return invalid(bad);
1221
+ await ask(request, "POST", "/v1/auth/password/reset", { email: String(b.email).trim() }, pages.resetPassword);
1222
+ return json({ status: "sent" });
1223
+ }
1224
+ if (route.startsWith("/me")) {
1225
+ const mine = await signedInAs(request);
1226
+ if ("response" in mine) return mine.response;
1227
+ const as = (path, payload, method = "POST") => ask(request, method, path, payload, void 0, mine.token);
1228
+ const refused = (res, field) => problem(res.status, res.error.code, res.error.message, field, mine.headers);
1229
+ const password = typeof b.password === "string" ? b.password : "";
1230
+ if (route === "/me") {
1231
+ const bad = checkName(b.name);
1232
+ if (bad) return invalid(bad);
1233
+ const res = await as("/v1/auth/me", { name: typeof b.name === "string" ? b.name.trim() : "" }, "PATCH");
1234
+ if (res.error) return refused(res, "name");
1235
+ const h = mine.headers;
1236
+ if (mine.kept !== null) {
1237
+ const again = await stored({ access_token: mine.kept.at, refresh_token: mine.kept.rt, expires_in: Math.max(1, mine.kept.exp - Math.floor(Date.now() / 1e3)) });
1238
+ if (again !== null) for (const c of await write(request, again, readCookies(request.headers.get("cookie")))) h.append("set-cookie", c);
1239
+ }
1240
+ return json({ status: "saved", account: accountOf(res.data) }, 200, h);
1241
+ }
1242
+ if (route === "/me/password") {
1243
+ const bad = checkNewPassword(b.newPassword);
1244
+ if (bad) return problem(400, bad.code, bad.message, "newPassword");
1245
+ const res = await as("/v1/auth/me/password", { current_password: password, new_password: b.newPassword });
1246
+ if (res.error) return refused(res, res.error.code === "wrong_password" ? "password" : res.error.code === "weak_password" ? "newPassword" : void 0);
1247
+ const next = await stored(res.data);
1248
+ const h = new Headers();
1249
+ const had = readCookies(request.headers.get("cookie"));
1250
+ for (const c of next !== null ? await write(request, next, had) : clearParts(cookieName, attrs(request, 0), had)) h.append("set-cookie", c);
1251
+ return json({ status: "changed", signedIn: next !== null }, 200, h);
1252
+ }
1253
+ if (route === "/me/mfa/setup") {
1254
+ const res = await as("/v1/auth/mfa/setup", { password });
1255
+ if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
1256
+ return json({ status: "started", secret: String(res.data?.secret ?? ""), otpauthUri: String(res.data?.otpauth_uri ?? "") }, 200, mine.headers);
1257
+ }
1258
+ if (route === "/me/mfa/verify") {
1259
+ const bad = checkCode(b.code);
1260
+ if (bad) return invalid(bad);
1261
+ const res = await as("/v1/auth/mfa/verify", { code: String(b.code).trim() });
1262
+ if (res.error) return refused(res, "code");
1263
+ return json({ status: "enabled", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
1264
+ }
1265
+ if (route === "/me/mfa/disable") {
1266
+ const res = await as("/v1/auth/me/mfa/disable", { password });
1267
+ if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
1268
+ return json({ status: "disabled" }, 200, mine.headers);
1269
+ }
1270
+ if (route === "/me/mfa/recovery-codes") {
1271
+ const res = await as("/v1/auth/me/mfa/recovery-codes", { password });
1272
+ if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
1273
+ return json({ status: "renewed", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
1274
+ }
1275
+ if (route === "/me/sessions/end") {
1276
+ const res = await as("/v1/auth/me/sessions", void 0, "DELETE");
1277
+ if (res.error) return refused(res);
1278
+ const h = new Headers();
1279
+ for (const c of clearParts(cookieName, attrs(request, 0), readCookies(request.headers.get("cookie")))) h.append("set-cookie", c);
1280
+ orgCache.delete(mine.token);
1281
+ return json({ status: "signed_out" }, 200, h);
1282
+ }
1283
+ return null;
1284
+ }
1285
+ const isToken = (v) => typeof v === "string" && v !== "" && v.length <= 512;
1286
+ const badLink = () => problem(422, "invalid_token", "This link is not valid, or it has expired.");
1287
+ if (route === "/reset-password") {
1288
+ if (!isToken(b.token)) return badLink();
1289
+ const bad = checkNewPassword(b.password);
1290
+ if (bad) return invalid(bad);
1291
+ const res = await ask(request, "POST", "/v1/auth/password/change", { token: String(b.token), new_password: b.password });
1292
+ if (res.error) return problem(res.status, res.error.code, res.error.message, res.error.code === "weak_password" ? "password" : void 0);
1293
+ return json({ status: "changed" });
1294
+ }
1295
+ if (route === "/verify-email") {
1296
+ if (!isToken(b.token)) return badLink();
1297
+ const res = await ask(request, "POST", "/v1/auth/email/verify", { token: String(b.token) });
1298
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1299
+ return json({ status: "verified" });
1300
+ }
1301
+ if (route === "/sign-in/magic-link") {
1302
+ const bad = checkEmail(b.email);
1303
+ if (bad) return invalid(bad);
1304
+ if (pages.signIn === void 0) return problem(500, "not_configured", "cauthy-auth: pages.signIn is required for sign-in links: it is the page the link opens");
1305
+ const email = String(b.email).trim();
1306
+ await ask(request, "POST", "/v1/auth/magic-link/request", { email }, pages.signIn);
1307
+ const step = { kind: "code", token: email, returnTo: afterSignIn };
1308
+ return json({ status: "sent" }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
1309
+ }
1310
+ if (route === "/sign-in/email-code") {
1311
+ const bad = checkCode(b.code);
1312
+ if (bad) return invalid(bad);
1313
+ const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
1314
+ const step = sealed ? await unseal(sealed, await key) : null;
1315
+ if (step === null || step.kind !== "code") return problem(400, "step_expired", "Ask for a new link: this one was asked for too long ago.");
1316
+ const res = await ask(request, "POST", "/v1/auth/magic-link/verify-code", { email: step.token, code: String(b.code).trim() });
1317
+ if (res.error) return problem(res.status, res.error.code, res.error.message, "code");
1318
+ return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))]);
1319
+ }
1320
+ if (route === "/sign-in/magic-link/verify") {
1321
+ if (!isToken(b.token)) return badLink();
1322
+ const res = await ask(request, "POST", "/v1/auth/magic-link/verify", { token: String(b.token) });
1323
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1324
+ return signedIn(request, res.data);
1325
+ }
1326
+ return null;
1327
+ }
855
1328
  async function handler(request) {
856
1329
  const url = new URL(request.url);
857
1330
  if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return new Response("Not found", { status: 404 });
858
1331
  const route = url.pathname.slice(basePath.length).replace(/\/+$/, "") || "/";
859
1332
  const get = request.method === "GET" || request.method === "HEAD";
1333
+ const mine = await ownRoute(request, url, route);
1334
+ if (mine !== null) return mine;
860
1335
  if (route === "/login" && get) return login(request, url);
861
1336
  if (route === "/callback" && get) return callback(request, url);
862
1337
  if (route === "/logout" && (get || request.method === "POST")) return logout(request);
@@ -900,7 +1375,7 @@ function createCauthy(given = {}) {
900
1375
  return {
901
1376
  handler,
902
1377
  webhook,
903
- system: createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch }),
1378
+ system: ownSystem,
904
1379
  getSession,
905
1380
  status,
906
1381
  resolve: resolve2,
@@ -925,12 +1400,101 @@ function can(organization, resource, action) {
925
1400
  if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
926
1401
  return organization.permissions.some((p) => matches(p, resource, action));
927
1402
  }
1403
+ function definePolicy(roles) {
1404
+ const entry = (p) => `${p.resource}:${p.action}`;
1405
+ const byName = new Map(roles.map((r) => [r.role, r]));
1406
+ const split = (permission) => {
1407
+ const i = permission.lastIndexOf(":");
1408
+ return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
1409
+ };
1410
+ return {
1411
+ roles: roles.map((r) => ({
1412
+ role: r.role,
1413
+ ...r.name !== void 0 ? { name: r.name } : {},
1414
+ ...r.description !== void 0 ? { description: r.description } : {},
1415
+ ...r.inherits ? { inherits: [...r.inherits] } : {},
1416
+ ...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
1417
+ })),
1418
+ roleNames: roles.map((r) => r.role),
1419
+ permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
1420
+ can(organization, permission) {
1421
+ const [resource, action] = split(permission);
1422
+ return can(organization, resource, action);
1423
+ },
1424
+ hasRole(organization, ...wanted) {
1425
+ return organization != null && wanted.includes(organization.role);
1426
+ },
1427
+ permissionsOf(role) {
1428
+ const seen = /* @__PURE__ */ new Set();
1429
+ const out2 = /* @__PURE__ */ new Set();
1430
+ const walk = (name) => {
1431
+ if (seen.has(name)) return;
1432
+ seen.add(name);
1433
+ const r = byName.get(name);
1434
+ for (const p of r?.permissions ?? []) out2.add(entry(p));
1435
+ for (const parent of r?.inherits ?? []) walk(parent);
1436
+ };
1437
+ walk(role);
1438
+ return [...out2];
1439
+ }
1440
+ };
1441
+ }
1442
+ var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1443
+ function policySource(system, roles) {
1444
+ const text = (v) => JSON.stringify(v);
1445
+ const key = (k) => IDENT.test(k) ? k : text(k);
1446
+ const one = (r) => {
1447
+ const parts = [`role: ${text(r.role)}`];
1448
+ if (r.name) parts.push(`name: ${text(r.name)}`);
1449
+ if (r.description) parts.push(`description: ${text(r.description)}`);
1450
+ if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
1451
+ if (r.permissions?.length) {
1452
+ const list = r.permissions.map((p) => ` { ${[...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [], `resource: ${text(p.resource)}`, `action: ${text(p.action)}`].join(", ")} },`);
1453
+ parts.push(`permissions: [
1454
+ ${list.join("\n")}
1455
+ ]`);
1456
+ }
1457
+ return ` {
1458
+ ${parts.join(",\n ")},
1459
+ },`;
1460
+ };
1461
+ return `// The catalog of roles of ${text(system)}, as Cauthy has it.
1462
+ // Written by \`cauthy-auth policy --pull\`: run it again after the catalog
1463
+ // changes, and do not edit this file by hand.
1464
+
1465
+ import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
1466
+
1467
+ export const policy = definePolicy([
1468
+ ${roles.map(one).join("\n")}
1469
+ ])
1470
+
1471
+ export const { can, hasRole } = policy
1472
+ export type Role = RoleOf<typeof policy>
1473
+ export type Permission = PermissionOf<typeof policy>
1474
+ `;
1475
+ }
1476
+ async function pullPolicy(o = {}) {
1477
+ const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
1478
+ const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
1479
+ const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
1480
+ const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
1481
+ const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
1482
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
1483
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
1484
+ }
1485
+ const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
1486
+ headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, accept: "application/json" }
1487
+ });
1488
+ const body = await res.json().catch(() => null);
1489
+ if (!res.ok) throw new Error(`cauthy-auth: Cauthy refused to give the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
1490
+ return body?.data?.roles ?? [];
1491
+ }
928
1492
  async function pushPolicy(o) {
929
- const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
930
- const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
931
- const system = o.system ?? env3("CAUTHY_SYSTEM") ?? "";
932
- const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
933
- const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
1493
+ const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
1494
+ const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
1495
+ const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
1496
+ const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
1497
+ const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
934
1498
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
935
1499
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
936
1500
  }
@@ -953,10 +1517,10 @@ function sessionSecret() {
953
1517
  return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
954
1518
  }
955
1519
  async function connectSystem(o) {
956
- const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
957
- const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
958
- const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
959
- const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
1520
+ const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
1521
+ const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
1522
+ const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
1523
+ const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
960
1524
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
961
1525
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to connect a system`);
962
1526
  }
@@ -988,18 +1552,89 @@ async function connectSystem(o) {
988
1552
  env: { ...d.env, CAUTHY_SECRET: sessionSecret() }
989
1553
  };
990
1554
  }
1555
+
1556
+ // src/verify.ts
1557
+ var import_jose4 = require("jose");
1558
+ var CauthyTokenError = class extends Error {
1559
+ constructor(reason) {
1560
+ super(`cauthy-auth: token refused: ${reason}`);
1561
+ this.reason = reason;
1562
+ this.name = "CauthyTokenError";
1563
+ }
1564
+ reason;
1565
+ };
1566
+ var keys2 = /* @__PURE__ */ new Map();
1567
+ function keysOf2(issuer) {
1568
+ let set = keys2.get(issuer);
1569
+ if (set === void 0) {
1570
+ set = (0, import_jose4.createRemoteJWKSet)(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 3e4 });
1571
+ keys2.set(issuer, set);
1572
+ }
1573
+ return set;
1574
+ }
1575
+ function env3(name) {
1576
+ return typeof process !== "undefined" ? process.env?.[name] : void 0;
1577
+ }
1578
+ function bearer(from) {
1579
+ if (typeof from === "string") return from.replace(/^Bearer\s+/i, "").trim();
1580
+ const header = (from instanceof Headers ? from : from.headers).get("authorization") ?? "";
1581
+ return /^Bearer\s+/i.test(header) ? header.replace(/^Bearer\s+/i, "").trim() : "";
1582
+ }
1583
+ async function verifyToken(from, options = {}) {
1584
+ const issuer = (options.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
1585
+ if (issuer === "") throw new CauthyTokenError("issuer (CAUTHY_ISSUER) is not set");
1586
+ const token = bearer(from);
1587
+ if (token === "") throw new CauthyTokenError("no token");
1588
+ let claims;
1589
+ try {
1590
+ const verified = await (0, import_jose4.jwtVerify)(token, options.jwks ?? keysOf2(issuer), {
1591
+ algorithms: ["RS256"],
1592
+ issuer,
1593
+ audience: options.audience ?? env3("CAUTHY_AUDIENCE"),
1594
+ requiredClaims: ["exp", "sub"],
1595
+ currentDate: options.now
1596
+ });
1597
+ claims = verified.payload;
1598
+ } catch (err) {
1599
+ throw new CauthyTokenError(err instanceof Error ? err.message : "bad signature");
1600
+ }
1601
+ const tenantId = typeof claims.tid === "string" ? claims.tid : "";
1602
+ const wanted = options.tenantId ?? env3("CAUTHY_TENANT_ID");
1603
+ if (wanted && tenantId !== wanted) throw new CauthyTokenError("issued for another workspace");
1604
+ return {
1605
+ userId: String(claims.sub),
1606
+ tenantId,
1607
+ ...typeof claims.email === "string" && claims.email !== "" ? { email: claims.email } : {},
1608
+ roles: Array.isArray(claims.roles) ? claims.roles.filter((r) => typeof r === "string") : [],
1609
+ ...typeof claims.sys === "string" && claims.sys !== "" ? { system: claims.sys } : {},
1610
+ expiresAt: new Date(claims.exp * 1e3).toISOString(),
1611
+ token
1612
+ };
1613
+ }
991
1614
  // Annotate the CommonJS export names for ESM import in node:
992
1615
  0 && (module.exports = {
993
1616
  CauthyApiError,
994
1617
  CauthyConfigError,
1618
+ CauthyTokenError,
995
1619
  CauthyWebhookError,
1620
+ MIN_PASSWORD,
996
1621
  can,
1622
+ checkCode,
1623
+ checkEmail,
1624
+ checkName,
1625
+ checkNewPassword,
1626
+ checkPassword,
997
1627
  connectSystem,
998
1628
  createCauthy,
999
1629
  createSystem,
1630
+ definePolicy,
1631
+ policySource,
1632
+ pullPolicy,
1000
1633
  pushPolicy,
1001
1634
  pushTheme,
1002
1635
  themeFromStackmyth,
1636
+ tokensFromStackmyth,
1637
+ verifyToken,
1003
1638
  verifyWebhook
1004
1639
  });
1005
1640
  //# sourceMappingURL=index.js.map