wacrm/next.config.ts
Arnas Donauskas 312683ca05
feat(inbox): send photo, video, document & voice in chat (#213)
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>
2026-06-18 10:07:16 +03:00

129 lines
5.1 KiB
TypeScript

import type { NextConfig } from "next";
/**
* Baseline security headers applied to every response.
*
* CSP ships as `Content-Security-Policy-Report-Only` so the browser
* surfaces violations in the console without blocking anything — once
* we have confidence nothing legit trips it (two deploys, a pass on
* every route), flip the key to `Content-Security-Policy` to enforce.
*
* The rest of the headers are straight blocks, safe to enforce today:
* - HSTS: only meaningful on HTTPS (no-op on http://localhost).
* - X-Content-Type-Options / X-Frame-Options / Referrer-Policy:
* baseline OWASP hardening, no behavioural cost.
* - Permissions-Policy: we don't use camera / microphone / etc, so
* deny them. A supply-chain compromise or a forgotten plugin
* can't silently opt back in.
*/
const SECURITY_HEADERS = [
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
// Microphone is allowed for same-origin (`self`) so the inbox
// composer can record voice notes via MediaRecorder. Everything
// else stays denied — a compromised dependency can't silently grab
// the camera / geolocation / etc.
key: "Permissions-Policy",
value: "camera=(), microphone=(self), geolocation=(), payment=(), usb=()",
},
{
key: "Content-Security-Policy-Report-Only",
value: [
"default-src 'self'",
// Next.js needs 'unsafe-inline' for its inline hydration script
// and 'unsafe-eval' in dev + some production optimisations.
// Nonce-based CSP is a later project.
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
// Tailwind + inline style attributes on lots of components.
"style-src 'self' 'unsafe-inline'",
// Supabase public-bucket avatars, contact avatars (arbitrary
// https URLs paste-able from the UI), OG images, data URLs for
// tiny inline assets.
"img-src 'self' data: blob: https:",
// Outbound media previews (blob: from MediaRecorder + file picker)
// and Supabase public-bucket audio/video the inbox renders.
"media-src 'self' blob: https://*.supabase.co",
"font-src 'self' data:",
// Supabase REST + realtime (WSS). All Meta API calls happen
// server-side, so graph.facebook.com does not belong here.
"connect-src 'self' https://*.supabase.co wss://*.supabase.co",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join("; "),
},
] as const;
const nextConfig: NextConfig = {
/**
* Cache-Control policy.
*
* Why this exists:
* Hostinger's CDN was applying `s-maxage=31536000` (1 year) to
* prerendered HTML pages by default. When a new deploy shipped
* fresh Turbopack chunk hashes, the edge kept serving year-old
* HTML referencing chunk filenames that no longer existed on
* disk — result: HTML 200, every /_next/static/*.js and .css
* came back 404, the page rendered unstyled. Private/incognito
* did nothing because the cache is server-side.
*
* Strategy:
* - /_next/static/* — leave to Next. Turbopack dev chunks can go
* stale if we force immutable caching here; Next already emits
* the correct production headers for hashed assets.
* - /api/* — no-store. API responses are per-user and
* must never be shared across requests at the edge.
* - Everything else — public, brief s-maxage + generous
* stale-while-revalidate. The edge serves instantly from cache
* for the first 5 min, then returns cached content while
* refreshing in the background for up to 24 h. A deploy's
* chunk-hash drift self-heals within ~5 min with no user-
* visible latency.
*
* Note: dynamic dashboard routes (/inbox, /contacts, /pipelines,
* /broadcasts, etc.) are server-rendered per request — Next.js
* and Supabase auth already prevent them from being served
* from a shared cache. The s-maxage here is a ceiling; Next.js
* and auth middleware still set `private` / `no-store` for
* per-user responses.
*
* Security headers are appended via a separate catch-all rule
* below — Next.js merges headers from every matching rule, so
* they apply to every response regardless of which cache rule
* matched.
*/
async headers() {
return [
{
source: "/api/:path*",
headers: [{ key: "Cache-Control", value: "no-store" }],
},
{
source: "/:path((?!_next/static|_next/image|api).*)",
headers: [
{
key: "Cache-Control",
value:
"public, max-age=0, s-maxage=300, stale-while-revalidate=86400",
},
],
},
{
// Security headers on every response, including /_next/static
// assets (nosniff matters there) and /api/* (HSTS + referrer-
// policy don't hurt).
source: "/:path*",
headers: [...SECURITY_HEADERS],
},
];
},
};
export default nextConfig;