cauthy-auth 0.5.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +68 -3
  2. package/dist/{cauthy-BCTCqubt.d.ts → cauthy-Bn3iv50y.d.ts} +1 -1
  3. package/dist/{cauthy-hswMoS7B.d.mts → cauthy-C29HCvag.d.mts} +1 -1
  4. package/dist/cli.js +490 -25
  5. package/dist/client.d.mts +273 -0
  6. package/dist/client.d.ts +273 -0
  7. package/dist/client.js +342 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/client.mjs +308 -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 +713 -37
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +701 -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 +592 -7
  42. package/dist/react.d.ts +592 -7
  43. package/dist/react.js +1025 -14
  44. package/dist/react.js.map +1 -1
  45. package/dist/react.mjs +1013 -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 +413 -0
  51. package/dist/svelte.js.map +1 -0
  52. package/dist/svelte.mjs +384 -0
  53. package/dist/svelte.mjs.map +1 -0
  54. package/dist/{system-DvcQjORr.d.ts → system-DhRr_WxR.d.ts} +139 -1
  55. package/dist/{system-CR8p4BjB.d.mts → system-ZD_Wa8Bm.d.mts} +139 -1
  56. package/dist/theme.d.mts +56 -1
  57. package/dist/theme.d.ts +56 -1
  58. package/dist/theme.js +181 -5
  59. package/dist/theme.js.map +1 -1
  60. package/dist/theme.mjs +176 -4
  61. package/dist/theme.mjs.map +1 -1
  62. package/dist/validate-B_iC-4y8.d.mts +19 -0
  63. package/dist/validate-B_iC-4y8.d.ts +19 -0
  64. package/dist/vue.d.mts +28 -0
  65. package/dist/vue.d.ts +28 -0
  66. package/dist/vue.js +431 -0
  67. package/dist/vue.js.map +1 -0
  68. package/dist/vue.mjs +402 -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;
@@ -259,7 +341,8 @@ function toWire(theme) {
259
341
  panelIconUrl: "panel_icon_url",
260
342
  texts: "texts",
261
343
  inputBackground: "input_background",
262
- inputBorder: "input_border"
344
+ inputBorder: "input_border",
345
+ tokens: "tokens"
263
346
  };
264
347
  const wire = {};
265
348
  for (const [k, v] of Object.entries(theme)) {
@@ -293,18 +376,19 @@ function fromWire(wire) {
293
376
  panel_icon_url: "panelIconUrl",
294
377
  texts: "texts",
295
378
  input_background: "inputBackground",
296
- input_border: "inputBorder"
379
+ input_border: "inputBorder",
380
+ tokens: "tokens"
297
381
  };
298
382
  const theme = {};
299
383
  for (const [k, v] of Object.entries(wire ?? {})) if (names[k] && v !== void 0 && v !== null && v !== "") theme[names[k]] = v;
300
384
  return theme;
301
385
  }
302
386
  async function pushTheme(o) {
303
- const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
304
- const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
305
- const clientId = o.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
306
- const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
307
- 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") ?? "";
308
392
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
309
393
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
310
394
  }
@@ -484,6 +568,11 @@ function createSystem(given = {}) {
484
568
  return fromWire(await data("GET", `/v1/oauth/clients/${id(of)}/theme`));
485
569
  }
486
570
  },
571
+ policy: {
572
+ async get(system) {
573
+ return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
574
+ }
575
+ },
487
576
  async publicJwks() {
488
577
  if (!key) return { keys: [] };
489
578
  const { kty, n, e } = asJwk(key) ?? await exportJWK(await signingKey());
@@ -492,6 +581,45 @@ function createSystem(given = {}) {
492
581
  };
493
582
  }
494
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
+
495
623
  // src/webhook.ts
496
624
  import { createRemoteJWKSet, jwtVerify } from "jose";
497
625
  var CauthyWebhookError = class extends Error {
@@ -513,8 +641,8 @@ function keysOf(issuer) {
513
641
  }
514
642
  var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
515
643
  async function verifyWebhook(request, options = {}) {
516
- const env3 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
517
- 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(/\/+$/, "");
518
646
  if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
519
647
  const signature = request.headers.get("x-cauthy-signature-jwt");
520
648
  if (!signature) throw new CauthyWebhookError("no signature");
@@ -565,9 +693,19 @@ async function verifyWebhook(request, options = {}) {
565
693
 
566
694
  // src/cauthy.ts
567
695
  var SESSION_DAYS = 30;
568
- var FLOW_SECONDS = 600;
696
+ var FLOW_SECONDS = 3600;
697
+ var AGAIN_SECONDS = 120;
569
698
  var SKEW_SECONDS = 30;
570
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
+ }
571
709
  function env2(name) {
572
710
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
573
711
  }
@@ -600,6 +738,7 @@ function createCauthy(given = {}) {
600
738
  const clientSecret = given.clientSecret ?? env2("CAUTHY_CLIENT_SECRET");
601
739
  const secret = given.secret ?? env2("CAUTHY_SECRET") ?? "";
602
740
  const system = given.system ?? env2("CAUTHY_SYSTEM") ?? "";
741
+ const tenant = given.tenant ?? env2("CAUTHY_TENANT") ?? "";
603
742
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
604
743
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
605
744
  if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
@@ -608,18 +747,26 @@ function createCauthy(given = {}) {
608
747
  const afterSignOut = localPath(given.afterSignOut, "/");
609
748
  const cookieName = given.cookieName ?? "cauthy_session";
610
749
  const flowCookie = `${cookieName}_flow`;
750
+ const againCookie = `${cookieName}_again`;
751
+ const stepCookie = `${cookieName}_step`;
611
752
  const doFetch = (...args) => (given.fetch ?? fetch)(...args);
612
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 });
613
760
  const originOf = (request) => (given.baseURL ?? new URL(request.url).origin).replace(/\/+$/, "");
614
761
  const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
615
762
  async function token(form) {
616
- const body = new URLSearchParams({ client_id: clientId, ...form });
617
- if (clientSecret) body.set("client_secret", clientSecret);
763
+ const body2 = new URLSearchParams({ client_id: clientId, ...form });
764
+ if (clientSecret) body2.set("client_secret", clientSecret);
618
765
  try {
619
766
  const res = await doFetch(`${issuer}/v1/oauth/token`, {
620
767
  method: "POST",
621
768
  headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
622
- body
769
+ body: body2
623
770
  });
624
771
  if (res.ok) return { ok: true, body: await res.json() };
625
772
  return { ok: false, ended: res.status >= 400 && res.status < 500 };
@@ -649,12 +796,12 @@ function createCauthy(given = {}) {
649
796
  return null;
650
797
  }
651
798
  }
652
- async function stored(body) {
653
- if (typeof body?.access_token !== "string" || typeof body?.refresh_token !== "string") return null;
654
- 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);
655
802
  if (who === null || who.u.id === "") return null;
656
- const lifetime = typeof body.expires_in === "number" && body.expires_in > 0 ? body.expires_in : 300;
657
- 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 };
658
805
  }
659
806
  function toSession(s) {
660
807
  return { user: s.u, tenantId: s.tid, roles: s.roles, accessToken: s.at, expiresAt: new Date(s.exp * 1e3).toISOString() };
@@ -728,8 +875,8 @@ function createCauthy(given = {}) {
728
875
  const url = `${issuer}/v1/oauth/organizations${system ? `?system=${encodeURIComponent(system)}` : ""}`;
729
876
  const res = await doFetch(url, { headers: { authorization: `Bearer ${session.accessToken}` } });
730
877
  if (!res.ok) throw new Error(`cauthy-auth: reading organizations: ${res.status}`);
731
- const body = await res.json();
732
- const value = (body.data ?? []).map(
878
+ const body2 = await res.json();
879
+ const value = (body2.data ?? []).map(
733
880
  (o) => ({
734
881
  id: String(o.id),
735
882
  system: String(o.system ?? ""),
@@ -774,20 +921,28 @@ function createCauthy(given = {}) {
774
921
  if (prompt === "login" || prompt === "none") q.set("prompt", prompt);
775
922
  const locale = url.searchParams.get("locale") ?? url.searchParams.get("ui_locales");
776
923
  if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set("ui_locales", locale);
777
- 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}`, [
778
926
  writeCookie(flowCookie, await seal(flow, await key), attrs(request, FLOW_SECONDS))
779
927
  ]);
780
928
  }
781
929
  function failed(request, reason) {
782
930
  const h = new Headers({ "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
783
931
  h.append("set-cookie", writeCookie(flowCookie, "", attrs(request, 0)));
932
+ h.append("set-cookie", writeCookie(againCookie, "", attrs(request, 0)));
784
933
  return new Response(`Sign-in could not be completed (${reason}). Go back and try again.`, { status: 400, headers: h });
785
934
  }
786
935
  async function callback(request, url) {
787
936
  const had = readCookies(request.headers.get("cookie"));
788
937
  const sealed = had.get(flowCookie);
789
938
  const flow = sealed ? await unseal(sealed, await key) : null;
790
- 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
+ }
791
946
  const error = url.searchParams.get("error");
792
947
  if (error !== null) {
793
948
  if (error === "login_required") return redirect(flow.returnTo, [writeCookie(flowCookie, "", attrs(request, 0))]);
@@ -803,28 +958,377 @@ function createCauthy(given = {}) {
803
958
  });
804
959
  const s = res.ok ? await stored(res.body) : null;
805
960
  if (s === null) return failed(request, "exchange_failed");
806
- 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))]);
807
962
  }
808
963
  async function logout(request) {
809
964
  const { had, value } = await read(request);
810
965
  if (value !== null) {
811
- const body = new URLSearchParams({ client_id: clientId, token: value.rt });
812
- 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);
813
968
  await doFetch(`${issuer}/v1/oauth/revoke`, {
814
969
  method: "POST",
815
970
  headers: { "content-type": "application/x-www-form-urlencoded" },
816
- body
971
+ body: body2
817
972
  }).catch(() => void 0);
818
973
  orgCache.delete(value.at);
819
974
  }
820
975
  const q = new URLSearchParams({ client_id: clientId, post_logout_redirect_uri: `${originOf(request)}${afterSignOut}` });
821
976
  return redirect(`${issuer}/v1/oauth/logout?${q}`, clearParts(cookieName, attrs(request, 0), had));
822
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", "/me/email", "/me/identities", "/me/identities/unlink", "/sign-out", "/methods", "/look"].includes(route) || route.startsWith("/sign-in/social/") || route.startsWith("/callback/") || route.startsWith("/me/identities/link/");
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.kind !== "link" || 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
+ if (step.kind === "link") {
1106
+ const mine = await signedInAs(request);
1107
+ if ("response" in mine) return failedTo("unauthenticated");
1108
+ const linked = await ask(request, "GET", `/v1/auth/identities/${provider}/link/callback?${q}`, void 0, void 0, mine.token);
1109
+ if (linked.error) return failedTo(linked.error.code);
1110
+ const h = new Headers(mine.headers);
1111
+ h.append("set-cookie", clear);
1112
+ return redirect(`${back}${back.includes("?") ? "&" : "?"}linked=${encodeURIComponent(provider)}`, [...h.getSetCookie()]);
1113
+ }
1114
+ const done = await ask(request, "GET", `/v1/auth/social/${provider}/callback?${q}`);
1115
+ if (done.error) return failedTo(done.error.code);
1116
+ const s = await stored(done.data);
1117
+ if (s === null) return failedTo("unavailable");
1118
+ return redirect(back, [...await write(request, s, readCookies(request.headers.get("cookie"))), clear]);
1119
+ }
1120
+ if (route.startsWith("/me/identities/link/") && !post) {
1121
+ const provider = providerIn(route.slice("/me/identities/link/".length));
1122
+ if (!PROVIDER_ID.test(provider)) return new Response("Not found", { status: 404 });
1123
+ const returnTo = localPath(url.searchParams.get("return_to"), afterSignIn);
1124
+ const failed2 = (code) => redirect(`${returnTo}${returnTo.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code)}`);
1125
+ const mine = await signedInAs(request);
1126
+ if ("response" in mine) return failed2("unauthenticated");
1127
+ const callback2 = `${basePath}/callback/${provider}`;
1128
+ const started = await ask(request, "GET", `/v1/auth/identities/${provider}/link?redirect_uri=${encodeURIComponent(`${originOf(request)}${callback2}`)}`, void 0, callback2, mine.token);
1129
+ if (started.error || typeof started.data?.redirect_url !== "string") return failed2(started.error?.code ?? "provider_unavailable");
1130
+ const step = { kind: "link", token: String(started.data.state ?? ""), provider, returnTo };
1131
+ return redirect(started.data.redirect_url, [...mine.headers.getSetCookie(), writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS))]);
1132
+ }
1133
+ if (route === "/me/identities" && !post) {
1134
+ const mine = await signedInAs(request);
1135
+ if ("response" in mine) return mine.response;
1136
+ const res = await ask(request, "GET", "/v1/auth/identities", void 0, void 0, mine.token);
1137
+ if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
1138
+ const list = Array.isArray(res.data) ? res.data : [];
1139
+ return json({ identities: list.map((x) => ({ provider: String(x.provider ?? ""), email: String(x.email ?? ""), name: String(x.name ?? ""), linkedAt: String(x.linked_at ?? "") })) }, 200, mine.headers);
1140
+ }
1141
+ if ((route === "/me" || route === "/me/sessions") && !post) {
1142
+ const mine = await signedInAs(request);
1143
+ if ("response" in mine) return mine.response;
1144
+ const res = await ask(request, "GET", route === "/me" ? "/v1/auth/me" : "/v1/auth/me/sessions", void 0, void 0, mine.token);
1145
+ if (res.error) return problem(res.status, res.error.code, res.error.message, void 0, mine.headers);
1146
+ if (route === "/me/sessions") {
1147
+ const list = Array.isArray(res.data) ? res.data : [];
1148
+ 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);
1149
+ }
1150
+ return json({ account: accountOf(res.data) }, 200, mine.headers);
1151
+ }
1152
+ if (!post) return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
1153
+ if (!ownRequest(request)) return problem(403, "cross_site", "This request did not come from this application.");
1154
+ if (route === "/sign-out") {
1155
+ const { had, value } = await read(request);
1156
+ if (value !== null) {
1157
+ const form = new URLSearchParams({ client_id: clientId, token: value.rt });
1158
+ if (clientSecret) form.set("client_secret", clientSecret);
1159
+ await doFetch(`${issuer}/v1/oauth/revoke`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form }).catch(() => void 0);
1160
+ orgCache.delete(value.at);
1161
+ }
1162
+ const h = new Headers();
1163
+ for (const c of clearParts(cookieName, attrs(request, 0), had)) h.append("set-cookie", c);
1164
+ return json({ status: "signed_out" }, 200, h);
1165
+ }
1166
+ const b = await body(request);
1167
+ if (b === null) return problem(400, "bad_request", "Send the form as JSON.");
1168
+ if (route === "/sign-in/email") {
1169
+ const bad = firstProblem(checkEmail(b.email), checkPassword(b.password));
1170
+ if (bad) return invalid(bad);
1171
+ const res = await ask(request, "POST", "/v1/auth/login", { email: String(b.email).trim(), password: b.password });
1172
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1173
+ if (res.data?.mfa_required === true) {
1174
+ const step = { kind: "mfa", token: String(res.data.mfa_token ?? ""), returnTo: afterSignIn };
1175
+ return json({ status: "mfa_required" }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
1176
+ }
1177
+ return signedIn(request, res.data);
1178
+ }
1179
+ if (route === "/sign-in/mfa") {
1180
+ const bad = checkCode(b.code);
1181
+ if (bad) return invalid(bad);
1182
+ const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
1183
+ const step = sealed ? await unseal(sealed, await key) : null;
1184
+ if (step === null || step.kind !== "mfa") return problem(400, "step_expired", "Sign in again: the code was asked for too long ago.");
1185
+ const res = await ask(request, "POST", "/v1/auth/mfa/challenge", { mfa_token: step.token, code: String(b.code).trim() });
1186
+ if (res.error) return problem(res.status, res.error.code, res.error.message, "code");
1187
+ return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))]);
1188
+ }
1189
+ if (route === "/sign-up/email") {
1190
+ const bad = firstProblem(checkName(b.name), checkEmail(b.email), checkNewPassword(b.password));
1191
+ if (bad) return invalid(bad);
1192
+ const email = String(b.email).trim();
1193
+ const made = await ask(request, "POST", "/v1/auth/register", { email, password: b.password, display_name: typeof b.name === "string" ? b.name.trim() : "" }, pages.verifyEmail);
1194
+ if (made.error) return problem(made.status, made.error.code, made.error.message);
1195
+ if (made.data?.verification_required === true) return json({ status: "verification_required", email });
1196
+ const res = await ask(request, "POST", "/v1/auth/login", { email, password: b.password });
1197
+ if (res.error || res.data?.mfa_required === true) return json({ status: "verification_required", email });
1198
+ return signedIn(request, res.data);
1199
+ }
1200
+ if (route === "/forgot-password") {
1201
+ const bad = checkEmail(b.email);
1202
+ if (bad) return invalid(bad);
1203
+ await ask(request, "POST", "/v1/auth/password/reset", { email: String(b.email).trim() }, pages.resetPassword);
1204
+ return json({ status: "sent" });
1205
+ }
1206
+ if (route.startsWith("/me")) {
1207
+ const mine = await signedInAs(request);
1208
+ if ("response" in mine) return mine.response;
1209
+ const as = (path, payload, method = "POST") => ask(request, method, path, payload, void 0, mine.token);
1210
+ const refused = (res, field) => problem(res.status, res.error.code, res.error.message, field, mine.headers);
1211
+ const password = typeof b.password === "string" ? b.password : "";
1212
+ if (route === "/me") {
1213
+ const bad = checkName(b.name);
1214
+ if (bad) return invalid(bad);
1215
+ const res = await as("/v1/auth/me", { name: typeof b.name === "string" ? b.name.trim() : "" }, "PATCH");
1216
+ if (res.error) return refused(res, "name");
1217
+ const h = mine.headers;
1218
+ if (mine.kept !== null) {
1219
+ 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)) });
1220
+ if (again !== null) for (const c of await write(request, again, readCookies(request.headers.get("cookie")))) h.append("set-cookie", c);
1221
+ }
1222
+ return json({ status: "saved", account: accountOf(res.data) }, 200, h);
1223
+ }
1224
+ if (route === "/me/password") {
1225
+ const bad = checkNewPassword(b.newPassword);
1226
+ if (bad) return problem(400, bad.code, bad.message, "newPassword");
1227
+ const res = await as("/v1/auth/me/password", { current_password: password, new_password: b.newPassword });
1228
+ if (res.error) return refused(res, res.error.code === "wrong_password" ? "password" : res.error.code === "weak_password" ? "newPassword" : void 0);
1229
+ const next = await stored(res.data);
1230
+ const h = new Headers();
1231
+ const had = readCookies(request.headers.get("cookie"));
1232
+ for (const c of next !== null ? await write(request, next, had) : clearParts(cookieName, attrs(request, 0), had)) h.append("set-cookie", c);
1233
+ return json({ status: "changed", signedIn: next !== null }, 200, h);
1234
+ }
1235
+ if (route === "/me/mfa/setup") {
1236
+ const res = await as("/v1/auth/mfa/setup", { password });
1237
+ if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
1238
+ return json({ status: "started", secret: String(res.data?.secret ?? ""), otpauthUri: String(res.data?.otpauth_uri ?? "") }, 200, mine.headers);
1239
+ }
1240
+ if (route === "/me/mfa/verify") {
1241
+ const bad = checkCode(b.code);
1242
+ if (bad) return invalid(bad);
1243
+ const res = await as("/v1/auth/mfa/verify", { code: String(b.code).trim() });
1244
+ if (res.error) return refused(res, "code");
1245
+ return json({ status: "enabled", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
1246
+ }
1247
+ if (route === "/me/mfa/disable") {
1248
+ const res = await as("/v1/auth/me/mfa/disable", { password });
1249
+ if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
1250
+ return json({ status: "disabled" }, 200, mine.headers);
1251
+ }
1252
+ if (route === "/me/mfa/recovery-codes") {
1253
+ const res = await as("/v1/auth/me/mfa/recovery-codes", { password });
1254
+ if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : void 0);
1255
+ return json({ status: "renewed", recoveryCodes: Array.isArray(res.data?.recovery_codes) ? res.data.recovery_codes.map(String) : [] }, 200, mine.headers);
1256
+ }
1257
+ if (route === "/me/email") {
1258
+ const bad = checkEmail(b.email);
1259
+ if (bad) return invalid(bad);
1260
+ const email = String(b.email).trim();
1261
+ const res = await as("/v1/auth/me/email", { email, password });
1262
+ if (res.error) return refused(res, res.error.code === "step_up_required" ? "password" : ["invalid_email", "email_unchanged", "email_taken"].includes(res.error.code) ? "email" : void 0);
1263
+ return json({ status: "sent", email }, 200, mine.headers);
1264
+ }
1265
+ if (route === "/me/identities/unlink") {
1266
+ const provider = typeof b.provider === "string" ? b.provider : "";
1267
+ if (!PROVIDER_ID.test(provider)) return problem(400, "invalid_provider", "Say which provider to unlink.", "provider", mine.headers);
1268
+ const res = await as(`/v1/auth/identities/${provider}`, void 0, "DELETE");
1269
+ if (res.error) return refused(res);
1270
+ return json({ status: "unlinked", provider }, 200, mine.headers);
1271
+ }
1272
+ if (route === "/me/sessions/end") {
1273
+ const res = await as("/v1/auth/me/sessions", void 0, "DELETE");
1274
+ if (res.error) return refused(res);
1275
+ const h = new Headers();
1276
+ for (const c of clearParts(cookieName, attrs(request, 0), readCookies(request.headers.get("cookie")))) h.append("set-cookie", c);
1277
+ orgCache.delete(mine.token);
1278
+ return json({ status: "signed_out" }, 200, h);
1279
+ }
1280
+ return null;
1281
+ }
1282
+ const isToken = (v) => typeof v === "string" && v !== "" && v.length <= 512;
1283
+ const badLink = () => problem(422, "invalid_token", "This link is not valid, or it has expired.");
1284
+ if (route === "/reset-password") {
1285
+ if (!isToken(b.token)) return badLink();
1286
+ const bad = checkNewPassword(b.password);
1287
+ if (bad) return invalid(bad);
1288
+ const res = await ask(request, "POST", "/v1/auth/password/change", { token: String(b.token), new_password: b.password });
1289
+ if (res.error) return problem(res.status, res.error.code, res.error.message, res.error.code === "weak_password" ? "password" : void 0);
1290
+ return json({ status: "changed" });
1291
+ }
1292
+ if (route === "/verify-email") {
1293
+ if (!isToken(b.token)) return badLink();
1294
+ const res = await ask(request, "POST", "/v1/auth/email/verify", { token: String(b.token) });
1295
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1296
+ return json({ status: "verified" });
1297
+ }
1298
+ if (route === "/sign-in/magic-link") {
1299
+ const bad = checkEmail(b.email);
1300
+ if (bad) return invalid(bad);
1301
+ 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");
1302
+ const email = String(b.email).trim();
1303
+ await ask(request, "POST", "/v1/auth/magic-link/request", { email }, pages.signIn);
1304
+ const step = { kind: "code", token: email, returnTo: afterSignIn };
1305
+ return json({ status: "sent" }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
1306
+ }
1307
+ if (route === "/sign-in/email-code") {
1308
+ const bad = checkCode(b.code);
1309
+ if (bad) return invalid(bad);
1310
+ const sealed = readCookies(request.headers.get("cookie")).get(stepCookie);
1311
+ const step = sealed ? await unseal(sealed, await key) : null;
1312
+ if (step === null || step.kind !== "code") return problem(400, "step_expired", "Ask for a new link: this one was asked for too long ago.");
1313
+ const res = await ask(request, "POST", "/v1/auth/magic-link/verify-code", { email: step.token, code: String(b.code).trim() });
1314
+ if (res.error) return problem(res.status, res.error.code, res.error.message, "code");
1315
+ return signedIn(request, res.data, [writeCookie(stepCookie, "", attrs(request, 0))]);
1316
+ }
1317
+ if (route === "/sign-in/magic-link/verify") {
1318
+ if (!isToken(b.token)) return badLink();
1319
+ const res = await ask(request, "POST", "/v1/auth/magic-link/verify", { token: String(b.token) });
1320
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1321
+ return signedIn(request, res.data);
1322
+ }
1323
+ return null;
1324
+ }
823
1325
  async function handler(request) {
824
1326
  const url = new URL(request.url);
825
1327
  if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return new Response("Not found", { status: 404 });
826
1328
  const route = url.pathname.slice(basePath.length).replace(/\/+$/, "") || "/";
827
1329
  const get = request.method === "GET" || request.method === "HEAD";
1330
+ const mine = await ownRoute(request, url, route);
1331
+ if (mine !== null) return mine;
828
1332
  if (route === "/login" && get) return login(request, url);
829
1333
  if (route === "/callback" && get) return callback(request, url);
830
1334
  if (route === "/logout" && (get || request.method === "POST")) return logout(request);
@@ -868,7 +1372,7 @@ function createCauthy(given = {}) {
868
1372
  return {
869
1373
  handler,
870
1374
  webhook,
871
- system: createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch }),
1375
+ system: ownSystem,
872
1376
  getSession,
873
1377
  status,
874
1378
  resolve: resolve2,
@@ -893,12 +1397,101 @@ function can(organization, resource, action) {
893
1397
  if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
894
1398
  return organization.permissions.some((p) => matches(p, resource, action));
895
1399
  }
1400
+ function definePolicy(roles) {
1401
+ const entry = (p) => `${p.resource}:${p.action}`;
1402
+ const byName = new Map(roles.map((r) => [r.role, r]));
1403
+ const split = (permission) => {
1404
+ const i = permission.lastIndexOf(":");
1405
+ return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
1406
+ };
1407
+ return {
1408
+ roles: roles.map((r) => ({
1409
+ role: r.role,
1410
+ ...r.name !== void 0 ? { name: r.name } : {},
1411
+ ...r.description !== void 0 ? { description: r.description } : {},
1412
+ ...r.inherits ? { inherits: [...r.inherits] } : {},
1413
+ ...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
1414
+ })),
1415
+ roleNames: roles.map((r) => r.role),
1416
+ permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
1417
+ can(organization, permission) {
1418
+ const [resource, action] = split(permission);
1419
+ return can(organization, resource, action);
1420
+ },
1421
+ hasRole(organization, ...wanted) {
1422
+ return organization != null && wanted.includes(organization.role);
1423
+ },
1424
+ permissionsOf(role) {
1425
+ const seen = /* @__PURE__ */ new Set();
1426
+ const out2 = /* @__PURE__ */ new Set();
1427
+ const walk = (name) => {
1428
+ if (seen.has(name)) return;
1429
+ seen.add(name);
1430
+ const r = byName.get(name);
1431
+ for (const p of r?.permissions ?? []) out2.add(entry(p));
1432
+ for (const parent of r?.inherits ?? []) walk(parent);
1433
+ };
1434
+ walk(role);
1435
+ return [...out2];
1436
+ }
1437
+ };
1438
+ }
1439
+ var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1440
+ function policySource(system, roles) {
1441
+ const text = (v) => JSON.stringify(v);
1442
+ const key = (k) => IDENT.test(k) ? k : text(k);
1443
+ const one = (r) => {
1444
+ const parts = [`role: ${text(r.role)}`];
1445
+ if (r.name) parts.push(`name: ${text(r.name)}`);
1446
+ if (r.description) parts.push(`description: ${text(r.description)}`);
1447
+ if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
1448
+ if (r.permissions?.length) {
1449
+ const list = r.permissions.map((p) => ` { ${[...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [], `resource: ${text(p.resource)}`, `action: ${text(p.action)}`].join(", ")} },`);
1450
+ parts.push(`permissions: [
1451
+ ${list.join("\n")}
1452
+ ]`);
1453
+ }
1454
+ return ` {
1455
+ ${parts.join(",\n ")},
1456
+ },`;
1457
+ };
1458
+ return `// The catalog of roles of ${text(system)}, as Cauthy has it.
1459
+ // Written by \`cauthy-auth policy --pull\`: run it again after the catalog
1460
+ // changes, and do not edit this file by hand.
1461
+
1462
+ import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
1463
+
1464
+ export const policy = definePolicy([
1465
+ ${roles.map(one).join("\n")}
1466
+ ])
1467
+
1468
+ export const { can, hasRole } = policy
1469
+ export type Role = RoleOf<typeof policy>
1470
+ export type Permission = PermissionOf<typeof policy>
1471
+ `;
1472
+ }
1473
+ async function pullPolicy(o = {}) {
1474
+ const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
1475
+ const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
1476
+ const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
1477
+ const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
1478
+ const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
1479
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
1480
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
1481
+ }
1482
+ const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
1483
+ headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, accept: "application/json" }
1484
+ });
1485
+ const body = await res.json().catch(() => null);
1486
+ if (!res.ok) throw new Error(`cauthy-auth: Cauthy refused to give the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
1487
+ return body?.data?.roles ?? [];
1488
+ }
896
1489
  async function pushPolicy(o) {
897
- const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
898
- const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
899
- const system = o.system ?? env3("CAUTHY_SYSTEM") ?? "";
900
- const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
901
- const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
1490
+ const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
1491
+ const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
1492
+ const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
1493
+ const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
1494
+ const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
902
1495
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
903
1496
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
904
1497
  }
@@ -921,10 +1514,10 @@ function sessionSecret() {
921
1514
  return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
922
1515
  }
923
1516
  async function connectSystem(o) {
924
- const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
925
- const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
926
- const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
927
- const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
1517
+ const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
1518
+ const issuer = (o.issuer ?? env4("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
1519
+ const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
1520
+ const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
928
1521
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
929
1522
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to connect a system`);
930
1523
  }
@@ -956,17 +1549,88 @@ async function connectSystem(o) {
956
1549
  env: { ...d.env, CAUTHY_SECRET: sessionSecret() }
957
1550
  };
958
1551
  }
1552
+
1553
+ // src/verify.ts
1554
+ import { createRemoteJWKSet as createRemoteJWKSet2, jwtVerify as jwtVerify2 } from "jose";
1555
+ var CauthyTokenError = class extends Error {
1556
+ constructor(reason) {
1557
+ super(`cauthy-auth: token refused: ${reason}`);
1558
+ this.reason = reason;
1559
+ this.name = "CauthyTokenError";
1560
+ }
1561
+ reason;
1562
+ };
1563
+ var keys2 = /* @__PURE__ */ new Map();
1564
+ function keysOf2(issuer) {
1565
+ let set = keys2.get(issuer);
1566
+ if (set === void 0) {
1567
+ set = createRemoteJWKSet2(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 3e4 });
1568
+ keys2.set(issuer, set);
1569
+ }
1570
+ return set;
1571
+ }
1572
+ function env3(name) {
1573
+ return typeof process !== "undefined" ? process.env?.[name] : void 0;
1574
+ }
1575
+ function bearer(from) {
1576
+ if (typeof from === "string") return from.replace(/^Bearer\s+/i, "").trim();
1577
+ const header = (from instanceof Headers ? from : from.headers).get("authorization") ?? "";
1578
+ return /^Bearer\s+/i.test(header) ? header.replace(/^Bearer\s+/i, "").trim() : "";
1579
+ }
1580
+ async function verifyToken(from, options = {}) {
1581
+ const issuer = (options.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
1582
+ if (issuer === "") throw new CauthyTokenError("issuer (CAUTHY_ISSUER) is not set");
1583
+ const token = bearer(from);
1584
+ if (token === "") throw new CauthyTokenError("no token");
1585
+ let claims;
1586
+ try {
1587
+ const verified = await jwtVerify2(token, options.jwks ?? keysOf2(issuer), {
1588
+ algorithms: ["RS256"],
1589
+ issuer,
1590
+ audience: options.audience ?? env3("CAUTHY_AUDIENCE"),
1591
+ requiredClaims: ["exp", "sub"],
1592
+ currentDate: options.now
1593
+ });
1594
+ claims = verified.payload;
1595
+ } catch (err) {
1596
+ throw new CauthyTokenError(err instanceof Error ? err.message : "bad signature");
1597
+ }
1598
+ const tenantId = typeof claims.tid === "string" ? claims.tid : "";
1599
+ const wanted = options.tenantId ?? env3("CAUTHY_TENANT_ID");
1600
+ if (wanted && tenantId !== wanted) throw new CauthyTokenError("issued for another workspace");
1601
+ return {
1602
+ userId: String(claims.sub),
1603
+ tenantId,
1604
+ ...typeof claims.email === "string" && claims.email !== "" ? { email: claims.email } : {},
1605
+ roles: Array.isArray(claims.roles) ? claims.roles.filter((r) => typeof r === "string") : [],
1606
+ ...typeof claims.sys === "string" && claims.sys !== "" ? { system: claims.sys } : {},
1607
+ expiresAt: new Date(claims.exp * 1e3).toISOString(),
1608
+ token
1609
+ };
1610
+ }
959
1611
  export {
960
1612
  CauthyApiError,
961
1613
  CauthyConfigError,
1614
+ CauthyTokenError,
962
1615
  CauthyWebhookError,
1616
+ MIN_PASSWORD,
963
1617
  can,
1618
+ checkCode,
1619
+ checkEmail,
1620
+ checkName,
1621
+ checkNewPassword,
1622
+ checkPassword,
964
1623
  connectSystem,
965
1624
  createCauthy,
966
1625
  createSystem,
1626
+ definePolicy,
1627
+ policySource,
1628
+ pullPolicy,
967
1629
  pushPolicy,
968
1630
  pushTheme,
969
1631
  themeFromStackmyth,
1632
+ tokensFromStackmyth,
1633
+ verifyToken,
970
1634
  verifyWebhook
971
1635
  };
972
1636
  //# sourceMappingURL=index.mjs.map