# ADR-049: Recce View Files Are Served via HMAC-Signed Proxy — Not Direct Traefik Serve

## Status

Accepted, 2026-06-22.

## Status History

```yaml
status_history:
  - date: 2026-06-22
    status: Proposed
    changed_by: hkl
    reason: Formalising recce-view-proxy architecture after auditing finding of public file exposure
    changed_via: adr-kit (360lm)
  - date: 2026-06-22
    status: Accepted
    changed_by: hkl
    reason: recce-view-proxy live; HMAC-signed tokens with 15-min TTL; direct file serving removed
    changed_via: adr-kit (360lm)
```

## Context

Recce submissions generate HTML view files and PDF reports stored at `/var/www/360lm/screenshots/recce/views/` on the VPS. These files contain sensitive brand and store data. Initially, `recce.submissions.view_url` and `pdf_url` stored direct file paths served as static files by Traefik. An auditing finding revealed: **directory listing was enabled on that path and no authentication was required** — anyone with the URL pattern could enumerate and download all Recce views without logging in. The fix required serving these files only to authenticated users with time-limited access tokens.

## Decision

Recce HTML view files and PDFs are served exclusively via `/recce-view-proxy/` Node service:
1. When a user needs to view a Recce file, the PWA requests a signed URL from the proxy via an authenticated endpoint (requires valid session).
2. The proxy generates an HMAC-signed token encoding: file path + expiry timestamp + user ID.
3. The signed URL (with token as query param) is returned and used by the browser to fetch the file.
4. The proxy validates the token on each file request: signature check + TTL check (default 15 min, max 6 h).
5. If the token is invalid or expired, the proxy returns 403.

Direct file serving via Traefik for the `screenshots/recce/views/` path is disabled. Directory listing is off.

**Decision Maker:** hkl

## Alternatives Considered

- **Continue direct file serving, add HTTP Basic Auth via Traefik middleware.** Rejected: HTTP Basic Auth is per-path, not per-user — all users would share one credential; integrating with per-user session tokens requires a custom Traefik plugin; complexity comparable to the proxy with less control.
- **Store files in a private S3/GCS bucket with presigned URLs.** Rejected: requires external cloud storage dependency and per-file upload overhead; files are already on VPS disk; a local proxy achieves presigned-URL semantics without external storage.
- **Serve files only to logged-in users by checking session in a Traefik forward-auth middleware.** Rejected: Traefik forward-auth middleware would need to parse the session cookie/localStorage token — session tokens are in localStorage, not cookies, so Traefik cannot read them without a custom implementation.
- **Store view files in PostgREST (bytea column) with row-level security.** Rejected: binary file storage in PG is an anti-pattern for large files; generates significant table bloat; file-level streaming is better served by a file proxy.

## Consequences

**Positive:**
- Recce files are inaccessible without a valid session + signed token.
- Token TTL (15 min default) limits exposure window if a URL is accidentally shared.
- No directory listing — file enumeration is impossible.
- Audit logging possible at proxy level (every file access logged with user ID + timestamp).

**Negative / Trade-offs:**
- Extra hop: browser → proxy → disk (vs. browser → disk directly); acceptable latency for file serving.
- Proxy must remain running — Recce view files are inaccessible if the proxy container is down.
- Token generation requires authenticated session — users must be logged in before requesting a view URL.

**Risks and mitigations:**
- HMAC secret compromised: mitigated by storing secret in Docker environment variable, not in code; rotate by redeploying proxy container with new secret (all existing tokens immediately invalidated).
- Proxy container down: mitigated by `restart: always`; users see 502 and can retry; files are safe on disk.

## Related Decisions

- ADR-017 (Traefik routing) — proxy is exposed via Traefik; direct file path is closed via Traefik configuration.
- ADR-048 (external client magic-link) — external clients viewing Recce files use this proxy; their session token is validated before a signed URL is issued.

## References

- `memory/dbt_recce.md` — auditing finding, recce-view-proxy HMAC-signed tokens, TTL settings
- `/opt/recce-view-proxy/` — Node service implementation
- `recce/index.html` — signed URL request before file view
