Algorithm: a fixed-window token bucket implemented as a pure function in
`@tasks/shared` (`consumeTokenBucket`) plus a thin `apps/web` wrapper that
holds per-key state in a module-scoped `Map`. No Redis, no external deps —
horizontally-scaled deploys will need a Redis-backed swap behind the same
`rateLimit()` signature; called out in the JSDoc as a follow-up. The pure
core is unit-tested in `packages/shared` (6 new vitest cases covering
allow/deny, window reset, key isolation, monotonic retryAfterMs, denied-
flood pegging, and option validation); the wrapper is intentionally not
tested here because apps/web has no vitest harness yet.
Wire-ins (the two narrow surfaces called out in the v1 spec):
1. Credentials `authorize` in `apps/web/lib/auth.ts`: 5 attempts per
IP per 60s. IP comes from `next/headers` (x-forwarded-for first
entry, then x-real-ip); when headers() throws or returns nothing we
fall back to keying on "unknown" in prod and skipping the limiter
entirely in dev so a local test loop doesn't lock itself out. On a
trip we `console.warn` and return null — the standard Auth.js
"auth failed" signal — without consulting the DB.
2. `invites.create` in `apps/web/server/routers/invites.ts`: 10
invite-creates per inviter per hour. Keyed by inviter id (not
workspace) so a multi-workspace admin can't multiply their
allowance. On trip we throw TRPCError TOO_MANY_REQUESTS with a
retry-after seconds count baked into the message.
Co-authored-by: Cursor <cursoragent@cursor.com>