Login signed in client-side then soft-navigated with router.push into
/dashboard, a route the middleware gates on the Supabase session cookie.
The soft navigation can reach the protected route before the server
observes the freshly written cookies, so the middleware bounces it back
to /login -- which looks like the page "just refreshing" instead of
signing in. It surfaced over ngrok/HTTPS tunnels (added latency widens
the window) while staying hidden on localhost.
- login: navigate with window.location.href after signInWithPassword,
mirroring the deliberate full reload the invite-accept flow already
uses in join/[token]/page.tsx; drops the now-unused useRouter.
- next.config: allow-list common dev tunnel origins (ngrok, cloudflared,
localtunnel) so Next 16 stops 403-ing dev-only resources over the
tunnel, extendable via ALLOWED_DEV_ORIGINS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This introduces the base i18n setup using next-intl without routing. It adds the default messages/en.json and sets up the app layout and next.config.ts to consume the locale from the environment. The login page has been migrated as a first example.
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>