Skip to content

security: enforce AI daily limit server-side via atomic RPC#14

Open
jaxhemopo wants to merge 1 commit into
mainfrom
security/ai-rate-limit
Open

security: enforce AI daily limit server-side via atomic RPC#14
jaxhemopo wants to merge 1 commit into
mainfrom
security/ai-rate-limit

Conversation

@jaxhemopo
Copy link
Copy Markdown
Owner

The 35/day AI cap (UsageService.AI_LIMIT) was enforced only client-side, so a JWT holder could call functions.invoke('openai-chat') in a loop and run up the OpenAI bill (CODE_REVIEW.md #4). The client daily-reset also had a read-then-write race (#17).

  • New increment_ai_call(p_limit) RPC (migration): SECURITY DEFINER, atomic upsert + daily rollover, keyed on auth.uid() so a caller can only ever touch their own counter (no spoofing a p_user_id). Returns { allowed, count_today }. The atomic upsert also fixes the #17 reset race.
  • openai-chat calls it after validation and returns 429 when the cap is hit.
  • UsageService.recordAICall is now a no-op (server is authoritative) to avoid double-counting; the canMakeAICall pre-check stays as advisory UX. (One method changed rather than 5 call sites across 3 screens.)

⚠️ Deploy order matters

Apply the migration + deploy openai-chat first, then ship the client. Edge-first keeps counting conservative during rollout (old clients briefly double-count) rather than risking an uncounted window (new client shipped before the server enforces).

Note

openai-chat is also touched by PR #12 (model allowlist) — different region, merges cleanly. openai-vision/openai-test aren't client-called so they're not rate-limited here; add the same RPC call if they're ever exposed.

🤖 Generated with Claude Code

… RPC

The 35/day AI limit (UsageService.AI_LIMIT) was enforced only client-side, so a
user with a valid JWT could call functions.invoke('openai-chat') in a loop and
run up the OpenAI bill (CODE_REVIEW.md #4). The client daily-reset also had a
read-then-write race (#17).

- New increment_ai_call(p_limit) RPC (migration): SECURITY DEFINER, atomic
  upsert + daily rollover, keyed on auth.uid() so a caller can only touch their
  own counter. Returns { allowed, count_today }.
- openai-chat calls it after validation and returns 429 when the cap is hit.
- UsageService.recordAICall is now a no-op (server is authoritative) to avoid
  double-counting; the client pre-check canMakeAICall stays as advisory UX.

DEPLOY ORDER (important): apply the migration and deploy openai-chat FIRST, then
ship the client. Edge-first means counting stays conservative during rollout
(old clients briefly double-count) rather than risking an uncounted window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant