@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.
- package/package.json +94 -69
- package/src/auth/__tests__/login.test.ts +419 -0
- package/src/auth/__tests__/password.test.ts +122 -0
- package/src/auth/__tests__/reset.test.ts +296 -0
- package/src/auth/__tests__/tokens.test.ts +274 -0
- package/src/auth/__tests__/verification.test.ts +274 -0
- package/src/auth/index.ts +76 -0
- package/src/auth/login.ts +225 -0
- package/src/auth/password.ts +120 -0
- package/src/auth/reset.ts +243 -0
- package/src/auth/tokens.ts +612 -0
- package/src/auth/verification.ts +253 -0
- package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
- package/src/bundler/__tests__/cold-start.test.ts +504 -0
- package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
- package/src/bundler/__tests__/extended-watch.test.ts +710 -0
- package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
- package/src/bundler/__tests__/hdr.test.ts +353 -0
- package/src/bundler/__tests__/hmr-client.test.ts +532 -0
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
- package/src/bundler/build.test.ts +8 -1
- package/src/bundler/build.ts +310 -18
- package/src/bundler/css.ts +326 -323
- package/src/bundler/dev.ts +1611 -59
- package/src/bundler/fast-refresh-plugin.ts +307 -0
- package/src/bundler/hmr-types.ts +252 -0
- package/src/bundler/manifest-schema.ts +301 -0
- package/src/bundler/safe-build.test.ts +128 -0
- package/src/bundler/safe-build.ts +77 -0
- package/src/bundler/scenario-matrix.ts +229 -0
- package/src/bundler/types.ts +11 -0
- package/src/bundler/vendor-cache-types.ts +130 -0
- package/src/bundler/vendor-cache.ts +526 -0
- package/src/client/router.ts +214 -56
- package/src/db/__tests__/db.test.ts +485 -0
- package/src/db/index.ts +513 -0
- package/src/db/migrations/__tests__/runner.test.ts +661 -0
- package/src/db/migrations/history-table.ts +345 -0
- package/src/db/migrations/lock.ts +269 -0
- package/src/db/migrations/runner.ts +633 -0
- package/src/desktop/__tests__/smoke.test.ts +100 -0
- package/src/desktop/__tests__/window.test.ts +172 -0
- package/src/desktop/__tests__/worker.test.ts +266 -0
- package/src/desktop/index.ts +43 -0
- package/src/desktop/types.ts +158 -0
- package/src/desktop/window.ts +492 -0
- package/src/desktop/worker.ts +180 -0
- package/src/email/__tests__/email.test.ts +355 -0
- package/src/email/index.ts +282 -0
- package/src/email/resend.ts +163 -0
- package/src/email/smtp.ts +64 -0
- package/src/filling/__tests__/session-sqlite.test.ts +454 -0
- package/src/filling/context.ts +72 -78
- package/src/filling/cookie-codec.ts +299 -0
- package/src/filling/deps.ts +25 -1
- package/src/filling/filling.ts +28 -3
- package/src/filling/session-sqlite.ts +617 -0
- package/src/filling/session.ts +265 -216
- package/src/guard/decision-memory.test.ts +52 -22
- package/src/id/__tests__/id.test.ts +120 -0
- package/src/id/index.ts +105 -0
- package/src/kitchen/index.ts +2 -2
- package/src/kitchen/kitchen-handler.ts +86 -0
- package/src/kitchen/stream/activity-sse.ts +2 -1
- package/src/middleware/csrf.ts +328 -0
- package/src/middleware/index.ts +40 -0
- package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
- package/src/middleware/oauth/index.ts +505 -0
- package/src/middleware/oauth/providers.ts +115 -0
- package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
- package/src/middleware/rate-limit/index.ts +522 -0
- package/src/middleware/rate-limit/sqlite-store.ts +382 -0
- package/src/middleware/secure/__tests__/secure.test.ts +360 -0
- package/src/middleware/secure/csp.ts +193 -0
- package/src/middleware/secure/index.ts +417 -0
- package/src/middleware/session.ts +174 -0
- package/src/observability/event-bus.ts +81 -79
- package/src/paths.ts +37 -0
- package/src/perf/hmr-markers.ts +215 -0
- package/src/perf/index.ts +104 -0
- package/src/resource/__tests__/generator.test.ts +603 -2
- package/src/resource/ddl/__tests__/diff.test.ts +639 -0
- package/src/resource/ddl/__tests__/emit.test.ts +799 -0
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
- package/src/resource/ddl/diff.ts +392 -0
- package/src/resource/ddl/emit.ts +548 -0
- package/src/resource/ddl/persistence-types.ts +218 -0
- package/src/resource/ddl/snapshot.ts +447 -0
- package/src/resource/ddl/type-map.ts +223 -0
- package/src/resource/ddl/types.ts +232 -0
- package/src/resource/generator-repo.ts +610 -0
- package/src/resource/generator-schema.ts +476 -0
- package/src/resource/generator.ts +117 -1
- package/src/resource/index.ts +17 -1
- package/src/resource/schema.ts +30 -0
- package/src/router/fs-scanner.ts +3 -0
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
- package/src/runtime/__tests__/hdr-client.test.ts +223 -0
- package/src/runtime/__tests__/http-errors.test.ts +117 -0
- package/src/runtime/__tests__/not-found.test.ts +152 -0
- package/src/runtime/boundary.tsx +21 -1
- package/src/runtime/fast-refresh-runtime.ts +322 -0
- package/src/runtime/fast-refresh-types.ts +128 -0
- package/src/runtime/hmr-client.ts +409 -0
- package/src/runtime/http-errors.ts +113 -0
- package/src/runtime/index.ts +6 -0
- package/src/runtime/logger.ts +678 -677
- package/src/runtime/not-found.ts +93 -0
- package/src/runtime/redirect.ts +133 -0
- package/src/runtime/server.ts +518 -20
- package/src/runtime/ssr.ts +340 -10
- package/src/runtime/streaming-ssr.ts +222 -19
- package/src/scheduler/__tests__/scheduler.test.ts +514 -0
- package/src/scheduler/index.ts +343 -0
- package/src/storage/s3/__tests__/s3.test.ts +479 -0
- package/src/storage/s3/index.ts +412 -0
- 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 === '
|
|
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
|
|