Serve Hesychia public routes as real files with security headers.

Unknown URLs were returning the app shell with status 200. Return 404 for missing paths, keep the extensionless pages, and send the security headers. Production short links redirect for HeyCatch.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
westfarnandCursor committed 2026-10-19 09:56:58 -05:00
1 parent 3174c34a6f
commit d0c8b3f419
2 files changed
+43

No files matched your search

+13
View File
@@ -125,6 +125,19 @@ app_catalog:
api_proxy_prefix: /api/finance/webhooks/
# /robots.txt, /sitemap.xml, and /llms.txt must not fall through to index.html.
exact_root_files: true
# Homepage was missing these. Enforcing CSP stays loose so Tianji, the API,
# websockets, and inline styles keep working. Report-Only is the strict policy.
security_headers: true
# Unknown URLs are HTTP 404 with public/404.html, not 200 + the SPA shell.
# Slash routes are directory index.html files from scripts/stampCanonicals.js.
# These extensionless routes are the matching files (keep in sync with
# llm-fe/src/llm-fe/utils/canonicals.js paths that do not end in /).
error_page_404: /404.html
html_exact_paths:
- /legal/terms-of-service
- /billing/success
- /billing/cancel
- /document_storage
chat_backend:
type: django
repo: "{{ git_base_url }}/ai_ml_operations/chat_backend.git"
+30
View File
@@ -17,6 +17,14 @@ log_format observability escape=json
'"user_agent":"$http_user_agent"'
'}';
{% macro security_headers() %}
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self' https: wss: data: blob: 'unsafe-inline'; frame-ancestors 'none'; base-uri 'self'; object-src 'none'" always;
add_header Content-Security-Policy-Report-Only "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none'; form-action 'self'" always;
{% endmacro %}
{% for a in _static_apps %}
server {
listen {{ a.port }};
@@ -29,6 +37,9 @@ server {
root {{ (app_catalog[a.name].webroot_pattern | default(web_static_root ~ '/{env}_' ~ a.name)) | replace('{env}', a.env) }};
index index.html;
{% if app_catalog[a.name].security_headers | default(false) %}
{{ security_headers() }}
{% endif %}
{%- set api_backend_name = app_catalog[a.name].api_backend | default('') %}
{%- if api_backend_name %}
@@ -74,6 +85,22 @@ server {
}
{% endif %}
{% if app_catalog[a.name].html_exact_paths | default([]) %}
{% for p in app_catalog[a.name].html_exact_paths %}
location = {{ p }} {
default_type text/html;
try_files {{ p }} =404;
}
{% endfor %}
{% endif %}
{% if a.name == 'chat_web_app' and a.env == 'prod' %}
# HeyCatch short links. Unknown paths 404, so the SDK never sees /x.
location ~ ^/([a-z0-9])$ {
return 302 /?utm_source=heycatch&utm_campaign=$1;
}
{% endif %}
{% if app_catalog[a.name].error_page_404 | default('') %}
error_page 404 {{ app_catalog[a.name].error_page_404 }};
@@ -89,6 +116,9 @@ server {
location ~* \.(?:js|css|woff2?|png|jpg|jpeg|gif|svg|ico)$ {
expires 7d;
add_header Cache-Control "public";
{% if app_catalog[a.name].security_headers | default(false) %}
{{ security_headers() }}
{% endif %}
}
}
{% endfor %}