Skip to content

fix(proxy): atomic Durable Object rate limiting#151

Open
zhongnansu wants to merge 1 commit into
mainfrom
fix/proxy-atomic-rate-limit
Open

fix(proxy): atomic Durable Object rate limiting#151
zhongnansu wants to merge 1 commit into
mainfrom
fix/proxy-atomic-rate-limit

Conversation

@zhongnansu

Copy link
Copy Markdown
Member

Problem

The proxy's rate limiting used Cloudflare KV, which is eventually consistent. Concurrent requests all read the same low count and passed the cap before any increment landed — so perMinute/perDay/globalPerDay were not actually enforced (cost / denial-of-wallet risk).

Fix

  • Replace KV counters with a Cloudflare Durable Object that does check-and-increment inside blockConcurrencyWhile (serialized, strongly consistent).
  • Per-IP DO holds minute/day/burst counters.
  • The service-wide global cap routes through a single dedicated __global__ DO instance via one atomic /global/check-increment endpoint — decision + mutation in one serialized gate, so there's no cross-DO TOCTOU. Fail-closed on missing binding / fetch error / malformed response.
  • Per-IP counters are incremented only after the global check passes (a global denial never inflates per-IP counts).
  • Lower globalPerDay 5000 → 500 as a cost backstop.
  • Prune previous-window bucket keys so DO storage stays O(1) per IP.

Note: the real cost backstop is an upstream OpenAI account hard spend cap (an account dashboard setting, outside this repo).

Testing

  • 86 proxy tests pass, including a new concurrent distinct-IP test that seeds the global counter one slot from the cap, fires 4 concurrent requests from 2 IPs sharing the __global__ DO, and asserts exactly one is allowed (would fail against the old split check/increment design).
  • node --check clean on all changed sources.

Deploy note

Not deployed. wrangler.toml adds the RateLimiter DO binding + new_sqlite_classes migration; deploying will require wrangler deploy.

🤖 Generated with Claude Code

Replace eventually-consistent KV counters (read-then-write race let
concurrent requests bypass the perMinute/perDay caps) with a Cloudflare
Durable Object that does check-and-increment inside blockConcurrencyWhile.

- Per-IP DO holds minute/day/burst counters
- Service-wide global cap routed through a single dedicated `__global__`
  DO instance via one atomic /global/check-increment endpoint (no cross-DO
  TOCTOU); fail-closed on binding/fetch/parse errors
- Lower globalPerDay 5000 -> 500 as a cost backstop (real backstop is an
  upstream OpenAI account spend cap)
- Prune previous-window bucket keys to bound DO storage growth

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📦 PR Preview — Ask AI Extension

Version: 1.2.1
Zip size: 342.6 KB

⬇️ Download artifact


Permissions

No permission changes detected. ✅


Preview Checklist

Before merging, manually test with the artifact above:

  • Load unpacked extension in chrome://extensions (developer mode)
  • Select text on any webpage and verify the trigger button appears
  • Click the trigger button and confirm the popup renders correctly
  • Right-click selected text and verify context menu items are present
  • Open a CSP-restricted page (e.g. chrome://extensions) and verify fallback works
  • Check the DevTools console for errors

Updated by PR Preview Bot — workflow run

@github-actions

Copy link
Copy Markdown
Contributor

📊 Code Coverage Report

File Coverage Status
proxy/src/index.js 98.2% 🟢
proxy/src/openai.js 100.0% 🟢
proxy/src/rate-limit-do.js 92.7% 🟢
proxy/src/rate-limit.js 100.0% 🟢
proxy/src/validate.js 95.3% 🟢
src/options.js 97.6% 🟢
src/popup.js 100.0% 🟢
src/background/index.js 83.2% 🟢
src/content/api.js 88.2% 🟢
src/content/detection.js 99.3% 🟢
src/content/history.js 86.1% 🟢
src/content/image-capture.js 91.6% 🟢
src/content/presets.js 100.0% 🟢
src/content/prompt.js 97.4% 🟢
src/content/autosuggest/context.js 100.0% 🟢
src/content/autosuggest/debounce.js 100.0% 🟢
src/content/autosuggest/ghost-text.js 100.0% 🟢
src/content/autosuggest/index.js 75.9% 🔴
src/content/autosuggest/styles.js 100.0% 🟢
src/content/bubble/core.js 75.5% 🔴
src/content/bubble/history.js 85.3% 🟢
src/content/bubble/markdown.js 96.0% 🟢
src/content/bubble/stream.js 97.0% 🟢
src/content/bubble/styles.js 100.0% 🟢
src/content/shared/constants.js 100.0% 🟢
src/content/shared/dom-utils.js 100.0% 🟢
src/content/shared/preset-usage.js 91.8% 🟢
src/content/shared/state.js 100.0% 🟢
src/content/trigger/button.js 92.6% 🟢
src/content/trigger/progress-ring.js 100.0% 🟢
src/content/trigger/screenshot.js 98.6% 🟢
src/content/trigger/selection.js 72.8% 🔴
src/content/trigger/styles.js 100.0% 🟢
src/shared/autosuggest-limits.js 100.0% 🟢
src/shared/brand.js 100.0% 🟢
Overall 93.1% 🟢 PASS

Threshold: 80%

✅ Coverage meets the minimum threshold.


Updated by Code Coverage workflow

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