Agents can now attach a photo/video/document or record a voice note in
the inbox composer and send it live in a 1:1 thread — previously media
could only be received from customers or sent via the Flows builder.
- 023_chat_media.sql: new account-scoped `chat-media` Storage bucket
(mirrors flow-media RLS from 020) plus Meta-accepted audio MIME types.
- meta-api: sendMediaMessage gains an `audio` kind (link-only — Meta
rejects caption/filename on audio; WhatsApp renders OGG/Opus as a
voice note).
- /api/whatsapp/send: media branch reusing the phone-variant retry loop.
- /api/whatsapp/transcode-audio: Node-runtime route remuxing the
WebM/Opus that Chromium's MediaRecorder produces to OGG/Opus via
ffmpeg-static (Firefox/Safari record accepted formats and skip it).
- composer: attach menu, file pickers, voice recorder + auto-transcode,
preview with caption; thread: optimistic handleSendMedia.
- shared uploadAccountMedia helper (flows form refactored onto it).
- next.config: allow microphone=(self) (was fully blocked) + CSP
media-src; document optional FFMPEG_PATH override.
Tests: meta-api audio payload shape, buildMediaPath, audio-transcode
helpers (14 new). typecheck/lint/build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a catch-all rule in next.config.ts that merges a set of baseline
security headers into every response — served alongside the existing
Cache-Control rules, not instead of them.
Enforced today:
- Strict-Transport-Security: 2yr, includeSubDomains, preload-eligible.
No-op on http://localhost.
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY (same defense as frame-ancestors below, but
older browsers only read this one)
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy: deny camera/mic/geolocation/payment/usb. We
don't use any of those APIs; closing them off prevents a supply-
chain compromise or a forgotten plugin from silently enabling them.
Report-only:
- Content-Security-Policy-Report-Only with a pragmatic allowlist:
default-src 'self'
script-src 'self' 'unsafe-inline' 'unsafe-eval'
style-src 'self' 'unsafe-inline'
img-src 'self' data: blob: https:
font-src 'self' data:
connect-src 'self' https://*.supabase.co wss://*.supabase.co
frame-ancestors 'none'
base-uri 'self'
form-action 'self'
'unsafe-inline' / 'unsafe-eval' are there because Next's inline
hydration script + dev-mode require them; nonce-based CSP is a
later project. Violations appear in the browser console but
nothing is blocked until we flip the header name to
`Content-Security-Policy`.
Verified against the running dev server:
- /, /docs, /login, /api/whatsapp/webhook, /_next/static/* all
return the six headers.
- No CSP violations on / or /docs in the browser console.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause: Hostinger's CDN was applying s-maxage=31536000 (1 year)
to prerendered HTML pages by default. Each deploy produced fresh
Turbopack chunk hashes, but the edge kept serving year-old HTML that
referenced chunk filenames the new build no longer had on disk —
resulting in HTML 200 + every /_next/static/*.{js,css} 404, page
rendered unstyled. Private/incognito didn't help because the cache
is at the edge, not the browser.
Fix: explicit Cache-Control via next.config.ts headers():
- /_next/static/* — public, max-age=31536000, immutable
Content-hashed filenames; safe forever.
- /api/* — no-store
Per-user, must never be shared at the edge.
- everything else — public, max-age=0, s-maxage=300,
stale-while-revalidate=86400
Edge serves instantly from cache for 5 min, then keeps serving
cached content while refreshing in the background for up to 24h.
Chunk-hash drift after a deploy self-heals within ~5 min.
Verification after deploy:
curl -I https://<host>/login | grep -i cache-control
Expect: public, max-age=0, s-maxage=300, stale-while-revalidate=86400
(If Hostinger overwrites this, fall back to configuring
purge-on-deploy at the platform layer.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>