@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/brain/outcomes.md +1 -1
- package/dist/index.cjs +46 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +46 -39
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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);
|