@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.cjs CHANGED
@@ -57,6 +57,8 @@ __export(index_exports, {
57
57
  AUTH_CONFIG_KEYS: () => AUTH_CONFIG_KEYS,
58
58
  AuthModule: () => AuthModule,
59
59
  MESSAGE_KEYS: () => MESSAGE_KEYS,
60
+ buildAuthAccountLimiter: () => buildAuthAccountLimiter,
61
+ buildAuthIpLimiter: () => buildAuthIpLimiter,
60
62
  normalizeEmail: () => normalizeEmail,
61
63
  normalizeEmailSafe: () => normalizeEmailSafe,
62
64
  requireAuth: () => import_middlewares3.requireAuth,
@@ -86,6 +88,55 @@ var requireEmailLogin = async (ctx, next) => {
86
88
  // src/middlewares/validate.ts
87
89
  var import_middlewares = require("@fonderie/core/middlewares");
88
90
 
91
+ // src/services/rate-limit.ts
92
+ var import_rate_limit = require("@fonderie/rate-limit");
93
+ var min = (n) => n * 60;
94
+ var DEFAULTS = {
95
+ // login: 10/15min per IP AND 5/15min per account (credential stuffing
96
+ // rotates IPs, so the per-account bucket is the one that bites).
97
+ login: {
98
+ ip: { capacity: 10, refillPerSec: 10 / min(15) },
99
+ account: { capacity: 5, refillPerSec: 5 / min(15) },
100
+ accountField: "email"
101
+ },
102
+ // register: 5/hour per IP — signup abuse is IP-shaped.
103
+ register: { ip: { capacity: 5, refillPerSec: 5 / min(60) } },
104
+ // forgot: 5/hour per IP AND 3/hour per account (email-bombing protection).
105
+ forgot: {
106
+ ip: { capacity: 5, refillPerSec: 5 / min(60) },
107
+ account: { capacity: 3, refillPerSec: 3 / min(60) },
108
+ accountField: "email"
109
+ },
110
+ // mfaVerify: 10/15min per IP — TOTP brute-force.
111
+ mfaVerify: { ip: { capacity: 10, refillPerSec: 10 / min(15) } }
112
+ };
113
+ function resolve(route, store, config) {
114
+ if (config === false) return null;
115
+ const override = config?.rules?.[route];
116
+ if (override === false) return null;
117
+ const backing = config?.store ?? new import_rate_limit.StoreAdapterStore(store);
118
+ const def = DEFAULTS[route];
119
+ return {
120
+ store: backing,
121
+ ipRule: override ?? def.ip,
122
+ ...def.account && def.accountField ? { account: { rule: def.account, field: def.accountField } } : {}
123
+ };
124
+ }
125
+ function buildAuthIpLimiter(route, store, config) {
126
+ const r = resolve(route, store, config);
127
+ if (!r) return null;
128
+ return (0, import_rate_limit.rateLimit)({ store: r.store, rule: r.ipRule, key: (0, import_rate_limit.byIp)(`auth:${route}`) });
129
+ }
130
+ function buildAuthAccountLimiter(route, store, config) {
131
+ const r = resolve(route, store, config);
132
+ if (!r || !r.account) return null;
133
+ return (0, import_rate_limit.rateLimit)({
134
+ store: r.store,
135
+ rule: r.account.rule,
136
+ key: (0, import_rate_limit.byBodyField)(`auth:${route}`, r.account.field)
137
+ });
138
+ }
139
+
89
140
  // src/schemas.ts
90
141
  var schemas_exports = {};
91
142
  __export(schemas_exports, {
@@ -151,6 +202,26 @@ var changePasswordSchema = import_zod.z.object({
151
202
  });
152
203
  var mfaTokenSchema = import_zod.z.object({ token: import_zod.z.string().trim().min(6).max(64) });
153
204
 
205
+ // src/services/cookies.ts
206
+ function secureAttr(config) {
207
+ const secure = config.secureCookies ?? process.env["NODE_ENV"] === "production";
208
+ return secure ? "; Secure" : "";
209
+ }
210
+ function tokenPairCookies(accessToken, refreshToken, config) {
211
+ const secure = secureAttr(config);
212
+ return [
213
+ `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/${secure}`,
214
+ `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh${secure}`
215
+ ].join(", ");
216
+ }
217
+ function clearedTokenCookies(config) {
218
+ const secure = secureAttr(config);
219
+ return [
220
+ `access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure}`,
221
+ `refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0${secure}`
222
+ ].join(", ");
223
+ }
224
+
154
225
  // src/controllers/mfa.controller.ts
155
226
  var import_qrcode = __toESM(require("qrcode"), 1);
156
227
  var import_core3 = require("@fonderie/core");
@@ -735,10 +806,7 @@ function mfaController(store, config, issuer, bus) {
735
806
  {
736
807
  status: 200,
737
808
  headers: {
738
- "Set-Cookie": [
739
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
740
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
741
- ].join(", ")
809
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
742
810
  }
743
811
  }
744
812
  );
@@ -1072,10 +1140,7 @@ function authController(store, config, bus) {
1072
1140
  {
1073
1141
  status: 201,
1074
1142
  headers: {
1075
- "Set-Cookie": [
1076
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1077
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1078
- ].join(", ")
1143
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1079
1144
  }
1080
1145
  }
1081
1146
  );
@@ -1137,10 +1202,7 @@ function authController(store, config, bus) {
1137
1202
  {
1138
1203
  status: 202,
1139
1204
  headers: {
1140
- "Set-Cookie": [
1141
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1142
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1143
- ].join(", ")
1205
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1144
1206
  }
1145
1207
  }
1146
1208
  );
@@ -1199,10 +1261,7 @@ function authController(store, config, bus) {
1199
1261
  {
1200
1262
  status: 200,
1201
1263
  headers: {
1202
- "Set-Cookie": [
1203
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1204
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1205
- ].join(", ")
1264
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1206
1265
  }
1207
1266
  }
1208
1267
  );
@@ -1245,10 +1304,7 @@ function authController(store, config, bus) {
1245
1304
  {
1246
1305
  status: 202,
1247
1306
  headers: {
1248
- "Set-Cookie": [
1249
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1250
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1251
- ].join(", ")
1307
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1252
1308
  }
1253
1309
  }
1254
1310
  );
@@ -1269,10 +1325,7 @@ function authController(store, config, bus) {
1269
1325
  {
1270
1326
  status: 200,
1271
1327
  headers: {
1272
- "Set-Cookie": [
1273
- "access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
1274
- "refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
1275
- ].join(", ")
1328
+ "Set-Cookie": clearedTokenCookies(config)
1276
1329
  }
1277
1330
  }
1278
1331
  );
@@ -1313,10 +1366,7 @@ function authController(store, config, bus) {
1313
1366
  {
1314
1367
  status: 200,
1315
1368
  headers: {
1316
- "Set-Cookie": [
1317
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1318
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1319
- ].join(", ")
1369
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1320
1370
  }
1321
1371
  }
1322
1372
  );
@@ -1344,12 +1394,9 @@ function authController(store, config, bus) {
1344
1394
  const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
1345
1395
  if (remaining > 0) {
1346
1396
  return (0, import_core4.setApiResponse)(
1347
- import_core4.HTTP.TOO_MANY_REQUESTS,
1348
- "VERIFICATION_COOLDOWN",
1349
- "Please wait before requesting a new password reset code.",
1350
- {
1351
- retryAfter: Math.ceil(remaining / 1e3)
1352
- }
1397
+ import_core4.HTTP.OK,
1398
+ "PASSWORD_RESET_EMAIL_SENT",
1399
+ "Password reset email sent (if account exists)."
1353
1400
  );
1354
1401
  }
1355
1402
  const pin = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
@@ -1468,10 +1515,7 @@ function authController(store, config, bus) {
1468
1515
  {
1469
1516
  status: 200,
1470
1517
  headers: {
1471
- "Set-Cookie": [
1472
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1473
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1474
- ].join(", ")
1518
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1475
1519
  }
1476
1520
  }
1477
1521
  );
@@ -1588,7 +1632,7 @@ function normalizePhone2(phone2) {
1588
1632
  function isValidPhone2(phone2) {
1589
1633
  return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone2(phone2));
1590
1634
  }
1591
- function userController(store, bus) {
1635
+ function userController(store, config, bus) {
1592
1636
  const users = new UserModel(store);
1593
1637
  const emailVerif = new EmailVerificationModel(store);
1594
1638
  const phoneVerif = new PhoneVerificationModel(store);
@@ -1777,10 +1821,7 @@ function userController(store, bus) {
1777
1821
  {
1778
1822
  status: 200,
1779
1823
  headers: {
1780
- "Set-Cookie": [
1781
- "access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
1782
- "refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
1783
- ].join(", ")
1824
+ "Set-Cookie": clearedTokenCookies(config)
1784
1825
  }
1785
1826
  }
1786
1827
  );
@@ -1884,10 +1925,7 @@ function oauthController(store, config) {
1884
1925
  {
1885
1926
  status: 200,
1886
1927
  headers: {
1887
- "Set-Cookie": [
1888
- `access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
1889
- `refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
1890
- ].join(", ")
1928
+ "Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
1891
1929
  }
1892
1930
  }
1893
1931
  );
@@ -1897,19 +1935,22 @@ function oauthController(store, config) {
1897
1935
 
1898
1936
  // src/routes.ts
1899
1937
  function buildAuthRoutes(store, config, bus) {
1900
- const user = userController(store, bus);
1938
+ const user = userController(store, config, bus);
1901
1939
  const auth = authController(store, config, bus);
1902
1940
  const oauth = oauthController(store, config);
1903
1941
  const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
1904
1942
  const verifyGate = config.requireVerification ? import_middlewares2.requireVerified : (_ctx, next) => next();
1943
+ const passthrough = (_ctx, next) => next();
1944
+ const ipLimit = (route) => buildAuthIpLimiter(route, store, config.rateLimit) ?? passthrough;
1945
+ const acctLimit = (route) => buildAuthAccountLimiter(route, store, config.rateLimit) ?? passthrough;
1905
1946
  const routes = [
1906
1947
  // Registration & Login (Public)
1907
- ["POST", "/auth/register", (0, import_middlewares.validate)(registerSchema), auth.register],
1908
- ["POST", "/auth/login", (0, import_middlewares.validate)(loginSchema), auth.login],
1948
+ ["POST", "/auth/register", ipLimit("register"), (0, import_middlewares.validate)(registerSchema), auth.register],
1949
+ ["POST", "/auth/login", ipLimit("login"), (0, import_middlewares.validate)(loginSchema), acctLimit("login"), auth.login],
1909
1950
  // Token Management (Public)
1910
1951
  ["POST", "/auth/refresh", (0, import_middlewares.validate)(refreshSchema), auth.refresh],
1911
1952
  // Email — Password Recovery (Public)
1912
- ["POST", "/auth/email/forgot", (0, import_middlewares.validate)(forgotPasswordSchema), auth.forgotPassword],
1953
+ ["POST", "/auth/email/forgot", ipLimit("forgot"), (0, import_middlewares.validate)(forgotPasswordSchema), acctLimit("forgot"), auth.forgotPassword],
1913
1954
  ["POST", "/auth/email/reset", (0, import_middlewares.validate)(resetPasswordSchema), auth.resetPassword],
1914
1955
  // Verification (Protected — email or phone, determined by loginMethod)
1915
1956
  ["POST", "/auth/verify", import_middlewares2.requireAuth, (0, import_middlewares.validate)(verifySchema), auth.verify],
@@ -1929,7 +1970,7 @@ function buildAuthRoutes(store, config, bus) {
1929
1970
  ["POST", "/auth/mfa/setup", import_middlewares2.requireAuth, requireEmailLogin, import_middlewares2.requireVerified, mfa.setup],
1930
1971
  // /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)
1931
1972
  // and full tokens (setup confirmation), so requireAnyAuth is used here.
1932
- ["POST", "/auth/mfa/verify", import_middlewares2.requireAnyAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.verify],
1973
+ ["POST", "/auth/mfa/verify", ipLimit("mfaVerify"), import_middlewares2.requireAnyAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.verify],
1933
1974
  ["POST", "/auth/mfa/disable", import_middlewares2.requireAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.disable],
1934
1975
  [
1935
1976
  "POST",
@@ -2014,6 +2055,8 @@ var import_middlewares3 = require("@fonderie/core/middlewares");
2014
2055
  AUTH_CONFIG_KEYS,
2015
2056
  AuthModule,
2016
2057
  MESSAGE_KEYS,
2058
+ buildAuthAccountLimiter,
2059
+ buildAuthIpLimiter,
2017
2060
  normalizeEmail,
2018
2061
  normalizeEmailSafe,
2019
2062
  requireAuth,