Split out of CON-44, where the code review raised it as the one major finding and it was deliberately not fixed. CON-44 defined the policy, proved it on a real production build, and documented the gap honestly; this ticket closes it.
State after CON-44
src/csp.ts holds the single definition of CONTENT_SECURITY_POLICY, guarded by src/csp.test.ts. vite.config.ts attaches it through preview.headers, so npm run build && npm run preview serves the production artefact under the real header — verified with curl -sI.
That is the entire delivery mechanism. vite preview is a local static server. The repository contains no app Dockerfile, no nginx or Caddy config, no _headers, netlify.toml or vercel.json. docs/09-security-privacy.md says so in as many words and warns the reader not to read the section as "the app has a CSP".
So the ticket that was titled "the app has no second line of defence behind the Markdown sanitiser" has, so far, given the shipped app no second line of defence. The policy is real, tested and pinned — it is simply never sent to a browser outside a developer's machine.
Why this was split rather than fixed
Every concrete delivery mechanism picks a hosting family. public/_headers is Netlify and Cloudflare Pages; it is an inert text file on nginx, Caddy, S3 + CloudFront or a container. Adding one would produce something that looks like delivery while doing nothing almost everywhere — worse than a gap that is written down. The project has not chosen a host yet (see CON-4), so the choice belongs to whoever does.
Generating the file from CONTENT_SECURITY_POLICY rather than retyping it solves duplication, not this. Keep that in mind when the host is known: one definition, generated artefacts.
Task
- Pick the host, or confirm the one CON-4 picks.
- Wire the header there, generated from
src/csp.ts so the policy still has exactly one definition — a small postbuild step writing the host's own format is preferable to a second copy a human keeps in sync.
- Verify against the deployed origin, not the preview: the response header is present on the document, and the browser console is free of CSP violations while rendering a page, highlighting a code block, loading an image from a foreign host, connecting to the relay with NIP-42 AUTH, and uploading through Blossom.
docs/09-security-privacy.md carries the by-hand recipe for the last two, including the VITE_RELAY_URL=wss://… note — the local ws:// relay is blocked by the policy on purpose, so a default checkout cannot exercise them.
- Rewrite the closing risk paragraph in
docs/09-security-privacy.md once it is no longer true. Do not leave a doc claiming a gap that has been closed — that is the same failure as claiming protection that does not exist, pointing the other way.
Acceptance
- The header is delivered by the production host and shown to be, with evidence (response headers from the deployed origin).
- The policy has one definition; nothing is retyped.
- The five paths above are exercised under the live policy, with the result recorded rather than inferred.
docs/09-security-privacy.md matches reality again.
npm run typecheck && npm run lint && npm test && npm run build pass.
Depends on
CON-4 (choosing the host). If that is still open when this is picked up, this ticket is blocked on it rather than a reason to decide it in passing.
Affected: deployment configuration (to be determined), possibly a postbuild script, docs/09-security-privacy.md
Source: CON-44 code review, finding 1 (major), deferred 2026-09-14.
Task: xyj6z5hn7e6jstu59f6dwkzu
Split out of CON-44, where the code review raised it as the one major finding and it was deliberately not fixed. CON-44 defined the policy, proved it on a real production build, and documented the gap honestly; this ticket closes it.
State after CON-44
src/csp.tsholds the single definition ofCONTENT_SECURITY_POLICY, guarded bysrc/csp.test.ts.vite.config.tsattaches it throughpreview.headers, sonpm run build && npm run previewserves the production artefact under the real header — verified withcurl -sI.That is the entire delivery mechanism.
vite previewis a local static server. The repository contains no appDockerfile, no nginx or Caddy config, no_headers,netlify.tomlorvercel.json.docs/09-security-privacy.mdsays so in as many words and warns the reader not to read the section as "the app has a CSP".So the ticket that was titled "the app has no second line of defence behind the Markdown sanitiser" has, so far, given the shipped app no second line of defence. The policy is real, tested and pinned — it is simply never sent to a browser outside a developer's machine.
Why this was split rather than fixed
Every concrete delivery mechanism picks a hosting family.
public/_headersis Netlify and Cloudflare Pages; it is an inert text file on nginx, Caddy, S3 + CloudFront or a container. Adding one would produce something that looks like delivery while doing nothing almost everywhere — worse than a gap that is written down. The project has not chosen a host yet (see CON-4), so the choice belongs to whoever does.Generating the file from
CONTENT_SECURITY_POLICYrather than retyping it solves duplication, not this. Keep that in mind when the host is known: one definition, generated artefacts.Task
src/csp.tsso the policy still has exactly one definition — a small postbuild step writing the host's own format is preferable to a second copy a human keeps in sync.docs/09-security-privacy.mdcarries the by-hand recipe for the last two, including theVITE_RELAY_URL=wss://…note — the localws://relay is blocked by the policy on purpose, so a default checkout cannot exercise them.docs/09-security-privacy.mdonce it is no longer true. Do not leave a doc claiming a gap that has been closed — that is the same failure as claiming protection that does not exist, pointing the other way.Acceptance
docs/09-security-privacy.mdmatches reality again.npm run typecheck && npm run lint && npm test && npm run buildpass.Depends on
CON-4 (choosing the host). If that is still open when this is picked up, this ticket is blocked on it rather than a reason to decide it in passing.
Affected: deployment configuration (to be determined), possibly a postbuild script,
docs/09-security-privacy.mdSource: CON-44 code review, finding 1 (major), deferred 2026-09-14.
Task: xyj6z5hn7e6jstu59f6dwkzu