Repository navigation
Expand file tree
/
Copy pathnext.config.js
More file actions
165 lines (155 loc) · 6.4 KB
/
Copy pathnext.config.js
File metadata and controls
165 lines (155 loc) · 6.4 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
164
165
/** @type {import('next').NextConfig} */
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
// Validate required environment variables at build time.
if (!process.env.NEXT_PUBLIC_API_URL?.trim() && process.env.NODE_ENV === "production") {
console.warn(
"\n⚠️ [Build Warning] NEXT_PUBLIC_API_URL is not set.\n" +
"The app will default to http://localhost:3001, which is a mock/local fallback and will fail against the live backend.\n" +
"Please set NEXT_PUBLIC_API_URL before building or deploying.\n",
);
}
// ── Content Security Policy (issue #777) ─────────────────────────────────────
// Every directive is an explicit allowlist; nothing falls back to a bare
// `https:` wildcard. Network origins come from the same env vars the app uses
// at runtime (lib/config.ts), so a deployment pointed at mainnet Horizon or a
// custom Soroban RPC is allowed automatically.
const isDev = process.env.NODE_ENV === "development";
/** Origin of an absolute URL, or null when unset / not a valid URL. */
function originOf(url) {
if (!url) return null;
try {
return new URL(url).origin;
} catch {
return null;
}
}
const unique = (values) => [...new Set(values.filter(Boolean))];
// Google Identity Services (@react-oauth/google) loads its script and
// stylesheet from accounts.google.com and renders the sign-in button in an
// iframe from the same origin.
const GOOGLE_ACCOUNTS = "https://accounts.google.com";
const connectSrc = unique([
"'self'",
// Backend API — same fallback as lib/api/axios.ts DEFAULT_API_BASE_URL.
originOf(process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"),
// Public gateway used by the docs explorer's "Try it" requests
// (lib/docs/navigation.ts DOCS_BASE_URL).
originOf(process.env.NEXT_PUBLIC_DOCS_API_URL || "https://bettapay-backend.onrender.com"),
// Horizon + Soroban RPCs: configured endpoints plus the public SDF defaults.
originOf(process.env.NEXT_PUBLIC_STELLAR_HORIZON_URL),
"https://horizon-testnet.stellar.org",
"https://horizon.stellar.org",
originOf(process.env.NEXT_PUBLIC_SOROBAN_RPC_URL),
"https://soroban-testnet.stellar.org",
// WalletConnect relay (lib/stellar/walletconnect.ts opens a WebSocket).
originOf(process.env.NEXT_PUBLIC_WALLETCONNECT_RELAY_URL) ||
"wss://relay.walletconnect.com",
GOOGLE_ACCOUNTS,
// Next.js dev server HMR websocket.
isDev && "ws://localhost:*",
isDev && "ws://127.0.0.1:*",
]);
const csp = {
"default-src": ["'self'"],
// Next.js App Router injects inline bootstrap scripts, so 'unsafe-inline'
// is required until nonce-based CSP is introduced. Dev mode (React Refresh /
// webpack HMR) also evaluates code at runtime and needs 'unsafe-eval' —
// without it React never hydrates locally. Production never gets it.
"script-src": unique([
"'self'",
"'unsafe-inline'",
isDev && "'unsafe-eval'",
GOOGLE_ACCOUNTS,
]),
"style-src": ["'self'", "'unsafe-inline'", GOOGLE_ACCOUNTS],
// `data:` covers QR codes and KYB upload previews; ui-avatars serves the
// About page team avatars.
"img-src": ["'self'", "data:", "https://ui-avatars.com"],
"font-src": ["'self'", "data:"],
"connect-src": connectSrc,
"frame-src": [GOOGLE_ACCOUNTS],
"worker-src": ["'self'"],
"object-src": ["'none'"],
"base-uri": ["'self'"],
"form-action": ["'self'"],
// Nobody may frame the app — blocks clickjacking of payment/admin actions.
"frame-ancestors": ["'none'"],
};
const contentSecurityPolicy = [
...Object.entries(csp).map(([directive, sources]) => `${directive} ${sources.join(" ")}`),
...(isDev ? [] : ["upgrade-insecure-requests"]),
].join("; ");
const nextConfig = {
reactStrictMode: true,
// Transpile @noble/* under Jest (next/jest consults this list to build its
// transformIgnorePatterns): @noble/hashes and @noble/ed25519 ship ESM-only
// sources, so without it the suites that import @stellar/stellar-sdk fail
// with "Cannot use import statement outside a module". Effect on the
// production build is negligible (tiny pure-JS packages).
transpilePackages: ["@noble/hashes", "@noble/ed25519", "uint8array-extras"],
experimental: {
// `isomorphic-dompurify` (used by components/shared/SafeHtmlRenderer.tsx)
// falls back to jsdom on the server, and jsdom reads its own files off
// disk at runtime (`browser/default-stylesheet.css`). Bundling it makes
// those paths relative to `.next/server`, so page-data collection fails
// with ENOENT. Requiring it from node_modules at runtime instead keeps the
// package's own directory layout intact. The client build is unaffected:
// it resolves the package's `browser` entry, which never touches jsdom.
serverComponentsExternalPackages: ["isomorphic-dompurify", "jsdom"],
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "lh3.googleusercontent.com",
pathname: "/**",
},
{
protocol: "https",
hostname: "**.googleusercontent.com",
pathname: "/**",
},
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
pathname: "/**",
},
],
},
eslint: {
// Existing repo has many pre-existing lint violations (unused vars, any, etc.)
// that are outside the scope of the current fix batch. Ignore during builds
// so functional changes can still produce a successful production build.
ignoreDuringBuilds: true,
},
typescript: {
// Type errors fail the production build (default). The codebase's
// typecheck is clean (`npm run typecheck` runs tsc with the same config
// in CI), so keep Next's own check enabled.
ignoreBuildErrors: false,
},
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "no-referrer-when-downgrade" },
{ key: "Permissions-Policy", value: "geolocation=(), microphone=()" },
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{
key: "Content-Security-Policy",
value: contentSecurityPolicy,
},
],
},
];
},
};
module.exports = withBundleAnalyzer(nextConfig);