@fonderie/auth 1.1.0 → 1.2.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/index.d.cts CHANGED
@@ -2,11 +2,12 @@ export { IMfaChallenge, ISession, IUser } from './types.cjs';
2
2
  import { IFonderieModule, IFonderieApp } from '@fonderie/core';
3
3
  import { IStoreAdapter } from '@fonderie/store';
4
4
  import { EventBus } from '@fonderie/events';
5
- import { I as IAuthConfig } from './session-CLD1WPJs.cjs';
6
- export { A as AUTH_CONFIG_KEYS, a as AuthMessageKey, b as IAuthRuntimeConfig, c as IAuthSecrets, M as MESSAGE_KEYS, w as withSession } from './session-CLD1WPJs.cjs';
5
+ import { I as IAuthConfig } from './session-C54ksYE8.cjs';
6
+ export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, M as MESSAGE_KEYS, f as buildAuthAccountLimiter, g as buildAuthIpLimiter, w as withSession } from './session-C54ksYE8.cjs';
7
7
  export { IUserDTO, toUserDTO } from './dtos/user.cjs';
8
8
  export { requireAuth, validate } from '@fonderie/core/middlewares';
9
9
  import { z } from 'zod';
10
+ import '@fonderie/rate-limit';
10
11
 
11
12
  declare class AuthModule implements IFonderieModule {
12
13
  private store;
package/dist/index.d.ts CHANGED
@@ -2,11 +2,12 @@ export { IMfaChallenge, ISession, IUser } from './types.js';
2
2
  import { IFonderieModule, IFonderieApp } from '@fonderie/core';
3
3
  import { IStoreAdapter } from '@fonderie/store';
4
4
  import { EventBus } from '@fonderie/events';
5
- import { I as IAuthConfig } from './session-CLD1WPJs.js';
6
- export { A as AUTH_CONFIG_KEYS, a as AuthMessageKey, b as IAuthRuntimeConfig, c as IAuthSecrets, M as MESSAGE_KEYS, w as withSession } from './session-CLD1WPJs.js';
5
+ import { I as IAuthConfig } from './session-C54ksYE8.js';
6
+ export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, M as MESSAGE_KEYS, f as buildAuthAccountLimiter, g as buildAuthIpLimiter, w as withSession } from './session-C54ksYE8.js';
7
7
  export { IUserDTO, toUserDTO } from './dtos/user.js';
8
8
  export { requireAuth, validate } from '@fonderie/core/middlewares';
9
9
  import { z } from 'zod';
10
+ import '@fonderie/rate-limit';
10
11
 
11
12
  declare class AuthModule implements IFonderieModule {
12
13
  private store;
package/dist/index.js CHANGED
@@ -48,6 +48,60 @@ var requireEmailLogin = async (ctx, next) => {
48
48
  // src/middlewares/validate.ts
49
49
  import { validate } from "@fonderie/core/middlewares";
50
50
 
51
+ // src/services/rate-limit.ts
52
+ import {
53
+ byBodyField,
54
+ byIp,
55
+ rateLimit,
56
+ StoreAdapterStore
57
+ } from "@fonderie/rate-limit";
58
+ var min = (n) => n * 60;
59
+ var DEFAULTS = {
60
+ // login: 10/15min per IP AND 5/15min per account (credential stuffing
61
+ // rotates IPs, so the per-account bucket is the one that bites).
62
+ login: {
63
+ ip: { capacity: 10, refillPerSec: 10 / min(15) },
64
+ account: { capacity: 5, refillPerSec: 5 / min(15) },
65
+ accountField: "email"
66
+ },
67
+ // register: 5/hour per IP — signup abuse is IP-shaped.
68
+ register: { ip: { capacity: 5, refillPerSec: 5 / min(60) } },
69
+ // forgot: 5/hour per IP AND 3/hour per account (email-bombing protection).
70
+ forgot: {
71
+ ip: { capacity: 5, refillPerSec: 5 / min(60) },
72
+ account: { capacity: 3, refillPerSec: 3 / min(60) },
73
+ accountField: "email"
74
+ },
75
+ // mfaVerify: 10/15min per IP — TOTP brute-force.
76
+ mfaVerify: { ip: { capacity: 10, refillPerSec: 10 / min(15) } }
77
+ };
78
+ function resolve(route, store, config) {
79
+ if (config === false) return null;
80
+ const override = config?.rules?.[route];
81
+ if (override === false) return null;
82
+ const backing = config?.store ?? new StoreAdapterStore(store);
83
+ const def = DEFAULTS[route];
84
+ return {
85
+ store: backing,
86
+ ipRule: override ?? def.ip,
87
+ ...def.account && def.accountField ? { account: { rule: def.account, field: def.accountField } } : {}
88
+ };
89
+ }
90
+ function buildAuthIpLimiter(route, store, config) {
91
+ const r = resolve(route, store, config);
92
+ if (!r) return null;
93
+ return rateLimit({ store: r.store, rule: r.ipRule, key: byIp(`auth:${route}`) });
94
+ }
95
+ function buildAuthAccountLimiter(route, store, config) {
96
+ const r = resolve(route, store, config);
97
+ if (!r || !r.account) return null;
98
+ return rateLimit({
99
+ store: r.store,
100
+ rule: r.account.rule,
101
+ key: byBodyField(`auth:${route}`, r.account.field)
102
+ });
103
+ }
104
+
51
105
  // src/schemas.ts
52
106
  var schemas_exports = {};
53
107
  __export(schemas_exports, {
@@ -113,6 +167,26 @@ var changePasswordSchema = z.object({
113
167
  });
114
168
  var mfaTokenSchema = z.object({ token: z.string().trim().min(6).max(64) });
115
169
 
170
+ // src/services/cookies.ts
171
+ function secureAttr(config) {
172
+ const secure = config.secureCookies ?? process.env["NODE_ENV"] === "production";
173
+ return secure ? "; Secure" : "";
174
+ }
175
+ function tokenPairCookies(accessToken, refreshToken, config) {
176
+ const secure = secureAttr(config);
177
+ return [
178
+ `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/${secure}`,
179
+ `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh${secure}`
180
+ ].join(", ");
181
+ }
182
+ function clearedTokenCookies(config) {
183
+ const secure = secureAttr(config);
184
+ return [
185
+ `access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure}`,
186
+ `refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0${secure}`
187
+ ].join(", ");
188
+ }
189
+
116
190
  // src/controllers/mfa.controller.ts
117
191
  import QRCode from "qrcode";
118
192
  import { setApiResponse as setApiResponse2, HTTP as HTTP2 } from "@fonderie/core";
@@ -697,10 +771,7 @@ function mfaController(store, config, issuer, bus) {
697
771
  {
698
772
  status: 200,
699
773
  headers: {
700
- "Set-Cookie": [
701
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
702
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
703
- ].join(", ")
774
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
704
775
  }
705
776
  }
706
777
  );
@@ -1034,10 +1105,7 @@ function authController(store, config, bus) {
1034
1105
  {
1035
1106
  status: 201,
1036
1107
  headers: {
1037
- "Set-Cookie": [
1038
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1039
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1040
- ].join(", ")
1108
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1041
1109
  }
1042
1110
  }
1043
1111
  );
@@ -1099,10 +1167,7 @@ function authController(store, config, bus) {
1099
1167
  {
1100
1168
  status: 202,
1101
1169
  headers: {
1102
- "Set-Cookie": [
1103
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1104
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1105
- ].join(", ")
1170
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1106
1171
  }
1107
1172
  }
1108
1173
  );
@@ -1161,10 +1226,7 @@ function authController(store, config, bus) {
1161
1226
  {
1162
1227
  status: 200,
1163
1228
  headers: {
1164
- "Set-Cookie": [
1165
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1166
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1167
- ].join(", ")
1229
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1168
1230
  }
1169
1231
  }
1170
1232
  );
@@ -1207,10 +1269,7 @@ function authController(store, config, bus) {
1207
1269
  {
1208
1270
  status: 202,
1209
1271
  headers: {
1210
- "Set-Cookie": [
1211
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1212
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1213
- ].join(", ")
1272
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1214
1273
  }
1215
1274
  }
1216
1275
  );
@@ -1231,10 +1290,7 @@ function authController(store, config, bus) {
1231
1290
  {
1232
1291
  status: 200,
1233
1292
  headers: {
1234
- "Set-Cookie": [
1235
- "access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
1236
- "refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
1237
- ].join(", ")
1293
+ "Set-Cookie": clearedTokenCookies(config)
1238
1294
  }
1239
1295
  }
1240
1296
  );
@@ -1275,10 +1331,7 @@ function authController(store, config, bus) {
1275
1331
  {
1276
1332
  status: 200,
1277
1333
  headers: {
1278
- "Set-Cookie": [
1279
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1280
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1281
- ].join(", ")
1334
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1282
1335
  }
1283
1336
  }
1284
1337
  );
@@ -1306,12 +1359,9 @@ function authController(store, config, bus) {
1306
1359
  const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
1307
1360
  if (remaining > 0) {
1308
1361
  return setApiResponse3(
1309
- HTTP3.TOO_MANY_REQUESTS,
1310
- "VERIFICATION_COOLDOWN",
1311
- "Please wait before requesting a new password reset code.",
1312
- {
1313
- retryAfter: Math.ceil(remaining / 1e3)
1314
- }
1362
+ HTTP3.OK,
1363
+ "PASSWORD_RESET_EMAIL_SENT",
1364
+ "Password reset email sent (if account exists)."
1315
1365
  );
1316
1366
  }
1317
1367
  const pin = randomInt(1e5, 1e6).toString();
@@ -1430,10 +1480,7 @@ function authController(store, config, bus) {
1430
1480
  {
1431
1481
  status: 200,
1432
1482
  headers: {
1433
- "Set-Cookie": [
1434
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1435
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1436
- ].join(", ")
1483
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1437
1484
  }
1438
1485
  }
1439
1486
  );
@@ -1550,7 +1597,7 @@ function normalizePhone2(phone2) {
1550
1597
  function isValidPhone2(phone2) {
1551
1598
  return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone2(phone2));
1552
1599
  }
1553
- function userController(store, bus) {
1600
+ function userController(store, config, bus) {
1554
1601
  const users = new UserModel(store);
1555
1602
  const emailVerif = new EmailVerificationModel(store);
1556
1603
  const phoneVerif = new PhoneVerificationModel(store);
@@ -1739,10 +1786,7 @@ function userController(store, bus) {
1739
1786
  {
1740
1787
  status: 200,
1741
1788
  headers: {
1742
- "Set-Cookie": [
1743
- "access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
1744
- "refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
1745
- ].join(", ")
1789
+ "Set-Cookie": clearedTokenCookies(config)
1746
1790
  }
1747
1791
  }
1748
1792
  );
@@ -1846,10 +1890,7 @@ function oauthController(store, config) {
1846
1890
  {
1847
1891
  status: 200,
1848
1892
  headers: {
1849
- "Set-Cookie": [
1850
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1851
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1852
- ].join(", ")
1893
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1853
1894
  }
1854
1895
  }
1855
1896
  );
@@ -1859,19 +1900,22 @@ function oauthController(store, config) {
1859
1900
 
1860
1901
  // src/routes.ts
1861
1902
  function buildAuthRoutes(store, config, bus) {
1862
- const user = userController(store, bus);
1903
+ const user = userController(store, config, bus);
1863
1904
  const auth = authController(store, config, bus);
1864
1905
  const oauth = oauthController(store, config);
1865
1906
  const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
1866
1907
  const verifyGate = config.requireVerification ? requireVerified : (_ctx, next) => next();
1908
+ const passthrough = (_ctx, next) => next();
1909
+ const ipLimit = (route) => buildAuthIpLimiter(route, store, config.rateLimit) ?? passthrough;
1910
+ const acctLimit = (route) => buildAuthAccountLimiter(route, store, config.rateLimit) ?? passthrough;
1867
1911
  const routes = [
1868
1912
  // Registration & Login (Public)
1869
- ["POST", "/auth/register", validate(registerSchema), auth.register],
1870
- ["POST", "/auth/login", validate(loginSchema), auth.login],
1913
+ ["POST", "/auth/register", ipLimit("register"), validate(registerSchema), auth.register],
1914
+ ["POST", "/auth/login", ipLimit("login"), validate(loginSchema), acctLimit("login"), auth.login],
1871
1915
  // Token Management (Public)
1872
1916
  ["POST", "/auth/refresh", validate(refreshSchema), auth.refresh],
1873
1917
  // Email — Password Recovery (Public)
1874
- ["POST", "/auth/email/forgot", validate(forgotPasswordSchema), auth.forgotPassword],
1918
+ ["POST", "/auth/email/forgot", ipLimit("forgot"), validate(forgotPasswordSchema), acctLimit("forgot"), auth.forgotPassword],
1875
1919
  ["POST", "/auth/email/reset", validate(resetPasswordSchema), auth.resetPassword],
1876
1920
  // Verification (Protected — email or phone, determined by loginMethod)
1877
1921
  ["POST", "/auth/verify", requireAuth, validate(verifySchema), auth.verify],
@@ -1891,7 +1935,7 @@ function buildAuthRoutes(store, config, bus) {
1891
1935
  ["POST", "/auth/mfa/setup", requireAuth, requireEmailLogin, requireVerified, mfa.setup],
1892
1936
  // /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)
1893
1937
  // and full tokens (setup confirmation), so requireAnyAuth is used here.
1894
- ["POST", "/auth/mfa/verify", requireAnyAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.verify],
1938
+ ["POST", "/auth/mfa/verify", ipLimit("mfaVerify"), requireAnyAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.verify],
1895
1939
  ["POST", "/auth/mfa/disable", requireAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.disable],
1896
1940
  [
1897
1941
  "POST",
@@ -1975,6 +2019,8 @@ export {
1975
2019
  AUTH_CONFIG_KEYS,
1976
2020
  AuthModule,
1977
2021
  MESSAGE_KEYS,
2022
+ buildAuthAccountLimiter,
2023
+ buildAuthIpLimiter,
1978
2024
  normalizeEmail,
1979
2025
  normalizeEmailSafe,
1980
2026
  requireAuth2 as requireAuth,