@fonderie/auth 5.3.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 CHANGED
@@ -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` | `requireAuth → auth.sendVerification` |
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` | `requireAuth → validate(verifySchema) → 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` |
@@ -203,5 +203,5 @@ interface IAuthRateLimitConfig {
203
203
  rules?: Partial<Record<AuthLimitedRoute, IRateLimitRule | false>>;
204
204
  }
205
205
 
206
- type AuthLimitedRoute = 'login' | 'register' | 'forgot' | 'reset' | 'mfaVerify';
206
+ type AuthLimitedRoute = 'login' | 'register' | 'forgot' | 'reset' | 'verify' | 'mfaVerify';
207
207
  ```
package/dist/index.cjs CHANGED
@@ -132,6 +132,9 @@ var DEFAULTS = {
132
132
  // so this IP bucket is the guard. (Moving reset to a high-entropy opaque
133
133
  // token is tracked as a follow-up hardening.)
134
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) } },
135
138
  // mfaVerify: 10/15min per IP — TOTP brute-force.
136
139
  mfaVerify: { ip: { capacity: 10, refillPerSec: 10 / min(15) } }
137
140
  };
@@ -1464,23 +1467,17 @@ function authController(store, config, bus) {
1464
1467
  reqOpts2
1465
1468
  ).catch(() => {
1466
1469
  });
1467
- const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
1468
- loginMethod: "phone"
1469
- });
1470
- await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, requestMeta(ctx));
1470
+ const otpToken = issueMfaPendingToken(user.id, config, "phone");
1471
1471
  return Response.json(
1472
1472
  {
1473
1473
  reason: "USER_PHONE_REGISTERED",
1474
1474
  explanation: "Account created. A verification code has been sent to your phone.",
1475
1475
  result: {
1476
- tokens: { access: accessToken, refresh: refreshToken },
1476
+ otpToken,
1477
1477
  user: toUserDTO(user)
1478
1478
  }
1479
1479
  },
1480
- {
1481
- status: 202,
1482
- headers: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config))
1483
- }
1480
+ { status: 202 }
1484
1481
  );
1485
1482
  }
1486
1483
  return (0, import_core5.setApiResponse)(
@@ -1594,23 +1591,14 @@ function authController(store, config, bus) {
1594
1591
  recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
1595
1592
  }).catch(() => {
1596
1593
  });
1597
- const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
1598
- loginMethod: "phone"
1599
- });
1600
- await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid, meta);
1594
+ const otpToken = issueMfaPendingToken(user.id, config, "phone");
1601
1595
  return Response.json(
1602
1596
  {
1603
1597
  reason: "USER_PHONE_OTP_SENT",
1604
1598
  explanation: "A verification code has been sent to your phone.",
1605
- result: {
1606
- tokens: { access: accessToken, refresh: refreshToken },
1607
- user: toUserDTO(user, false)
1608
- }
1599
+ result: { otpToken }
1609
1600
  },
1610
- {
1611
- status: 202,
1612
- headers: cookieHeaders(tokenPairCookies(accessToken, refreshToken, config))
1613
- }
1601
+ { status: 202 }
1614
1602
  );
1615
1603
  }
1616
1604
  return (0, import_core5.setApiResponse)(
@@ -1762,6 +1750,9 @@ function authController(store, config, bus) {
1762
1750
  return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "PASSWORD_RESET_SUCCESSFUL", "Password reset successfully.");
1763
1751
  },
1764
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
+ }
1765
1756
  if (ctx.user.loginMethod !== "phone" && ctx.user.emailVerifiedAt) {
1766
1757
  return (0, import_core5.setApiResponse)(import_core5.HTTP.OK, "VERIFIED", "Email verified successfully.", {
1767
1758
  verified: true,
@@ -1848,6 +1839,9 @@ function authController(store, config, bus) {
1848
1839
  });
1849
1840
  },
1850
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
+ }
1851
1845
  const resolved = { ...config, ...config.resolve?.(ctx) };
1852
1846
  const cooldown = resolved.verificationCooldown ?? DEFAULT_VERIFICATION_COOLDOWN;
1853
1847
  if (ctx.user.loginMethod === "phone") {
@@ -2414,9 +2408,13 @@ function buildAuthRoutes(store, config, bus) {
2414
2408
  // Email — Password Recovery (Public)
2415
2409
  R("forgotPassword", "POST", "/auth/email/forgot", ipLimit("forgot"), (0, import_middlewares.validate)(forgotPasswordSchema), acctLimit("forgot"), auth.forgotPassword),
2416
2410
  R("resetPassword", "POST", "/auth/email/reset", ipLimit("reset"), (0, import_middlewares.validate)(resetPasswordSchema), auth.resetPassword),
2417
- // Verification (Protected — email or phone, determined by loginMethod)
2418
- R("verifyEmail", "POST", "/auth/verify", import_middlewares2.requireAuth, (0, import_middlewares.validate)(verifySchema), auth.verify),
2419
- R("sendVerification", "GET", "/auth/send-verification", import_middlewares2.requireAuth, auth.sendVerification),
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),
2420
2418
  // Account Management (Protected)
2421
2419
  R("logout", "POST", "/auth/logout", import_middlewares2.requireAuth, (0, import_middlewares.validate)(refreshSchema), auth.logout),
2422
2420
  // User Profile (Protected; writes also gate on requireVerification)