import type { RateLimitRule } from "./rate-limit.decorator.js"; const MINUTE = 60_000; /** * Concrete limits for every public (no-session) route in this API. * Numbers are deliberately round, chosen for two constraints at once: * - generous enough that this project's own real-app integration test * suite (apps/api/test/*.spec.ts — real login/refresh/signature flows, * all from the same loopback IP) never trips them by accident; * - tight enough to turn an unthrottled endpoint (previously: unlimited * requests/second) into a hard, auditable ceiling per IP or per link. * None of these claim to fully stop a determined, IP-rotating attacker — * that's a documented residual risk in docs/security/threat-model.md, * not a gap this change pretends to close. */ // --- apps/api/src/auth/auth.controller.ts ----------------------------- /** Credential guessing target — argon2 already raises the per-guess cost; this caps guess *rate*. */ export const AUTH_LOGIN_RULE: RateLimitRule = { name: "auth-login", keyBy: "ip", limit: 10, windowMs: MINUTE }; /** Same order of magnitude as login: this is the second half of an MFA login, equally sensitive. */ export const AUTH_MFA_CHALLENGE_RULE: RateLimitRule = { name: "auth-mfa-challenge", keyBy: "ip", limit: 10, windowMs: MINUTE, }; /** Doesn't leak account existence (anti-enumeration response either way), but still caps inbox-spam/abuse potential. */ export const AUTH_FORGOT_PASSWORD_RULE: RateLimitRule = { name: "auth-forgot-password", keyBy: "ip", limit: 10, windowMs: MINUTE, }; /** Token is 256-bit random (auth.service.ts) — not brute-forceable; this is generic abuse/DoS hygiene. */ export const AUTH_RESET_PASSWORD_RULE: RateLimitRule = { name: "auth-reset-password", keyBy: "ip", limit: 10, windowMs: MINUTE, }; /** Refresh tokens are 256-bit random too; higher ceiling because legitimate multi-tab/background refresh is frequent by design. */ export const AUTH_REFRESH_RULE: RateLimitRule = { name: "auth-refresh", keyBy: "ip", limit: 30, windowMs: MINUTE }; /** * Not in the task's original enumerated list, but POST /v1/auth/logout has * no JwtAuthGuard either (auth.controller.ts) — genuinely public. No * guessing target (needs an already-known refresh token), so this is * generic abuse/DoS hygiene only, same ceiling as refresh. */ export const AUTH_LOGOUT_RULE: RateLimitRule = { name: "auth-logout", keyBy: "ip", limit: 30, windowMs: MINUTE }; // --- apps/api/src/signatures/public-signature.controller.ts ----------- /** * Shared across every /v1/public-signature/:token/* handler, keyed by IP — * catches one source scanning/hammering many DIFFERENT tokens (link * enumeration or scraping), which a per-token limit alone can't see. */ export const PUBLIC_SIGNATURE_IP_RULE: RateLimitRule = { name: "pubsig-ip", keyBy: "ip", limit: 60, windowMs: MINUTE, }; /** * Per-token ceiling for the ordinary read/ack steps of the flow — a real * signer hits each of these once or twice (reload, double-click); this * only bites automated hammering of one leaked link, from any number of * source IPs (that's the point of keying by token, not IP, here). */ export const PUBLIC_SIGNATURE_TOKEN_ACTION_RULE: RateLimitRule = { name: "pubsig-token-action", keyBy: "token", limit: 20, windowMs: 5 * MINUTE, }; /** Sends an email — tighter, since abuse here has a real cost (spamming the signer's inbox), not just a wasted read. */ export const PUBLIC_SIGNATURE_OTP_REQUEST_RULE: RateLimitRule = { name: "pubsig-otp-request", keyBy: "token", limit: 5, windowMs: 15 * MINUTE, }; /** * Complementary to the DB-persisted per-challenge lockout already in * public-signature.service.ts::verifyOtp (blocks after N wrong codes, * survives across source IPs) — this caps raw request *rate* against the * endpoint itself, independent of whether attempts are right or wrong. */ export const PUBLIC_SIGNATURE_OTP_VERIFY_RULE: RateLimitRule = { name: "pubsig-otp-verify", keyBy: "token", limit: 15, windowMs: 5 * MINUTE, }; // --- apps/api/src/signatures/public-verify.controller.ts -------------- export const PUBLIC_VERIFY_IP_RULE: RateLimitRule = { name: "pubverify-ip", keyBy: "ip", limit: 60, windowMs: MINUTE, }; export const PUBLIC_VERIFY_ID_RULE: RateLimitRule = { name: "pubverify-id", keyBy: "verificationId", limit: 30, windowMs: MINUTE, };