/** * CSP builders for GameCore storefront tenants. * * One TenantCspConfig produces BOTH policies a storefront needs: * - buildLegacyCsp — host-allowlist + 'unsafe-inline'. This is the * always-on ENFORCED policy (next.config.ts headers()). It keeps * 'unsafe-inline' because Next injects inline hydration scripts on * every SSR/SSG page. * - buildStrictCsp — nonce + 'strict-dynamic' (+ sha256 hashes for the * tenant's own stable inline scripts). Emitted per-request by the edge * proxy: as Report-Only during observation, as enforced on the routes * in the tenant's STRICT_ENFORCE list once the tenant flips its flag. * * Building both from one config kills the "keep two hand-written policies * in sync" failure mode by construction. * * FOOTGUN (do not "fix"): inlineScriptHashes are added ONLY to the strict * policy. In CSP2+ browsers the presence of a hash/nonce in a directive * makes 'unsafe-inline' IGNORED — adding the hashes to the legacy policy * would silently block every OTHER inline script (Next hydration) on every * page. See docs/csp.md. * * Edge-runtime safe: string ops only; generateNonce uses btoa + * crypto.randomUUID (available in edge, browsers, Node 19+, Bun). */ export type CspDirective = "default-src" | "base-uri" | "form-action" | "frame-ancestors" | "object-src" | "script-src" | "style-src" | "img-src" | "font-src" | "connect-src" | "frame-src" | "worker-src" | "manifest-src" | "upgrade-insecure-requests" | "report-uri"; export interface TenantCspConfig { /** Origin of the GameCore API this storefront talks to (connect-src). */ gamecoreApiOrigin: string; /** Opora support widget origin; wss:// counterpart derived automatically. */ oporaOrigin?: string; analytics?: { yandexMetrika?: boolean; googleAnalytics?: boolean; cloudflareInsights?: boolean; }; /** Telegram Login widget (script + oauth iframe). */ telegramAuth?: boolean; /** * UNQUOTED 'sha256-…'/'sha384-…'/'sha512-…' source expressions — БЕЗ * кавычек — валидируются и квотируются здесь (buildStrictCsp throws on * an invalid or pre-quoted entry) — for the tenant's OWN stable inline * scripts (theme bootstrap). Strict policy only — see module docstring. */ inlineScriptHashes?: string[]; /** Rare tenant-specific additions, appended per directive (deduped). */ extra?: Partial>; } export interface LegacyCspOptions { /** Dev builds need 'unsafe-eval' for React Fast Refresh / HMR. */ dev?: boolean; } export interface StrictCspOptions { dev?: boolean; /** Violation report sink (e.g. "/api/csp-report"). Omitted → no report-uri. */ reportUri?: string; /** * Пометь true, когда политика уйдёт как ENFORCED Content-Security-Policy. * Next при совпадении имени заголовка ЗАМЕНЯЕТ legacy-политику из * next.config этой — на enforce-роутах strict остаётся единственной, и * обязана нести всё, что несла legacy (superset-тест закрепляет). * Добавляет 'upgrade-insecure-requests' (в report-only браузер молча * игнорирует action-директивы, поэтому там она опущена). */ enforced?: boolean; } /** Extract an origin from a URL-ish env value; fall back if unset/garbage. */ export declare function resolveOrigin(value: string | undefined, fallback: string): string; /** Per-request nonce: base64 of a UUID. Edge-safe (no node imports). */ export declare function generateNonce(): string; /** Host-allowlist policy with 'unsafe-inline' — the always-on enforced CSP. */ export declare function buildLegacyCsp(cfg: TenantCspConfig, opts?: LegacyCspOptions): string; /** Strict nonce + strict-dynamic policy (report-only or enforced by caller). */ export declare function buildStrictCsp(cfg: TenantCspConfig, nonce: string, opts?: StrictCspOptions): string;