@@ -59,6 +59,8 @@ export async function ssrPage(route, params, url, opts) {
5959 [ route . file , ...route . layouts ] ,
6060 opts . appDir ,
6161 ) ;
62+ // Extract CSP nonce from request headers (if present).
63+ const nonce = opts . req ? getNonce ( opts . req ) : undefined ;
6264 return streamingHtmlResponse (
6365 wrapHead ( {
6466 metadata,
@@ -67,6 +69,7 @@ export async function ssrPage(route, params, url, opts) {
6769 streaming : suspenseCtx . pending . length > 0 ,
6870 preloads,
6971 lazyComponents,
72+ nonce,
7073 } ) ,
7174 body ,
7275 suspenseCtx ,
@@ -230,25 +233,25 @@ function wrapInDocument(body, opts) {
230233 * (breaks the ES-module waterfall without a bundler) and any user-declared
231234 * `metadata.preload` entries.
232235 *
233- * @param {{ metadata: Record<string,any>, moduleUrls: string[], dev: boolean, streaming: boolean, preloads?: string[], lazyComponents?: Record<string, string> } } opts
236+ * @param {{ metadata: Record<string,any>, moduleUrls: string[], dev: boolean, streaming: boolean, preloads?: string[], lazyComponents?: Record<string, string>, nonce?: string } } opts
234237 */
235238function wrapHead ( opts ) {
239+ // CSP nonce: if provided, all inline <script> tags get nonce="…" so they
240+ // pass strict Content-Security-Policy headers. The nonce is extracted from
241+ // the request's CSP header by the caller.
242+ const n = opts . nonce ? ` nonce="${ escapeAttr ( opts . nonce ) } "` : '' ;
243+
236244 const imports = opts . moduleUrls . map ( ( u ) => `import ${ JSON . stringify ( u ) } ;` ) . join ( '\n' ) ;
237- // Lazy-loader boot script: loads the IntersectionObserver-based lazy loader
238- // and registers tag → URL pairs for below-the-fold components.
239245 const lazyEntries = opts . lazyComponents && Object . keys ( opts . lazyComponents ) . length
240246 ? opts . lazyComponents
241247 : null ;
242248 const lazyBoot = lazyEntries
243249 ? `\nimport { observeLazy } from 'webjs/lazy-loader';\nobserveLazy(${ JSON . stringify ( lazyEntries ) } );`
244250 : '' ;
245- const boot = ( imports || lazyBoot ) ? `<script type="module">\n${ imports } ${ lazyBoot } \n</script>` : '' ;
246- const reload = opts . dev ? `<script type="module" src="/__webjs/reload.js"></script>` : '' ;
247- // Suspense resolver: a single script that auto-resolves streamed-in
248- // <template data-webjs-resolve> elements. Uses a MutationObserver to
249- // detect new templates as they stream in — no per-chunk inline scripts.
251+ const boot = ( imports || lazyBoot ) ? `<script type="module"${ n } >\n${ imports } ${ lazyBoot } \n</script>` : '' ;
252+ const reload = opts . dev ? `<script type="module"${ n } src="/__webjs/reload.js"></script>` : '' ;
250253 const suspenseBoot = opts . streaming
251- ? `<script>(function(){` +
254+ ? `<script${ n } >(function(){` +
252255 `function r(id){var t=document.querySelector('template[data-webjs-resolve="'+id+'"]');` +
253256 `var b=document.getElementById(id);if(t&&b){b.replaceWith(t.content.cloneNode(true));t.remove();}}` +
254257 `window.__webjsResolve=r;` +
@@ -472,6 +475,18 @@ function toUrlPath(file, appDir) {
472475 return rel ;
473476}
474477
478+ /**
479+ * Extract a CSP nonce from the request's Content-Security-Policy header.
480+ * Matches `'nonce-<base64>'` in the script-src directive.
481+ * @param {Request } req
482+ * @returns {string | undefined }
483+ */
484+ function getNonce ( req ) {
485+ const csp = req . headers . get ( 'content-security-policy' ) || '' ;
486+ const match = / \b n o n c e - ( [ A - Z a - z 0 - 9 + / = ] + ) / . exec ( csp ) ;
487+ return match ? match [ 1 ] : undefined ;
488+ }
489+
475490/** @param {string } s */
476491function escapeHtml ( s ) {
477492 return String ( s ) . replace ( / & / g, '&' ) . replace ( / < / g, '<' ) ;
0 commit comments