#!/usr/bin/env python3.14# -*- coding: utf-8 -*-"""Content-Security-Policy middleware with a per-response nonce (shared).Every page an app service renders carries the locked baseline CSP(docs/web-server-guide.md § Content-Security-Policy): same-origin everything,no `unsafe-inline`, no `unsafe-eval`, no framing. The middleware mints afresh nonce per response and exposes it as `request['csp_nonce']` so atemplate *can* mark an unavoidable inline `<script>`/`<style>` — the authpages don't need it (all their JS/CSS is served from `/assets`), but thecontract is established here once and every rendering service imports the samemiddleware.Caddy adds the transport-level headers (HSTS, `X-Content-Type-Options`, …)and a fallback CSP for purely static responses; this header wins on renderedpages because the app sets it per response."""from__future__importannotations__all__=['csp_middleware','NONCE_KEY']importsecretsfromtypingimportAny,Awaitable,Callablefromaiohttpimportweb#: Request key under which the per-response nonce is exposed to handlers/templates.NONCE_KEY:str='csp_nonce'#: `style-src` carries `'unsafe-inline'` for exactly one consumer: MathJax#: (and htmx's indicator rules) inject their stylesheets at runtime with no#: nonce hook — verified blocked under bare `'self'` (headless-Chrome CSP#: violations, garbled math). Scripts stay strict: `'self'` + nonce only.#: `frame-ancestors 'self'` (not `'none'`): the app shell frames its own#: `/terminal` document — cross-origin embedding stays blocked. See#: docs/web-server-guide.md § Content-Security-Policy for the recorded trade-offs.#: `'wasm-unsafe-eval'` lets a page compile WebAssembly — the Argon2id in the vendored#: hash-wasm that `srp.js` derives the password key with — and nothing else: JavaScript#: `eval` stays blocked. It is on every page, not the sign-in ones alone, because the sign-in#: form sits in the public site's right pane and the password change swaps into the signed-in#: shell, whose policy is the one its document loaded with._POLICY=("default-src 'self'; ""script-src 'self' 'nonce-{nonce}' 'wasm-unsafe-eval'; ""style-src 'self' 'unsafe-inline'; ""img-src 'self' data:; ""connect-src 'self'; ""frame-ancestors 'self'; ""base-uri 'none'; ""object-src 'none'")_Handler=Callable[[web.Request],Awaitable[web.StreamResponse]]@web.middlewareasyncdefcsp_middleware(request:web.Request,handler:_Handler)->web.StreamResponse:"""Mint `request['csp_nonce']` and stamp the baseline CSP on the response."""nonce=secrets.token_urlsafe(16)request[NONCE_KEY]=nonceresponse=awaithandler(request)response.headers.setdefault('Content-Security-Policy',_POLICY.format(nonce=nonce))response.headers.setdefault('X-Content-Type-Options','nosniff')response.headers.setdefault('Referrer-Policy','no-referrer')returnresponsedefnonce_of(request:web.Request)->str:"""The request's per-response nonce (for template contexts)."""value:Any=request.get(NONCE_KEY,'')returnstr(value)