@fonderie/auth 5.2.0 → 6.0.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/brain/outcomes.md +3 -3
- package/brain/signatures.md +1 -1
- package/dist/index.cjs +29 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +29 -25
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/{session-DJNd1xp_.d.cts → session-Db6KZca3.d.cts} +1 -1
- package/dist/{session-DJNd1xp_.d.ts → session-Db6KZca3.d.ts} +1 -1
- package/package.json +6 -6
package/brain/outcomes.md
CHANGED
|
@@ -129,7 +129,7 @@ Raw SQL ships in `node_modules/@fonderie/auth/dist/migrations/sql/` — read it
|
|
|
129
129
|
| Method | Path | Middleware chain (auth / validation / handler) |
|
|
130
130
|
|---|---|---|
|
|
131
131
|
| POST | `/auth/email/forgot` | `ipLimit('forgot') → validate(forgotPasswordSchema) → acctLimit('forgot') → auth.forgotPassword` |
|
|
132
|
-
| POST | `/auth/email/reset` | `validate(resetPasswordSchema) → auth.resetPassword` |
|
|
132
|
+
| POST | `/auth/email/reset` | `ipLimit('reset') → validate(resetPasswordSchema) → auth.resetPassword` |
|
|
133
133
|
| GET | `/auth/google` | `oauth.googleInit` |
|
|
134
134
|
| GET | `/auth/google/callback` | `oauth.googleCallback` |
|
|
135
135
|
| POST | `/auth/login` | `ipLimit('login') → validate(loginSchema) → acctLimit('login') → auth.login` |
|
|
@@ -141,11 +141,11 @@ Raw SQL ships in `node_modules/@fonderie/auth/dist/migrations/sql/` — read it
|
|
|
141
141
|
| POST | `/auth/mfa/verify` | `ipLimit('mfaVerify') → requireAnyAuth → requireEmailLogin → requireVerified → validate(mfaTokenSchema) → mfa.verify` |
|
|
142
142
|
| POST | `/auth/refresh` | `validate(refreshSchema) → auth.refresh` |
|
|
143
143
|
| POST | `/auth/register` | `ipLimit('register') → validate(registerSchema) → auth.register` |
|
|
144
|
-
| GET | `/auth/send-verification` | `
|
|
144
|
+
| GET | `/auth/send-verification` | `requireAnyAuth → auth.sendVerification` |
|
|
145
145
|
| GET | `/auth/sessions` | `requireAuth → user.listSessions` |
|
|
146
146
|
| DELETE | `/auth/sessions/:id` | `requireAuth → user.terminateSession` |
|
|
147
147
|
| DELETE | `/auth/sessions/others` | `requireAuth → user.terminateOtherSessions` |
|
|
148
|
-
| POST | `/auth/verify` | `
|
|
148
|
+
| POST | `/auth/verify` | `ipLimit('verify') → requireAnyAuth → validate(verifySchema) → auth.verify` |
|
|
149
149
|
| DELETE | `/users` | `requireAuth → verifyGate → user.deleteMe` |
|
|
150
150
|
| GET | `/users` | `requireAuth → user.me` |
|
|
151
151
|
| PUT | `/users/email` | `requireAuth → verifyGate → validate(updateEmailSchema) → user.updateEmail` |
|
package/brain/signatures.md
CHANGED
|
@@ -203,5 +203,5 @@ interface IAuthRateLimitConfig {
|
|
|
203
203
|
rules?: Partial<Record<AuthLimitedRoute, IRateLimitRule | false>>;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
type AuthLimitedRoute = 'login' | 'register' | 'forgot' | 'mfaVerify';
|
|
206
|
+
type AuthLimitedRoute = 'login' | 'register' | 'forgot' | 'reset' | 'verify' | 'mfaVerify';
|
|
207
207
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -126,6 +126,15 @@ var DEFAULTS = {
|
|
|
126
126
|
account: { capacity: 3, refillPerSec: 3 / min(60) },
|
|
127
127
|
accountField: "email"
|
|
128
128
|
},
|
|
129
|
+
// reset: 10/15min per IP — the reset PIN is a 6-digit code looked up
|
|
130
|
+
// globally, so an unthrottled endpoint is brute-forceable into an account
|
|
131
|
+
// takeover. There's no email in the reset body to key an account bucket on,
|
|
132
|
+
// so this IP bucket is the guard. (Moving reset to a high-entropy opaque
|
|
133
|
+
// token is tracked as a follow-up hardening.)
|
|
134
|
+
reset: { ip: { capacity: 10, refillPerSec: 10 / min(15) } },
|
|
135
|
+
// verify: 10/15min per IP — /auth/verify checks a 6-digit email/phone OTP,
|
|
136
|
+
// and in the phone flow that OTP is the login credential itself.
|
|
137
|
+
verify: { ip: { capacity: 10, refillPerSec: 10 / min(15) } },
|
|
129
138
|
// mfaVerify: 10/15min per IP — TOTP brute-force.
|
|
130
139
|
mfaVerify: { ip: { capacity: 10, refillPerSec: 10 / min(15) } }
|
|
131
140
|
};
|
|
@@ -1458,23 +1467,17 @@ function authController(store, config, bus) {
|
|
|
1458
1467
|
reqOpts2
|
|
1459
1468
|
).catch(() => {
|
|
1460
1469
|
});
|
|
1461
|
-
const
|
|
1462
|
-
loginMethod: "phone"
|
|
1463
|
-
});
|
|
1464
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, requestMeta(ctx));
|
|
1470
|
+
const otpToken = issueMfaPendingToken(user.id, config, "phone");
|
|
1465
1471
|
return Response.json(
|
|
1466
1472
|
{
|
|
1467
1473
|
reason: "USER_PHONE_REGISTERED",
|
|
1468
1474
|
explanation: "Account created. A verification code has been sent to your phone.",
|
|
1469
1475
|
result: {
|
|
1470
|
-
|
|
1476
|
+
otpToken,
|
|
1471
1477
|
user: toUserDTO(user)
|
|
1472
1478
|
}
|
|
1473
1479
|
},
|
|
1474
|
-
{
|
|
1475
|
-
status: 202,
|
|
1476
|
-
headers: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config))
|
|
1477
|
-
}
|
|
1480
|
+
{ status: 202 }
|
|
1478
1481
|
);
|
|
1479
1482
|
}
|
|
1480
1483
|
return (0, import_core5.setApiResponse)(
|
|
@@ -1588,23 +1591,14 @@ function authController(store, config, bus) {
|
|
|
1588
1591
|
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1589
1592
|
}).catch(() => {
|
|
1590
1593
|
});
|
|
1591
|
-
const
|
|
1592
|
-
loginMethod: "phone"
|
|
1593
|
-
});
|
|
1594
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
|
|
1594
|
+
const otpToken = issueMfaPendingToken(user.id, config, "phone");
|
|
1595
1595
|
return Response.json(
|
|
1596
1596
|
{
|
|
1597
1597
|
reason: "USER_PHONE_OTP_SENT",
|
|
1598
1598
|
explanation: "A verification code has been sent to your phone.",
|
|
1599
|
-
result: {
|
|
1600
|
-
tokens: { access: accessToken, refresh: refreshToken },
|
|
1601
|
-
user: toUserDTO(user, false)
|
|
1602
|
-
}
|
|
1599
|
+
result: { otpToken }
|
|
1603
1600
|
},
|
|
1604
|
-
{
|
|
1605
|
-
status: 202,
|
|
1606
|
-
headers: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config))
|
|
1607
|
-
}
|
|
1601
|
+
{ status: 202 }
|
|
1608
1602
|
);
|
|
1609
1603
|
}
|
|
1610
1604
|
return (0, import_core5.setApiResponse)(
|
|
@@ -1756,6 +1750,9 @@ function authController(store, config, bus) {
|
|
|
1756
1750
|
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
|
|
1757
1751
|
},
|
|
1758
1752
|
verify: async (ctx) => {
|
|
1753
|
+
if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
|
|
1754
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
|
|
1755
|
+
}
|
|
1759
1756
|
if (ctx.user.loginMethod !== "phone" && ctx.user.emailVerifiedAt) {
|
|
1760
1757
|
return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "VERIFIED", "Email verified successfully.", {
|
|
1761
1758
|
verified: true,
|
|
@@ -1842,6 +1839,9 @@ function authController(store, config, bus) {
|
|
|
1842
1839
|
});
|
|
1843
1840
|
},
|
|
1844
1841
|
sendVerification: async (ctx) => {
|
|
1842
|
+
if (ctx.user.mfaPending && ctx.user.loginMethod !== "phone") {
|
|
1843
|
+
return (0, import_core5.setApiResponse)(import_core5.HTTP.FORBIDDEN, "MFA_REQUIRED", "Complete MFA verification to continue");
|
|
1844
|
+
}
|
|
1845
1845
|
const resolved = { ...config, ...config.resolve?.(ctx) };
|
|
1846
1846
|
const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
|
|
1847
1847
|
if (ctx.user.loginMethod === "phone") {
|
|
@@ -2407,10 +2407,14 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
2407
2407
|
R("refresh", "POST", "/auth/refresh", (0, import_middlewares.validate)(refreshSchema), auth.refresh),
|
|
2408
2408
|
// Email — Password Recovery (Public)
|
|
2409
2409
|
R("forgotPassword", "POST", "/auth/email/forgot", ipLimit("forgot"), (0, import_middlewares.validate)(forgotPasswordSchema), acctLimit("forgot"), auth.forgotPassword),
|
|
2410
|
-
R("resetPassword", "POST", "/auth/email/reset", (0, import_middlewares.validate)(resetPasswordSchema), auth.resetPassword),
|
|
2411
|
-
// Verification (
|
|
2412
|
-
|
|
2413
|
-
|
|
2410
|
+
R("resetPassword", "POST", "/auth/email/reset", ipLimit("reset"), (0, import_middlewares.validate)(resetPasswordSchema), auth.resetPassword),
|
|
2411
|
+
// Verification (email or phone, determined by loginMethod). requireAnyAuth,
|
|
2412
|
+
// not requireAuth: the phone-OTP flow reaches these with a short-lived
|
|
2413
|
+
// pending token (login/register no longer issue real tokens before the OTP
|
|
2414
|
+
// round-trip); the controllers reject pending tokens for any other flow.
|
|
2415
|
+
// ipLimit guards the 6-digit OTP against brute force (like mfaVerify).
|
|
2416
|
+
R("verifyEmail", "POST", "/auth/verify", ipLimit("verify"), import_middlewares2.requireAnyAuth, (0, import_middlewares.validate)(verifySchema), auth.verify),
|
|
2417
|
+
R("sendVerification", "GET", "/auth/send-verification", import_middlewares2.requireAnyAuth, auth.sendVerification),
|
|
2414
2418
|
// Account Management (Protected)
|
|
2415
2419
|
R("logout", "POST", "/auth/logout", import_middlewares2.requireAuth, (0, import_middlewares.validate)(refreshSchema), auth.logout),
|
|
2416
2420
|
// User Profile (Protected; writes also gate on requireVerification)
|