-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnext.config.ts
More file actions
103 lines (94 loc) · 4.52 KB
/
Copy pathnext.config.ts
File metadata and controls
103 lines (94 loc) · 4.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
import { withSentryConfig } from "@sentry/nextjs";
const withNextIntl = createNextIntlPlugin();
// The browser Sentry SDK POSTs events to its ingest host; derive that origin
// from the public DSN so the CSP `connect-src` can allow exactly it — and only
// when Sentry is configured, keeping the CSP tight when it is off. Runs at build
// time (next.config), same moment NEXT_PUBLIC_SENTRY_DSN is baked into the bundle.
function sentryIngestOrigin(): string | null {
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN;
if (!dsn) return null;
try {
return new URL(dsn).origin;
} catch {
return null;
}
}
const sentryConnectSrc = sentryIngestOrigin();
// Security headers (DEV-PHASES-PLAN W0 — closes the "live SaaS without headers"
// hole; approach mirrors the RUMI frontend's next.config.ts, tightened to what
// this app actually loads: fonts are self-hosted via next/font, there are no
// external scripts/styles/images/connections, JSON-LD + the theme-init snippet
// are the only inline scripts.
//
// - `script-src 'unsafe-inline'`: required by Next.js App Router streaming
// (self.__next_f.push) + the theme-init snippet. 'unsafe-eval' is dev-only
// (React Refresh needs it; production must not carry it).
// - `frame-ancestors 'none'` (+ X-Frame-Options DENY): nothing embeds sofra.
// - HSTS without `preload`: preload is a hard-to-reverse, list-submission
// decision for the whole apex (incl. every *.sofrapiwas.com tenant) — owner
// call, not a default.
const isDev = process.env.NODE_ENV !== "production";
const contentSecurityPolicy = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ""}`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob:",
"font-src 'self' data:",
`connect-src 'self'${sentryConnectSrc ? ` ${sentryConnectSrc}` : ""}`,
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
// Kept out of dev because some RSC fetches get upgraded and log
// ERR_SSL_PROTOCOL_ERROR. Local `next start` over http still carries it, so
// expect that error class there — but note it does NOT stop the page from
// hydrating: Chromium exempts http://localhost as potentially-trustworthy, the
// bundle loads, and the E2E suite's client-side assertions depend on that
// (tests/e2e/control-auth.spec.ts explains where the stronger claim came from).
...(isDev ? [] : ["upgrade-insecure-requests"]),
].join("; ");
const securityHeaders = [
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
{ key: "Content-Security-Policy", value: contentSecurityPolicy },
];
const nextConfig: NextConfig = {
// Standalone output for the Docker image (same pattern as the RUMI frontend).
output: "standalone",
poweredByHeader: false,
async headers() {
return [
{
source: "/:path*",
headers: securityHeaders,
},
// The one route whose URL *is* a credential (E4): the path segment is a
// 32-byte token that lets its holder open this restaurant's Stripe
// onboarding. The site-wide policy above sends the ORIGIN cross-origin,
// which is already safe — but this page redirects to Stripe and may one day
// render HTML with a link on it, and at that moment `strict-origin-...`
// would still send the full URL SAME-origin and the whole answer would hinge
// on nobody adding an outbound link. `no-referrer` costs nothing and does
// not depend on that. Last match wins in Next's header merge, so this
// overrides the entry above for these paths only.
{
source: "/:locale/onboarding/payments/:token",
headers: [{ key: "Referrer-Policy", value: "no-referrer" }],
},
];
},
};
// Sentry wraps the config last: it wires the client SDK into the bundle and the
// server SDK via instrumentation.ts. Source-map upload is disabled (no auth
// token wired), so the build stays green offline and in CI; add org/project/
// authToken later to get readable stack traces. `enabled` on each Sentry.init
// keeps the whole thing inert until a DSN is set.
export default withSentryConfig(withNextIntl(nextConfig), {
silent: !process.env.CI,
sourcemaps: { disable: true },
});