@mandujs/core 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/package.json +94 -69
  2. package/src/auth/__tests__/login.test.ts +419 -0
  3. package/src/auth/__tests__/password.test.ts +122 -0
  4. package/src/auth/__tests__/reset.test.ts +296 -0
  5. package/src/auth/__tests__/tokens.test.ts +274 -0
  6. package/src/auth/__tests__/verification.test.ts +274 -0
  7. package/src/auth/index.ts +76 -0
  8. package/src/auth/login.ts +225 -0
  9. package/src/auth/password.ts +120 -0
  10. package/src/auth/reset.ts +243 -0
  11. package/src/auth/tokens.ts +612 -0
  12. package/src/auth/verification.ts +253 -0
  13. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
  14. package/src/bundler/__tests__/cold-start.test.ts +504 -0
  15. package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
  16. package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
  17. package/src/bundler/__tests__/extended-watch.test.ts +710 -0
  18. package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
  19. package/src/bundler/__tests__/hdr.test.ts +353 -0
  20. package/src/bundler/__tests__/hmr-client.test.ts +532 -0
  21. package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
  22. package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
  23. package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
  24. package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
  25. package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
  26. package/src/bundler/build.test.ts +8 -1
  27. package/src/bundler/build.ts +310 -18
  28. package/src/bundler/css.ts +326 -323
  29. package/src/bundler/dev.ts +1611 -59
  30. package/src/bundler/fast-refresh-plugin.ts +307 -0
  31. package/src/bundler/hmr-types.ts +252 -0
  32. package/src/bundler/manifest-schema.ts +301 -0
  33. package/src/bundler/safe-build.test.ts +128 -0
  34. package/src/bundler/safe-build.ts +77 -0
  35. package/src/bundler/scenario-matrix.ts +229 -0
  36. package/src/bundler/types.ts +11 -0
  37. package/src/bundler/vendor-cache-types.ts +130 -0
  38. package/src/bundler/vendor-cache.ts +526 -0
  39. package/src/client/router.ts +214 -56
  40. package/src/db/__tests__/db.test.ts +485 -0
  41. package/src/db/index.ts +513 -0
  42. package/src/db/migrations/__tests__/runner.test.ts +661 -0
  43. package/src/db/migrations/history-table.ts +345 -0
  44. package/src/db/migrations/lock.ts +269 -0
  45. package/src/db/migrations/runner.ts +633 -0
  46. package/src/desktop/__tests__/smoke.test.ts +100 -0
  47. package/src/desktop/__tests__/window.test.ts +172 -0
  48. package/src/desktop/__tests__/worker.test.ts +266 -0
  49. package/src/desktop/index.ts +43 -0
  50. package/src/desktop/types.ts +158 -0
  51. package/src/desktop/window.ts +492 -0
  52. package/src/desktop/worker.ts +180 -0
  53. package/src/email/__tests__/email.test.ts +355 -0
  54. package/src/email/index.ts +282 -0
  55. package/src/email/resend.ts +163 -0
  56. package/src/email/smtp.ts +64 -0
  57. package/src/filling/__tests__/session-sqlite.test.ts +454 -0
  58. package/src/filling/context.ts +72 -78
  59. package/src/filling/cookie-codec.ts +299 -0
  60. package/src/filling/deps.ts +25 -1
  61. package/src/filling/filling.ts +28 -3
  62. package/src/filling/session-sqlite.ts +617 -0
  63. package/src/filling/session.ts +265 -216
  64. package/src/guard/decision-memory.test.ts +52 -22
  65. package/src/id/__tests__/id.test.ts +120 -0
  66. package/src/id/index.ts +105 -0
  67. package/src/kitchen/index.ts +2 -2
  68. package/src/kitchen/kitchen-handler.ts +86 -0
  69. package/src/kitchen/stream/activity-sse.ts +2 -1
  70. package/src/middleware/csrf.ts +328 -0
  71. package/src/middleware/index.ts +40 -0
  72. package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
  73. package/src/middleware/oauth/index.ts +505 -0
  74. package/src/middleware/oauth/providers.ts +115 -0
  75. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
  76. package/src/middleware/rate-limit/index.ts +522 -0
  77. package/src/middleware/rate-limit/sqlite-store.ts +382 -0
  78. package/src/middleware/secure/__tests__/secure.test.ts +360 -0
  79. package/src/middleware/secure/csp.ts +193 -0
  80. package/src/middleware/secure/index.ts +417 -0
  81. package/src/middleware/session.ts +174 -0
  82. package/src/observability/event-bus.ts +81 -79
  83. package/src/paths.ts +37 -0
  84. package/src/perf/hmr-markers.ts +215 -0
  85. package/src/perf/index.ts +104 -0
  86. package/src/resource/__tests__/generator.test.ts +603 -2
  87. package/src/resource/ddl/__tests__/diff.test.ts +639 -0
  88. package/src/resource/ddl/__tests__/emit.test.ts +799 -0
  89. package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
  90. package/src/resource/ddl/diff.ts +392 -0
  91. package/src/resource/ddl/emit.ts +548 -0
  92. package/src/resource/ddl/persistence-types.ts +218 -0
  93. package/src/resource/ddl/snapshot.ts +447 -0
  94. package/src/resource/ddl/type-map.ts +223 -0
  95. package/src/resource/ddl/types.ts +232 -0
  96. package/src/resource/generator-repo.ts +610 -0
  97. package/src/resource/generator-schema.ts +476 -0
  98. package/src/resource/generator.ts +117 -1
  99. package/src/resource/index.ts +17 -1
  100. package/src/resource/schema.ts +30 -0
  101. package/src/router/fs-scanner.ts +3 -0
  102. package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
  103. package/src/runtime/__tests__/hdr-client.test.ts +223 -0
  104. package/src/runtime/__tests__/http-errors.test.ts +117 -0
  105. package/src/runtime/__tests__/not-found.test.ts +152 -0
  106. package/src/runtime/boundary.tsx +21 -1
  107. package/src/runtime/fast-refresh-runtime.ts +322 -0
  108. package/src/runtime/fast-refresh-types.ts +128 -0
  109. package/src/runtime/hmr-client.ts +409 -0
  110. package/src/runtime/http-errors.ts +113 -0
  111. package/src/runtime/index.ts +6 -0
  112. package/src/runtime/logger.ts +678 -677
  113. package/src/runtime/not-found.ts +93 -0
  114. package/src/runtime/redirect.ts +133 -0
  115. package/src/runtime/server.ts +518 -20
  116. package/src/runtime/ssr.ts +340 -10
  117. package/src/runtime/streaming-ssr.ts +222 -19
  118. package/src/scheduler/__tests__/scheduler.test.ts +514 -0
  119. package/src/scheduler/index.ts +343 -0
  120. package/src/storage/s3/__tests__/s3.test.ts +479 -0
  121. package/src/storage/s3/index.ts +412 -0
  122. package/src/testing/index.ts +58 -0
@@ -13,6 +13,7 @@ import { getRenderToReadableStream } from "./react-renderer";
13
13
  import type { ReactElement, ReactNode } from "react";
14
14
  import React, { Suspense } from "react";
15
15
  import type { BundleManifest } from "../bundler/types";
16
+ import { isSafeManduUrl } from "../bundler/manifest-schema";
16
17
  import type { HydrationConfig, HydrationPriority } from "../spec/schema";
17
18
  import { serializeProps } from "../client/serialize";
18
19
  import type { Metadata, MetadataItem } from "../seo/types";
@@ -21,6 +22,8 @@ import { PORTS, TIMEOUTS } from "../constants";
21
22
  import { escapeHtmlAttr, escapeHtmlText, escapeJsonForInlineScript, escapeJsString } from "./escape";
22
23
  import { REACT_INTERNALS_SHIM_SCRIPT } from "./shims";
23
24
  import { getRenderToString } from "./react-renderer";
25
+ import { mark, measure } from "../perf";
26
+ import { generateFastRefreshPreamble } from "../bundler/dev";
24
27
 
25
28
  // ========== Types ==========
26
29
 
@@ -141,6 +144,16 @@ export interface StreamingSSROptions {
141
144
  _skipHtmlClose?: boolean;
142
145
  /** CSS 파일 경로 (자동 주입, 기본: /.mandu/client/globals.css) */
143
146
  cssPath?: string | false;
147
+ /**
148
+ * Phase 7.2 R1 Agent C (H1) — Content-Security-Policy nonce for the
149
+ * Fast Refresh inline preamble. Mirrors `SSROptions.cspNonce`:
150
+ * - `true` → auto-generate fresh 128-bit base64 nonce
151
+ * - non-empty string → use caller-provided nonce verbatim
152
+ * - `false` / unset → no nonce attribute / CSP header (legacy)
153
+ * Also forced off when `MANDU_CSP_NONCE=0` env is set.
154
+ * Dev + hydration + populated `shared.fastRefresh` only.
155
+ */
156
+ cspNonce?: string | boolean;
144
157
  }
145
158
 
146
159
  export interface StreamingLoaderResult<T = unknown> {
@@ -374,6 +387,71 @@ export function DeferredData<T>({
374
387
  /**
375
388
  * Streaming용 HTML Shell 생성 (<!DOCTYPE> ~ <div id="root">)
376
389
  */
390
+ // ============================================================================
391
+ // Phase 7.2 R1 Agent C (H1) — CSP nonce helpers. Mirror the ones in
392
+ // ssr.ts but live locally here so Streaming SSR does not depend on
393
+ // internals of the non-streaming path. Keep the implementations in
394
+ // sync; the single source of truth is documented in the ssr.ts
395
+ // equivalents.
396
+ // ============================================================================
397
+
398
+ /** 16-byte (128-bit) base64 nonce, matching csp.ts#resolveNonce. */
399
+ function generateStreamingCspNonce(): string {
400
+ const bytes = new Uint8Array(16);
401
+ crypto.getRandomValues(bytes);
402
+ let bin = "";
403
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
404
+ return btoa(bin);
405
+ }
406
+
407
+ /**
408
+ * Resolve the nonce option honoring `MANDU_CSP_NONCE=0` opt-out. See
409
+ * `ssr.ts#resolveFastRefreshCspNonce` for the rationale.
410
+ */
411
+ function resolveStreamingCspNonce(
412
+ opt: StreamingSSROptions["cspNonce"],
413
+ ): string | undefined {
414
+ try {
415
+ if (typeof process !== "undefined" && process.env && process.env.MANDU_CSP_NONCE === "0") {
416
+ return undefined;
417
+ }
418
+ } catch {
419
+ /* sandboxed process access — treat as unset */
420
+ }
421
+ if (opt === false || opt === undefined) return undefined;
422
+ if (typeof opt === "string" && opt.length > 0) return opt;
423
+ if (opt === true) return generateStreamingCspNonce();
424
+ return undefined;
425
+ }
426
+
427
+ /** Mirror of ssr.ts#buildFastRefreshCspHeader. */
428
+ function buildStreamingCspHeader(nonce: string): string {
429
+ const safe = nonce.replace(/["\\\r\n]/g, "");
430
+ return `script-src 'self' 'nonce-${safe}' 'strict-dynamic'`;
431
+ }
432
+
433
+ /**
434
+ * WeakMap that ferries the nonce chosen inside `renderToStream` back
435
+ * to `renderStreamingResponse` / `renderWithDeferredData` so they can
436
+ * emit the matching CSP header on the Response. Keyed by the options
437
+ * object identity — typical usage shares the same instance across
438
+ * the streaming pipeline. WeakMap entries GC naturally.
439
+ */
440
+ const STREAMING_OPTIONS_TO_NONCE = new WeakMap<object, string>();
441
+
442
+ /** @internal test helper — use only from unit tests. */
443
+ export const _testOnly_generateStreamingCspNonce = generateStreamingCspNonce;
444
+ /** @internal test helper — use only from unit tests. */
445
+ export const _testOnly_resolveStreamingCspNonce = resolveStreamingCspNonce;
446
+ /** @internal test helper — use only from unit tests. */
447
+ export const _testOnly_buildStreamingCspHeader = buildStreamingCspHeader;
448
+ /** @internal test helper — use only from unit tests. */
449
+ export function _testOnly_getStreamingAttachedCspNonce(
450
+ options: StreamingSSROptions,
451
+ ): string | undefined {
452
+ return STREAMING_OPTIONS_TO_NONCE.get(options as object);
453
+ }
454
+
377
455
  function generateHTMLShell(options: StreamingSSROptions): string {
378
456
  const {
379
457
  title = "Mandu App",
@@ -432,6 +510,37 @@ function generateHTMLShell(options: StreamingSSROptions): string {
432
510
  islandOpenTag = `<div data-mandu-island="${escapeHtmlAttr(routeId)}" data-mandu-src="${escapeHtmlAttr(bundleSrc)}" data-mandu-priority="${escapeHtmlAttr(priority)}" style="display:contents">`;
433
511
  }
434
512
 
513
+ // Phase 7.1 R2 Agent D: Fast Refresh preamble. Must land in <head>
514
+ // BEFORE any island script evaluates — the stubs it installs for
515
+ // $RefreshReg$ / $RefreshSig$ are required by every module the
516
+ // bundler transformed with `reactFastRefresh: true`. Dev mode only;
517
+ // prod manifests omit `shared.fastRefresh` so `fr` is undefined and
518
+ // we emit no preamble (HTML stays byte-identical to pre-7.1 prod).
519
+ //
520
+ // Phase 7.2 R1 Agent C (H1): when `cspNonce` is enabled, resolve
521
+ // the nonce up-front, ferry it to the response layer via the
522
+ // WeakMap, and rewrite the preamble's `<script>` to
523
+ // `<script nonce="...">`.
524
+ let fastRefreshPreamble = "";
525
+ if (isDev && needsHydration) {
526
+ const fr = bundleManifest.shared?.fastRefresh;
527
+ // Phase 7.2.R3 M-01 — manifest wire-up. Reject tampered entries
528
+ // (protocol, traversal, non-/.mandu paths, >2KB) — fail closed so
529
+ // an attacker-controlled <script src> cannot slip in via a
530
+ // manipulated .mandu/manifest.json.
531
+ if (fr && fr.glue && fr.runtime && isSafeManduUrl(fr.glue) && isSafeManduUrl(fr.runtime)) {
532
+ fastRefreshPreamble = generateFastRefreshPreamble(fr.glue, fr.runtime);
533
+ const resolvedNonce = resolveStreamingCspNonce(options.cspNonce);
534
+ if (resolvedNonce) {
535
+ STREAMING_OPTIONS_TO_NONCE.set(options as object, resolvedNonce);
536
+ fastRefreshPreamble = fastRefreshPreamble.replace(
537
+ /<script>/,
538
+ `<script nonce="${escapeHtmlAttr(resolvedNonce)}">`,
539
+ );
540
+ }
541
+ }
542
+ }
543
+
435
544
  // Import map은 module 스크립트보다 먼저 정의되어야 bare specifier 해석 가능
436
545
  return `<!DOCTYPE html>
437
546
  <html lang="${escapeHtmlAttr(lang)}">
@@ -443,6 +552,7 @@ function generateHTMLShell(options: StreamingSSROptions): string {
443
552
  ${loadingStyles}
444
553
  ${importMapScript}
445
554
  ${headTags}
555
+ ${fastRefreshPreamble}
446
556
  </head>
447
557
  <body>
448
558
  <div id="root">${islandOpenTag}`;
@@ -600,6 +710,9 @@ function generateDeferredDataScript(routeId: string, key: string, data: unknown)
600
710
  /**
601
711
  * HMR 스크립트 생성
602
712
  * ssr.ts의 generateHMRScript와 동일한 구현을 유지해야 함 (#114)
713
+ *
714
+ * Phase 7.2 — mirrors the HDR (Hot Data Revalidation) extension in
715
+ * `ssr.ts:generateHMRScript`. Keep in sync.
603
716
  */
604
717
  function generateHMRScript(port: number): string {
605
718
  const hmrPort = port + PORTS.HMR_OFFSET;
@@ -619,6 +732,66 @@ window.__MANDU_HMR_PORT__ = ${hmrPort};
619
732
  }
620
733
  }
621
734
 
735
+ function hdrCurrentRouteId() {
736
+ var rs = window.__MANDU_ROUTER_STATE__;
737
+ if (rs && rs.currentRoute && rs.currentRoute.id) return String(rs.currentRoute.id);
738
+ var r = window.__MANDU_ROUTE__;
739
+ if (r && r.id) return String(r.id);
740
+ return null;
741
+ }
742
+
743
+ function hdrFallback(reason) {
744
+ console.log('[Mandu HDR] Fallback full reload' + (reason ? ' (' + reason + ')' : ''));
745
+ location.reload();
746
+ }
747
+
748
+ function hdrMark(name, data) {
749
+ try {
750
+ if (window.__MANDU_HDR__ && typeof window.__MANDU_HDR__.perfMark === 'function') {
751
+ window.__MANDU_HDR__.perfMark(name, data);
752
+ }
753
+ } catch (_) {}
754
+ }
755
+
756
+ function handleSlotRefetch(data) {
757
+ var routeId = data && typeof data.routeId === 'string' ? data.routeId : null;
758
+ if (!routeId) { hdrFallback('no-routeId'); return; }
759
+ if (window.__MANDU_HDR_DISABLED__ === true) { hdrFallback('disabled'); return; }
760
+ var currentId = hdrCurrentRouteId();
761
+ if (currentId !== routeId) {
762
+ console.log('[Mandu HDR] slot-refetch for ' + routeId + ' ignored (current route: ' + currentId + ')');
763
+ return;
764
+ }
765
+ var started = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
766
+ hdrMark('hdr:refetch-start', { routeId: routeId, slotPath: data.slotPath });
767
+ var url = window.location.pathname + window.location.search;
768
+ var sep = url.indexOf('?') >= 0 ? '&' : '?';
769
+ var dataUrl = url + sep + '_data=1';
770
+ fetch(dataUrl, { credentials: 'same-origin', headers: { 'X-Mandu-HDR': '1' } })
771
+ .then(function (res) {
772
+ if (!res.ok) { hdrFallback('status-' + res.status); return null; }
773
+ return res.json();
774
+ })
775
+ .then(function (payload) {
776
+ if (!payload) return;
777
+ var revalidate = window.__MANDU_ROUTER_REVALIDATE__;
778
+ if (typeof revalidate !== 'function') { hdrFallback('no-router'); return; }
779
+ try {
780
+ revalidate(routeId, payload.loaderData);
781
+ var elapsed = (typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now()) - started;
782
+ console.log('[Mandu HDR] Applied loader data for ' + routeId + ' in ' + elapsed.toFixed(0) + 'ms');
783
+ hdrMark('hdr:refetch', { routeId: routeId, slotPath: data.slotPath, elapsed: elapsed });
784
+ } catch (err) {
785
+ console.error('[Mandu HDR] Revalidate threw:', err);
786
+ hdrFallback('revalidate-throw');
787
+ }
788
+ })
789
+ .catch(function (err) {
790
+ console.error('[Mandu HDR] Fetch failed:', err);
791
+ hdrFallback('fetch-failed');
792
+ });
793
+ }
794
+
622
795
  function connect() {
623
796
  try {
624
797
  ws = new WebSocket('ws://' + window.location.hostname + ':${hmrPort}');
@@ -629,9 +802,21 @@ window.__MANDU_HMR_PORT__ = ${hmrPort};
629
802
  ws.onmessage = function(e) {
630
803
  try {
631
804
  var msg = JSON.parse(e.data);
632
- if (msg.type === 'reload' || msg.type === 'island-update') {
805
+ if ((msg.type === 'vite' || msg.type === 'vite-replay') && msg.payload) {
806
+ if (msg.payload.type === 'custom' && msg.payload.event === 'mandu:slot-refetch') {
807
+ handleSlotRefetch(msg.payload.data || {});
808
+ return;
809
+ }
810
+ if (msg.payload.type === 'full-reload') {
811
+ location.reload();
812
+ return;
813
+ }
814
+ }
815
+ if (msg.type === 'reload' || msg.type === 'island-update' || msg.type === 'full-reload' || msg.type === 'layout-update' || msg.type === 'invalidate') {
633
816
  console.log('[Mandu HMR] Reloading...');
634
817
  location.reload();
818
+ } else if (msg.type === 'slot-refetch') {
819
+ handleSlotRefetch(msg.data || {});
635
820
  } else if (msg.type === 'css-update') {
636
821
  var cssPath = (msg.data && msg.data.cssPath) || '/.mandu/client/globals.css';
637
822
  var links = document.querySelectorAll('link[rel="stylesheet"]');
@@ -676,6 +861,7 @@ export async function renderToStream(
676
861
  element: ReactElement,
677
862
  options: StreamingSSROptions = {}
678
863
  ): Promise<ReadableStream<Uint8Array>> {
864
+ mark("ssr:render");
679
865
  const {
680
866
  onShellReady,
681
867
  onAllReady,
@@ -827,6 +1013,7 @@ export async function renderToStream(
827
1013
  controller.enqueue(encoder.encode(htmlShell));
828
1014
  shellSent = true;
829
1015
  metrics.shellReadyTime = Date.now() - metrics.startTime;
1016
+ measure("ssr:render", "ssr:render");
830
1017
  onShellReady?.();
831
1018
  },
832
1019
 
@@ -966,19 +1153,27 @@ export async function renderStreamingResponse(
966
1153
  try {
967
1154
  const stream = await renderToStream(element, options);
968
1155
 
1156
+ // Phase 7.2 R1 Agent C (H1): if a CSP nonce was produced during
1157
+ // shell generation, emit the matching header so the inline
1158
+ // preamble is authorized under strict CSP.
1159
+ const nonce = STREAMING_OPTIONS_TO_NONCE.get(options as object);
1160
+ const baseHeaders: Record<string, string> = {
1161
+ "Content-Type": "text/html; charset=utf-8",
1162
+ // Transfer-Encoding은 런타임이 자동 처리 (명시 안 함)
1163
+ "X-Content-Type-Options": "nosniff",
1164
+ // nginx 버퍼링 비활성화 힌트
1165
+ "X-Accel-Buffering": "no",
1166
+ // 캐시 및 변환 방지 (Streaming은 동적)
1167
+ "Cache-Control": "no-store, no-transform",
1168
+ // CDN 힌트
1169
+ "CDN-Cache-Control": "no-store",
1170
+ };
1171
+ if (nonce) {
1172
+ baseHeaders["Content-Security-Policy"] = buildStreamingCspHeader(nonce);
1173
+ }
969
1174
  return new Response(stream, {
970
1175
  status: 200,
971
- headers: {
972
- "Content-Type": "text/html; charset=utf-8",
973
- // Transfer-Encoding은 런타임이 자동 처리 (명시 안 함)
974
- "X-Content-Type-Options": "nosniff",
975
- // nginx 버퍼링 비활성화 힌트
976
- "X-Accel-Buffering": "no",
977
- // 캐시 및 변환 방지 (Streaming은 동적)
978
- "Cache-Control": "no-store, no-transform",
979
- // CDN 힌트
980
- "CDN-Cache-Control": "no-store",
981
- },
1176
+ headers: baseHeaders,
982
1177
  });
983
1178
  } catch (error) {
984
1179
  // renderToStream에서 throw된 에러 → 500 응답 (단일 책임)
@@ -1163,15 +1358,23 @@ export async function renderWithDeferredData(
1163
1358
  },
1164
1359
  });
1165
1360
 
1361
+ // Phase 7.2 R1 Agent C (H1): if the base stream produced a CSP
1362
+ // nonce during shell emission, forward the matching header on the
1363
+ // deferred Response as well.
1364
+ const deferredNonce = STREAMING_OPTIONS_TO_NONCE.get(options as object);
1365
+ const deferredHeaders: Record<string, string> = {
1366
+ "Content-Type": "text/html; charset=utf-8",
1367
+ "X-Content-Type-Options": "nosniff",
1368
+ "X-Accel-Buffering": "no",
1369
+ "Cache-Control": "no-store, no-transform",
1370
+ "CDN-Cache-Control": "no-store",
1371
+ };
1372
+ if (deferredNonce) {
1373
+ deferredHeaders["Content-Security-Policy"] = buildStreamingCspHeader(deferredNonce);
1374
+ }
1166
1375
  return new Response(finalStream, {
1167
1376
  status: 200,
1168
- headers: {
1169
- "Content-Type": "text/html; charset=utf-8",
1170
- "X-Content-Type-Options": "nosniff",
1171
- "X-Accel-Buffering": "no",
1172
- "Cache-Control": "no-store, no-transform",
1173
- "CDN-Cache-Control": "no-store",
1174
- },
1377
+ headers: deferredHeaders,
1175
1378
  });
1176
1379
  }
1177
1380