AgentPay applies its baseline browser security posture from next.config.ts by calling defaultSecurityHeaders() in src/lib/securityHeaders.ts for every route (/:path*). The header map is generated at build time so the shipped Content-Security-Policy (CSP) matches the same API origin the client code will use.
This document is a reference for contributors changing headers, CSP directives, external integrations, or the theme pre-paint script.
src/lib/securityHeaders.tsowns the full header map and CSP directive list.next.config.tsresolves the API base once withresolveApiBase()and passes it intodefaultSecurityHeaders().src/app/layout.tsxcontains the inline theme pre-paint script that explains part of the current CSP tradeoff.
defaultSecurityHeaders() returns these headers for every Next.js response:
| Header | Current value / behavior | Purpose |
|---|---|---|
Content-Security-Policy |
Generated by buildCsp() |
Restricts where scripts, styles, images, fonts, fetches, forms, frames, objects, and base URLs can load or execute. |
X-Content-Type-Options |
nosniff |
Prevents browsers from MIME-sniffing a response away from its declared Content-Type. |
Referrer-Policy |
strict-origin-when-cross-origin |
Sends full referrers on same-origin navigation, but only the origin for cross-origin HTTPS requests and no referrer on HTTPS-to-HTTP downgrades. |
X-Frame-Options |
DENY |
Legacy clickjacking defense that prevents the dashboard from being framed. This complements CSP frame-ancestors 'none'. |
Permissions-Policy |
camera=(), microphone=(), geolocation=(), payment=(), browsing-topics=(), interest-cohort=() |
Denies sensitive browser capabilities and tracking-related APIs by default. |
Strict-Transport-Security |
max-age=63072000; includeSubDomains; preload in production only |
Tells browsers to use HTTPS for this host for two years, including subdomains, and opts into preload eligibility. It is omitted in development so local browsers do not cache an HTTPS upgrade for dev servers. |
buildCsp() currently emits the following directives. Keep this table in sync with the directives object in src/lib/securityHeaders.ts.
| Directive | Current sources | Why it exists |
|---|---|---|
default-src |
'self' |
Baseline fallback: resources are same-origin unless a more specific directive below allows something else. |
script-src |
Production: 'self' 'unsafe-inline'; development: 'self' 'unsafe-inline' 'unsafe-eval' |
Allows dashboard scripts from this origin. 'unsafe-inline' is currently required because the app ships inline scripts, including Next.js hydration/bootstrap output when CSP is applied through next.config.ts headers and the theme pre-paint script in src/app/layout.tsx. 'unsafe-eval' is development-only for Next.js Fast Refresh and related dev tooling. Do not add 'unsafe-eval' to production. |
style-src |
'self' 'unsafe-inline' |
Allows same-origin styles plus inline style tags generated by Next.js and next/font. |
font-src |
'self' data: |
Allows bundled same-origin fonts and data: font URLs. |
img-src |
'self' data: |
Allows same-origin images and small inline data: images. |
connect-src |
'self' plus the resolved API origin |
Allows browser fetch/WebSocket-style connections to the dashboard origin and the AgentPay API origin derived from NEXT_PUBLIC_AGENTPAY_API_BASE. |
frame-ancestors |
'none' |
Modern clickjacking defense that prevents any parent page from embedding the dashboard. |
form-action |
'self' |
Restricts form submissions to the dashboard origin. |
base-uri |
'self' |
Prevents injected <base> tags from rewriting relative URLs to an attacker-controlled origin. |
object-src |
'none' |
Blocks legacy plugin/embed/object execution surfaces. |
The policy intentionally does not include navigate-to, so normal top-level navigation from links such as external documentation or Stellar links remains possible.
The API base URL is resolved in next.config.ts with resolveApiBase(). That value is passed to defaultSecurityHeaders(), and buildCsp() calls originOf(apiBase) before adding it to connect-src.
Only the origin is used. For example, https://api.example.com/v1 becomes https://api.example.com in CSP. This keeps the CSP aligned with browser origin checks and with the actual backend origin used by the frontend API client.
If apiBase cannot be parsed as a URL, originOf() falls back to the default local API origin from DEFAULT_API_BASE. This keeps the generated CSP valid instead of emitting an invalid source expression.
'unsafe-inline' is present in two places for specific reasons:
script-srcincludes'unsafe-inline'because the app uses CSP fromnext.config.tsrather than request-time nonce middleware, and becausesrc/app/layout.tsxinjects a blocking pre-paint theme script. That script readslocalStorage, applies thedarkorlightclass to<html>before the first paint, and avoids a flash of the wrong theme before React hydration.style-srcincludes'unsafe-inline'because Next.js andnext/fontcan inject inline style tags needed for rendering.
'unsafe-eval' appears only when isDev is true. It supports the development server experience, including Fast Refresh. Production builds omit it.
If the theme script is ever replaced with a nonce-compatible or external-script approach, revisit script-src and remove 'unsafe-inline' only after confirming Next.js hydration/bootstrap scripts and the pre-paint behavior still work under the new policy.
Before adding a source, decide whether it is actually needed by browser-enforced CSP:
- API calls, analytics beacons, EventSource, or WebSocket connections usually require
connect-src. - Images require
img-src. - Fonts require
font-src. - Scripts require
script-srcand should be avoided unless the origin is trusted and stable. - Top-level link navigation does not require a CSP source unless the app adds
navigate-tolater.
Recommended workflow:
- Prefer configuration-derived origins. For the primary backend, set
NEXT_PUBLIC_AGENTPAY_API_BASE; do not hard-code a duplicate API origin in CSP. - Add the narrowest source expression to the specific directive in
src/lib/securityHeaders.ts. Prefer a full origin such ashttps://api.example.comover a scheme or wildcard. - Keep development-only relaxations behind
isDevso production remains stricter. - Add or update tests in
src/__tests__/securityHeaders.test.tswhen behavior changes. - Update this document in the same PR and verify every documented directive/header still exists in source.
- Run
npm run lint,npm run build, and a source cross-check such asrg -n "default-src|script-src|style-src|font-src|img-src|connect-src|frame-ancestors|form-action|base-uri|object-src|Content-Security-Policy|X-Content-Type-Options|Referrer-Policy|X-Frame-Options|Permissions-Policy|Strict-Transport-Security" src/lib/securityHeaders.ts next.config.ts docs/security-headers.md.
Relaxing CSP or hardening headers should be treated as a security-sensitive change:
- Document the product requirement and why a narrower alternative is not enough.
- Avoid broad wildcards such as
*,https:, ordata:unless the resource type truly requires them. - Never add
'unsafe-eval'to productionscript-srcwithout a clear migration plan. - Never remove both
X-Frame-Options: DENYandframe-ancestors 'none'unless the dashboard is intentionally becoming embeddable; if embedding is required, allow only the exact parent origins. - Re-run the source cross-check and update this reference doc so future contributors can audit the change.
When changing src/lib/securityHeaders.ts, confirm:
- Every emitted response header is documented above.
- Every emitted CSP directive is documented above.
- Development-vs-production differences are described.
connect-srcstill reflects the configured API origin.- The relationship between
script-src 'unsafe-inline'and the layout pre-paint script remains accurate.