cauthy-auth 0.5.2 → 0.8.0

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