deckhearth/lib/rate-limit.js

124 lines
3.5 KiB
JavaScript
Raw Normal View History

import Redis from 'ioredis';
import { RateLimiterRedis } from 'rate-limiter-flexible';
const LIMITER_CONFIG = {
auth: { limit: 5, durationSec: 15 * 60, prefix: 'deckhearth:auth' },
search: { limit: 60, durationSec: 60, prefix: 'deckhearth:search' },
upload: { limit: 10, durationSec: 60 * 60, prefix: 'deckhearth:upload' },
generate: { limit: 5, durationSec: 60 * 60, prefix: 'deckhearth:generate' },
import: { limit: 5, durationSec: 60 * 60, prefix: 'deckhearth:import' },
scan: { limit: 5, durationSec: 60, prefix: 'deckhearth:scan' },
};
fix(auth): tighten public auth surface — CORS + rate limit (Brief 4 of fix-auth-bypass) Adds rate limiting to /api/auth/login and /api/auth/register and removes their wide-open CORS allowlist. Rate limiting (@upstash/ratelimit + @upstash/redis): - 5 attempts per 15-minute sliding window per IP, prefix "tcgvault:auth" - new lib/rate-limit.js, lazy singleton, single source of truth - reads KV_REST_API_URL / KV_REST_API_TOKEN (Vercel Upstash Marketplace convention — auto-provisioned, no manual env-var setup needed) - fail-closed in production if env vars are missing (better to error one login than silently disable brute-force protection on live) - fail-open in dev/test if env vars are missing (single console.warn) - fail-open on Upstash backend outage (defense-in-depth — don't lock the entire userbase out if Upstash is down) - IP extracted from x-forwarded-for first hop, with socket fallback; NOT req.body.email (rotates) or Authorization header (absent on unauthenticated login) CORS: - Removed Access-Control-Allow-Origin: * + companion headers + OPTIONS preflight from login.js and register.js - These are first-party endpoints called from the same-origin SPA; the "*" allowlist was a development convenience that shipped to prod - verify.js is OUT OF SCOPE per architect's "cors-tighten" deferral (see convoy plan § Architect's calls) Other handler ordering preserved verbatim per brief: method gate first, then rate-limit check (returns 429 with Retry-After header), then the existing try/catch + body parsing + DB work. Pre-merge requirements: KV_REST_API_URL + KV_REST_API_TOKEN must be set in Vercel Production (already done — Upstash marketplace integration auto-provisioned both, confirmed by maintainer 2026-05-23). Convoy: fix-auth-bypass / Brief 4 Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 11:51:33 -04:00
let cached = null;
function init() {
const url = process.env.REDIS_URL;
if (url) {
const storeClient = new Redis(url, {
maxRetriesPerRequest: 1,
enableOfflineQueue: false,
});
const instances = new Map();
for (const [name, cfg] of Object.entries(LIMITER_CONFIG)) {
instances.set(
name,
new RateLimiterRedis({
storeClient,
keyPrefix: cfg.prefix,
points: cfg.limit,
duration: cfg.durationSec,
})
);
}
return { mode: 'live', instances };
fix(auth): tighten public auth surface — CORS + rate limit (Brief 4 of fix-auth-bypass) Adds rate limiting to /api/auth/login and /api/auth/register and removes their wide-open CORS allowlist. Rate limiting (@upstash/ratelimit + @upstash/redis): - 5 attempts per 15-minute sliding window per IP, prefix "tcgvault:auth" - new lib/rate-limit.js, lazy singleton, single source of truth - reads KV_REST_API_URL / KV_REST_API_TOKEN (Vercel Upstash Marketplace convention — auto-provisioned, no manual env-var setup needed) - fail-closed in production if env vars are missing (better to error one login than silently disable brute-force protection on live) - fail-open in dev/test if env vars are missing (single console.warn) - fail-open on Upstash backend outage (defense-in-depth — don't lock the entire userbase out if Upstash is down) - IP extracted from x-forwarded-for first hop, with socket fallback; NOT req.body.email (rotates) or Authorization header (absent on unauthenticated login) CORS: - Removed Access-Control-Allow-Origin: * + companion headers + OPTIONS preflight from login.js and register.js - These are first-party endpoints called from the same-origin SPA; the "*" allowlist was a development convenience that shipped to prod - verify.js is OUT OF SCOPE per architect's "cors-tighten" deferral (see convoy plan § Architect's calls) Other handler ordering preserved verbatim per brief: method gate first, then rate-limit check (returns 429 with Retry-After header), then the existing try/catch + body parsing + DB work. Pre-merge requirements: KV_REST_API_URL + KV_REST_API_TOKEN must be set in Vercel Production (already done — Upstash marketplace integration auto-provisioned both, confirmed by maintainer 2026-05-23). Convoy: fix-auth-bypass / Brief 4 Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 11:51:33 -04:00
}
if (process.env.NODE_ENV === 'production') {
throw new Error(
'[rate-limit] REDIS_URL is not configured. Set REDIS_URL to the homelab Redis URL (CT 102) before serving traffic.'
fix(auth): tighten public auth surface — CORS + rate limit (Brief 4 of fix-auth-bypass) Adds rate limiting to /api/auth/login and /api/auth/register and removes their wide-open CORS allowlist. Rate limiting (@upstash/ratelimit + @upstash/redis): - 5 attempts per 15-minute sliding window per IP, prefix "tcgvault:auth" - new lib/rate-limit.js, lazy singleton, single source of truth - reads KV_REST_API_URL / KV_REST_API_TOKEN (Vercel Upstash Marketplace convention — auto-provisioned, no manual env-var setup needed) - fail-closed in production if env vars are missing (better to error one login than silently disable brute-force protection on live) - fail-open in dev/test if env vars are missing (single console.warn) - fail-open on Upstash backend outage (defense-in-depth — don't lock the entire userbase out if Upstash is down) - IP extracted from x-forwarded-for first hop, with socket fallback; NOT req.body.email (rotates) or Authorization header (absent on unauthenticated login) CORS: - Removed Access-Control-Allow-Origin: * + companion headers + OPTIONS preflight from login.js and register.js - These are first-party endpoints called from the same-origin SPA; the "*" allowlist was a development convenience that shipped to prod - verify.js is OUT OF SCOPE per architect's "cors-tighten" deferral (see convoy plan § Architect's calls) Other handler ordering preserved verbatim per brief: method gate first, then rate-limit check (returns 429 with Retry-After header), then the existing try/catch + body parsing + DB work. Pre-merge requirements: KV_REST_API_URL + KV_REST_API_TOKEN must be set in Vercel Production (already done — Upstash marketplace integration auto-provisioned both, confirmed by maintainer 2026-05-23). Convoy: fix-auth-bypass / Brief 4 Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 11:51:33 -04:00
);
}
console.warn('[rate-limit] REDIS_URL not set — rate limiting disabled (dev/test only)');
fix(auth): tighten public auth surface — CORS + rate limit (Brief 4 of fix-auth-bypass) Adds rate limiting to /api/auth/login and /api/auth/register and removes their wide-open CORS allowlist. Rate limiting (@upstash/ratelimit + @upstash/redis): - 5 attempts per 15-minute sliding window per IP, prefix "tcgvault:auth" - new lib/rate-limit.js, lazy singleton, single source of truth - reads KV_REST_API_URL / KV_REST_API_TOKEN (Vercel Upstash Marketplace convention — auto-provisioned, no manual env-var setup needed) - fail-closed in production if env vars are missing (better to error one login than silently disable brute-force protection on live) - fail-open in dev/test if env vars are missing (single console.warn) - fail-open on Upstash backend outage (defense-in-depth — don't lock the entire userbase out if Upstash is down) - IP extracted from x-forwarded-for first hop, with socket fallback; NOT req.body.email (rotates) or Authorization header (absent on unauthenticated login) CORS: - Removed Access-Control-Allow-Origin: * + companion headers + OPTIONS preflight from login.js and register.js - These are first-party endpoints called from the same-origin SPA; the "*" allowlist was a development convenience that shipped to prod - verify.js is OUT OF SCOPE per architect's "cors-tighten" deferral (see convoy plan § Architect's calls) Other handler ordering preserved verbatim per brief: method gate first, then rate-limit check (returns 429 with Retry-After header), then the existing try/catch + body parsing + DB work. Pre-merge requirements: KV_REST_API_URL + KV_REST_API_TOKEN must be set in Vercel Production (already done — Upstash marketplace integration auto-provisioned both, confirmed by maintainer 2026-05-23). Convoy: fix-auth-bypass / Brief 4 Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 11:51:33 -04:00
return { mode: 'noop' };
}
function extractIpIdentifier(req) {
fix(auth): tighten public auth surface — CORS + rate limit (Brief 4 of fix-auth-bypass) Adds rate limiting to /api/auth/login and /api/auth/register and removes their wide-open CORS allowlist. Rate limiting (@upstash/ratelimit + @upstash/redis): - 5 attempts per 15-minute sliding window per IP, prefix "tcgvault:auth" - new lib/rate-limit.js, lazy singleton, single source of truth - reads KV_REST_API_URL / KV_REST_API_TOKEN (Vercel Upstash Marketplace convention — auto-provisioned, no manual env-var setup needed) - fail-closed in production if env vars are missing (better to error one login than silently disable brute-force protection on live) - fail-open in dev/test if env vars are missing (single console.warn) - fail-open on Upstash backend outage (defense-in-depth — don't lock the entire userbase out if Upstash is down) - IP extracted from x-forwarded-for first hop, with socket fallback; NOT req.body.email (rotates) or Authorization header (absent on unauthenticated login) CORS: - Removed Access-Control-Allow-Origin: * + companion headers + OPTIONS preflight from login.js and register.js - These are first-party endpoints called from the same-origin SPA; the "*" allowlist was a development convenience that shipped to prod - verify.js is OUT OF SCOPE per architect's "cors-tighten" deferral (see convoy plan § Architect's calls) Other handler ordering preserved verbatim per brief: method gate first, then rate-limit check (returns 429 with Retry-After header), then the existing try/catch + body parsing + DB work. Pre-merge requirements: KV_REST_API_URL + KV_REST_API_TOKEN must be set in Vercel Production (already done — Upstash marketplace integration auto-provisioned both, confirmed by maintainer 2026-05-23). Convoy: fix-auth-bypass / Brief 4 Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 11:51:33 -04:00
const xff = req.headers?.['x-forwarded-for'];
const firstHop = Array.isArray(xff) ? xff[0] : xff?.split(',')[0]?.trim();
return firstHop || req.socket?.remoteAddress || 'anonymous';
}
function extractUserIdentifier(userId) {
if (
userId === null ||
userId === undefined ||
userId === '' ||
(typeof userId === 'number' && Number.isNaN(userId))
) {
throw new Error(
'[rate-limit] extractUserIdentifier called without an authenticated userId. Place the rate-limit gate AFTER the auth check, never before.'
);
}
return `user:${userId}`;
}
async function check(className, identifier) {
fix(auth): tighten public auth surface — CORS + rate limit (Brief 4 of fix-auth-bypass) Adds rate limiting to /api/auth/login and /api/auth/register and removes their wide-open CORS allowlist. Rate limiting (@upstash/ratelimit + @upstash/redis): - 5 attempts per 15-minute sliding window per IP, prefix "tcgvault:auth" - new lib/rate-limit.js, lazy singleton, single source of truth - reads KV_REST_API_URL / KV_REST_API_TOKEN (Vercel Upstash Marketplace convention — auto-provisioned, no manual env-var setup needed) - fail-closed in production if env vars are missing (better to error one login than silently disable brute-force protection on live) - fail-open in dev/test if env vars are missing (single console.warn) - fail-open on Upstash backend outage (defense-in-depth — don't lock the entire userbase out if Upstash is down) - IP extracted from x-forwarded-for first hop, with socket fallback; NOT req.body.email (rotates) or Authorization header (absent on unauthenticated login) CORS: - Removed Access-Control-Allow-Origin: * + companion headers + OPTIONS preflight from login.js and register.js - These are first-party endpoints called from the same-origin SPA; the "*" allowlist was a development convenience that shipped to prod - verify.js is OUT OF SCOPE per architect's "cors-tighten" deferral (see convoy plan § Architect's calls) Other handler ordering preserved verbatim per brief: method gate first, then rate-limit check (returns 429 with Retry-After header), then the existing try/catch + body parsing + DB work. Pre-merge requirements: KV_REST_API_URL + KV_REST_API_TOKEN must be set in Vercel Production (already done — Upstash marketplace integration auto-provisioned both, confirmed by maintainer 2026-05-23). Convoy: fix-auth-bypass / Brief 4 Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 11:51:33 -04:00
if (!cached) {
cached = init();
}
if (cached.mode === 'noop') {
return { allowed: true, remaining: Infinity, reset: 0 };
}
const limiter = cached.instances.get(className);
if (!limiter) {
throw new Error(`[rate-limit] Unknown limiter class: ${className}`);
}
fix(auth): tighten public auth surface — CORS + rate limit (Brief 4 of fix-auth-bypass) Adds rate limiting to /api/auth/login and /api/auth/register and removes their wide-open CORS allowlist. Rate limiting (@upstash/ratelimit + @upstash/redis): - 5 attempts per 15-minute sliding window per IP, prefix "tcgvault:auth" - new lib/rate-limit.js, lazy singleton, single source of truth - reads KV_REST_API_URL / KV_REST_API_TOKEN (Vercel Upstash Marketplace convention — auto-provisioned, no manual env-var setup needed) - fail-closed in production if env vars are missing (better to error one login than silently disable brute-force protection on live) - fail-open in dev/test if env vars are missing (single console.warn) - fail-open on Upstash backend outage (defense-in-depth — don't lock the entire userbase out if Upstash is down) - IP extracted from x-forwarded-for first hop, with socket fallback; NOT req.body.email (rotates) or Authorization header (absent on unauthenticated login) CORS: - Removed Access-Control-Allow-Origin: * + companion headers + OPTIONS preflight from login.js and register.js - These are first-party endpoints called from the same-origin SPA; the "*" allowlist was a development convenience that shipped to prod - verify.js is OUT OF SCOPE per architect's "cors-tighten" deferral (see convoy plan § Architect's calls) Other handler ordering preserved verbatim per brief: method gate first, then rate-limit check (returns 429 with Retry-After header), then the existing try/catch + body parsing + DB work. Pre-merge requirements: KV_REST_API_URL + KV_REST_API_TOKEN must be set in Vercel Production (already done — Upstash marketplace integration auto-provisioned both, confirmed by maintainer 2026-05-23). Convoy: fix-auth-bypass / Brief 4 Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 11:51:33 -04:00
try {
const result = await limiter.consume(identifier);
return {
allowed: true,
remaining: result.remainingPoints,
reset: Date.now() + result.msBeforeNext,
};
} catch (rej) {
if (rej && typeof rej.msBeforeNext === 'number') {
return {
allowed: false,
remaining: 0,
reset: Date.now() + rej.msBeforeNext,
};
}
console.error('[rate-limit]', rej);
fix(auth): tighten public auth surface — CORS + rate limit (Brief 4 of fix-auth-bypass) Adds rate limiting to /api/auth/login and /api/auth/register and removes their wide-open CORS allowlist. Rate limiting (@upstash/ratelimit + @upstash/redis): - 5 attempts per 15-minute sliding window per IP, prefix "tcgvault:auth" - new lib/rate-limit.js, lazy singleton, single source of truth - reads KV_REST_API_URL / KV_REST_API_TOKEN (Vercel Upstash Marketplace convention — auto-provisioned, no manual env-var setup needed) - fail-closed in production if env vars are missing (better to error one login than silently disable brute-force protection on live) - fail-open in dev/test if env vars are missing (single console.warn) - fail-open on Upstash backend outage (defense-in-depth — don't lock the entire userbase out if Upstash is down) - IP extracted from x-forwarded-for first hop, with socket fallback; NOT req.body.email (rotates) or Authorization header (absent on unauthenticated login) CORS: - Removed Access-Control-Allow-Origin: * + companion headers + OPTIONS preflight from login.js and register.js - These are first-party endpoints called from the same-origin SPA; the "*" allowlist was a development convenience that shipped to prod - verify.js is OUT OF SCOPE per architect's "cors-tighten" deferral (see convoy plan § Architect's calls) Other handler ordering preserved verbatim per brief: method gate first, then rate-limit check (returns 429 with Retry-After header), then the existing try/catch + body parsing + DB work. Pre-merge requirements: KV_REST_API_URL + KV_REST_API_TOKEN must be set in Vercel Production (already done — Upstash marketplace integration auto-provisioned both, confirmed by maintainer 2026-05-23). Convoy: fix-auth-bypass / Brief 4 Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 11:51:33 -04:00
return { allowed: true, remaining: Infinity, reset: 0 };
}
}
export async function checkAuthRateLimit(req) {
return check('auth', extractIpIdentifier(req));
}
export async function checkSearchRateLimit(req) {
return check('search', extractIpIdentifier(req));
}
export async function checkUploadRateLimit(req, userId) {
return check('upload', extractUserIdentifier(userId));
}
export async function checkGenerateRateLimit(req, userId) {
return check('generate', extractUserIdentifier(userId));
}
export async function checkImportRateLimit(req, userId) {
return check('import', extractUserIdentifier(userId));
}
export async function checkScanRateLimit(req, userId) {
return check('scan', extractUserIdentifier(userId));
}