/**
* Shared security headers (Helmet + Content-Security-Policy) for Oxy backends.
*
* WHY THIS EXISTS
* ---------------
* A CSP only governs an origin that serves DOCUMENTS; on a JSON API it governs
* no browsing context. The Oxy origins that serve HTML through Cloudflare have
* so far either hand-written their own policy or shipped none at all, and two
* bugs follow from that:
*
* 1. THE CLOUDFLARE INSIGHTS BEACON IS BLOCKED BY A HAND-WRITTEN POLICY.
* Cloudflare injects ``,
* which is what tells the client entry to call `hydrateRoot` instead of
* `createRoot().render()`. Nothing in app code puts it there, so — like the
* Cloudflare beacon above — an app cannot allowlist it from the app side.
* Measured on `accounts.oxy.so` 2026-08-21: blocked, so every visit threw
* away the server-rendered markup and re-rendered from scratch, with only a
* console error to show for it. The hashes are therefore DERIVED from the
* built output rather than hand-written (see {@link extractInlineScripts}):
* a hash pasted into config is correct exactly until the build changes one
* byte, and then it fails the same silent way.
*
* WHAT IT PROVIDES
* ----------------
* `createOxySecurityHeaders(options)` returns the Helmet middleware with the
* Oxy-wide CSP baseline applied, plus per-app extensions merged (and deduped)
* into it. Everything Helmet does that is NOT the CSP (HSTS, frameguard,
* referrer policy, CORP/COOP, …) is passed straight through, so an app keeps
* full control of those.
*
* `buildOxyCspDirectives(extensions)` is the same resolution as a pure
* function, for the Oxy document origins that are NOT Express — a Next.js
* `headers()`, a Cloudflare Pages `_headers` generator — so one policy can
* cover them without a second implementation.
*
* SCOPE: mount this on backends that serve HTML. A JSON-only API gains nothing
* from a source-list CSP; harden those with the non-CSP headers instead
* (`hsts`, `noSniff`, `frameguard`, CORP) rather than adding directives that
* apply to no document.
*
* Node/Express-only: exported solely from `@oxy.so/core/server`.
*/
import { createHash } from 'node:crypto';
import type { RequestHandler } from 'express';
import helmet, { type HelmetOptions } from 'helmet';
/** CSP keyword for "this origin". Always present in every open baseline directive. */
const SELF = "'self'";
/** CSP keyword for a fully closed directive. Meaningless alongside any other source. */
const NONE = "'none'";
/**
* Cloudflare Web Analytics. Injected at the edge into proxied HTML — no Oxy app
* loads it, and no Oxy app should have to know these hostnames. Both are
* required: the script host, and the host the beacon reports to.
*/
const CLOUDFLARE_INSIGHTS_SCRIPT_ORIGIN = 'https://static.cloudflareinsights.com';
const CLOUDFLARE_INSIGHTS_REPORT_ORIGIN = 'https://cloudflareinsights.com';
/**
* Oxy platform origins. Every Oxy web origin runs the SDK, which calls the Oxy
* API over HTTPS and Socket.IO, and resolves all canonical media through the
* Oxy CDN (`getFileDownloadUrl` → `cloud.oxy.so`).
*/
const OXY_API_ORIGIN = 'https://api.oxy.so';
const OXY_API_WEBSOCKET_ORIGIN = 'wss://api.oxy.so';
const OXY_CDN_ORIGIN = 'https://cloud.oxy.so';
/**
* Where a browser sends an upload's bytes: the Oxy Files R2 bucket (OxyHQ/oxy
* ADR 0047). `assets.upload` over 64 MiB and `assets.uploadLarge` PUT every
* part to a presigned URL on this host, never through the API, so a page whose
* `connect-src` lacks it fails every large upload before a byte is sent. The
* API signs virtual-hosted URLs — the bucket is the first label of the host,
* `oxy-files..r2.cloudflarestorage.com` — and R2 serves the S3 API
* only on the account's hosts (a custom domain cannot carry it). The Oxy API's
* tests sign a real part URL with production's endpoint and bucket and require
* its origin to equal this.
*/
export const OXY_FILES_UPLOAD_ORIGIN =
'https://oxy-files.fa4392797e57d4e63ceaaf7b1dc687ed.r2.cloudflarestorage.com';
/** The CSP directives an Oxy app may extend, in Helmet's camelCase spelling. */
export type OxyCspDirective =
| 'baseUri'
| 'connectSrc'
| 'defaultSrc'
| 'fontSrc'
| 'formAction'
| 'frameAncestors'
| 'frameSrc'
| 'imgSrc'
| 'manifestSrc'
| 'mediaSrc'
| 'objectSrc'
| 'scriptSrc'
| 'scriptSrcAttr'
| 'scriptSrcElem'
| 'styleSrc'
| 'styleSrcElem'
| 'workerSrc';
/**
* Per-app ADDITIONS to the Oxy baseline, keyed by directive. Values are merged
* into the baseline and deduped — they never replace it, so `'self'` (and the
* Cloudflare beacon hosts) cannot be lost. Extending a directive the baseline
* does not define seeds it with `'self'` first, for the same reason.
*/
export type OxyCspExtensions = Partial>;
/**
* The Oxy-wide CSP baseline. Deliberately the floor every Oxy origin needs, not
* a superset of what any one app allows — permissive sources an individual app
* wants (`https:` images, `blob:` media, embed hosts, LiveKit) are that app's
* extension, so each widening stays visible at its call site.
*
* `style-src` carries `'unsafe-inline'` because react-native-web injects its
* stylesheet as inline `