cauthy-auth 0.27.1 → 0.28.1

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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/{cauthy-DGAy45G2.d.ts → cauthy-BAwAJChs.d.ts} +1 -1
  3. package/dist/{cauthy-XkIbG5gE.d.mts → cauthy-CTVg4kvo.d.mts} +1 -1
  4. package/dist/client.d.mts +13 -4
  5. package/dist/client.d.ts +13 -4
  6. package/dist/client.js +130 -15
  7. package/dist/client.js.map +1 -1
  8. package/dist/client.mjs +124 -14
  9. package/dist/client.mjs.map +1 -1
  10. package/dist/express.d.mts +2 -2
  11. package/dist/express.d.ts +2 -2
  12. package/dist/fastify.d.mts +2 -2
  13. package/dist/fastify.d.ts +2 -2
  14. package/dist/hono.d.mts +2 -2
  15. package/dist/hono.d.ts +2 -2
  16. package/dist/index.d.mts +3 -3
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.js +174 -19
  19. package/dist/index.js.map +1 -1
  20. package/dist/index.mjs +169 -19
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/next.d.mts +2 -2
  23. package/dist/next.d.ts +2 -2
  24. package/dist/react.d.mts +27 -1
  25. package/dist/react.d.ts +27 -1
  26. package/dist/react.js +146 -19
  27. package/dist/react.js.map +1 -1
  28. package/dist/react.mjs +146 -19
  29. package/dist/react.mjs.map +1 -1
  30. package/dist/styles.css +6 -0
  31. package/dist/svelte.d.mts +3 -3
  32. package/dist/svelte.d.ts +3 -3
  33. package/dist/svelte.js +118 -13
  34. package/dist/svelte.js.map +1 -1
  35. package/dist/svelte.mjs +118 -13
  36. package/dist/svelte.mjs.map +1 -1
  37. package/dist/{system-DTIeDXDP.d.ts → system-Bn8uckvQ.d.ts} +5 -3
  38. package/dist/{system-BxMsSIu1.d.mts → system-CVBW_AV8.d.mts} +5 -3
  39. package/dist/validate-sEXG82po.d.mts +66 -0
  40. package/dist/validate-sEXG82po.d.ts +66 -0
  41. package/dist/vue.d.mts +3 -3
  42. package/dist/vue.d.ts +3 -3
  43. package/dist/vue.js +118 -13
  44. package/dist/vue.js.map +1 -1
  45. package/dist/vue.mjs +118 -13
  46. package/dist/vue.mjs.map +1 -1
  47. package/package.json +1 -1
  48. package/dist/validate-YbEfIzfl.d.mts +0 -19
  49. package/dist/validate-YbEfIzfl.d.ts +0 -19
package/dist/index.mjs CHANGED
@@ -609,7 +609,111 @@ function createSystem(given = {}) {
609
609
  // src/validate.ts
610
610
  var MIN_PASSWORD = 12;
611
611
  var MAX_PASSWORD = 128;
612
+ var FLOOR_PASSWORD = 8;
612
613
  var MAX_NAME = 120;
614
+ var CLASSES = ["lowercase", "uppercase", "digit", "symbol"];
615
+ var DEFAULT_PASSWORD_POLICY = Object.freeze({ min: MIN_PASSWORD, max: MAX_PASSWORD, requires: [], min_classes: 0, reject_breached: false });
616
+ var int = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : fallback;
617
+ function passwordPolicyOf(raw) {
618
+ const r = raw && typeof raw === "object" ? raw : {};
619
+ const min = Math.min(Math.max(int(r.min ?? r.min_length, MIN_PASSWORD) || MIN_PASSWORD, FLOOR_PASSWORD), MAX_PASSWORD);
620
+ const max = Math.max(Math.min(int(r.max ?? r.max_length, MAX_PASSWORD) || MAX_PASSWORD, MAX_PASSWORD), min);
621
+ const asked = Array.isArray(r.requires) ? r.requires : [];
622
+ return {
623
+ min,
624
+ max,
625
+ requires: CLASSES.filter((c) => asked.includes(c)),
626
+ min_classes: Math.min(Math.max(int(r.min_classes ?? r.minClasses, 0), 0), CLASSES.length),
627
+ reject_breached: (r.reject_breached ?? r.rejectBreached) === true
628
+ };
629
+ }
630
+ function utf8Length(v) {
631
+ let n = 0;
632
+ for (const ch of v) {
633
+ const c = ch.codePointAt(0);
634
+ n += c < 128 ? 1 : c < 2048 ? 2 : c < 65536 ? 3 : 4;
635
+ }
636
+ return n;
637
+ }
638
+ function classesOf(v) {
639
+ const has = /* @__PURE__ */ new Set();
640
+ for (const ch of v) {
641
+ if (/\p{Ll}/u.test(ch)) has.add("lowercase");
642
+ else if (/\p{Lu}/u.test(ch)) has.add("uppercase");
643
+ else if (/\p{Nd}/u.test(ch)) has.add("digit");
644
+ else if (!/\p{L}/u.test(ch)) has.add("symbol");
645
+ }
646
+ return has;
647
+ }
648
+ function passwordProblems(value, policy = DEFAULT_PASSWORD_POLICY) {
649
+ const p = passwordPolicyOf(policy);
650
+ const out2 = [];
651
+ const length = utf8Length(value);
652
+ if (length < p.min) out2.push("too_short");
653
+ if (length > p.max) out2.push("too_long");
654
+ const has = classesOf(value);
655
+ for (const c of p.requires) if (!has.has(c)) out2.push(`needs_${c}`);
656
+ if (p.min_classes > 0 && CLASSES.filter((c) => has.has(c)).length < p.min_classes) out2.push("needs_classes");
657
+ return out2;
658
+ }
659
+ var RULES = {
660
+ en: {
661
+ between: "Between {min} and {max} characters.",
662
+ requires: "Include {kinds}.",
663
+ and: "and",
664
+ classes: "Use at least {n} of these: lowercase letters, uppercase letters, digits, symbols.",
665
+ breached: "Passwords found in known data breaches are not accepted.",
666
+ kinds: { lowercase: "a lowercase letter", uppercase: "an uppercase letter", digit: "a digit", symbol: "a symbol" },
667
+ reasons: {
668
+ too_short: "Use at least {min} characters.",
669
+ too_long: "Use at most {max} characters.",
670
+ needs_lowercase: "Include a lowercase letter.",
671
+ needs_uppercase: "Include an uppercase letter.",
672
+ needs_digit: "Include a digit.",
673
+ needs_symbol: "Include a symbol.",
674
+ needs_classes: "Use at least {n} of these: lowercase letters, uppercase letters, digits, symbols.",
675
+ breached: "This password appears in a known data breach. Choose a different one."
676
+ }
677
+ },
678
+ es: {
679
+ between: "Entre {min} y {max} caracteres.",
680
+ requires: "Incluye {kinds}.",
681
+ and: "y",
682
+ classes: "Usa al menos {n} de estos tipos: min\xFAsculas, may\xFAsculas, n\xFAmeros, s\xEDmbolos.",
683
+ breached: "No se aceptan contrase\xF1as que aparecen en filtraciones de datos conocidas.",
684
+ kinds: { lowercase: "una min\xFAscula", uppercase: "una may\xFAscula", digit: "un n\xFAmero", symbol: "un s\xEDmbolo" },
685
+ reasons: {
686
+ too_short: "Usa al menos {min} caracteres.",
687
+ too_long: "Usa como m\xE1ximo {max} caracteres.",
688
+ needs_lowercase: "Incluye una min\xFAscula.",
689
+ needs_uppercase: "Incluye una may\xFAscula.",
690
+ needs_digit: "Incluye un n\xFAmero.",
691
+ needs_symbol: "Incluye un s\xEDmbolo.",
692
+ needs_classes: "Usa al menos {n} de estos tipos: min\xFAsculas, may\xFAsculas, n\xFAmeros, s\xEDmbolos.",
693
+ breached: "Esta contrase\xF1a aparece en una filtraci\xF3n de datos conocida. Elige otra."
694
+ }
695
+ }
696
+ };
697
+ var fill = (s, p) => s.replace("{min}", String(p.min)).replace("{max}", String(p.max)).replace("{n}", String(p.min_classes));
698
+ function describePasswordPolicy(policy = DEFAULT_PASSWORD_POLICY, locale = "en") {
699
+ const p = passwordPolicyOf(policy);
700
+ const t = RULES[locale] ?? RULES.en;
701
+ const parts = [fill(t.between, p)];
702
+ if (p.requires.length > 0) {
703
+ const kinds = p.requires.map((c) => t.kinds[c]);
704
+ const listed = kinds.length === 1 ? kinds[0] : `${kinds.slice(0, -1).join(", ")} ${t.and} ${kinds[kinds.length - 1]}`;
705
+ parts.push(t.requires.replace("{kinds}", listed));
706
+ }
707
+ if (p.min_classes > 0) parts.push(fill(t.classes, p));
708
+ if (p.reject_breached) parts.push(t.breached);
709
+ return parts.join(" ");
710
+ }
711
+ function explainPasswordProblems(reasons, policy = DEFAULT_PASSWORD_POLICY, locale = "en") {
712
+ const p = passwordPolicyOf(policy);
713
+ const t = RULES[locale] ?? RULES.en;
714
+ const said = reasons.filter((r) => r in t.reasons).map((r) => fill(t.reasons[r], p));
715
+ return said.length > 0 ? said.join(" ") : describePasswordPolicy(p, locale);
716
+ }
613
717
  var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
614
718
  function checkEmail(value) {
615
719
  const v = typeof value === "string" ? value.trim() : "";
@@ -622,13 +726,12 @@ function checkPassword(value) {
622
726
  if (value.length > MAX_PASSWORD) return { field: "password", code: "too_long", message: "That password is too long." };
623
727
  return null;
624
728
  }
625
- function checkNewPassword(value) {
626
- const basic = checkPassword(value);
627
- if (basic) return basic;
628
- if (value.length < MIN_PASSWORD) {
629
- return { field: "password", code: "too_short", message: `Use at least ${MIN_PASSWORD} characters.` };
630
- }
631
- return null;
729
+ function checkNewPassword(value, policy) {
730
+ if (typeof value !== "string" || value === "") return { field: "password", code: "required", message: "Enter your password." };
731
+ const p = policy ? passwordPolicyOf(policy) : DEFAULT_PASSWORD_POLICY;
732
+ const reasons = passwordProblems(value, p);
733
+ if (reasons.length === 0) return null;
734
+ return { field: "password", code: reasons[0], message: explainPasswordProblems(reasons, p) };
632
735
  }
633
736
  function checkName(value) {
634
737
  if (value === void 0 || value === null || value === "") return null;
@@ -743,6 +846,13 @@ function providerIn(segment) {
743
846
  function env2(name) {
744
847
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
745
848
  }
849
+ function hostOf(origin) {
850
+ try {
851
+ return new URL(origin).hostname;
852
+ } catch {
853
+ return void 0;
854
+ }
855
+ }
746
856
  function b64url(bytes) {
747
857
  let s = "";
748
858
  for (const b of bytes) s += String.fromCharCode(b);
@@ -776,6 +886,10 @@ function createCauthy(given = {}) {
776
886
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
777
887
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
778
888
  if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
889
+ const baseURL = given.baseURL ?? env2("CAUTHY_BASE_URL");
890
+ if (baseURL !== void 0 && baseURL !== "" && !/^https?:\/\/[^/]+\/*$/.test(baseURL)) {
891
+ throw new CauthyConfigError("baseURL (CAUTHY_BASE_URL) must be the application's public origin, e.g. https://app.example.com");
892
+ }
779
893
  const basePath = "/" + (given.basePath ?? "/api/auth").replace(/^\/+|\/+$/g, "");
780
894
  const afterSignIn = localPath(given.afterSignIn, "/");
781
895
  const afterSignOut = localPath(given.afterSignOut, "/");
@@ -791,7 +905,12 @@ function createCauthy(given = {}) {
791
905
  verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0
792
906
  };
793
907
  const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch });
794
- const originOf = (request) => withoutTrailingSlashes(given.baseURL ?? new URL(request.url).origin);
908
+ const originOf = (request) => {
909
+ if (baseURL) return withoutTrailingSlashes(baseURL);
910
+ const url = new URL(request.url);
911
+ const proto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim().toLowerCase();
912
+ return proto === "https" || proto === "http" ? `${proto}://${url.host}` : url.origin;
913
+ };
795
914
  const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
796
915
  async function token(form) {
797
916
  const body2 = new URLSearchParams({ client_id: clientId, ...form });
@@ -1087,11 +1206,20 @@ function createCauthy(given = {}) {
1087
1206
  connectedAt: String(d?.created_at ?? ""),
1088
1207
  lastUsedAt: String(d?.last_used_at ?? "")
1089
1208
  });
1090
- const problem = (status2, code, message, field, headers) => json({ error: { code, message, ...field ? { field } : {} } }, status2, headers);
1209
+ const problem = (status2, code, message, field, headers, reasons) => json({ error: { code, message, ...field ? { field } : {}, ...reasons && reasons.length > 0 ? { reasons } : {} } }, status2, headers);
1091
1210
  const invalid = (p) => problem(400, p.code, p.message, p.field);
1211
+ let explained = false;
1092
1212
  function ownRequest(request) {
1093
1213
  const origin = request.headers.get("origin");
1094
- if (origin !== null) return origin === originOf(request) || origin === new URL(request.url).origin;
1214
+ if (origin !== null) {
1215
+ const own = originOf(request);
1216
+ if (origin === own || origin === new URL(request.url).origin) return true;
1217
+ if (!explained && hostOf(origin) === new URL(own).hostname) {
1218
+ explained = true;
1219
+ console.warn(`cauthy-auth: refused a request from ${origin} as cross_site: this application sees itself as ${own}. Behind a proxy? Set CAUTHY_BASE_URL (or baseURL) to the public origin.`);
1220
+ }
1221
+ return false;
1222
+ }
1095
1223
  return (request.headers.get("sec-fetch-site") ?? "same-origin") === "same-origin";
1096
1224
  }
1097
1225
  async function body(request) {
@@ -1145,7 +1273,8 @@ function createCauthy(given = {}) {
1145
1273
  const b = res.status === 204 || res.status === 202 ? null : await res.json().catch(() => null);
1146
1274
  if (res.ok) return { status: res.status, data: b?.data ?? b };
1147
1275
  const first = b?.errors?.[0];
1148
- return { status: res.status, data: null, error: { code: String(first?.code ?? "error"), message: String(first?.message ?? "The request could not be completed.") } };
1276
+ const reasons = Array.isArray(first?.meta?.reasons) ? first.meta.reasons.map(String) : void 0;
1277
+ return { status: res.status, data: null, error: { code: String(first?.code ?? "error"), message: String(first?.message ?? "The request could not be completed."), ...reasons ? { reasons } : {} } };
1149
1278
  } catch {
1150
1279
  return { status: 503, data: null, error: { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." } };
1151
1280
  }
@@ -1173,6 +1302,19 @@ function createCauthy(given = {}) {
1173
1302
  return json({ status: "signed_in", session: await toPublic(toSession(s)), ...more }, 200, h);
1174
1303
  }
1175
1304
  let lookCache = null;
1305
+ let policyCache = null;
1306
+ const keepPolicy = (settings) => {
1307
+ policyCache = { until: Date.now() + LOOK_TTL_MS, policy: settings.error || !settings.data?.password_policy ? null : passwordPolicyOf(settings.data.password_policy) };
1308
+ return policyCache.policy;
1309
+ };
1310
+ async function workspacePolicy(request) {
1311
+ if (policyCache !== null && policyCache.until >= Date.now()) return policyCache.policy;
1312
+ return keepPolicy(await ask(request, "GET", "/v1/auth/settings"));
1313
+ }
1314
+ async function newPasswordProblem(request, value) {
1315
+ const policy = await workspacePolicy(request);
1316
+ return checkNewPassword(value, policy ?? { min: 8, max: 128, requires: [], min_classes: 0, reject_breached: false });
1317
+ }
1176
1318
  async function ownRoute(request, url, route) {
1177
1319
  const post = request.method === "POST";
1178
1320
  const own = [
@@ -1250,6 +1392,7 @@ function createCauthy(given = {}) {
1250
1392
  ask(request, "GET", "/v1/auth/sso/connections")
1251
1393
  ]);
1252
1394
  const list = Array.isArray(providers.data) ? providers.data : [];
1395
+ const password = keepPolicy(settings);
1253
1396
  const here = place.data?.here === true;
1254
1397
  return json({
1255
1398
  providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? "") })).filter((p) => p.id !== ""),
@@ -1265,7 +1408,9 @@ function createCauthy(given = {}) {
1265
1408
  sso: Array.isArray(sso.data) && sso.data.some((c) => c?.discoverable === true),
1266
1409
  // The terms a sign-up accepts and the consents it answers, when the workspace asks any.
1267
1410
  ...settings.data?.legal ? { legal: legalOf(settings.data.legal) } : {},
1268
- ...settings.data?.captcha_enabled === true && settings.data?.captcha_site_key ? { captchaSiteKey: String(settings.data.captcha_site_key) } : {}
1411
+ ...settings.data?.captcha_enabled === true && settings.data?.captcha_site_key ? { captchaSiteKey: String(settings.data.captcha_site_key) } : {},
1412
+ // What a new password must be: for the form to say it and check it.
1413
+ ...password ? { password } : {}
1269
1414
  });
1270
1415
  }
1271
1416
  if (route === "/sign-in/sso" && !post) {
@@ -1555,13 +1700,13 @@ function createCauthy(given = {}) {
1555
1700
  return json({ status: "sent", sentTo: String(res.data?.sent_to ?? ""), channel: String(res.data?.channel ?? "sms") });
1556
1701
  }
1557
1702
  if (route === "/sign-up/email") {
1558
- const bad = firstProblem(checkName(b.name), checkEmail(b.email), checkNewPassword(b.password), checkOrganization(b.organization));
1703
+ const bad = firstProblem(checkName(b.name), checkEmail(b.email), await newPasswordProblem(request, b.password), checkOrganization(b.organization));
1559
1704
  if (bad) return invalid(bad);
1560
1705
  const email = String(b.email).trim();
1561
1706
  const organization = typeof b.organization === "string" && b.organization.trim() !== "" ? { organization: { name: b.organization.trim() } } : {};
1562
1707
  const legal = { ...b.acceptLegal === true ? { accept_legal: true } : {}, ...b.consents && typeof b.consents === "object" ? { consents: b.consents } : {} };
1563
1708
  const made = await ask(request, "POST", "/v1/auth/register", { email, password: b.password, display_name: typeof b.name === "string" ? b.name.trim() : "", ...organization, ...legal }, pages.verifyEmail);
1564
- if (made.error) return problem(made.status, made.error.code, made.error.message);
1709
+ if (made.error) return problem(made.status, made.error.code, made.error.message, made.error.code === "weak_password" ? "password" : void 0, void 0, made.error.reasons);
1565
1710
  if (made.data?.verification_required === true) return json({ status: "verification_required", email });
1566
1711
  const res = await ask(request, "POST", "/v1/auth/login", { email, password: b.password });
1567
1712
  if (res.error || res.data?.mfa_required === true) return json({ status: "verification_required", email });
@@ -1577,7 +1722,7 @@ function createCauthy(given = {}) {
1577
1722
  const mine = await signedInAs(request);
1578
1723
  if ("response" in mine) return mine.response;
1579
1724
  const as = (path, payload, method = "POST") => ask(request, method, path, payload, void 0, mine.token);
1580
- const refused = (res, field) => problem(res.status, res.error.code, res.error.message, field, mine.headers);
1725
+ const refused = (res, field) => problem(res.status, res.error.code, res.error.message, field, mine.headers, res.error.reasons);
1581
1726
  const password = typeof b.password === "string" ? b.password : "";
1582
1727
  if (route === "/me") {
1583
1728
  const bad = checkName(b.name);
@@ -1598,7 +1743,7 @@ function createCauthy(given = {}) {
1598
1743
  return json({ status: "accepted" }, 200, mine.headers);
1599
1744
  }
1600
1745
  if (route === "/me/password") {
1601
- const bad = checkNewPassword(b.newPassword);
1746
+ const bad = await newPasswordProblem(request, b.newPassword);
1602
1747
  if (bad) return problem(400, bad.code, bad.message, "newPassword");
1603
1748
  const res = await as("/v1/auth/me/password", { current_password: password, new_password: b.newPassword });
1604
1749
  if (res.error) return refused(res, res.error.code === "wrong_password" ? "password" : res.error.code === "weak_password" ? "newPassword" : void 0);
@@ -1691,7 +1836,7 @@ function createCauthy(given = {}) {
1691
1836
  return json({ status: "removed", id }, 200, mine.headers);
1692
1837
  }
1693
1838
  if (route === "/me/upgrade") {
1694
- const bad = firstProblem(checkName(b.name), checkEmail(b.email), checkNewPassword(b.password));
1839
+ const bad = firstProblem(checkName(b.name), checkEmail(b.email), await newPasswordProblem(request, b.password));
1695
1840
  if (bad) return invalid(bad);
1696
1841
  const res = await as("/v1/auth/anonymous/upgrade", { email: String(b.email).trim(), password: b.password, display_name: typeof b.name === "string" ? b.name.trim() : "" });
1697
1842
  if (res.error) return refused(res, res.error.code === "weak_password" ? "password" : "email");
@@ -1758,10 +1903,10 @@ function createCauthy(given = {}) {
1758
1903
  const badLink = () => problem(422, "invalid_token", "This link is not valid, or it has expired.");
1759
1904
  if (route === "/reset-password") {
1760
1905
  if (!isToken(b.token)) return badLink();
1761
- const bad = checkNewPassword(b.password);
1906
+ const bad = await newPasswordProblem(request, b.password);
1762
1907
  if (bad) return invalid(bad);
1763
1908
  const res = await ask(request, "POST", "/v1/auth/password/change", { token: String(b.token), new_password: b.password });
1764
- if (res.error) return problem(res.status, res.error.code, res.error.message, res.error.code === "weak_password" ? "password" : void 0);
1909
+ if (res.error) return problem(res.status, res.error.code, res.error.message, res.error.code === "weak_password" ? "password" : void 0, void 0, res.error.reasons);
1765
1910
  return json({ status: "changed" });
1766
1911
  }
1767
1912
  if (route === "/verify-email") {
@@ -2134,6 +2279,7 @@ export {
2134
2279
  CauthyConfigError,
2135
2280
  CauthyTokenError,
2136
2281
  CauthyWebhookError,
2282
+ DEFAULT_PASSWORD_POLICY,
2137
2283
  MIN_PASSWORD,
2138
2284
  can,
2139
2285
  checkCode,
@@ -2145,6 +2291,10 @@ export {
2145
2291
  createCauthy,
2146
2292
  createSystem,
2147
2293
  definePolicy,
2294
+ describePasswordPolicy,
2295
+ explainPasswordProblems,
2296
+ passwordPolicyOf,
2297
+ passwordProblems,
2148
2298
  policySource,
2149
2299
  pullPolicy,
2150
2300
  pushPolicy,