cauthy-auth 0.27.0 → 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
@@ -495,7 +495,9 @@ function createSystem(given = {}) {
495
495
  async function fail(res, what) {
496
496
  const body = await res.json().catch(() => null);
497
497
  const first = body?.errors?.[0];
498
- throw new CauthyApiError(res.status, first?.code ?? "error", `Cauthy refused ${what} (${res.status}): ${first?.message ?? res.statusText}`);
498
+ const code = typeof body?.error === "string" ? body.error : first?.code;
499
+ const message = typeof body?.error === "string" ? body.error_description : first?.message;
500
+ throw new CauthyApiError(res.status, code ?? "error", `Cauthy refused ${what} (${res.status}): ${message ?? res.statusText}`);
499
501
  }
500
502
  let held;
501
503
  let asking;
@@ -605,9 +607,113 @@ function createSystem(given = {}) {
605
607
  }
606
608
 
607
609
  // src/validate.ts
608
- var MIN_PASSWORD = 8;
609
- var MAX_PASSWORD = 256;
610
+ var MIN_PASSWORD = 12;
611
+ var MAX_PASSWORD = 128;
612
+ var FLOOR_PASSWORD = 8;
610
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
+ }
611
717
  var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
612
718
  function checkEmail(value) {
613
719
  const v = typeof value === "string" ? value.trim() : "";
@@ -620,13 +726,12 @@ function checkPassword(value) {
620
726
  if (value.length > MAX_PASSWORD) return { field: "password", code: "too_long", message: "That password is too long." };
621
727
  return null;
622
728
  }
623
- function checkNewPassword(value) {
624
- const basic = checkPassword(value);
625
- if (basic) return basic;
626
- if (value.length < MIN_PASSWORD) {
627
- return { field: "password", code: "too_short", message: `Use at least ${MIN_PASSWORD} characters.` };
628
- }
629
- 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) };
630
735
  }
631
736
  function checkName(value) {
632
737
  if (value === void 0 || value === null || value === "") return null;
@@ -1085,7 +1190,7 @@ function createCauthy(given = {}) {
1085
1190
  connectedAt: String(d?.created_at ?? ""),
1086
1191
  lastUsedAt: String(d?.last_used_at ?? "")
1087
1192
  });
1088
- 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);
1089
1194
  const invalid = (p) => problem(400, p.code, p.message, p.field);
1090
1195
  function ownRequest(request) {
1091
1196
  const origin = request.headers.get("origin");
@@ -1143,7 +1248,8 @@ function createCauthy(given = {}) {
1143
1248
  const b = res.status === 204 || res.status === 202 ? null : await res.json().catch(() => null);
1144
1249
  if (res.ok) return { status: res.status, data: b?.data ?? b };
1145
1250
  const first = b?.errors?.[0];
1146
- 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 } : {} } };
1147
1253
  } catch {
1148
1254
  return { status: 503, data: null, error: { code: "unavailable", message: "Sign-in is unavailable right now. Try again in a moment." } };
1149
1255
  }
@@ -1171,6 +1277,19 @@ function createCauthy(given = {}) {
1171
1277
  return json({ status: "signed_in", session: await toPublic(toSession(s)), ...more }, 200, h);
1172
1278
  }
1173
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
+ }
1174
1293
  async function ownRoute(request, url, route) {
1175
1294
  const post = request.method === "POST";
1176
1295
  const own = [
@@ -1248,6 +1367,7 @@ function createCauthy(given = {}) {
1248
1367
  ask(request, "GET", "/v1/auth/sso/connections")
1249
1368
  ]);
1250
1369
  const list = Array.isArray(providers.data) ? providers.data : [];
1370
+ const password = keepPolicy(settings);
1251
1371
  const here = place.data?.here === true;
1252
1372
  return json({
1253
1373
  providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? "") })).filter((p) => p.id !== ""),
@@ -1263,7 +1383,9 @@ function createCauthy(given = {}) {
1263
1383
  sso: Array.isArray(sso.data) && sso.data.some((c) => c?.discoverable === true),
1264
1384
  // The terms a sign-up accepts and the consents it answers, when the workspace asks any.
1265
1385
  ...settings.data?.legal ? { legal: legalOf(settings.data.legal) } : {},
1266
- ...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 } : {}
1267
1389
  });
1268
1390
  }
1269
1391
  if (route === "/sign-in/sso" && !post) {
@@ -1553,13 +1675,13 @@ function createCauthy(given = {}) {
1553
1675
  return json({ status: "sent", sentTo: String(res.data?.sent_to ?? ""), channel: String(res.data?.channel ?? "sms") });
1554
1676
  }
1555
1677
  if (route === "/sign-up/email") {
1556
- 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));
1557
1679
  if (bad) return invalid(bad);
1558
1680
  const email = String(b.email).trim();
1559
1681
  const organization = typeof b.organization === "string" && b.organization.trim() !== "" ? { organization: { name: b.organization.trim() } } : {};
1560
1682
  const legal = { ...b.acceptLegal === true ? { accept_legal: true } : {}, ...b.consents && typeof b.consents === "object" ? { consents: b.consents } : {} };
1561
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);
1562
- 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);
1563
1685
  if (made.data?.verification_required === true) return json({ status: "verification_required", email });
1564
1686
  const res = await ask(request, "POST", "/v1/auth/login", { email, password: b.password });
1565
1687
  if (res.error || res.data?.mfa_required === true) return json({ status: "verification_required", email });
@@ -1575,7 +1697,7 @@ function createCauthy(given = {}) {
1575
1697
  const mine = await signedInAs(request);
1576
1698
  if ("response" in mine) return mine.response;
1577
1699
  const as = (path, payload, method = "POST") => ask(request, method, path, payload, void 0, mine.token);
1578
- 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);
1579
1701
  const password = typeof b.password === "string" ? b.password : "";
1580
1702
  if (route === "/me") {
1581
1703
  const bad = checkName(b.name);
@@ -1596,7 +1718,7 @@ function createCauthy(given = {}) {
1596
1718
  return json({ status: "accepted" }, 200, mine.headers);
1597
1719
  }
1598
1720
  if (route === "/me/password") {
1599
- const bad = checkNewPassword(b.newPassword);
1721
+ const bad = await newPasswordProblem(request, b.newPassword);
1600
1722
  if (bad) return problem(400, bad.code, bad.message, "newPassword");
1601
1723
  const res = await as("/v1/auth/me/password", { current_password: password, new_password: b.newPassword });
1602
1724
  if (res.error) return refused(res, res.error.code === "wrong_password" ? "password" : res.error.code === "weak_password" ? "newPassword" : void 0);
@@ -1689,7 +1811,7 @@ function createCauthy(given = {}) {
1689
1811
  return json({ status: "removed", id }, 200, mine.headers);
1690
1812
  }
1691
1813
  if (route === "/me/upgrade") {
1692
- 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));
1693
1815
  if (bad) return invalid(bad);
1694
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() : "" });
1695
1817
  if (res.error) return refused(res, res.error.code === "weak_password" ? "password" : "email");
@@ -1756,10 +1878,10 @@ function createCauthy(given = {}) {
1756
1878
  const badLink = () => problem(422, "invalid_token", "This link is not valid, or it has expired.");
1757
1879
  if (route === "/reset-password") {
1758
1880
  if (!isToken(b.token)) return badLink();
1759
- const bad = checkNewPassword(b.password);
1881
+ const bad = await newPasswordProblem(request, b.password);
1760
1882
  if (bad) return invalid(bad);
1761
1883
  const res = await ask(request, "POST", "/v1/auth/password/change", { token: String(b.token), new_password: b.password });
1762
- 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);
1763
1885
  return json({ status: "changed" });
1764
1886
  }
1765
1887
  if (route === "/verify-email") {
@@ -2132,6 +2254,7 @@ export {
2132
2254
  CauthyConfigError,
2133
2255
  CauthyTokenError,
2134
2256
  CauthyWebhookError,
2257
+ DEFAULT_PASSWORD_POLICY,
2135
2258
  MIN_PASSWORD,
2136
2259
  can,
2137
2260
  checkCode,
@@ -2143,6 +2266,10 @@ export {
2143
2266
  createCauthy,
2144
2267
  createSystem,
2145
2268
  definePolicy,
2269
+ describePasswordPolicy,
2270
+ explainPasswordProblems,
2271
+ passwordPolicyOf,
2272
+ passwordProblems,
2146
2273
  policySource,
2147
2274
  pullPolicy,
2148
2275
  pushPolicy,