@fonderie/auth 7.22.0 → 7.22.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.
package/dist/index.js CHANGED
@@ -74,6 +74,41 @@ import {
74
74
  rateLimit,
75
75
  StoreAdapterStore
76
76
  } from "@fonderie/rate-limit";
77
+
78
+ // src/services/email.ts
79
+ function normalizeEmail(email2) {
80
+ if (typeof email2 !== "string" || email2.length === 0) {
81
+ throw new Error("Invalid email");
82
+ }
83
+ const lower = email2.trim().toLowerCase();
84
+ if (lower.length === 0) {
85
+ throw new Error("Email cannot be empty");
86
+ }
87
+ const atIndex = lower.indexOf("@");
88
+ if (atIndex === -1 || lower.lastIndexOf("@") !== atIndex) {
89
+ throw new Error("Invalid email format");
90
+ }
91
+ const local = lower.substring(0, atIndex);
92
+ const domain = lower.substring(atIndex + 1);
93
+ if (!local || !domain) {
94
+ throw new Error("Invalid email format");
95
+ }
96
+ const plusIndex = local.indexOf("+");
97
+ const normalizedLocal = plusIndex === -1 ? local : local.substring(0, plusIndex);
98
+ if (!normalizedLocal) {
99
+ throw new Error("Invalid email format");
100
+ }
101
+ return `${normalizedLocal}@${domain}`;
102
+ }
103
+ function normalizeEmailSafe(email2) {
104
+ try {
105
+ return normalizeEmail(email2);
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ // src/services/rate-limit.ts
77
112
  var min = (n) => n * 60;
78
113
  var DEFAULTS = {
79
114
  // login: 10/15min per IP AND 5/15min per account (credential stuffing
@@ -126,7 +161,9 @@ function buildAuthAccountLimiter(route, store, config) {
126
161
  return rateLimit({
127
162
  store: r.store,
128
163
  rule: r.account.rule,
129
- key: byBodyField(`auth:${route}`, r.account.field)
164
+ // The account bucket is keyed the way accounts are stored, so a rotating
165
+ // '+tag' on the same address does not buy fresh attempts.
166
+ key: byBodyField(`auth:${route}`, r.account.field, r.account.field === "email" ? normalizeEmailSafe : void 0)
130
167
  });
131
168
  }
132
169
 
@@ -1582,39 +1619,6 @@ function checkCooldown(lastSentAt, cooldownMs) {
1582
1619
  // src/controllers/auth.controller.ts
1583
1620
  init_password();
1584
1621
 
1585
- // src/services/email.ts
1586
- function normalizeEmail(email2) {
1587
- if (typeof email2 !== "string" || email2.length === 0) {
1588
- throw new Error("Invalid email");
1589
- }
1590
- const lower = email2.trim().toLowerCase();
1591
- if (lower.length === 0) {
1592
- throw new Error("Email cannot be empty");
1593
- }
1594
- const atIndex = lower.indexOf("@");
1595
- if (atIndex === -1 || lower.lastIndexOf("@") !== atIndex) {
1596
- throw new Error("Invalid email format");
1597
- }
1598
- const local = lower.substring(0, atIndex);
1599
- const domain = lower.substring(atIndex + 1);
1600
- if (!local || !domain) {
1601
- throw new Error("Invalid email format");
1602
- }
1603
- const plusIndex = local.indexOf("+");
1604
- const normalizedLocal = plusIndex === -1 ? local : local.substring(0, plusIndex);
1605
- if (!normalizedLocal) {
1606
- throw new Error("Invalid email format");
1607
- }
1608
- return `${normalizedLocal}@${domain}`;
1609
- }
1610
- function normalizeEmailSafe(email2) {
1611
- try {
1612
- return normalizeEmail(email2);
1613
- } catch {
1614
- return null;
1615
- }
1616
- }
1617
-
1618
1622
  // src/models/password-reset.model.ts
1619
1623
  import { createHash as createHash3 } from "crypto";
1620
1624
  var hashSecret = (value) => createHash3("sha256").update(value).digest("hex");
@@ -3304,7 +3308,7 @@ function oauthController(store, config, bus) {
3304
3308
  const meta = requestMeta(ctx);
3305
3309
  await loginEvents.recordSafe({
3306
3310
  userId: null,
3307
- emailAttempted: claims.email ?? null,
3311
+ emailAttempted: claims.email ? normalizeEmailSafe(claims.email) ?? claims.email : null,
3308
3312
  method: "oauth-apple",
3309
3313
  outcome: "failed",
3310
3314
  failureReason: "token_replayed",
@@ -3352,7 +3356,7 @@ function oauthController(store, config, bus) {
3352
3356
  if (!await consumedTokens.consumeOnce(tokenHash, new Date(expMs))) {
3353
3357
  await loginEvents.recordSafe({
3354
3358
  userId: null,
3355
- emailAttempted: claims.email ?? null,
3359
+ emailAttempted: claims.email ? normalizeEmailSafe(claims.email) ?? claims.email : null,
3356
3360
  method: "oauth-google",
3357
3361
  outcome: "failed",
3358
3362
  failureReason: "token_replayed",
@@ -3710,7 +3714,8 @@ function adminRouteTable(store, bus) {
3710
3714
  // see anything cannot discover who signed up this morning.
3711
3715
  async (ctx) => {
3712
3716
  const params = new URL(ctx.request.url).searchParams;
3713
- const email2 = params.get("email")?.trim().toLowerCase();
3717
+ const raw = params.get("email")?.trim();
3718
+ const email2 = raw ? normalizeEmailSafe(raw) ?? raw.toLowerCase() : void 0;
3714
3719
  if (email2) {
3715
3720
  const user = await users.findByEmail(email2);
3716
3721
  return user ? setApiResponse7(HTTP7.OK, "USER", "User", toAdminUserDTO(user)) : NOT_FOUND();
@@ -3814,7 +3819,7 @@ var AuthModule = class {
3814
3819
  name = "@fonderie/auth";
3815
3820
  // Baked in at build time by tsup.base, so the operator's Modules page can
3816
3821
  // say what is actually deployed rather than 'not reported'.
3817
- version = "7.22.0";
3822
+ version = "7.22.1";
3818
3823
  // Users, sessions, login history, suspend — only through @fonderie/admin.
3819
3824
  describeAdmin() {
3820
3825
  return { routes: describeAuthAdminRoutes(this.store, this.bus) };
@@ -4670,7 +4675,9 @@ async function importUser(store, user) {
4670
4675
  placeholders.push(`$${vals.length}`);
4671
4676
  };
4672
4677
  if (user.id !== void 0) add("id", user.id);
4673
- add("email", user.email.toLowerCase().trim());
4678
+ const email2 = normalizeEmailSafe(user.email);
4679
+ if (!email2) throw new Error(`[auth] importUser: '${user.email}' is not an email address`);
4680
+ add("email", email2);
4674
4681
  if (user.passwordHash !== void 0) add("password_hash", user.passwordHash);
4675
4682
  if (user.firstName !== void 0) add("first_name", user.firstName);
4676
4683
  if (user.lastName !== void 0) add("last_name", user.lastName);