Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
fi
if [ -n "$changed" ]; then
printf 'Changed files:\n%s\n' "$changed"
grep -qE '^(services/gateway/|packages/realtime-gateway/|packages/game/|packages/core/|packages/api/|scripts/(nginx-trusted-edge-acceptance|lib/wait-for-health)\.mjs|docker/web/nginx\.conf\.template|package(-lock)?\.json|\.github/workflows/)' <<<"$changed" && gateway=true || gateway=false
grep -qE '^(services/gateway/|packages/realtime-gateway/|packages/game/|packages/core/|packages/api/|packages/web/|scripts/(nginx-trusted-edge-acceptance|nginx-web-delivery-acceptance|lib/wait-for-health)\.mjs|docker/web/nginx\.conf\.template|package(-lock)?\.json|\.github/workflows/)' <<<"$changed" && gateway=true || gateway=false
grep -qE '^(packages/web/|packages/e2e-harness/|packages/api/|packages/game/|packages/realtime-gateway/|packages/core/|package(-lock)?\.json|\.github/workflows/)' <<<"$changed" && web=true || web=false
grep -qE '^(deploy/|\.github/workflows/)' <<<"$changed" && deploy=true || deploy=false
# Only what changes how an image is *built*. Application code is already compiled by
Expand Down Expand Up @@ -609,6 +609,12 @@ jobs:
env:
REQUIRE_DOCKER: '1'

- name: Test web delivery caching and compression through real Nginx
run: npm run test:web-delivery
working-directory: services/gateway
env:
REQUIRE_DOCKER: '1'

# M6 acceptance gate: Playwright e2e (full game vs bot + vs human) +
# Lighthouse a11y audit (score must be >= 95). Runs the e2e harness
# (in-memory API + gateway + bot) alongside vite preview so the specs
Expand Down
47 changes: 47 additions & 0 deletions docker/web/nginx.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,51 @@ server {
root /usr/share/nginx/html;
index index.html;

# Compression: enable gzip for sufficiently large compressible assets.
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
application/json
application/javascript
text/javascript
application/xml
image/svg+xml
application/manifest+json;

# Only Vite-style content-hashed assets are safe for long-lived immutable caching.
# Regex locations take precedence over the /assets/ prefix fallback below.
location ~ "^/assets/(?:.*/)?[^/]+-[A-Za-z0-9_-]{8}\.[^/]+$" {
add_header Content-Security-Policy "frame-ancestors 'none'; object-src 'none'; base-uri 'self'" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Permissions-Policy "camera=(), geolocation=(), microphone=(), payment=()" always;
add_header Referrer-Policy "no-referrer" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}

# Existing unhashed assets remain reachable, but must revalidate and must never fall through
# to the SPA shell. Missing assets return 404 without an immutable Cache-Control header.
location /assets/ {
add_header Content-Security-Policy "frame-ancestors 'none'; object-src 'none'; base-uri 'self'" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Permissions-Policy "camera=(), geolocation=(), microphone=(), payment=()" always;
add_header Referrer-Policy "no-referrer" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Cache-Control "no-cache";
try_files $uri =404;
}

# SPA fallback: serve index.html for any route not matching a static file
location / {
add_header Content-Security-Policy "frame-ancestors 'none'; object-src 'none'; base-uri 'self'" always;
Expand All @@ -17,6 +62,7 @@ server {
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Cache-Control "no-cache";
try_files $uri $uri/ /index.html;
}

Expand Down Expand Up @@ -45,6 +91,7 @@ server {
# literal at config load, so a wrong value is a startup failure ("host not found in
# upstream"), not a runtime 502 — see ADR-0075.
location /v1/ {
gzip off;
proxy_pass http://${API_UPSTREAM};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
Expand Down
20 changes: 14 additions & 6 deletions docs/PROJECT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
> to read **only this file** and continue immediately. Updated after every
> milestone and every significant architectural step.

_Last updated: 2026-09-15 — M15 Increment 58: kubelet probe NetworkPolicy contract._
_Last updated: 2026-09-22 — M15 Increment 60: hash-aware immutable asset delivery contract._

Prior: _Last updated: 2026-09-16 — M15 Increment 59: production web delivery caching and compression contract._

Prior: _Last updated: 2026-09-15 — M15 Increment 58: kubelet probe NetworkPolicy contract._

Prior: _Last updated: 2026-09-15 — M15 Increment 57: search-indexer API NetworkPolicy reachability._

Expand Down Expand Up @@ -4236,11 +4240,15 @@ Per package: `cd packages/<pkg> && npm install && npm run build && npm test`.
- Reserve backup files exclusively and clean up only resources created by the drill. Preserve restore and cleanup errors together while continuing other cleanup. Redact connection secrets from diagnostics, including CLI argument errors.
- Verify append-only protection and valid, ready HNSW indexes on their specific public-schema relations. Added database-boundary and disposable-file regressions for native/Docker custom/plain orchestration and failure paths; live integration remains opt-in and was not run against an existing database.

## M15 Increment 59 — Production web delivery caching and compression contract (2026-09-16)

- Hardened production web delivery in `docker/web/nginx.conf.template` to implement optimized HTTP compression and safe caching headers.
- **Compression**: Enabled `gzip on;`, `gzip_vary on;`, `gzip_proxied any;`, `gzip_comp_level 6;`, `gzip_min_length 1024;`, and MIME types for text/plain, text/css, text/xml, application/json, application/javascript, text/javascript, application/xml, image/svg+xml, and application/manifest+json.
- **Content-Hashed Assets**: Route `/assets/*` enforces long-lived immutable caching (`Cache-Control: public, max-age=31536000, immutable`), returns 404 for missing assets without attaching immutable headers, and preserves all 7 security headers with `always`.
- **SPA Shell & Static Mutable Files**: Root route `/` enforces safe revalidation (`Cache-Control: no-cache`), ensuring `index.html` and SPA deep-link fallback routes (`try_files $uri $uri/ /index.html;`) cannot permanently pin clients to stale asset references after deployments.
- **Real Nginx Acceptance Suite**: Added `scripts/nginx-web-delivery-acceptance.mjs` and `npm run test:web-delivery` in `services/gateway`, validating all 10 delivery assertions (hashed asset caching, SPA shell freshness, deep-link fallback, gzip encoding, Vary header, API proxy safety, WebSocket upgrades, security headers preservation, 404 error routes, and uncompressed representation) through real Nginx containers in CI and local runners with zero test skips.

## M15 Increment 60 — Hash-aware immutable asset delivery contract (2026-09-22)






- Restricted one-year immutable caching to Vite-style content-hashed filenames under `/assets/`; existing unhashed assets now use `Cache-Control: no-cache`, and both hashed and unhashed missing assets remain strict 404 responses without immutable headers.
- Extended the real-Nginx acceptance suite with a deterministic unhashed `/assets/runtime-config.json` fixture, hashed JS and CSS cache assertions, root static-file revalidation checks, both hashed/unhashed 404 paths, and shared security-header assertions while preserving gzip, SPA, REST, and WebSocket coverage.
1 change: 1 addition & 0 deletions scripts/ci-local.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const SERVICE_JOBS = [
env: { REQUIRE_DOCKER: '1' },
steps: [
['trusted edge through real Nginx', 'npm run test:trusted-edge'],
['web delivery caching and compression through real Nginx', 'npm run test:web-delivery'],
],
},
];
Expand Down
Loading
Loading