cauthy-auth 0.27.1 → 0.28.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.
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;
@@ -1087,7 +1190,7 @@ function createCauthy(given = {}) {
1087
1190
  connectedAt: String(d?.created_at ?? ""),
1088
1191
  lastUsedAt: String(d?.last_used_at ?? "")
1089
1192
  });
1090
- const problem = (status2, code, message, field, headers) => json({ error: { code, message, ...field ? { field } : {} } }, status2, headers);
1193
+ const problem = (status2, code, message, field, headers, reasons) => json({ error: { code, message, ...field ? { field } : {}, ...reasons && reasons.length > 0 ? { reasons } : {} } }, status2, headers);
1091
1194
  const invalid = (p) => problem(400, p.code, p.message, p.field);
1092
1195
  function ownRequest(request) {
1093
1196
  const origin = request.headers.get("origin");
@@ -1145,7 +1248,8 @@ function createCauthy(given = {}) {
1145
1248
  const b = res.status === 204 || res.status === 202 ? null : await res.json().catch(() => null);
1146
1249
  if (res.ok) return { status: res.status, data: b?.data ?? b };
1147
1250
  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.") } };
1251
+ const reasons = Array.isArray(first?.meta?.reasons) ? first.meta.reasons.map(String) : void 0;
1252
+ 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
1253
  } catch {
1150
1254
  return { status: 503, data: null, error: { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." } };
1151
1255
  }
@@ -1173,6 +1277,19 @@ function createCauthy(given = {}) {
1173
1277
  return json({ status: "signed_in", session: await toPublic(toSession(s)), ...more }, 200, h);
1174
1278
  }
1175
1279
  let lookCache = null;
1280
+ let policyCache = null;
1281
+ const keepPolicy = (settings) => {
1282
+ policyCache = { until: Date.now() + LOOK_TTL_MS, policy: settings.error || !settings.data?.password_policy ? null : passwordPolicyOf(settings.data.password_policy) };
1283
+ return policyCache.policy;
1284
+ };
1285
+ async function workspacePolicy(request) {
1286
+ if (policyCache !== null && policyCache.until >= Date.now()) return policyCache.policy;
1287
+ return keepPolicy(await ask(request, "GET", "/v1/auth/settings"));
1288
+ }
1289
+ async function newPasswordProblem(request, value) {
1290
+ const policy = await workspacePolicy(request);
1291
+ return checkNewPassword(value, policy ?? { min: 8, max: 128, requires: [], min_classes: 0, reject_breached: false });
1292
+ }
1176
1293
  async function ownRoute(request, url, route) {
1177
1294
  const post = request.method === "POST";
1178
1295
  const own = [
@@ -1250,6 +1367,7 @@ function createCauthy(given = {}) {
1250
1367
  ask(request, "GET", "/v1/auth/sso/connections")
1251
1368
  ]);
1252
1369
  const list = Array.isArray(providers.data) ? providers.data : [];
1370
+ const password = keepPolicy(settings);
1253
1371
  const here = place.data?.here === true;
1254
1372
  return json({
1255
1373
  providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? "") })).filter((p) => p.id !== ""),
@@ -1265,7 +1383,9 @@ function createCauthy(given = {}) {
1265
1383
  sso: Array.isArray(sso.data) && sso.data.some((c) => c?.discoverable === true),
1266
1384
  // The terms a sign-up accepts and the consents it answers, when the workspace asks any.
1267
1385
  ...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) } : {}
1386
+ ...settings.data?.captcha_enabled === true && settings.data?.captcha_site_key ? { captchaSiteKey: String(settings.data.captcha_site_key) } : {},
1387
+ // What a new password must be: for the form to say it and check it.
1388
+ ...password ? { password } : {}
1269
1389
  });
1270
1390
  }
1271
1391
  if (route === "/sign-in/sso" && !post) {
@@ -1555,13 +1675,13 @@ function createCauthy(given = {}) {
1555
1675
  return json({ status: "sent", sentTo: String(res.data?.sent_to ?? ""), channel: String(res.data?.channel ?? "sms") });
1556
1676
  }
1557
1677
  if (route === "/sign-up/email") {
1558
- const bad = firstProblem(checkName(b.name), checkEmail(b.email), checkNewPassword(b.password), checkOrganization(b.organization));
1678
+ const bad = firstProblem(checkName(b.name), checkEmail(b.email), await newPasswordProblem(request, b.password), checkOrganization(b.organization));
1559
1679
  if (bad) return invalid(bad);
1560
1680
  const email = String(b.email).trim();
1561
1681
  const organization = typeof b.organization === "string" && b.organization.trim() !== "" ? { organization: { name: b.organization.trim() } } : {};
1562
1682
  const legal = { ...b.acceptLegal === true ? { accept_legal: true } : {}, ...b.consents && typeof b.consents === "object" ? { consents: b.consents } : {} };
1563
1683
  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);
1684
+ 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
1685
  if (made.data?.verification_required === true) return json({ status: "verification_required", email });
1566
1686
  const res = await ask(request, "POST", "/v1/auth/login", { email, password: b.password });
1567
1687
  if (res.error || res.data?.mfa_required === true) return json({ status: "verification_required", email });
@@ -1577,7 +1697,7 @@ function createCauthy(given = {}) {
1577
1697
  const mine = await signedInAs(request);
1578
1698
  if ("response" in mine) return mine.response;
1579
1699
  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);
1700
+ const refused = (res, field) => problem(res.status, res.error.code, res.error.message, field, mine.headers, res.error.reasons);
1581
1701
  const password = typeof b.password === "string" ? b.password : "";
1582
1702
  if (route === "/me") {
1583
1703
  const bad = checkName(b.name);
@@ -1598,7 +1718,7 @@ function createCauthy(given = {}) {
1598
1718
  return json({ status: "accepted" }, 200, mine.headers);
1599
1719
  }
1600
1720
  if (route === "/me/password") {
1601
- const bad = checkNewPassword(b.newPassword);
1721
+ const bad = await newPasswordProblem(request, b.newPassword);
1602
1722
  if (bad) return problem(400, bad.code, bad.message, "newPassword");
1603
1723
  const res = await as("/v1/auth/me/password", { current_password: password, new_password: b.newPassword });
1604
1724
  if (res.error) return refused(res, res.error.code === "wrong_password" ? "password" : res.error.code === "weak_password" ? "newPassword" : void 0);
@@ -1691,7 +1811,7 @@ function createCauthy(given = {}) {
1691
1811
  return json({ status: "removed", id }, 200, mine.headers);
1692
1812
  }
1693
1813
  if (route === "/me/upgrade") {
1694
- const bad = firstProblem(checkName(b.name), checkEmail(b.email), checkNewPassword(b.password));
1814
+ const bad = firstProblem(checkName(b.name), checkEmail(b.email), await newPasswordProblem(request, b.password));
1695
1815
  if (bad) return invalid(bad);
1696
1816
  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
1817
  if (res.error) return refused(res, res.error.code === "weak_password" ? "password" : "email");
@@ -1758,10 +1878,10 @@ function createCauthy(given = {}) {
1758
1878
  const badLink = () => problem(422, "invalid_token", "This link is not valid, or it has expired.");
1759
1879
  if (route === "/reset-password") {
1760
1880
  if (!isToken(b.token)) return badLink();
1761
- const bad = checkNewPassword(b.password);
1881
+ const bad = await newPasswordProblem(request, b.password);
1762
1882
  if (bad) return invalid(bad);
1763
1883
  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);
1884
+ 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
1885
  return json({ status: "changed" });
1766
1886
  }
1767
1887
  if (route === "/verify-email") {
@@ -2134,6 +2254,7 @@ export {
2134
2254
  CauthyConfigError,
2135
2255
  CauthyTokenError,
2136
2256
  CauthyWebhookError,
2257
+ DEFAULT_PASSWORD_POLICY,
2137
2258
  MIN_PASSWORD,
2138
2259
  can,
2139
2260
  checkCode,
@@ -2145,6 +2266,10 @@ export {
2145
2266
  createCauthy,
2146
2267
  createSystem,
2147
2268
  definePolicy,
2269
+ describePasswordPolicy,
2270
+ explainPasswordProblems,
2271
+ passwordPolicyOf,
2272
+ passwordProblems,
2148
2273
  policySource,
2149
2274
  pullPolicy,
2150
2275
  pushPolicy,