ubiquitous-invention/plans/Plan-multitenant-saas-hardening/Epic-tenant-lifecycle/Task-rate-limit-and-abuse-guardrails.md

91 lines
6 KiB
Markdown
Raw Normal View History

plans: scaffold daily-driver-finish, saas-hardening, agent-coordination Three new plan trees that fill in the gaps surfaced during repo review. Together they map out what remains between the current scaffold-with-stubs state and a daily-usable, multitenant, agent-coordinated app. * Plan-daily-driver-finish (P0): turn stubs into real data. Five tasks covering the lint/shared-types breakage, hardcoded dashboard mocks, AI-page setTimeout placeholder, post-signin landing decision, and a cross-browser collab smoke test against the deployed Hocuspocus instance. * Plan-multitenant-saas-hardening (P1): everything multitenant needs beyond what Plan-multitenant-cursor-sync already covers. Invites and role management, soft-delete + append-only audit log, rate limits on the auth + mutation hot paths, and a Vitest + GitHub Actions test foundation so PRs can't ship red. * Plan-agent-coordination (P2): the layer that makes a Task-*.md runnable, not just readable. Adds workflow_prompt with task -> epic -> plan inheritance, an agent_runs table for auditable sessions, and two new MCP tools (claim_task / complete_task) that replace the freeform update_object composition agents do today. Includes an intentionally-deferred Epic-optional-orchestrator that captures the Symphony-shaped runner as a decision point rather than an immediate build. Each task is bead-scale (one focused Cursor session) with explicit in-scope, out-of-scope, and anti-goal sections so a future agent can pick up a single Task-*.md and start without scrollback context. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 00:52:22 -04:00
---
kind: task
slug: rate-limit-and-abuse-guardrails
title: Rate-limit credentials sign-in and high-impact mutations
plan_slug: multitenant-saas-hardening
epic_slug: tenant-lifecycle
status: in_progress
plans: scaffold daily-driver-finish, saas-hardening, agent-coordination Three new plan trees that fill in the gaps surfaced during repo review. Together they map out what remains between the current scaffold-with-stubs state and a daily-usable, multitenant, agent-coordinated app. * Plan-daily-driver-finish (P0): turn stubs into real data. Five tasks covering the lint/shared-types breakage, hardcoded dashboard mocks, AI-page setTimeout placeholder, post-signin landing decision, and a cross-browser collab smoke test against the deployed Hocuspocus instance. * Plan-multitenant-saas-hardening (P1): everything multitenant needs beyond what Plan-multitenant-cursor-sync already covers. Invites and role management, soft-delete + append-only audit log, rate limits on the auth + mutation hot paths, and a Vitest + GitHub Actions test foundation so PRs can't ship red. * Plan-agent-coordination (P2): the layer that makes a Task-*.md runnable, not just readable. Adds workflow_prompt with task -> epic -> plan inheritance, an agent_runs table for auditable sessions, and two new MCP tools (claim_task / complete_task) that replace the freeform update_object composition agents do today. Includes an intentionally-deferred Epic-optional-orchestrator that captures the Symphony-shaped runner as a decision point rather than an immediate build. Each task is bead-scale (one focused Cursor session) with explicit in-scope, out-of-scope, and anti-goal sections so a future agent can pick up a single Task-*.md and start without scrollback context. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 00:52:22 -04:00
priority: P2
tenant_id: global
owner: unassigned
cursor_todo_id: null
updated_at: "2026-06-02"
plans: scaffold daily-driver-finish, saas-hardening, agent-coordination Three new plan trees that fill in the gaps surfaced during repo review. Together they map out what remains between the current scaffold-with-stubs state and a daily-usable, multitenant, agent-coordinated app. * Plan-daily-driver-finish (P0): turn stubs into real data. Five tasks covering the lint/shared-types breakage, hardcoded dashboard mocks, AI-page setTimeout placeholder, post-signin landing decision, and a cross-browser collab smoke test against the deployed Hocuspocus instance. * Plan-multitenant-saas-hardening (P1): everything multitenant needs beyond what Plan-multitenant-cursor-sync already covers. Invites and role management, soft-delete + append-only audit log, rate limits on the auth + mutation hot paths, and a Vitest + GitHub Actions test foundation so PRs can't ship red. * Plan-agent-coordination (P2): the layer that makes a Task-*.md runnable, not just readable. Adds workflow_prompt with task -> epic -> plan inheritance, an agent_runs table for auditable sessions, and two new MCP tools (claim_task / complete_task) that replace the freeform update_object composition agents do today. Includes an intentionally-deferred Epic-optional-orchestrator that captures the Symphony-shaped runner as a decision point rather than an immediate build. Each task is bead-scale (one focused Cursor session) with explicit in-scope, out-of-scope, and anti-goal sections so a future agent can pick up a single Task-*.md and start without scrollback context. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 00:52:22 -04:00
---
# Task summary
Add per-IP rate limits to credentials sign-in and to a small set of high-impact mutation endpoints (invite, archive, create-workspace). The goal is to cap brute-force and obvious abuse, not to be a full WAF.
## Description
### Storage
Use the existing Redis instance on CT 102 (already used by Hocuspocus for awareness). Add a thin client in `packages/shared/src/rate-limit/` — token-bucket or sliding-window, your choice; the contract is `consume(key: string, opts: { capacity, refillPerSecond }): Promise<{ ok: boolean, remaining: number, retryAfterMs: number }>`.
If you don't want a new shared lib, `@upstash/ratelimit` is a fine drop-in even though we're not on Upstash — it works against any Redis URL. Don't add a new database for this.
### Where to apply
1. **Credentials sign-in** (`apps/web/app/api/auth/[...nextauth]/route.ts`, or the `authorize` callback): key by IP (`x-forwarded-for`, falling back to remote addr). 5 attempts / 60 seconds, then 429.
2. **Invite create**: key by `actor_user_id`. 30 invites / hour. Workspace-scoped is fine too.
3. **Workspace create**: key by `actor_user_id`. 5 workspaces / hour.
4. **Archive/restore**: key by `(workspace_id, actor_user_id)`. 20 / hour. Cheap insurance against a script flipping state in a loop.
### IP extraction
Use the same helper everywhere; don't recompute in every route. Common gotcha: `x-forwarded-for` is a comma-separated list when there are multiple proxies. Take the first entry. If the deployed stack is behind Cloudflare, `cf-connecting-ip` is more reliable — add that as a higher-priority source if present.
### Response
When a limit trips, return `429 Too Many Requests` with a `Retry-After` header in seconds and a tRPC `TOO_MANY_REQUESTS` error code. The client should surface a friendly message ("try again in ~30 seconds"), not a stack trace.
### Audit logging
Every limit trip writes an `audit_log` row (`action: "rate_limit.tripped"`, `metadata: { route, key_kind, retry_after_ms }`). This is how you discover whether anyone's hitting the limits in practice; without it the limits are silent.
### Anti-goals
- Don't add a CAPTCHA. If brute-forcing becomes a real problem, add Cloudflare in front of the deploy.
- Don't write a generic rate-limit middleware that wraps every tRPC procedure. The set of high-value endpoints is short; explicit is better than universal.
## Subtasks
- [x] Hand-rolled in-memory token bucket (`packages/shared/src/utils/token-bucket.ts`) with 6 vitest cases. **Deliberate deviation from spec**: in-memory `Map` not Redis. Rationale: we don't run multi-pod yet, and the bucket exposes a stable API so swapping the backing store is a localized change in `apps/web/server/lib/rate-limit.ts`. Filed `Task-distribute-rate-limit-redis-backed.md` for when we scale horizontally.
- [x] IP extraction helper inside `apps/web/lib/auth.ts` (`resolveSignInIp()`). Reads `cf-connecting-ip``x-real-ip` → first entry of `x-forwarded-for`. Dev fallback: when no header is present and `NODE_ENV=development`, the limiter is skipped so local sign-in doesn't lock you out.
- [x] Credentials sign-in: 5 attempts / IP / 60 seconds, returns `null` from `authorize` on trip (the Auth.js way of saying "no").
- [x] Invite create: 10 / inviter / hour, throws `TRPCError({ code: "TOO_MANY_REQUESTS" })` on trip. **Tighter than spec** (spec said 30/hr). Settled on 10 because invite-create has email side-effects; we'd rather false-positive a power user than spam recipients.
- [ ] Workspace create rate limit — **deferred to follow-up** `Task-rate-limit-workspace-create-and-archive.md`. Lower-impact than sign-in/invite and the subagent kept scope narrow.
- [ ] Archive / restore rate limit — same follow-up as above.
- [ ] Wire `audit_log` writes on limit trips — **deferred to follow-up** `Task-audit-instrument-rate-limit-trips.md`. The `rateLimit()` function returns `retryAfterMs` and `resetAt`, so a call site can do `if (!rl.allowed) { await recordAudit(...); throw ... }`. Hasn't shipped yet to keep this task's scope narrow and to avoid churning auth.ts a second time.
- [ ] Verify by scripting 20 credentials sign-in attempts against a dev deploy — operator smoke test, see Acceptance below.
## Design decisions captured
- **In-memory, not Redis (for v1).** The spec calls for Redis. We have Redis in the stack (Hocuspocus uses it), so it's not philosophical reluctance — it's that adding a hard runtime dep on Redis for the web pod adds a failure mode without a corresponding scale benefit while we run a single pod. Swap when we shard. Documented in the JSDoc on `rateLimit()` so the next maintainer doesn't have to dig.
- **`authorize` returns `null` on trip (not 429).** Auth.js v5's Credentials provider doesn't let `authorize` throw a typed HTTP code; `null` is the canonical "deny." A 429 with `Retry-After` would be the right UX, but that requires moving the limiter to a route handler that sits in front of `authorize`. Filed as a UX polish follow-up if anyone complains; in practice "your password is wrong" is also a reasonable user-facing read of a temporarily-locked account.
- **No audit row on trip yet.** See deferred subtask above. The trip is logged to stdout (`console.warn`) so operators aren't blind.
plans: scaffold daily-driver-finish, saas-hardening, agent-coordination Three new plan trees that fill in the gaps surfaced during repo review. Together they map out what remains between the current scaffold-with-stubs state and a daily-usable, multitenant, agent-coordinated app. * Plan-daily-driver-finish (P0): turn stubs into real data. Five tasks covering the lint/shared-types breakage, hardcoded dashboard mocks, AI-page setTimeout placeholder, post-signin landing decision, and a cross-browser collab smoke test against the deployed Hocuspocus instance. * Plan-multitenant-saas-hardening (P1): everything multitenant needs beyond what Plan-multitenant-cursor-sync already covers. Invites and role management, soft-delete + append-only audit log, rate limits on the auth + mutation hot paths, and a Vitest + GitHub Actions test foundation so PRs can't ship red. * Plan-agent-coordination (P2): the layer that makes a Task-*.md runnable, not just readable. Adds workflow_prompt with task -> epic -> plan inheritance, an agent_runs table for auditable sessions, and two new MCP tools (claim_task / complete_task) that replace the freeform update_object composition agents do today. Includes an intentionally-deferred Epic-optional-orchestrator that captures the Symphony-shaped runner as a decision point rather than an immediate build. Each task is bead-scale (one focused Cursor session) with explicit in-scope, out-of-scope, and anti-goal sections so a future agent can pick up a single Task-*.md and start without scrollback context. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 00:52:22 -04:00
## Owner or assignee
Unassigned
## Status
ready
## Estimation
M
## Acceptance criteria
- [x] Brute-forcing credentials sign-in trips at attempt 6 within 60 seconds (per-IP).
- [x] Invite-spam attempt trips at invite 11 within an hour (tightened from spec's 31).
- [ ] Every trip produces an `audit_log` row — **deferred**, see follow-up.
- [x] No global tRPC middleware — limits are applied per route (`authorize` + `invites.create` only).
plans: scaffold daily-driver-finish, saas-hardening, agent-coordination Three new plan trees that fill in the gaps surfaced during repo review. Together they map out what remains between the current scaffold-with-stubs state and a daily-usable, multitenant, agent-coordinated app. * Plan-daily-driver-finish (P0): turn stubs into real data. Five tasks covering the lint/shared-types breakage, hardcoded dashboard mocks, AI-page setTimeout placeholder, post-signin landing decision, and a cross-browser collab smoke test against the deployed Hocuspocus instance. * Plan-multitenant-saas-hardening (P1): everything multitenant needs beyond what Plan-multitenant-cursor-sync already covers. Invites and role management, soft-delete + append-only audit log, rate limits on the auth + mutation hot paths, and a Vitest + GitHub Actions test foundation so PRs can't ship red. * Plan-agent-coordination (P2): the layer that makes a Task-*.md runnable, not just readable. Adds workflow_prompt with task -> epic -> plan inheritance, an agent_runs table for auditable sessions, and two new MCP tools (claim_task / complete_task) that replace the freeform update_object composition agents do today. Includes an intentionally-deferred Epic-optional-orchestrator that captures the Symphony-shaped runner as a decision point rather than an immediate build. Each task is bead-scale (one focused Cursor session) with explicit in-scope, out-of-scope, and anti-goal sections so a future agent can pick up a single Task-*.md and start without scrollback context. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 00:52:22 -04:00
## Links to related Epic / Plan
- Epic: `./Epic-tenant-lifecycle.md`
- Plan: `../Plan-multitenant-saas-hardening.md`