// Simple in-memory rate limiter. // Note: resets on server restart and does not share state across multiple processes. // Suitable for MVP; replace with Redis for production scale-out. interface RateLimitConfig { windowMs: number max: number } const CONFIGS: Record = { login: { windowMs: 60_000, max: 10 }, // 10 attempts per minute register: { windowMs: 3_600_000, max: 5 }, // 5 attempts per hour 'pair-start': { windowMs: 60_000, max: 10 }, // 10 QR-pairings per minute (M10) // v1-readiness item 3 — per-user mutation-rate-limits. // Limits zijn ruim genoeg voor normaal gebruik, eng genoeg om abuse-loops // (bv. een runaway-script dat duizenden tasks aanmaakt) te stoppen. 'create-pbi': { windowMs: 60_000, max: 30 }, 'create-story': { windowMs: 60_000, max: 50 }, 'create-task': { windowMs: 60_000, max: 100 }, 'create-todo': { windowMs: 60_000, max: 60 }, 'create-sprint': { windowMs: 60_000, max: 5 }, 'create-product': { windowMs: 60_000, max: 5 }, 'create-token': { windowMs: 3_600_000, max: 10 }, // 10 API-tokens/uur/user 'enqueue-job': { windowMs: 60_000, max: 30 }, 'log-story': { windowMs: 60_000, max: 60 }, 'upload-avatar': { windowMs: 3_600_000, max: 20 }, 'answer-question': { windowMs: 60_000, max: 30 }, // M12 — Idea entity (zie docs/plans/M12-ideas.md) 'create-idea': { windowMs: 60_000, max: 30 }, 'edit-idea-md': { windowMs: 60_000, max: 60 }, // grill_md / plan_md edits 'start-idea-job': { windowMs: 60_000, max: 10 }, // Grill / Make Plan triggers 'materialize-idea': { windowMs: 60_000, max: 5 }, 'create-user-question': { windowMs: 60_000, max: 20 }, // PLAN_CHAT vragen } const DEFAULT_CONFIG: RateLimitConfig = { windowMs: 60_000, max: 10 } const store = new Map() export function checkRateLimit(key: string): boolean { const prefix = key.split(':')[0] const config = CONFIGS[prefix] ?? DEFAULT_CONFIG const now = Date.now() const entry = store.get(key) if (!entry || now > entry.resetAt) { store.set(key, { count: 1, resetAt: now + config.windowMs }) return true } if (entry.count >= config.max) { return false } entry.count++ return true } /** * Wrapper voor server-actions: scope op (action, userId), retourneert het * standaard `{ error, code: 429 }` shape als de gebruiker over de limiet is. * * Gebruik in een action: * * const limited = enforceUserRateLimit('create-pbi', session.userId) * if (limited) return limited */ export function enforceUserRateLimit( scope: keyof typeof CONFIGS, userId: string, ): { error: string; code: 429 } | null { if (!checkRateLimit(`${scope}:${userId}`)) { return { error: 'Te veel acties achter elkaar. Probeer het over een minuut opnieuw.', code: 429, } } return null } /** * Voor test-isolatie: leegt de in-memory store. Niet exporteren in productie-paden. */ export function _resetRateLimit() { store.clear() }