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/react.mjs CHANGED
@@ -145,9 +145,113 @@ function fromWire(wire) {
145
145
  }
146
146
 
147
147
  // src/validate.ts
148
- var MIN_PASSWORD = 8;
149
- var MAX_PASSWORD = 256;
148
+ var MIN_PASSWORD = 12;
149
+ var MAX_PASSWORD = 128;
150
+ var FLOOR_PASSWORD = 8;
150
151
  var MAX_NAME = 120;
152
+ var CLASSES = ["lowercase", "uppercase", "digit", "symbol"];
153
+ var DEFAULT_PASSWORD_POLICY = Object.freeze({ min: MIN_PASSWORD, max: MAX_PASSWORD, requires: [], min_classes: 0, reject_breached: false });
154
+ var int = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : fallback;
155
+ function passwordPolicyOf(raw) {
156
+ const r = raw && typeof raw === "object" ? raw : {};
157
+ const min = Math.min(Math.max(int(r.min ?? r.min_length, MIN_PASSWORD) || MIN_PASSWORD, FLOOR_PASSWORD), MAX_PASSWORD);
158
+ const max = Math.max(Math.min(int(r.max ?? r.max_length, MAX_PASSWORD) || MAX_PASSWORD, MAX_PASSWORD), min);
159
+ const asked = Array.isArray(r.requires) ? r.requires : [];
160
+ return {
161
+ min,
162
+ max,
163
+ requires: CLASSES.filter((c) => asked.includes(c)),
164
+ min_classes: Math.min(Math.max(int(r.min_classes ?? r.minClasses, 0), 0), CLASSES.length),
165
+ reject_breached: (r.reject_breached ?? r.rejectBreached) === true
166
+ };
167
+ }
168
+ function utf8Length(v) {
169
+ let n = 0;
170
+ for (const ch of v) {
171
+ const c = ch.codePointAt(0);
172
+ n += c < 128 ? 1 : c < 2048 ? 2 : c < 65536 ? 3 : 4;
173
+ }
174
+ return n;
175
+ }
176
+ function classesOf(v) {
177
+ const has = /* @__PURE__ */ new Set();
178
+ for (const ch of v) {
179
+ if (/\p{Ll}/u.test(ch)) has.add("lowercase");
180
+ else if (/\p{Lu}/u.test(ch)) has.add("uppercase");
181
+ else if (/\p{Nd}/u.test(ch)) has.add("digit");
182
+ else if (!/\p{L}/u.test(ch)) has.add("symbol");
183
+ }
184
+ return has;
185
+ }
186
+ function passwordProblems(value, policy = DEFAULT_PASSWORD_POLICY) {
187
+ const p = passwordPolicyOf(policy);
188
+ const out = [];
189
+ const length = utf8Length(value);
190
+ if (length < p.min) out.push("too_short");
191
+ if (length > p.max) out.push("too_long");
192
+ const has = classesOf(value);
193
+ for (const c of p.requires) if (!has.has(c)) out.push(`needs_${c}`);
194
+ if (p.min_classes > 0 && CLASSES.filter((c) => has.has(c)).length < p.min_classes) out.push("needs_classes");
195
+ return out;
196
+ }
197
+ var RULES = {
198
+ en: {
199
+ between: "Between {min} and {max} characters.",
200
+ requires: "Include {kinds}.",
201
+ and: "and",
202
+ classes: "Use at least {n} of these: lowercase letters, uppercase letters, digits, symbols.",
203
+ breached: "Passwords found in known data breaches are not accepted.",
204
+ kinds: { lowercase: "a lowercase letter", uppercase: "an uppercase letter", digit: "a digit", symbol: "a symbol" },
205
+ reasons: {
206
+ too_short: "Use at least {min} characters.",
207
+ too_long: "Use at most {max} characters.",
208
+ needs_lowercase: "Include a lowercase letter.",
209
+ needs_uppercase: "Include an uppercase letter.",
210
+ needs_digit: "Include a digit.",
211
+ needs_symbol: "Include a symbol.",
212
+ needs_classes: "Use at least {n} of these: lowercase letters, uppercase letters, digits, symbols.",
213
+ breached: "This password appears in a known data breach. Choose a different one."
214
+ }
215
+ },
216
+ es: {
217
+ between: "Entre {min} y {max} caracteres.",
218
+ requires: "Incluye {kinds}.",
219
+ and: "y",
220
+ classes: "Usa al menos {n} de estos tipos: min\xFAsculas, may\xFAsculas, n\xFAmeros, s\xEDmbolos.",
221
+ breached: "No se aceptan contrase\xF1as que aparecen en filtraciones de datos conocidas.",
222
+ kinds: { lowercase: "una min\xFAscula", uppercase: "una may\xFAscula", digit: "un n\xFAmero", symbol: "un s\xEDmbolo" },
223
+ reasons: {
224
+ too_short: "Usa al menos {min} caracteres.",
225
+ too_long: "Usa como m\xE1ximo {max} caracteres.",
226
+ needs_lowercase: "Incluye una min\xFAscula.",
227
+ needs_uppercase: "Incluye una may\xFAscula.",
228
+ needs_digit: "Incluye un n\xFAmero.",
229
+ needs_symbol: "Incluye un s\xEDmbolo.",
230
+ needs_classes: "Usa al menos {n} de estos tipos: min\xFAsculas, may\xFAsculas, n\xFAmeros, s\xEDmbolos.",
231
+ breached: "Esta contrase\xF1a aparece en una filtraci\xF3n de datos conocida. Elige otra."
232
+ }
233
+ }
234
+ };
235
+ var fill = (s, p) => s.replace("{min}", String(p.min)).replace("{max}", String(p.max)).replace("{n}", String(p.min_classes));
236
+ function describePasswordPolicy(policy = DEFAULT_PASSWORD_POLICY, locale = "en") {
237
+ const p = passwordPolicyOf(policy);
238
+ const t = RULES[locale] ?? RULES.en;
239
+ const parts = [fill(t.between, p)];
240
+ if (p.requires.length > 0) {
241
+ const kinds = p.requires.map((c) => t.kinds[c]);
242
+ const listed = kinds.length === 1 ? kinds[0] : `${kinds.slice(0, -1).join(", ")} ${t.and} ${kinds[kinds.length - 1]}`;
243
+ parts.push(t.requires.replace("{kinds}", listed));
244
+ }
245
+ if (p.min_classes > 0) parts.push(fill(t.classes, p));
246
+ if (p.reject_breached) parts.push(t.breached);
247
+ return parts.join(" ");
248
+ }
249
+ function explainPasswordProblems(reasons, policy = DEFAULT_PASSWORD_POLICY, locale = "en") {
250
+ const p = passwordPolicyOf(policy);
251
+ const t = RULES[locale] ?? RULES.en;
252
+ const said = reasons.filter((r) => r in t.reasons).map((r) => fill(t.reasons[r], p));
253
+ return said.length > 0 ? said.join(" ") : describePasswordPolicy(p, locale);
254
+ }
151
255
  var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
152
256
  function checkEmail(value) {
153
257
  const v = typeof value === "string" ? value.trim() : "";
@@ -160,13 +264,12 @@ function checkPassword(value) {
160
264
  if (value.length > MAX_PASSWORD) return { field: "password", code: "too_long", message: "That password is too long." };
161
265
  return null;
162
266
  }
163
- function checkNewPassword(value) {
164
- const basic = checkPassword(value);
165
- if (basic) return basic;
166
- if (value.length < MIN_PASSWORD) {
167
- return { field: "password", code: "too_short", message: `Use at least ${MIN_PASSWORD} characters.` };
168
- }
169
- return null;
267
+ function checkNewPassword(value, policy) {
268
+ if (typeof value !== "string" || value === "") return { field: "password", code: "required", message: "Enter your password." };
269
+ const p = policy ? passwordPolicyOf(policy) : DEFAULT_PASSWORD_POLICY;
270
+ const reasons = passwordProblems(value, p);
271
+ if (reasons.length === 0) return null;
272
+ return { field: "password", code: reasons[0], message: explainPasswordProblems(reasons, p) };
170
273
  }
171
274
  function checkName(value) {
172
275
  if (value === void 0 || value === null || value === "") return null;
@@ -229,7 +332,7 @@ function createAuthClient(options = {}) {
229
332
  const body = await res.json().catch(() => null);
230
333
  if (res.ok && body && typeof body.status === "string") return { ok: true, ...body };
231
334
  const e = body?.error;
232
- return { ok: false, error: e && typeof e.code === "string" ? { code: e.code, message: String(e.message ?? ""), ...e.field ? { field: e.field } : {} } : UNAVAILABLE };
335
+ return { ok: false, error: e && typeof e.code === "string" ? { code: e.code, message: String(e.message ?? ""), ...e.field ? { field: e.field } : {}, ...Array.isArray(e.reasons) ? { reasons: e.reasons.map(String) } : {} } : UNAVAILABLE };
233
336
  } catch {
234
337
  return { ok: false, error: UNAVAILABLE };
235
338
  }
@@ -257,12 +360,14 @@ function createAuthClient(options = {}) {
257
360
  if (o.locale) q.set("locale", o.locale);
258
361
  return `${base}/login?${q}`;
259
362
  };
363
+ let policy;
260
364
  async function methods() {
261
365
  try {
262
366
  const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
263
367
  if (res.ok) {
264
368
  const m = await res.json();
265
- return { providers: [], signUp: true, magicLink: false, passkey: false, ...m, passkeyHere: m.passkeyHere === true, passkeyOnCauthy: m.passkeyOnCauthy ?? m.passkey === true, sso: m.sso === true };
369
+ if (m.password) policy = passwordPolicyOf(m.password);
370
+ return { providers: [], signUp: true, magicLink: false, passkey: false, ...m, ...policy ? { password: policy } : {}, passkeyHere: m.passkeyHere === true, passkeyOnCauthy: m.passkeyOnCauthy ?? m.passkey === true, sso: m.sso === true };
266
371
  }
267
372
  } catch {
268
373
  }
@@ -406,12 +511,12 @@ function createAuthClient(options = {}) {
406
511
  email: (f) => post(
407
512
  "/sign-up/email",
408
513
  { email: f.email, password: f.password, name: f.name, organization: f.organization, acceptLegal: f.acceptLegal, consents: f.consents },
409
- firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password), checkOrganization(f.organization)),
514
+ firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password, policy), checkOrganization(f.organization)),
410
515
  f.captchaToken
411
516
  )
412
517
  },
413
518
  forgotPassword: (f) => post("/forgot-password", { email: f.email }, checkEmail(f.email), f.captchaToken),
414
- resetPassword: (f) => post("/reset-password", { token: f.token, password: f.password }, checkNewPassword(f.password)),
519
+ resetPassword: (f) => post("/reset-password", { token: f.token, password: f.password }, checkNewPassword(f.password, policy)),
415
520
  verifyEmail: (f) => post("/verify-email", { token: f.token }, null),
416
521
  signOut: () => post("/sign-out", {}, null),
417
522
  organizations: {
@@ -445,7 +550,7 @@ function createAuthClient(options = {}) {
445
550
  },
446
551
  acceptLegal: (f = {}) => post("/me/legal/accept", { kinds: f.kinds ?? [] }, null),
447
552
  changePassword: (f) => {
448
- const bad = checkNewPassword(f.newPassword);
553
+ const bad = checkNewPassword(f.newPassword, policy);
449
554
  return post("/me/password", { password: f.password, newPassword: f.newPassword }, bad ? { ...bad, field: "newPassword" } : null);
450
555
  },
451
556
  mfa: {
@@ -476,7 +581,7 @@ function createAuthClient(options = {}) {
476
581
  return body && Array.isArray(body.sessions) ? body.sessions : [];
477
582
  },
478
583
  signOutEverywhere: () => post("/me/sessions/end", {}, null),
479
- upgrade: (f) => post("/me/upgrade", { email: f.email, password: f.password, name: f.name ?? "" }, firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password))),
584
+ upgrade: (f) => post("/me/upgrade", { email: f.email, password: f.password, name: f.name ?? "" }, firstProblem(checkName(f.name), checkEmail(f.email), checkNewPassword(f.password, policy))),
480
585
  upgradeUrl,
481
586
  upgradeWith(provider, o) {
482
587
  window.location.assign(upgradeUrl(provider, o));
@@ -690,7 +795,7 @@ var TEXTS = {
690
795
  user_not_active: "That account is disabled.",
691
796
  required: "This is required.",
692
797
  invalid_email: "That does not look like an email address.",
693
- too_short: "Use at least 8 characters.",
798
+ too_short: `Use at least ${MIN_PASSWORD} characters.`,
694
799
  invalid_credentials: "The address or the password is not right.",
695
800
  unavailable: "Sign-in is unavailable right now. Try again in a moment.",
696
801
  state_mismatch: "That sign-in could not be completed. Try again.",
@@ -846,7 +951,7 @@ var TEXTS = {
846
951
  user_not_active: "Esa cuenta est\xE1 desactivada.",
847
952
  required: "Este dato es obligatorio.",
848
953
  invalid_email: "Eso no parece un correo.",
849
- too_short: "Usa al menos 8 caracteres.",
954
+ too_short: `Usa al menos ${MIN_PASSWORD} caracteres.`,
850
955
  invalid_credentials: "El correo o la contrase\xF1a no son correctos.",
851
956
  unavailable: "No se puede entrar en este momento. Intenta de nuevo en un momento.",
852
957
  state_mismatch: "No se pudo completar el ingreso. Intenta de nuevo.",
@@ -998,18 +1103,36 @@ function useForm(props) {
998
1103
  const Input = props.components?.Input ?? PlainInput;
999
1104
  const [error, setError] = useState(null);
1000
1105
  const [busy, setBusy] = useState(false);
1001
- const say = (e) => t.errors[e.code] ?? e.message;
1106
+ const [policy, setPolicy] = useState(void 0);
1107
+ const locale = props.locale ?? "en";
1108
+ const say = (e) => {
1109
+ if (props.texts?.errors?.[e.code] === void 0 && isPasswordRule(e)) return explainPasswordProblems(e.reasons ?? [e.code], policy, locale);
1110
+ return t.errors[e.code] ?? e.message;
1111
+ };
1112
+ const passwordRule = describePasswordPolicy(policy, locale);
1002
1113
  const done = (to) => {
1003
1114
  if (props.onSuccess) props.onSuccess();
1004
1115
  else window.location.assign(sitePath(to) ?? sameOrigin(props.returnTo) ?? "/");
1005
1116
  };
1006
1117
  const root = useRoot(props);
1007
- return { t, cx, Button, Input, error, setError, busy, setBusy, say, done, root };
1118
+ return { t, cx, Button, Input, error, setError, busy, setBusy, say, done, root, policy, setPolicy, passwordRule };
1119
+ }
1120
+ var PASSWORD_RULES = ["needs_lowercase", "needs_uppercase", "needs_digit", "needs_symbol", "needs_classes", "breached"];
1121
+ function isPasswordRule(e) {
1122
+ if (e.code === "weak_password" || PASSWORD_RULES.includes(e.code)) return true;
1123
+ return (e.code === "too_short" || e.code === "too_long") && (e.field === "password" || e.field === "newPassword");
1124
+ }
1125
+ function usePasswordPolicy(f, methods) {
1126
+ const { setPolicy } = f;
1127
+ useEffect(() => {
1128
+ if (methods?.password) setPolicy(methods.password);
1129
+ }, [methods, setPolicy]);
1008
1130
  }
1009
1131
  function Field(p) {
1010
1132
  const id = useId();
1011
1133
  const { f } = p;
1012
1134
  const bad = f.error?.field === p.name ? f.error : null;
1135
+ const described = [bad ? `${id}-error` : "", p.hint ? `${id}-hint` : ""].filter(Boolean).join(" ");
1013
1136
  return /* @__PURE__ */ jsxs("div", { className: f.cx("field"), children: [
1014
1137
  /* @__PURE__ */ jsxs("label", { className: f.cx("label"), htmlFor: id, children: [
1015
1138
  p.label,
@@ -1026,10 +1149,11 @@ function Field(p) {
1026
1149
  required: p.required,
1027
1150
  className: f.cx("input"),
1028
1151
  "aria-invalid": bad ? true : void 0,
1029
- "aria-describedby": bad ? `${id}-error` : void 0,
1152
+ "aria-describedby": described || void 0,
1030
1153
  onChange: (e) => p.onChange(e.target.value)
1031
1154
  }
1032
1155
  ),
1156
+ p.hint && !bad && /* @__PURE__ */ jsx("span", { id: `${id}-hint`, className: f.cx("hint"), children: p.hint }),
1033
1157
  bad && /* @__PURE__ */ jsx("span", { id: `${id}-error`, role: "alert", className: f.cx("fieldError"), children: f.say(bad) })
1034
1158
  ] });
1035
1159
  }
@@ -1290,6 +1414,7 @@ function SignUp(props) {
1290
1414
  const [password, setPassword] = useState("");
1291
1415
  const [sent, setSent] = useState(false);
1292
1416
  const methods = useMethods(auth);
1417
+ usePasswordPolicy(f, methods);
1293
1418
  const challenge = useChallenge(methods?.captchaSiteKey, props.locale);
1294
1419
  const [acceptLegal, setAcceptLegal] = useState(false);
1295
1420
  const [consents, setConsents] = useState({});
@@ -1324,7 +1449,7 @@ function SignUp(props) {
1324
1449
  methods?.organization?.ask && /* @__PURE__ */ jsx(Field, { f, name: "organization", type: "text", label: f.t.company, value: organization, autoComplete: "organization", required: methods.organization.required, onChange: setOrganization }),
1325
1450
  /* @__PURE__ */ jsx(Field, { f, name: "name", type: "text", label: f.t.name, value: name, autoComplete: "name", onChange: setName }),
1326
1451
  /* @__PURE__ */ jsx(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
1327
- /* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.password, value: password, autoComplete: "new-password", required: true, onChange: setPassword }),
1452
+ /* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.password, value: password, autoComplete: "new-password", required: true, onChange: setPassword, hint: f.passwordRule }),
1328
1453
  legal && legal.documents.length > 0 && /* @__PURE__ */ jsxs("label", { className: f.cx("check"), children: [
1329
1454
  /* @__PURE__ */ jsx("input", { type: "checkbox", name: "acceptLegal", checked: acceptLegal, required: true, onChange: (e) => setAcceptLegal(e.target.checked) }),
1330
1455
  /* @__PURE__ */ jsxs("span", { children: [
@@ -1390,6 +1515,7 @@ function ResetPassword(props) {
1390
1515
  const [token, setToken] = useState(null);
1391
1516
  const [password, setPassword] = useState("");
1392
1517
  const [changed, setChanged] = useState(false);
1518
+ usePasswordPolicy(f, useMethods(auth));
1393
1519
  useMailedToken("token", (t) => {
1394
1520
  setToken(t ?? "");
1395
1521
  if (!t) f.setError({ code: "invalid_token", message: "This link is not valid, or it has expired." });
@@ -1419,7 +1545,7 @@ function ResetPassword(props) {
1419
1545
  }
1420
1546
  return /* @__PURE__ */ jsxs("form", { className: f.cx("form"), ...f.root, onSubmit: submit, noValidate: true, children: [
1421
1547
  general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
1422
- /* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.newPassword, value: password, autoComplete: "new-password", required: true, onChange: setPassword }),
1548
+ /* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.newPassword, value: password, autoComplete: "new-password", required: true, onChange: setPassword, hint: f.passwordRule }),
1423
1549
  /* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy || token === null, className: f.cx("button"), children: f.t.savePassword })
1424
1550
  ] });
1425
1551
  }
@@ -1760,6 +1886,7 @@ function PasswordPart({ props }) {
1760
1886
  const { f, done, setDone, run, general } = useSection(props);
1761
1887
  const [password, setPassword] = useState("");
1762
1888
  const [newPassword, setNewPassword] = useState("");
1889
+ usePasswordPolicy(f, useMethods(auth));
1763
1890
  const submit = async (e) => {
1764
1891
  e.preventDefault();
1765
1892
  const r = await run(() => auth.account.changePassword({ password, newPassword }));
@@ -1772,7 +1899,7 @@ function PasswordPart({ props }) {
1772
1899
  /* @__PURE__ */ jsx("h2", { className: f.cx("heading"), children: f.t.changePassword }),
1773
1900
  general && /* @__PURE__ */ jsx("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
1774
1901
  /* @__PURE__ */ jsx(Field, { f, name: "password", type: "password", label: f.t.currentPassword, value: password, autoComplete: "current-password", required: true, onChange: setPassword }),
1775
- /* @__PURE__ */ jsx(Field, { f, name: "newPassword", type: "password", label: f.t.newPassword, value: newPassword, autoComplete: "new-password", required: true, onChange: setNewPassword }),
1902
+ /* @__PURE__ */ jsx(Field, { f, name: "newPassword", type: "password", label: f.t.newPassword, value: newPassword, autoComplete: "new-password", required: true, onChange: setNewPassword, hint: f.passwordRule }),
1776
1903
  /* @__PURE__ */ jsx(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.changePassword }),
1777
1904
  done && /* @__PURE__ */ jsx("p", { role: "status", className: f.cx("notice"), children: done })
1778
1905
  ] });