"""
Viewer HTML rendering helpers.
Separated from app.py so the factory stays under 150 lines.
"""
import os
import secrets
import base64
from flask import make_response
def make_viewer_response(base_dir: str):
"""
Read src/viewer/index.html, inject nonce + version + token, set CSP headers.
Returns a Flask Response object ready to send to the client.
Raises an exception if the template file is missing.
"""
template_path = os.path.join(base_dir, "viewer", "index.html")
with open(template_path, "r", encoding="utf-8") as f:
template = f.read()
nonce = base64.urlsafe_b64encode(secrets.token_bytes(16)).decode("utf-8").rstrip("=")
# D2.2: Never embed the raw AGENTCACHE_SECRET in page source.
# Replace the placeholder with an empty string — the viewer authenticates
# via the Authorization header set programmatically after load.
html = (
template
.replace("__AGENTCACHE_VIEWER_NONCE__", nonce)
.replace("__AGENTCACHE_VERSION__", "0.9.8")
.replace("__AGENTCACHE_AUTO_TOKEN__", "")
)
csp = "; ".join([
"default-src 'none'",
"base-uri 'none'",
"frame-ancestors 'self' https://huggingface.co https://*.hf.space",
"object-src 'none'",
"form-action 'none'",
f"script-src 'nonce-{nonce}'",
"script-src-attr 'none'",
"style-src 'unsafe-inline'",
(
"connect-src 'self' https: http://localhost:* http://127.0.0.1:* "
"wss: ws://localhost:* ws://127.0.0.1:* wss://localhost:* wss://127.0.0.1:*"
),
"img-src 'self' data:",
"font-src 'self'",
])
res = make_response(html)
res.headers["Content-Type"] = "text/html; charset=utf-8"
res.headers["Content-Security-Policy"] = csp
res.headers["Cache-Control"] = "no-cache"
return res