/** * T11 · Rate-limits / quotas. Two layers (Finding-5): * 1. PRE-AUTH throttle keyed on the salted `remoteAddrHash` — the ONLY identifier before a * principal exists. Applied in T12 `onUpgrade` BEFORE token verification. Blunts subdomain * enumeration, capability-verifier brute-force, and WebAuthn/TOTP guessing DoS. * 2. PER-TENANT quotas keyed strictly on the authenticated `accountId` (INV3), never a client id. * * Pre-auth and per-tenant keys live in DISJOINT namespaces (no bleed). Keys are OPAQUE to the store. */ import type { PlanTier } from 'relay-contracts' import type { RateLimitPolicy, TokenBucketStore } from '../types.js' const SECONDS_PER_MINUTE = 60 const SECONDS_PER_HOUR = 3600 /** Fixed, plan-independent pre-auth policy (no account known yet). */ export const PRE_AUTH_POLICY: RateLimitPolicy = { connectPerMin: 30, enrollPerHour: 10, maxConcurrentSessions: 1, maxPairedHosts: 1, preAuthPerMinPerIp: 60, totpMaxFailsPerWindow: 5, totpLockoutWindowSec: 300, } const PLAN_POLICIES: Readonly> = { free: { connectPerMin: 20, enrollPerHour: 5, maxConcurrentSessions: 2, maxPairedHosts: 1, preAuthPerMinPerIp: 60, totpMaxFailsPerWindow: 5, totpLockoutWindowSec: 300, }, personal: { connectPerMin: 60, enrollPerHour: 20, maxConcurrentSessions: 5, maxPairedHosts: 5, preAuthPerMinPerIp: 60, totpMaxFailsPerWindow: 5, totpLockoutWindowSec: 300, }, pro: { connectPerMin: 120, enrollPerHour: 60, maxConcurrentSessions: 20, maxPairedHosts: 25, preAuthPerMinPerIp: 120, totpMaxFailsPerWindow: 5, totpLockoutWindowSec: 300, }, team: { connectPerMin: 300, enrollPerHour: 200, maxConcurrentSessions: 100, maxPairedHosts: 200, preAuthPerMinPerIp: 240, totpMaxFailsPerWindow: 5, totpLockoutWindowSec: 300, }, } export function policyForPlan(plan: PlanTier): RateLimitPolicy { return PLAN_POLICIES[plan] } /** PRE-AUTH throttle keyed on `remoteAddrHash`. false = throttled (caller returns 429-equiv). */ export function checkPreAuthRate( remoteAddrHash: string, store: TokenBucketStore, now: number, ): Promise { const perMin = PRE_AUTH_POLICY.preAuthPerMinPerIp return store.take(`preauth:ip:${remoteAddrHash}`, perMin / SECONDS_PER_MINUTE, perMin, now) } /** PER-TENANT connect rate keyed on the authenticated accountId (INV3). */ export function checkConnectRate( accountId: string, policy: RateLimitPolicy, store: TokenBucketStore, now: number, ): Promise { const perMin = policy.connectPerMin return store.take(`connect:acct:${accountId}`, perMin / SECONDS_PER_MINUTE, perMin, now) } /** Concurrent-session cap (synchronous; caller supplies the live count). */ export function checkConcurrentSessions( _accountId: string, active: number, policy: RateLimitPolicy, ): boolean { return active < policy.maxConcurrentSessions } /** Enrollment rate keyed on the authenticated accountId (INV3). */ export function checkEnrollRate( accountId: string, policy: RateLimitPolicy, store: TokenBucketStore, now: number, ): Promise { const perHour = policy.enrollPerHour return store.take(`enroll:acct:${accountId}`, perHour / SECONDS_PER_HOUR, perHour, now) }