1import type { NextConfig } from "next";
2
3/* ARCHITECTURE §8: header keamanan. Kamera dibatasi ke origin sendiri, halaman
4 strip tidak mengirim referrer dan tidak diindeks, dan halaman itu tidak boleh
5 dibingkai situs lain. M9 memasang CSP penuh: seluruh halaman dan rutenya
6 memakai satu kebijakan yang sama, termasuk `frame-ancestors 'none'` yang
7 sebelumnya hanya dipasang di `/s` dan `/r`. */
8
9/* Semua aset dan permintaan snapme berasal dari origin sendiri: fon di-host
10 sendiri, wasm dan model MediaPipe di-host sendiri, dan tidak ada unggahan
11 ke layanan luar, jadi CSP tidak punya host luar yang perlu diizinkan. */
12
13/* Alasan tiap direktif (rinciannya di docs/DECISIONS.md, M9):
14
15 - `script-src 'unsafe-inline'`: skrip bootstrap App Router ditulis inline dan
16 tanpa nonce. Nonce memaksa render dinamis di setiap permintaan, yang
17 bertabrakan dengan `cacheComponents` dan target LCP Welcome (PRD §8).
18 - `script-src 'wasm-unsafe-eval'`: Gesture Snap menjalankan model MediaPipe
19 yang di-host sendiri (ARCHITECTURE §6, §8).
20 - `'unsafe-eval'` hanya di pengembangan, karena React memakainya untuk
21 menyusun ulang jejak galat di peramban; produksi tidak butuh itu.
22 - `img-src`/`media-src` `blob:` dan `data:`: pratinjau dan ekspor lewat
23 kanvas (`toDataURL`), bingkai klip sebagai ImageBitmap, dan hasil
24 MediaRecorder sebagai URL blob.
25 - `connect-src` `blob:` dan `data:`: konversi hasil ekspor jadi Blob lewat
26 `fetch(dataUrl)`, tanpa jaringan keluar.
27 - `worker-src 'self' blob:`: MediaPipe membuat worker dari berkasnya sendiri
28 dan dari blob.
29 - `font-src 'self' data:`: fon di-host sendiri lewat next/font.
30 - `object-src 'none'`, `base-uri 'self'`, `form-action 'self'`,
31 `frame-ancestors 'none'`: pagar dasar tanpa pemakaian yang sah di produk
32 ini. */
33const CSP = [
34 "default-src 'self'",
35 `script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'${
36 process.env.NODE_ENV === "development" ? " 'unsafe-eval'" : ""
37 }`,
38 "style-src 'self' 'unsafe-inline'",
39 `img-src 'self' blob: data:`,
40 `media-src 'self' blob: data:`,
41 `connect-src 'self' blob: data:`,
42 "font-src 'self' data:",
43 "worker-src 'self' blob:",
44 "object-src 'none'",
45 "base-uri 'self'",
46 "form-action 'self'",
47 "frame-ancestors 'none'",
48].join("; ");
49
50const SECURITY_HEADERS = [
51 { key: "Content-Security-Policy", value: CSP },
52 { key: "Permissions-Policy", value: "camera=(self)" },
53 { key: "X-Content-Type-Options", value: "nosniff" },
54 { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
55];
56
57const PRIVATE_HEADERS = [
58 { key: "Referrer-Policy", value: "no-referrer" },
59 { key: "X-Robots-Tag", value: "noindex" },
60];
61
62const nextConfig: NextConfig = {
63 /* config options here */
64 /* M10: `cacheComponents`, `partialPrefetching`, dan `htmlLimitedBots`
65 DILEPAS. Ketiganya semula dipakai untuk PPR dan status 404 yang benar di
66 `/s/[id]` dan `/r/[id]`, tetapi mesin Cache Components menolak berjalan
67 stabil di bawah permintaan bersamaan: `CacheSignal` melempar
68 "Cannot perform I/O on behalf of a different request" (penanda waktu yang
69 dibuat di konteks permintaan lain) dan server menjawab 500 untuk semua
70 permintaan berikutnya. Tanpa PPR, kedua halaman itu dirender dinamis dan
71 `notFound()` tetap menjawab 404 sungguhan. Rinciannya di
72 `docs/DECISIONS.md` M10. */
73 turbopack: {
74 rules: {
75 "*.css": {
76 loaders: ["@tailwindcss/turbopack"],
77 as: "*.css",
78 },
79 },
80 },
81 async headers() {
82 return [
83 { source: "/:path*", headers: SECURITY_HEADERS },
84 /* `/r/[id]` milik M8; aturannya sudah dipasang sekarang supaya halaman
85 estafet tidak pernah bocor referrer saat dibangun. CSP-nya sudah
86 ikut dari baris pertama. */
87 { source: "/s/:path*", headers: PRIVATE_HEADERS },
88 { source: "/r/:path*", headers: PRIVATE_HEADERS },
89 ];
90 },
91};
92
93export default nextConfig;
94