# CSP note (#15 reconciled): the WASM app boot is served as an EXTERNAL same-origin module
# (/pahmoi-boot.js), so `script-src 'self' 'wasm-unsafe-eval'` permits it with NO 'unsafe-inline'
# /nonce/hash. The pre-paint theme bootstrap (/boot.js) is likewise external. Keep it that way:
# never inline app scripts, and never add 'unsafe-inline' to script-src. `style-src` no longer
# carries 'unsafe-inline' either (#17): the site_generator replaces the style-hash token below with
# a `'sha256-…'` source for each distinct inline <style> block, computed from the emitted HTML.
# (Naming the token here would spell it, and the fill replaces every occurrence in the file, comment
# included, which is why this sentence describes it instead.)
# The base connect-src retains the three original DoH resolvers (#704); the Dig-only
# rule below adds the browser-verified unfiltered profile (#812).
#
# `connect-src 'self'` blocked every one of them, so **Dig over DoH answered nothing on the live
# site**: the browser refused the fetch before it left, and the tool showed no status at all. It was
# invisible to the suite because the e2e web server sent no policy until #703.
#
# Three exact hosts, not a scheme. `https:` would let any script on any page of this site post
# anywhere, which is the whole thing `connect-src` is for; an allowlist of the endpoints one tool
# actually calls costs nothing and gives away nothing. The Dig-specific rule is checked against
# `tools_core::doh::request_url` by `dig_resolver_hosts_match_the_csp` so new profiles
# cannot be added to the menu without permission to reach their exact hosts.
#
# Dig's MANUAL ENDPOINT field (#317) cannot be covered by an allowlist and is not: a visitor's own
# DoH URL is refused by this policy, the tool says so where the field is, and `dig-cli` has no such
# limit. That is the honest boundary rather than a policy wide enough to make the feature work.

/*
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: geolocation=(), camera=(), microphone=(), interest-cohort=()
  Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'sha256-DsClwam3qttk9uvEyIQKbHjm1erpg4paxPCTI0boJLI=' 'sha256-yx98qgnCcq9fJHKqdgf7y/igJZSsBSAwDij4oKYk3VQ='; img-src 'self' data:; connect-src 'self' https://cloudflare-dns.com https://dns.google https://doh.sb; object-src 'none'; frame-ancestors 'none'; base-uri 'self'

# Browser-verified DNS4EU unfiltered resolver (#812), scoped to Dig.
/tools/dig/*
  ! Content-Security-Policy
  Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'sha256-DsClwam3qttk9uvEyIQKbHjm1erpg4paxPCTI0boJLI=' 'sha256-yx98qgnCcq9fJHKqdgf7y/igJZSsBSAwDij4oKYk3VQ='; img-src 'self' data:; connect-src 'self' https://cloudflare-dns.com https://dns.google https://doh.sb https://unfiltered.joindns4.eu; object-src 'none'; frame-ancestors 'none'; base-uri 'self'

# Project SPA artifacts (#631): each static/spa/<name>/ ships verbatim to /spa/<name>/, and the
# site_generator fills the two tokens below with a `'sha256-…'` source per inline <script> and per
# inline <style> in that area, computed from the emitted files. That is what lets a self-contained
# single-file artifact run with NO 'unsafe-inline' anywhere. The rule above is untouched: an
# artifact's hash is a source for its own area and nowhere else.
#
# The detach line is load-bearing. Cloudflare Pages applies EVERY matching rule and joins a header
# set twice with a comma, which a browser reads as two policies enforced together (the intersection
# wins) - so without `! Content-Security-Policy` the inherited hash-less script-src above would
# still block these scripts. Verify at deploy: `curl -sI https://pah.moi/spa/<name>/`.
#
# No 'wasm-unsafe-eval' here, deliberately: that source exists for the site's own WASM engine, which
# an artifact never loads. No 'unsafe-eval' and no 'unsafe-hashes' either - see
# docs/reference/publishing-blog-content.md for what the build-time guard rejects and why. A
# per-artifact relaxation is a recorded owner decision (spec section 10), never a quiet edit here.
#
# Those decisions live in `content/spa-policies.toml` (#768), one table per artifact, and the
# generator APPENDS their rules to the end of this file: `/spa/<path>/*`, each detaching the policy
# above before setting its own. They are last on purpose, because Pages applies every matching rule
# and the last set value is the one that stands once the inherited one is detached. Two rows ship
# today - `vibecode` and the `spa-day` benchmark area - and each artifact is checked against ITS
# rule at build time, so a hash written into the wrong rule fails the build rather than the page.
/spa/*
  ! Content-Security-Policy
  Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-0HKamL0sOMyV5CaW4jGbJREHBBe57QfLBMFN2jpMtdg=' 'sha256-Be3vVhgZw7TFKb1dZgIGfYkvzwqlfQ+GOP9ukz2G810=' 'sha256-HR6DoSUVuHE9rj5E6euaqVa5tgQcFx1acX4oDYs4S4E=' 'sha256-JkFc33/R+UIeZvM6sPEudKH01LV0OE1/5FDo5+Qv36M=' 'sha256-ljUwGs8sBa69+XHdSMsNgJdrtIk2eBIPBxYl9lb55E8=' 'sha256-ptjC9fzuev7im7hLx1jfsqPX3Qt6Fxl/yUXP9FlGRto=' 'sha256-rum/t2BbdWGHQkKOBzDdbitlN55NbitqlGtM102MWyg='; style-src 'self' 'sha256-B7ld07hK9l/lIEw1b1lAHAC8FStSoAHYJy54fuZB0QQ=' 'sha256-DMoF39/ehO5NdPNVy1+hZsMU1nVxrQSpBT05v5oyQMY=' 'sha256-F53y16bOgQGLOvXO0Cj9vtywh/ETmKOlyV6k8DU47lM=' 'sha256-jTeu/lKz0gLLp+CVhiDsasIi0o46CO/BjQHT5EDUUy4=' 'sha256-nne2QhqhTef/udXuYpssfKAIhDWWC/dbdmWqCPwJVd0=' 'sha256-wOhQKIdwq8XWxKjeXmSRhVByMnOv7SIl6YwyCIUpxfM='; img-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'

# Engine cache lifetime (#759). Measured live on 2026-09-06 (#753): Cloudflare Pages was serving
# `/pahmoi_bg.wasm` with its default `max-age=0, must-revalidate` and `cf-cache-status: DYNAMIC` on
# every cold run, so the 3.3 MB engine was fetched from the origin and recompressed per visitor,
# while `/pahmoi.js` got four hours. This file said nothing about either. It does now.
#
# Why four hours and not a year: `Trunk.toml` sets `filehash = false`, so these names are STABLE
# across deploys - `/pahmoi.js` and `/pahmoi_bg.wasm` are the same two URLs after every republish.
# `max-age=31536000, immutable` on an unhashed name would leave a returning visitor running last
# month's engine with no way to notice, which is worse than the round trip it saves. A content hash
# is what earns a year, and the build does not emit one. `must-revalidate` keeps the revalidation
# honest once the four hours are up; the 304 costs a round trip, not 3.3 MB.
#
# `Vary: Accept-Encoding` because the edge holds a Brotli and a gzip variant of the same URL.
#
# `site_generator::cache_rules` checks these rules against the filenames the build actually emitted
# and fails the build if a name here stops matching, or if a hashed name is given a short lifetime,
# or an unhashed one a long one. Turning `filehash` on is therefore a build failure until this
# block is rewritten as `max-age=31536000, immutable`, which is the point.
/pahmoi_bg.wasm
  Cache-Control: public, max-age=14400, must-revalidate
  Vary: Accept-Encoding

/pahmoi.js
  Cache-Control: public, max-age=14400, must-revalidate
  Vary: Accept-Encoding

# Precompressed engine (#772). This rule and the `pahmoi_bg.wasm.br` beside it are written by the build only when
# PAHMOI_PRECOMPRESS_WASM is set; with the flag off neither exists and this block is absent. The
# loader in /pahmoi-boot.js asks for this URL and falls back to /pahmoi_bg.wasm when the response
# does not carry the encoding, so a server that sends no `Content-Encoding` (every
# `make serve`, the whole e2e rig) still boots. See the #753 section of
# docs/reference/operations-runbook.md.
#
# `no-transform` is correct HERE and only here: the bytes stored at this route are already
# Brotli at quality 11, so there is nothing left for the edge to do but
# re-encode a compressed body. The same directive on /pahmoi_bg.wasm is REFUSED by
# site_generator::cache_rules, because the bytes there are the plain build output and
# `no-transform` would ship all of them uncompressed (#760).
#
# `Vary: Accept-Encoding` because this URL answers `br` to everybody, including a client
# that asked for `identity`; a shared cache in front of one has to know that.
/pahmoi_bg.wasm.br
  Content-Encoding: br
  Content-Type: application/wasm
  Cache-Control: public, max-age=14400, must-revalidate, no-transform
  Vary: Accept-Encoding

/spa/spa-day/*
  ! Content-Security-Policy
  Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-g+nyV99b/Ogeacwid3PmoharD9WZT5SzsDKe/BanMdU='; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; img-src 'self' data: blob:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'

/spa/vibecode/*
  ! Content-Security-Policy
  Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'sha256-HXqYccGEDjA/0XyLOVT/f4DOki+vnxsACtbjCtBAW8I=' 'sha256-JlyunPdpp9wsGsJTeE/OgP71Bi25oarFvn+l8gWvXoY=' 'sha256-MNA4RqVuHcHEXymIxy1Gh42hdVdmX9WZQMaSPH7Qj6s='; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; img-src 'self' data: blob:; media-src 'self' data: blob:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

/spa/plusminus/*
  ! Content-Security-Policy
  Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-EJ6EFthfhX3Q/7xvRjIiREsqR8bb7lmTlt70EdTGydg='; style-src 'self' 'sha256-KUcFhYJXVIV4dxME+3RS2rQg8Tk7hHX0fh4ul5vxag0='; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'

/spa/upward/*
  ! Content-Security-Policy
  Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-4Bn2rqePrloxmCA84H8NDtV8DNrCce8Z69MPKgYQg/s=' 'sha256-Z/fs63hKLyhsHkrcyRyNHBfjqnrRkvfrtt4c5WoXhA4=' 'sha256-duAq39l/lOoTxyBb4GAb9ejKweaYbhbdOFH9rB6Sq1M='; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

/spa/paint-1995/*
  ! Content-Security-Policy
  Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-EZBNQ+GsbVKzn9zuWCckbkiC5ZRZtj7o/KB6YwqNp+4='; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; img-src 'self' data: blob:; connect-src 'none'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

/spa/avoid/*
  ! Content-Security-Policy
  Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'sha256-HwMzHiw7thdwuuiEE40jBVXrnwjn0A+mEPuxWWFyJ3k='; media-src 'self' data:; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
