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.mjs CHANGED
@@ -109,10 +109,92 @@ function applies(d, o) {
109
109
  if (o.mode === "dark" && LIGHT.test(scope)) return false;
110
110
  const named = NAMED.exec(scope);
111
111
  if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false;
112
- if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host/.test(scope)) return false;
112
+ if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host|^\.(dark|light)\b/.test(scope)) return false;
113
113
  }
114
114
  return true;
115
115
  }
116
+ var TOKEN_CHARS = /^[A-Za-z0-9 #%.,()/'"_+*-]+$/;
117
+ var TOKEN_FUNCS = /* @__PURE__ */ new Set([
118
+ "var",
119
+ "calc",
120
+ "min",
121
+ "max",
122
+ "clamp",
123
+ "rgb",
124
+ "rgba",
125
+ "hsl",
126
+ "hsla",
127
+ "hwb",
128
+ "lab",
129
+ "lch",
130
+ "oklab",
131
+ "oklch",
132
+ "color",
133
+ "color-mix",
134
+ "light-dark",
135
+ "cubic-bezier",
136
+ "steps",
137
+ "linear",
138
+ "linear-gradient",
139
+ "radial-gradient",
140
+ "conic-gradient",
141
+ "repeating-linear-gradient",
142
+ "repeating-radial-gradient",
143
+ "drop-shadow",
144
+ "blur",
145
+ "translate",
146
+ "translatex",
147
+ "translatey",
148
+ "scale",
149
+ "rotate"
150
+ ]);
151
+ function tokenValue(value) {
152
+ const v = value.replace(/\s+/g, " ").trim();
153
+ if (v === "" || v.length > 400 || !TOKEN_CHARS.test(v)) return null;
154
+ for (const m of v.matchAll(/([A-Za-z-]+)\(/g)) if (!TOKEN_FUNCS.has(m[1].toLowerCase())) return null;
155
+ if (/^\(|[ ,(]\(/.test(v)) return null;
156
+ return v;
157
+ }
158
+ var THEME_SCOPE = /^(?:\s*(?::root|html|body|\[data-(?:theme|mode|color-scheme)(?:=["']?[\w-]+["']?)?\])\s*,?)+$/i;
159
+ function tokensFromStackmyth(css, options = {}) {
160
+ const mode = options.mode ?? "light";
161
+ const all = (Array.isArray(css) ? css : [css]).flatMap(declarations);
162
+ 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);
163
+ const vars = /* @__PURE__ */ new Map();
164
+ for (const d of all) {
165
+ if (!applies(d, { mode, theme: named })) continue;
166
+ if (!d.within.every((w) => w.startsWith("@") || THEME_SCOPE.test(w))) continue;
167
+ vars.set(d.name, d.value);
168
+ }
169
+ const inline = (value, depth = 0) => {
170
+ if (depth > 16) return null;
171
+ let failed = false;
172
+ let out2 = value;
173
+ for (let guard = 0; guard < 64; guard++) {
174
+ const next = out2.replace(/var\(\s*(--(?!sm-)[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/, (_m, name, fallback) => {
175
+ const known = vars.get(name);
176
+ const got = known !== void 0 ? inline(known, depth + 1) : fallback !== void 0 ? inline(fallback.trim(), depth + 1) : null;
177
+ if (got === null) failed = true;
178
+ return got ?? "";
179
+ });
180
+ if (failed) return null;
181
+ if (next === out2) break;
182
+ out2 = next;
183
+ }
184
+ return out2;
185
+ };
186
+ const tokens = {};
187
+ const warnings = [];
188
+ for (const [name, raw] of vars) {
189
+ if (!/^--sm-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) continue;
190
+ const written = inline(raw);
191
+ const value = written === null ? null : tokenValue(written);
192
+ if (value === null) warnings.push(`${name}: "${raw.length > 60 ? raw.slice(0, 57) + "\u2026" : raw}" is not a value Cauthy takes`);
193
+ else tokens[name] = value;
194
+ }
195
+ if (Object.keys(tokens).length === 0) warnings.push("no --sm-* token was found in these stylesheets");
196
+ return { tokens, warnings };
197
+ }
116
198
  function resolve(value, vars, seen = 0) {
117
199
  if (seen > 16) return null;
118
200
  let unresolved = false;
@@ -257,7 +339,10 @@ function toWire(theme) {
257
339
  panelColors: "panel_colors",
258
340
  panelText: "panel_text",
259
341
  panelIconUrl: "panel_icon_url",
260
- texts: "texts"
342
+ texts: "texts",
343
+ inputBackground: "input_background",
344
+ inputBorder: "input_border",
345
+ tokens: "tokens"
261
346
  };
262
347
  const wire = {};
263
348
  for (const [k, v] of Object.entries(theme)) {
@@ -289,18 +374,21 @@ function fromWire(wire) {
289
374
  panel_colors: "panelColors",
290
375
  panel_text: "panelText",
291
376
  panel_icon_url: "panelIconUrl",
292
- texts: "texts"
377
+ texts: "texts",
378
+ input_background: "inputBackground",
379
+ input_border: "inputBorder",
380
+ tokens: "tokens"
293
381
  };
294
382
  const theme = {};
295
383
  for (const [k, v] of Object.entries(wire ?? {})) if (names[k] && v !== void 0 && v !== null && v !== "") theme[names[k]] = v;
296
384
  return theme;
297
385
  }
298
386
  async function pushTheme(o) {
299
- const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
300
- const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
301
- const clientId = o.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
302
- const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
303
- const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
387
+ const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
388
+ const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
389
+ const clientId = o.clientId ?? env4("CAUTHY_CLIENT_ID") ?? "";
390
+ const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
391
+ const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
304
392
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
305
393
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
306
394
  }
@@ -480,6 +568,11 @@ function createSystem(given = {}) {
480
568
  return fromWire(await data("GET", `/v1/oauth/clients/${id(of)}/theme`));
481
569
  }
482
570
  },
571
+ policy: {
572
+ async get(system) {
573
+ return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
574
+ }
575
+ },
483
576
  async publicJwks() {
484
577
  if (!key) return { keys: [] };
485
578
  const { kty, n, e } = asJwk(key) ?? await exportJWK(await signingKey());
@@ -488,6 +581,45 @@ function createSystem(given = {}) {
488
581
  };
489
582
  }
490
583
 
584
+ // src/validate.ts
585
+ var MIN_PASSWORD = 8;
586
+ var MAX_PASSWORD = 256;
587
+ var MAX_NAME = 120;
588
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
589
+ function checkEmail(value) {
590
+ const v = typeof value === "string" ? value.trim() : "";
591
+ if (v === "") return { field: "email", code: "required", message: "Enter your email address." };
592
+ if (v.length > 254 || !EMAIL.test(v)) return { field: "email", code: "invalid_email", message: "That does not look like an email address." };
593
+ return null;
594
+ }
595
+ function checkPassword(value) {
596
+ if (typeof value !== "string" || value === "") return { field: "password", code: "required", message: "Enter your password." };
597
+ if (value.length > MAX_PASSWORD) return { field: "password", code: "too_long", message: "That password is too long." };
598
+ return null;
599
+ }
600
+ function checkNewPassword(value) {
601
+ const basic = checkPassword(value);
602
+ if (basic) return basic;
603
+ if (value.length < MIN_PASSWORD) {
604
+ return { field: "password", code: "too_short", message: `Use at least ${MIN_PASSWORD} characters.` };
605
+ }
606
+ return null;
607
+ }
608
+ function checkName(value) {
609
+ if (value === void 0 || value === null || value === "") return null;
610
+ if (typeof value !== "string" || value.trim().length > MAX_NAME) return { field: "name", code: "too_long", message: `Use at most ${MAX_NAME} characters.` };
611
+ return null;
612
+ }
613
+ function checkCode(value) {
614
+ const v = typeof value === "string" ? value.trim() : "";
615
+ if (v === "") return { field: "code", code: "required", message: "Enter the code." };
616
+ if (v.length > 40) return { field: "code", code: "too_long", message: "That is not a code." };
617
+ return null;
618
+ }
619
+ function firstProblem(...checks) {
620
+ return checks.find((c) => c !== null) ?? null;
621
+ }
622
+
491
623
  // src/webhook.ts
492
624
  import { createRemoteJWKSet, jwtVerify } from "jose";
493
625
  var CauthyWebhookError = class extends Error {
@@ -509,8 +641,8 @@ function keysOf(issuer) {
509
641
  }
510
642
  var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
511
643
  async function verifyWebhook(request, options = {}) {
512
- const env3 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
513
- const issuer = (options.issuer ?? env3 ?? "").replace(/\/+$/, "");
644
+ const env4 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
645
+ const issuer = (options.issuer ?? env4 ?? "").replace(/\/+$/, "");
514
646
  if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
515
647
  const signature = request.headers.get("x-cauthy-signature-jwt");
516
648
  if (!signature) throw new CauthyWebhookError("no signature");
@@ -561,9 +693,19 @@ async function verifyWebhook(request, options = {}) {
561
693
 
562
694
  // src/cauthy.ts
563
695
  var SESSION_DAYS = 30;
564
- var FLOW_SECONDS = 600;
696
+ var FLOW_SECONDS = 3600;
697
+ var AGAIN_SECONDS = 120;
565
698
  var SKEW_SECONDS = 30;
566
699
  var ORGANIZATIONS_TTL_MS = 6e4;
700
+ var LOOK_TTL_MS = 6e4;
701
+ var PROVIDER_ID = /^(custom:)?[a-z0-9_-]{1,40}$/;
702
+ function providerIn(segment) {
703
+ try {
704
+ return decodeURIComponent(segment);
705
+ } catch {
706
+ return "";
707
+ }
708
+ }
567
709
  function env2(name) {
568
710
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
569
711
  }
@@ -596,6 +738,7 @@ function createCauthy(given = {}) {
596
738
  const clientSecret = given.clientSecret ?? env2("CAUTHY_CLIENT_SECRET");
597
739
  const secret = given.secret ?? env2("CAUTHY_SECRET") ?? "";
598
740
  const system = given.system ?? env2("CAUTHY_SYSTEM") ?? "";
741
+ const tenant = given.tenant ?? env2("CAUTHY_TENANT") ?? "";
599
742
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
600
743
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
601
744
  if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
@@ -604,18 +747,26 @@ function createCauthy(given = {}) {
604
747
  const afterSignOut = localPath(given.afterSignOut, "/");
605
748
  const cookieName = given.cookieName ?? "cauthy_session";
606
749
  const flowCookie = `${cookieName}_flow`;
750
+ const againCookie = `${cookieName}_again`;
751
+ const stepCookie = `${cookieName}_step`;
607
752
  const doFetch = (...args) => (given.fetch ?? fetch)(...args);
608
753
  const key = keyOf(secret);
754
+ const pages = {
755
+ signIn: given.pages?.signIn ? localPath(given.pages.signIn, "/") : void 0,
756
+ resetPassword: given.pages?.resetPassword ? localPath(given.pages.resetPassword, "/") : void 0,
757
+ verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0
758
+ };
759
+ const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch });
609
760
  const originOf = (request) => (given.baseURL ?? new URL(request.url).origin).replace(/\/+$/, "");
610
761
  const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
611
762
  async function token(form) {
612
- const body = new URLSearchParams({ client_id: clientId, ...form });
613
- if (clientSecret) body.set("client_secret", clientSecret);
763
+ const body2 = new URLSearchParams({ client_id: clientId, ...form });
764
+ if (clientSecret) body2.set("client_secret", clientSecret);
614
765
  try {
615
766
  const res = await doFetch(`${issuer}/v1/oauth/token`, {
616
767
  method: "POST",
617
768
  headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
618
- body
769
+ body: body2
619
770
  });
620
771
  if (res.ok) return { ok: true, body: await res.json() };
621
772
  return { ok: false, ended: res.status >= 400 && res.status < 500 };
@@ -645,12 +796,12 @@ function createCauthy(given = {}) {
645
796
  return null;
646
797
  }
647
798
  }
648
- async function stored(body) {
649
- if (typeof body?.access_token !== "string" || typeof body?.refresh_token !== "string") return null;
650
- const who = await userinfo(body.access_token);
799
+ async function stored(body2) {
800
+ if (typeof body2?.access_token !== "string" || typeof body2?.refresh_token !== "string") return null;
801
+ const who = await userinfo(body2.access_token);
651
802
  if (who === null || who.u.id === "") return null;
652
- const lifetime = typeof body.expires_in === "number" && body.expires_in > 0 ? body.expires_in : 300;
653
- return { at: body.access_token, rt: body.refresh_token, exp: Math.floor(Date.now() / 1e3) + lifetime, ...who };
803
+ const lifetime = typeof body2.expires_in === "number" && body2.expires_in > 0 ? body2.expires_in : 300;
804
+ return { at: body2.access_token, rt: body2.refresh_token, exp: Math.floor(Date.now() / 1e3) + lifetime, ...who };
654
805
  }
655
806
  function toSession(s) {
656
807
  return { user: s.u, tenantId: s.tid, roles: s.roles, accessToken: s.at, expiresAt: new Date(s.exp * 1e3).toISOString() };
@@ -724,8 +875,8 @@ function createCauthy(given = {}) {
724
875
  const url = `${issuer}/v1/oauth/organizations${system ? `?system=${encodeURIComponent(system)}` : ""}`;
725
876
  const res = await doFetch(url, { headers: { authorization: `Bearer ${session.accessToken}` } });
726
877
  if (!res.ok) throw new Error(`cauthy-auth: reading organizations: ${res.status}`);
727
- const body = await res.json();
728
- const value = (body.data ?? []).map(
878
+ const body2 = await res.json();
879
+ const value = (body2.data ?? []).map(
729
880
  (o) => ({
730
881
  id: String(o.id),
731
882
  system: String(o.system ?? ""),
@@ -770,20 +921,28 @@ function createCauthy(given = {}) {
770
921
  if (prompt === "login" || prompt === "none") q.set("prompt", prompt);
771
922
  const locale = url.searchParams.get("locale") ?? url.searchParams.get("ui_locales");
772
923
  if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set("ui_locales", locale);
773
- return redirect(`${issuer}/v1/oauth/authorize?${q}`, [
924
+ const passkey = url.searchParams.get("method") === "passkey" ? "#passkey" : "";
925
+ return redirect(`${issuer}/v1/oauth/authorize?${q}${passkey}`, [
774
926
  writeCookie(flowCookie, await seal(flow, await key), attrs(request, FLOW_SECONDS))
775
927
  ]);
776
928
  }
777
929
  function failed(request, reason) {
778
930
  const h = new Headers({ "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
779
931
  h.append("set-cookie", writeCookie(flowCookie, "", attrs(request, 0)));
932
+ h.append("set-cookie", writeCookie(againCookie, "", attrs(request, 0)));
780
933
  return new Response(`Sign-in could not be completed (${reason}). Go back and try again.`, { status: 400, headers: h });
781
934
  }
782
935
  async function callback(request, url) {
783
936
  const had = readCookies(request.headers.get("cookie"));
784
937
  const sealed = had.get(flowCookie);
785
938
  const flow = sealed ? await unseal(sealed, await key) : null;
786
- if (flow === null || url.searchParams.get("state") !== flow.state) return failed(request, "state_mismatch");
939
+ if (flow === null || url.searchParams.get("state") !== flow.state) {
940
+ if (had.get(againCookie) === void 0) {
941
+ const back = flow?.returnTo ?? afterSignIn;
942
+ return redirect(`${basePath}/login?return_to=${encodeURIComponent(back)}`, [writeCookie(againCookie, "1", attrs(request, AGAIN_SECONDS))]);
943
+ }
944
+ return failed(request, "state_mismatch");
945
+ }
787
946
  const error = url.searchParams.get("error");
788
947
  if (error !== null) {
789
948
  if (error === "login_required") return redirect(flow.returnTo, [writeCookie(flowCookie, "", attrs(request, 0))]);
@@ -799,28 +958,332 @@ function createCauthy(given = {}) {
799
958
  });
800
959
  const s = res.ok ? await stored(res.body) : null;
801
960
  if (s === null) return failed(request, "exchange_failed");
802
- return redirect(flow.returnTo, [...await write(request, s, had), writeCookie(flowCookie, "", attrs(request, 0))]);
961
+ return redirect(flow.returnTo, [...await write(request, s, had), writeCookie(flowCookie, "", attrs(request, 0)), writeCookie(againCookie, "", attrs(request, 0))]);
803
962
  }
804
963
  async function logout(request) {
805
964
  const { had, value } = await read(request);
806
965
  if (value !== null) {
807
- const body = new URLSearchParams({ client_id: clientId, token: value.rt });
808
- if (clientSecret) body.set("client_secret", clientSecret);
966
+ const body2 = new URLSearchParams({ client_id: clientId, token: value.rt });
967
+ if (clientSecret) body2.set("client_secret", clientSecret);
809
968
  await doFetch(`${issuer}/v1/oauth/revoke`, {
810
969
  method: "POST",
811
970
  headers: { "content-type": "application/x-www-form-urlencoded" },
812
- body
971
+ body: body2
813
972
  }).catch(() => void 0);
814
973
  orgCache.delete(value.at);
815
974
  }
816
975
  const q = new URLSearchParams({ client_id: clientId, post_logout_redirect_uri: `${originOf(request)}${afterSignOut}` });
817
976
  return redirect(`${issuer}/v1/oauth/logout?${q}`, clearParts(cookieName, attrs(request, 0), had));
818
977
  }
978
+ const STEP_SECONDS = 600;
979
+ const problem = (status2, code, message, field, headers) => json({ error: { code, message, ...field ? { field } : {} } }, status2, headers);
980
+ const invalid = (p) => problem(400, p.code, p.message, p.field);
981
+ function ownRequest(request) {
982
+ const origin = request.headers.get("origin");
983
+ if (origin !== null) return origin === originOf(request) || origin === new URL(request.url).origin;
984
+ return (request.headers.get("sec-fetch-site") ?? "same-origin") === "same-origin";
985
+ }
986
+ async function body(request) {
987
+ if (!(request.headers.get("content-type") ?? "").includes("application/json")) return null;
988
+ try {
989
+ const b = await request.json();
990
+ return typeof b === "object" && b !== null && !Array.isArray(b) ? b : null;
991
+ } catch {
992
+ return null;
993
+ }
994
+ }
995
+ const clientIp = given.clientIp ?? ((request) => request.headers.get("x-forwarded-for")?.split(",").pop()?.trim() || void 0);
996
+ let noProofUntil = 0;
997
+ async function passedOn(request) {
998
+ const ip = clientIp(request);
999
+ if (!ip || Date.now() < noProofUntil) return {};
1000
+ try {
1001
+ return { "x-cauthy-client-ip": ip, "x-cauthy-system-token": await ownSystem.token() };
1002
+ } catch {
1003
+ noProofUntil = Date.now() + 5 * 6e4;
1004
+ return {};
1005
+ }
1006
+ }
1007
+ async function ask(request, method, path, payload, page, bearer2) {
1008
+ try {
1009
+ const res = await doFetch(`${issuer}${path}`, {
1010
+ method,
1011
+ headers: {
1012
+ "x-tenant-key": tenant,
1013
+ accept: "application/json",
1014
+ // A link Cauthy mails opens the application's page for it when it
1015
+ // has one; otherwise Cauthy's own. Either way in the reader's language.
1016
+ ...page ? { "x-cauthy-link-to": `${originOf(request)}${page}`, "x-cauthy-client-id": clientId } : { "x-cauthy-links": "hosted" },
1017
+ ...await passedOn(request),
1018
+ ...bearer2 ? { authorization: `Bearer ${bearer2}` } : {},
1019
+ ...request.headers.get("accept-language") ? { "accept-language": request.headers.get("accept-language") } : {},
1020
+ ...request.headers.get("user-agent") ? { "user-agent": request.headers.get("user-agent") } : {},
1021
+ ...payload === void 0 ? {} : { "content-type": "application/json" }
1022
+ },
1023
+ body: payload === void 0 ? void 0 : JSON.stringify(payload)
1024
+ });
1025
+ const b = res.status === 204 || res.status === 202 ? null : await res.json().catch(() => null);
1026
+ if (res.ok) return { status: res.status, data: b?.data ?? b };
1027
+ const first = b?.errors?.[0];
1028
+ return { status: res.status, data: null, error: { code: String(first?.code ?? "error"), message: String(first?.message ?? "The request could not be completed.") } };
1029
+ } catch {
1030
+ return { status: 503, data: null, error: { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." } };
1031
+ }
1032
+ }
1033
+ const accountOf = (d) => ({
1034
+ id: String(d?.id ?? ""),
1035
+ email: String(d?.email ?? ""),
1036
+ name: String(d?.name ?? ""),
1037
+ emailVerified: d?.email_verified === true,
1038
+ hasPassword: d?.has_password === true,
1039
+ mfaEnabled: d?.mfa_enabled === true
1040
+ });
1041
+ async function signedInAs(request) {
1042
+ const r = await resolve2(request);
1043
+ const headers = new Headers();
1044
+ for (const c of r.cookies) headers.append("set-cookie", c);
1045
+ if (r.session === null) return { response: problem(401, "unauthenticated", "Sign in first.", void 0, headers) };
1046
+ return { token: r.session.accessToken, kept: r.cookies.length === 0 ? (await read(request)).value : null, headers };
1047
+ }
1048
+ async function signedIn(request, tokens, extra = []) {
1049
+ const s = await stored(tokens);
1050
+ if (s === null) return problem(502, "unavailable", "Sign-in is unavailable right now. Try again in a moment.");
1051
+ const h = new Headers();
1052
+ for (const c of [...await write(request, s, readCookies(request.headers.get("cookie"))), ...extra]) h.append("set-cookie", c);
1053
+ return json({ status: "signed_in", session: await toPublic(toSession(s)) }, 200, h);
1054
+ }
1055
+ let lookCache = null;
1056
+ async function ownRoute(request, url, route) {
1057
+ const post = request.method === "POST";
1058
+ 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/");
1059
+ if (!own) return null;
1060
+ if (tenant === "") {
1061
+ return problem(500, "not_configured", "cauthy-auth: tenant (CAUTHY_TENANT) is required for the application's own sign-in screens");
1062
+ }
1063
+ if (route === "/look" && !post) {
1064
+ if (lookCache === null || lookCache.until < Date.now()) {
1065
+ const res = await ask(request, "GET", `/v1/auth/look/${encodeURIComponent(clientId)}`);
1066
+ lookCache = { until: Date.now() + LOOK_TTL_MS, look: res.error ? {} : res.data ?? {} };
1067
+ }
1068
+ return json(lookCache.look);
1069
+ }
1070
+ if (route === "/methods" && !post) {
1071
+ const [providers, settings] = await Promise.all([ask(request, "GET", "/v1/auth/social/providers"), ask(request, "GET", "/v1/auth/settings")]);
1072
+ const list = Array.isArray(providers.data) ? providers.data : [];
1073
+ return json({
1074
+ providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? "") })).filter((p) => p.id !== ""),
1075
+ signUp: settings.data?.allow_signup !== false,
1076
+ // A sign-in link needs a page of the application to open.
1077
+ magicLink: settings.data?.allow_magic_link === true && pages.signIn !== void 0,
1078
+ // Asked for on Cauthy's page: the person goes there for that step and comes back.
1079
+ passkey: true
1080
+ });
1081
+ }
1082
+ if (route.startsWith("/sign-in/social/") && !post) {
1083
+ const provider = providerIn(route.slice("/sign-in/social/".length));
1084
+ if (!PROVIDER_ID.test(provider)) return new Response("Not found", { status: 404 });
1085
+ const returnTo = localPath(url.searchParams.get("return_to"), afterSignIn);
1086
+ const callback2 = `${basePath}/callback/${provider}`;
1087
+ const started = await ask(request, "GET", `/v1/auth/social/${provider}?redirect_uri=${encodeURIComponent(`${originOf(request)}${callback2}`)}`, void 0, callback2);
1088
+ if (started.error || typeof started.data?.redirect_url !== "string") {
1089
+ return redirect(`${returnTo}${returnTo.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(started.error?.code ?? "provider_unavailable")}`);
1090
+ }
1091
+ const step = { kind: "social", token: String(started.data.state ?? ""), provider, returnTo };
1092
+ return redirect(started.data.redirect_url, [writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS))]);
1093
+ }
1094
+ if (route.startsWith("/callback/") && !post) {
1095
+ const provider = providerIn(route.slice("/callback/".length));
1096
+ const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
1097
+ const step = sealed ? await unseal(sealed, await key) : null;
1098
+ const clear = writeCookie(stepCookie, "", attrs(request, 0));
1099
+ const back = step?.returnTo ?? afterSignIn;
1100
+ const failedTo = (code2) => redirect(`${back}${back.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code2)}`, [clear]);
1101
+ if (step === null || step.kind !== "social" || step.provider !== provider || url.searchParams.get("state") !== step.token) return failedTo("state_mismatch");
1102
+ const code = url.searchParams.get("code");
1103
+ if (!code) return failedTo(url.searchParams.get("error") === "access_denied" ? "cancelled" : "provider_refused");
1104
+ const q = new URLSearchParams({ code, state: step.token, redirect_uri: `${originOf(request)}${basePath}/callback/${provider}` });
1105
+ const done = await ask(request, "GET", `/v1/auth/social/${provider}/callback?${q}`);
1106
+ if (done.error) return failedTo(done.error.code);
1107
+ const s = await stored(done.data);
1108
+ if (s === null) return failedTo("unavailable");
1109
+ return redirect(back, [...await write(request, s, readCookies(request.headers.get("cookie"))), clear]);
1110
+ }
1111
+ if ((route === "/me" || route === "/me/sessions") && !post) {
1112
+ const mine = await signedInAs(request);
1113
+ if ("response" in mine) return mine.response;
1114
+ const res = await ask(request, "GET", route === "/me" ? "/v1/auth/me" : "/v1/auth/me/sessions", void 0, void 0, mine.token);
1115
+ if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
1116
+ if (route === "/me/sessions") {
1117
+ const list = Array.isArray(res.data) ? res.data : [];
1118
+ 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);
1119
+ }
1120
+ return json({ account: accountOf(res.data) }, 200, mine.headers);
1121
+ }
1122
+ if (!post) return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
1123
+ if (!ownRequest(request)) return problem(403, "cross_site", "This request did not come from this application.");
1124
+ if (route === "/sign-out") {
1125
+ const { had, value } = await read(request);
1126
+ if (value !== null) {
1127
+ const form = new URLSearchParams({ client_id: clientId, token: value.rt });
1128
+ if (clientSecret) form.set("client_secret", clientSecret);
1129
+ await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }).catch(() => void 0);
1130
+ orgCache.delete(value.at);
1131
+ }
1132
+ const h = new Headers();
1133
+ for (const c of clearParts(cookieName, attrs(request, 0), had)) h.append("set-cookie", c);
1134
+ return json({ status: "signed_out" }, 200, h);
1135
+ }
1136
+ const b = await body(request);
1137
+ if (b === null) return problem(400, "bad_request", "Send the form as JSON.");
1138
+ if (route === "/sign-in/email") {
1139
+ const bad = firstProblem(checkEmail(b.email), checkPassword(b.password));
1140
+ if (bad) return invalid(bad);
1141
+ const res = await ask(request, "POST", "/v1/auth/login", { email: String(b.email).trim(), password: b.password });
1142
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1143
+ if (res.data?.mfa_required === true) {
1144
+ const step = { kind: "mfa", token: String(res.data.mfa_token ?? ""), returnTo: afterSignIn };
1145
+ return json({ status: "mfa_required" }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
1146
+ }
1147
+ return signedIn(request, res.data);
1148
+ }
1149
+ if (route === "/sign-in/mfa") {
1150
+ const bad = checkCode(b.code);
1151
+ if (bad) return invalid(bad);
1152
+ const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
1153
+ const step = sealed ? await unseal(sealed, await key) : null;
1154
+ if (step === null || step.kind !== "mfa") return problem(400, "step_expired", "Sign in again: the code was asked for too long ago.");
1155
+ const res = await ask(request, "POST", "/v1/auth/mfa/challenge", { mfa_token: step.token, code: String(b.code).trim() });
1156
+ if (res.error) return problem(res.status, res.error.code, res.error.message, "code");
1157
+ return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))]);
1158
+ }
1159
+ if (route === "/sign-up/email") {
1160
+ const bad = firstProblem(checkName(b.name), checkEmail(b.email), checkNewPassword(b.password));
1161
+ if (bad) return invalid(bad);
1162
+ const email = String(b.email).trim();
1163
+ const made = await ask(request, "POST", "/v1/auth/register", { email, password: b.password, display_name: typeof b.name === "string" ? b.name.trim() : "" }, pages.verifyEmail);
1164
+ if (made.error) return problem(made.status, made.error.code, made.error.message);
1165
+ if (made.data?.verification_required === true) return json({ status: "verification_required", email });
1166
+ const res = await ask(request, "POST", "/v1/auth/login", { email, password: b.password });
1167
+ if (res.error || res.data?.mfa_required === true) return json({ status: "verification_required", email });
1168
+ return signedIn(request, res.data);
1169
+ }
1170
+ if (route === "/forgot-password") {
1171
+ const bad = checkEmail(b.email);
1172
+ if (bad) return invalid(bad);
1173
+ await ask(request, "POST", "/v1/auth/password/reset", { email: String(b.email).trim() }, pages.resetPassword);
1174
+ return json({ status: "sent" });
1175
+ }
1176
+ if (route.startsWith("/me")) {
1177
+ const mine = await signedInAs(request);
1178
+ if ("response" in mine) return mine.response;
1179
+ const as = (path, payload, method = "POST") => ask(request, method, path, payload, void 0, mine.token);
1180
+ const refused = (res, field) => problem(res.status, res.error.code, res.error.message, field, mine.headers);
1181
+ const password = typeof b.password === "string" ? b.password : "";
1182
+ if (route === "/me") {
1183
+ const bad = checkName(b.name);
1184
+ if (bad) return invalid(bad);
1185
+ const res = await as("/v1/auth/me", { name: typeof b.name === "string" ? b.name.trim() : "" }, "PATCH");
1186
+ if (res.error) return refused(res, "name");
1187
+ const h = mine.headers;
1188
+ if (mine.kept !== null) {
1189
+ 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)) });
1190
+ if (again !== null) for (const c of await write(request, again, readCookies(request.headers.get("cookie")))) h.append("set-cookie", c);
1191
+ }
1192
+ return json({ status: "saved", account: accountOf(res.data) }, 200, h);
1193
+ }
1194
+ if (route === "/me/password") {
1195
+ const bad = checkNewPassword(b.newPassword);
1196
+ if (bad) return problem(400, bad.code, bad.message, "newPassword");
1197
+ const res = await as("/v1/auth/me/password", { current_password: password, new_password: b.newPassword });
1198
+ if (res.error) return refused(res, res.error.code === "wrong_password" ? "password" : res.error.code === "weak_password" ? "newPassword" : void 0);
1199
+ const next = await stored(res.data);
1200
+ const h = new Headers();
1201
+ const had = readCookies(request.headers.get("cookie"));
1202
+ for (const c of next !== null ? await write(request, next, had) : clearParts(cookieName, attrs(request, 0), had)) h.append("set-cookie", c);
1203
+ return json({ status: "changed", signedIn: next !== null }, 200, h);
1204
+ }
1205
+ if (route === "/me/mfa/setup") {
1206
+ const res = await as("/v1/auth/mfa/setup", { password });
1207
+ if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
1208
+ return json({ status: "started", secret: String(res.data?.secret ?? ""), otpauthUri: String(res.data?.otpauth_uri ?? "") }, 200, mine.headers);
1209
+ }
1210
+ if (route === "/me/mfa/verify") {
1211
+ const bad = checkCode(b.code);
1212
+ if (bad) return invalid(bad);
1213
+ const res = await as("/v1/auth/mfa/verify", { code: String(b.code).trim() });
1214
+ if (res.error) return refused(res, "code");
1215
+ return json({ status: "enabled", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
1216
+ }
1217
+ if (route === "/me/mfa/disable") {
1218
+ const res = await as("/v1/auth/me/mfa/disable", { password });
1219
+ if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
1220
+ return json({ status: "disabled" }, 200, mine.headers);
1221
+ }
1222
+ if (route === "/me/mfa/recovery-codes") {
1223
+ const res = await as("/v1/auth/me/mfa/recovery-codes", { password });
1224
+ if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
1225
+ return json({ status: "renewed", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
1226
+ }
1227
+ if (route === "/me/sessions/end") {
1228
+ const res = await as("/v1/auth/me/sessions", void 0, "DELETE");
1229
+ if (res.error) return refused(res);
1230
+ const h = new Headers();
1231
+ for (const c of clearParts(cookieName, attrs(request, 0), readCookies(request.headers.get("cookie")))) h.append("set-cookie", c);
1232
+ orgCache.delete(mine.token);
1233
+ return json({ status: "signed_out" }, 200, h);
1234
+ }
1235
+ return null;
1236
+ }
1237
+ const isToken = (v) => typeof v === "string" && v !== "" && v.length <= 512;
1238
+ const badLink = () => problem(422, "invalid_token", "This link is not valid, or it has expired.");
1239
+ if (route === "/reset-password") {
1240
+ if (!isToken(b.token)) return badLink();
1241
+ const bad = checkNewPassword(b.password);
1242
+ if (bad) return invalid(bad);
1243
+ const res = await ask(request, "POST", "/v1/auth/password/change", { token: String(b.token), new_password: b.password });
1244
+ if (res.error) return problem(res.status, res.error.code, res.error.message, res.error.code === "weak_password" ? "password" : void 0);
1245
+ return json({ status: "changed" });
1246
+ }
1247
+ if (route === "/verify-email") {
1248
+ if (!isToken(b.token)) return badLink();
1249
+ const res = await ask(request, "POST", "/v1/auth/email/verify", { token: String(b.token) });
1250
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1251
+ return json({ status: "verified" });
1252
+ }
1253
+ if (route === "/sign-in/magic-link") {
1254
+ const bad = checkEmail(b.email);
1255
+ if (bad) return invalid(bad);
1256
+ 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");
1257
+ const email = String(b.email).trim();
1258
+ await ask(request, "POST", "/v1/auth/magic-link/request", { email }, pages.signIn);
1259
+ const step = { kind: "code", token: email, returnTo: afterSignIn };
1260
+ return json({ status: "sent" }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
1261
+ }
1262
+ if (route === "/sign-in/email-code") {
1263
+ const bad = checkCode(b.code);
1264
+ if (bad) return invalid(bad);
1265
+ const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
1266
+ const step = sealed ? await unseal(sealed, await key) : null;
1267
+ if (step === null || step.kind !== "code") return problem(400, "step_expired", "Ask for a new link: this one was asked for too long ago.");
1268
+ const res = await ask(request, "POST", "/v1/auth/magic-link/verify-code", { email: step.token, code: String(b.code).trim() });
1269
+ if (res.error) return problem(res.status, res.error.code, res.error.message, "code");
1270
+ return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))]);
1271
+ }
1272
+ if (route === "/sign-in/magic-link/verify") {
1273
+ if (!isToken(b.token)) return badLink();
1274
+ const res = await ask(request, "POST", "/v1/auth/magic-link/verify", { token: String(b.token) });
1275
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1276
+ return signedIn(request, res.data);
1277
+ }
1278
+ return null;
1279
+ }
819
1280
  async function handler(request) {
820
1281
  const url = new URL(request.url);
821
1282
  if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return new Response("Not found", { status: 404 });
822
1283
  const route = url.pathname.slice(basePath.length).replace(/\/+$/, "") || "/";
823
1284
  const get = request.method === "GET" || request.method === "HEAD";
1285
+ const mine = await ownRoute(request, url, route);
1286
+ if (mine !== null) return mine;
824
1287
  if (route === "/login" && get) return login(request, url);
825
1288
  if (route === "/callback" && get) return callback(request, url);
826
1289
  if (route === "/logout" && (get || request.method === "POST")) return logout(request);
@@ -864,7 +1327,7 @@ function createCauthy(given = {}) {
864
1327
  return {
865
1328
  handler,
866
1329
  webhook,
867
- system: createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch }),
1330
+ system: ownSystem,
868
1331
  getSession,
869
1332
  status,
870
1333
  resolve: resolve2,
@@ -889,12 +1352,101 @@ function can(organization, resource, action) {
889
1352
  if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
890
1353
  return organization.permissions.some((p) => matches(p, resource, action));
891
1354
  }
1355
+ function definePolicy(roles) {
1356
+ const entry = (p) => `${p.resource}:${p.action}`;
1357
+ const byName = new Map(roles.map((r) => [r.role, r]));
1358
+ const split = (permission) => {
1359
+ const i = permission.lastIndexOf(":");
1360
+ return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
1361
+ };
1362
+ return {
1363
+ roles: roles.map((r) => ({
1364
+ role: r.role,
1365
+ ...r.name !== void 0 ? { name: r.name } : {},
1366
+ ...r.description !== void 0 ? { description: r.description } : {},
1367
+ ...r.inherits ? { inherits: [...r.inherits] } : {},
1368
+ ...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
1369
+ })),
1370
+ roleNames: roles.map((r) => r.role),
1371
+ permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
1372
+ can(organization, permission) {
1373
+ const [resource, action] = split(permission);
1374
+ return can(organization, resource, action);
1375
+ },
1376
+ hasRole(organization, ...wanted) {
1377
+ return organization != null && wanted.includes(organization.role);
1378
+ },
1379
+ permissionsOf(role) {
1380
+ const seen = /* @__PURE__ */ new Set();
1381
+ const out2 = /* @__PURE__ */ new Set();
1382
+ const walk = (name) => {
1383
+ if (seen.has(name)) return;
1384
+ seen.add(name);
1385
+ const r = byName.get(name);
1386
+ for (const p of r?.permissions ?? []) out2.add(entry(p));
1387
+ for (const parent of r?.inherits ?? []) walk(parent);
1388
+ };
1389
+ walk(role);
1390
+ return [...out2];
1391
+ }
1392
+ };
1393
+ }
1394
+ var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1395
+ function policySource(system, roles) {
1396
+ const text = (v) => JSON.stringify(v);
1397
+ const key = (k) => IDENT.test(k) ? k : text(k);
1398
+ const one = (r) => {
1399
+ const parts = [`role: ${text(r.role)}`];
1400
+ if (r.name) parts.push(`name: ${text(r.name)}`);
1401
+ if (r.description) parts.push(`description: ${text(r.description)}`);
1402
+ if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
1403
+ if (r.permissions?.length) {
1404
+ const list = r.permissions.map((p) => ` { ${[...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [], `resource: ${text(p.resource)}`, `action: ${text(p.action)}`].join(", ")} },`);
1405
+ parts.push(`permissions: [
1406
+ ${list.join("\n")}
1407
+ ]`);
1408
+ }
1409
+ return ` {
1410
+ ${parts.join(",\n ")},
1411
+ },`;
1412
+ };
1413
+ return `// The catalog of roles of ${text(system)}, as Cauthy has it.
1414
+ // Written by \`cauthy-auth policy --pull\`: run it again after the catalog
1415
+ // changes, and do not edit this file by hand.
1416
+
1417
+ import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
1418
+
1419
+ export const policy = definePolicy([
1420
+ ${roles.map(one).join("\n")}
1421
+ ])
1422
+
1423
+ export const { can, hasRole } = policy
1424
+ export type Role = RoleOf<typeof policy>
1425
+ export type Permission = PermissionOf<typeof policy>
1426
+ `;
1427
+ }
1428
+ async function pullPolicy(o = {}) {
1429
+ const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
1430
+ const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
1431
+ const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
1432
+ const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
1433
+ const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
1434
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
1435
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
1436
+ }
1437
+ const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
1438
+ headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, accept: "application/json" }
1439
+ });
1440
+ const body = await res.json().catch(() => null);
1441
+ if (!res.ok) throw new Error(`cauthy-auth: Cauthy refused to give the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
1442
+ return body?.data?.roles ?? [];
1443
+ }
892
1444
  async function pushPolicy(o) {
893
- const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
894
- const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
895
- const system = o.system ?? env3("CAUTHY_SYSTEM") ?? "";
896
- const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
897
- const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
1445
+ const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
1446
+ const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
1447
+ const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
1448
+ const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
1449
+ const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
898
1450
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
899
1451
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
900
1452
  }
@@ -917,10 +1469,10 @@ function sessionSecret() {
917
1469
  return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
918
1470
  }
919
1471
  async function connectSystem(o) {
920
- const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
921
- const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
922
- const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
923
- const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
1472
+ const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
1473
+ const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
1474
+ const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
1475
+ const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
924
1476
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
925
1477
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to connect a system`);
926
1478
  }
@@ -952,17 +1504,88 @@ async function connectSystem(o) {
952
1504
  env: { ...d.env, CAUTHY_SECRET: sessionSecret() }
953
1505
  };
954
1506
  }
1507
+
1508
+ // src/verify.ts
1509
+ import { createRemoteJWKSet as createRemoteJWKSet2, jwtVerify as jwtVerify2 } from "jose";
1510
+ var CauthyTokenError = class extends Error {
1511
+ constructor(reason) {
1512
+ super(`cauthy-auth: token refused: ${reason}`);
1513
+ this.reason = reason;
1514
+ this.name = "CauthyTokenError";
1515
+ }
1516
+ reason;
1517
+ };
1518
+ var keys2 = /* @__PURE__ */ new Map();
1519
+ function keysOf2(issuer) {
1520
+ let set = keys2.get(issuer);
1521
+ if (set === void 0) {
1522
+ set = createRemoteJWKSet2(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 3e4 });
1523
+ keys2.set(issuer, set);
1524
+ }
1525
+ return set;
1526
+ }
1527
+ function env3(name) {
1528
+ return typeof process !== "undefined" ? process.env?.[name] : void 0;
1529
+ }
1530
+ function bearer(from) {
1531
+ if (typeof from === "string") return from.replace(/^Bearer\s+/i, "").trim();
1532
+ const header = (from instanceof Headers ? from : from.headers).get("authorization") ?? "";
1533
+ return /^Bearer\s+/i.test(header) ? header.replace(/^Bearer\s+/i, "").trim() : "";
1534
+ }
1535
+ async function verifyToken(from, options = {}) {
1536
+ const issuer = (options.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
1537
+ if (issuer === "") throw new CauthyTokenError("issuer (CAUTHY_ISSUER) is not set");
1538
+ const token = bearer(from);
1539
+ if (token === "") throw new CauthyTokenError("no token");
1540
+ let claims;
1541
+ try {
1542
+ const verified = await jwtVerify2(token, options.jwks ?? keysOf2(issuer), {
1543
+ algorithms: ["RS256"],
1544
+ issuer,
1545
+ audience: options.audience ?? env3("CAUTHY_AUDIENCE"),
1546
+ requiredClaims: ["exp", "sub"],
1547
+ currentDate: options.now
1548
+ });
1549
+ claims = verified.payload;
1550
+ } catch (err) {
1551
+ throw new CauthyTokenError(err instanceof Error ? err.message : "bad signature");
1552
+ }
1553
+ const tenantId = typeof claims.tid === "string" ? claims.tid : "";
1554
+ const wanted = options.tenantId ?? env3("CAUTHY_TENANT_ID");
1555
+ if (wanted && tenantId !== wanted) throw new CauthyTokenError("issued for another workspace");
1556
+ return {
1557
+ userId: String(claims.sub),
1558
+ tenantId,
1559
+ ...typeof claims.email === "string" && claims.email !== "" ? { email: claims.email } : {},
1560
+ roles: Array.isArray(claims.roles) ? claims.roles.filter((r) => typeof r === "string") : [],
1561
+ ...typeof claims.sys === "string" && claims.sys !== "" ? { system: claims.sys } : {},
1562
+ expiresAt: new Date(claims.exp * 1e3).toISOString(),
1563
+ token
1564
+ };
1565
+ }
955
1566
  export {
956
1567
  CauthyApiError,
957
1568
  CauthyConfigError,
1569
+ CauthyTokenError,
958
1570
  CauthyWebhookError,
1571
+ MIN_PASSWORD,
959
1572
  can,
1573
+ checkCode,
1574
+ checkEmail,
1575
+ checkName,
1576
+ checkNewPassword,
1577
+ checkPassword,
960
1578
  connectSystem,
961
1579
  createCauthy,
962
1580
  createSystem,
1581
+ definePolicy,
1582
+ policySource,
1583
+ pullPolicy,
963
1584
  pushPolicy,
964
1585
  pushTheme,
965
1586
  themeFromStackmyth,
1587
+ tokensFromStackmyth,
1588
+ verifyToken,
966
1589
  verifyWebhook
967
1590
  };
968
1591
  //# sourceMappingURL=index.mjs.map