"""Compile the gateway configuration and version the Site's assets. Inline scripts are hashed for the CSP; local scripts and stylesheets get a content hash in their address. The Site's behaviour now lives in separate files, so normally there is nothing to hash and the policy is simply script-src 'self' — no allowlist to get wrong. The hashing stays because an inline script added later must not silently need 'unsafe-inline'; it is hashed automatically instead. """ import base64 import hashlib import os from pathlib import Path import re root = Path('/build') # Every local script and stylesheet is addressed by its content, so a release # can never pair new HTML with an old cached file: the proxy in front of the # stack caches assets for hours, and a new index.html calling an old script # broke the Site (2026-09-28). The HTML itself is served no-cache. def versioned(match): attribute, path = match.group(1), match.group(2) digest = hashlib.sha256((root / 'web' / path.lstrip('/')).read_bytes()).hexdigest()[:12] return f'{attribute}="{path}?v={digest}"' for html in (root / 'web').glob('*.html'): text = re.sub(r'\b(src|href)="(/[\w./-]+\.(?:js|css))(?:\?[^"]*)?"', versioned, html.read_text()) html.write_text(text) hashes = [] for html in (root / 'web').glob('*.html'): for attributes, script in re.findall(r']*)>(.*?)', html.read_text(), re.S | re.I): if not re.search(r'\bsrc\s*=', attributes, re.I) and script.strip(): hashes.append("'sha256-" + base64.b64encode(hashlib.sha256(script.encode()).digest()).decode() + "'") template = Path(os.environ.get('NGINX_TEMPLATE', root / 'infra/nginx.conf.template')).read_text() rendered = template.replace('@SCRIPT_HASHES@', ' '.join(hashes)) # Collapse the gap an empty hash list leaves behind, so the policy reads cleanly. rendered = re.sub(r"(script-src 'self')\s+;", r'\1;', rendered) (root / 'default.conf.template').write_text(rendered) print(f'CSP script-src: {len(hashes)} inline hash(es)')