Repository navigation
Expand file tree
/
Copy pathnext.config.js
More file actions
163 lines (150 loc) · 5.6 KB
/
Copy pathnext.config.js
File metadata and controls
163 lines (150 loc) · 5.6 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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
const path = require('path')
const { execSync } = require('child_process')
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
// Deterministic build ID. Resolution order:
// 1. NEXT_BUILD_ID env var (override for CI / reproducibility tests)
// 2. SOURCE_DATE_EPOCH-derived short SHA via `git rev-parse`
// 3. fallback "dev" — only when git isn't available (sandboxed CI, no .git)
// OpenSSF Best Practices Gold criterion `build_reproducible` requires
// byte-identical output across builds at the same commit; see
// docs/REPRODUCIBLE_BUILD.md.
function reproducibleBuildId() {
if (process.env.NEXT_BUILD_ID) return process.env.NEXT_BUILD_ID
try {
return execSync('git rev-parse --short=12 HEAD', { encoding: 'utf8' }).trim()
} catch {
return 'dev'
}
}
function isCiEnvironment() {
return process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true'
}
function shouldSkipTypecheck() {
if (process.env.SKIP_TYPECHECK !== '1') return false
if (isCiEnvironment()) {
throw new Error('SKIP_TYPECHECK=1 is local-only and must not be set in CI')
}
return true
}
/** @type {import('next').NextConfig} */
const nextConfig = {
// If a package-lock.json exists above this repo (e.g. in $HOME), Next.js would pick that
// directory as the monorepo root and resolve node_modules there — breaking imports such as
// `firebase/auth`. Pin the tracing / Turbopack root to this application directory.
outputFileTracingRoot: path.join(__dirname),
// Deterministic build ID derived from git HEAD — feeds into .next/BUILD_ID and chunk hashes.
generateBuildId: reproducibleBuildId,
// Standalone output is only for Docker builds (see docker/Dockerfile). Omit on Vercel.
...(process.env.DOCKER_BUILD === '1' ? { output: 'standalone' } : {}),
// Emergency local-build bypass — SKIP_TYPECHECK=1 disables both the
// TypeScript type-check and the ESLint check during `next build`. Use ONLY
// for local visual QA when pre-existing in-flight branch state has
// unrelated type/lint errors. NEVER set this in CI; CI is the boundary
// that catches real type errors. See CLAUDE.md "Local production
// verification — emergency typecheck bypass".
...(shouldSkipTypecheck()
? {
typescript: { ignoreBuildErrors: true },
eslint: { ignoreDuringBuilds: true },
}
: {}),
serverExternalPackages: ['@cursor/sdk'],
webpack: (config) => {
// Deterministic chunk/module IDs are the Next.js production default since v14,
// but pin them here explicitly so the configuration survives upstream changes.
if (config.optimization) {
config.optimization.moduleIds = 'deterministic'
config.optimization.chunkIds = 'deterministic'
}
return config
},
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'firebasestorage.googleapis.com',
pathname: '/**',
},
{
protocol: 'https',
hostname: '*.googleusercontent.com',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'lh3.googleusercontent.com',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
pathname: '/**',
},
],
},
async headers() {
const isDev = process.env.NODE_ENV === 'development'
const scriptSrc = [
"'self'",
"'unsafe-inline'",
// Webpack dev server and dynamic imports require eval in local development only.
...(isDev ? ["'unsafe-eval'"] : []),
'https://embed.lu.ma',
'https://apis.google.com',
'https://accounts.google.com',
'https://www.googletagmanager.com',
].join(' ')
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
// Firebase/Google OAuth popup flows load Google-hosted scripts.
// unsafe-eval is dev-only; production builds do not need it.
`script-src ${scriptSrc}`,
"style-src 'self' 'unsafe-inline' https://embed.lu.ma",
"img-src 'self' data: blob: https://firebasestorage.googleapis.com https://*.googleusercontent.com https://lh3.googleusercontent.com https://avatars.githubusercontent.com https://*.cartocdn.com https://unpkg.com",
"font-src 'self'",
"connect-src 'self' https://*.firebaseio.com https://*.firebaseapp.com https://*.googleapis.com https://accounts.google.com https://www.google-analytics.com https://www.google.com https://*.cartocdn.com https://unpkg.com",
"frame-src https://lu.ma https://luma.com https://accounts.google.com https://*.firebaseapp.com",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'",
].join('; '),
},
{
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',
},
{
key: 'X-XSS-Protection',
value: '0',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
],
},
]
},
}
module.exports = withBundleAnalyzer(nextConfig)