@mandujs/core 0.20.10 → 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 (127) hide show
  1. package/README.md +2 -1
  2. package/package.json +28 -3
  3. package/src/auth/__tests__/login.test.ts +419 -0
  4. package/src/auth/__tests__/password.test.ts +122 -0
  5. package/src/auth/__tests__/reset.test.ts +296 -0
  6. package/src/auth/__tests__/tokens.test.ts +274 -0
  7. package/src/auth/__tests__/verification.test.ts +274 -0
  8. package/src/auth/index.ts +76 -0
  9. package/src/auth/login.ts +225 -0
  10. package/src/auth/password.ts +120 -0
  11. package/src/auth/reset.ts +243 -0
  12. package/src/auth/tokens.ts +612 -0
  13. package/src/auth/verification.ts +253 -0
  14. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
  15. package/src/bundler/__tests__/cold-start.test.ts +504 -0
  16. package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
  17. package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
  18. package/src/bundler/__tests__/extended-watch.test.ts +710 -0
  19. package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
  20. package/src/bundler/__tests__/hdr.test.ts +353 -0
  21. package/src/bundler/__tests__/hmr-client.test.ts +532 -0
  22. package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
  23. package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
  24. package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
  25. package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
  26. package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
  27. package/src/bundler/build.test.ts +8 -1
  28. package/src/bundler/build.ts +495 -37
  29. package/src/bundler/css.ts +326 -323
  30. package/src/bundler/dev.ts +1671 -80
  31. package/src/bundler/fast-refresh-plugin.ts +307 -0
  32. package/src/bundler/hmr-types.ts +252 -0
  33. package/src/bundler/manifest-schema.ts +301 -0
  34. package/src/bundler/safe-build.test.ts +128 -0
  35. package/src/bundler/safe-build.ts +77 -0
  36. package/src/bundler/scenario-matrix.ts +229 -0
  37. package/src/bundler/types.ts +19 -0
  38. package/src/bundler/vendor-cache-types.ts +130 -0
  39. package/src/bundler/vendor-cache.ts +526 -0
  40. package/src/client/router.ts +214 -56
  41. package/src/config/validate.ts +1 -0
  42. package/src/db/__tests__/db.test.ts +485 -0
  43. package/src/db/index.ts +513 -0
  44. package/src/db/migrations/__tests__/runner.test.ts +661 -0
  45. package/src/db/migrations/history-table.ts +345 -0
  46. package/src/db/migrations/lock.ts +269 -0
  47. package/src/db/migrations/runner.ts +633 -0
  48. package/src/desktop/__tests__/smoke.test.ts +100 -0
  49. package/src/desktop/__tests__/window.test.ts +172 -0
  50. package/src/desktop/__tests__/worker.test.ts +266 -0
  51. package/src/desktop/index.ts +43 -0
  52. package/src/desktop/types.ts +158 -0
  53. package/src/desktop/window.ts +492 -0
  54. package/src/desktop/worker.ts +180 -0
  55. package/src/devtools/ai/mcp-connector.ts +18 -16
  56. package/src/devtools/client/components/mandu-character.tsx +4 -1
  57. package/src/devtools/client/components/panel/panel-container.tsx +20 -5
  58. package/src/email/__tests__/email.test.ts +355 -0
  59. package/src/email/index.ts +282 -0
  60. package/src/email/resend.ts +163 -0
  61. package/src/email/smtp.ts +64 -0
  62. package/src/filling/__tests__/session-sqlite.test.ts +454 -0
  63. package/src/filling/context.ts +72 -78
  64. package/src/filling/cookie-codec.ts +299 -0
  65. package/src/filling/deps.ts +25 -1
  66. package/src/filling/filling.ts +28 -3
  67. package/src/filling/session-sqlite.ts +617 -0
  68. package/src/filling/session.ts +265 -216
  69. package/src/guard/decision-memory.test.ts +52 -22
  70. package/src/id/__tests__/id.test.ts +120 -0
  71. package/src/id/index.ts +105 -0
  72. package/src/kitchen/index.ts +2 -2
  73. package/src/kitchen/kitchen-handler.ts +86 -0
  74. package/src/kitchen/stream/activity-sse.ts +2 -1
  75. package/src/middleware/csrf.ts +328 -0
  76. package/src/middleware/index.ts +40 -0
  77. package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
  78. package/src/middleware/oauth/index.ts +505 -0
  79. package/src/middleware/oauth/providers.ts +115 -0
  80. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
  81. package/src/middleware/rate-limit/index.ts +522 -0
  82. package/src/middleware/rate-limit/sqlite-store.ts +382 -0
  83. package/src/middleware/secure/__tests__/secure.test.ts +360 -0
  84. package/src/middleware/secure/csp.ts +193 -0
  85. package/src/middleware/secure/index.ts +417 -0
  86. package/src/middleware/session.ts +174 -0
  87. package/src/observability/event-bus.ts +81 -79
  88. package/src/paths.ts +37 -0
  89. package/src/perf/hmr-markers.ts +215 -0
  90. package/src/perf/index.ts +104 -0
  91. package/src/resource/__tests__/generator.test.ts +603 -2
  92. package/src/resource/ddl/__tests__/diff.test.ts +639 -0
  93. package/src/resource/ddl/__tests__/emit.test.ts +799 -0
  94. package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
  95. package/src/resource/ddl/diff.ts +392 -0
  96. package/src/resource/ddl/emit.ts +548 -0
  97. package/src/resource/ddl/persistence-types.ts +218 -0
  98. package/src/resource/ddl/snapshot.ts +447 -0
  99. package/src/resource/ddl/type-map.ts +223 -0
  100. package/src/resource/ddl/types.ts +232 -0
  101. package/src/resource/generator-repo.ts +610 -0
  102. package/src/resource/generator-schema.ts +476 -0
  103. package/src/resource/generator.ts +117 -1
  104. package/src/resource/index.ts +17 -1
  105. package/src/resource/schema.ts +30 -0
  106. package/src/router/fs-scanner.ts +3 -0
  107. package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
  108. package/src/runtime/__tests__/hdr-client.test.ts +223 -0
  109. package/src/runtime/__tests__/http-errors.test.ts +117 -0
  110. package/src/runtime/__tests__/not-found.test.ts +152 -0
  111. package/src/runtime/boundary.tsx +21 -1
  112. package/src/runtime/fast-refresh-runtime.ts +322 -0
  113. package/src/runtime/fast-refresh-types.ts +128 -0
  114. package/src/runtime/hmr-client.ts +409 -0
  115. package/src/runtime/http-errors.ts +113 -0
  116. package/src/runtime/index.ts +6 -0
  117. package/src/runtime/logger.ts +678 -677
  118. package/src/runtime/not-found.ts +93 -0
  119. package/src/runtime/redirect.ts +133 -0
  120. package/src/runtime/server.ts +679 -23
  121. package/src/runtime/ssr.ts +340 -10
  122. package/src/runtime/streaming-ssr.ts +222 -19
  123. package/src/scheduler/__tests__/scheduler.test.ts +514 -0
  124. package/src/scheduler/index.ts +343 -0
  125. package/src/storage/s3/__tests__/s3.test.ts +479 -0
  126. package/src/storage/s3/index.ts +412 -0
  127. package/src/testing/index.ts +247 -189
@@ -3,10 +3,12 @@ import { serializeProps } from "../client/serialize";
3
3
  import { createRequire } from "module";
4
4
  import type { ReactElement } from "react";
5
5
  import type { BundleManifest } from "../bundler/types";
6
+ import { isSafeManduUrl } from "../bundler/manifest-schema";
6
7
  import type { HydrationConfig, HydrationPriority } from "../spec/schema";
7
8
  import { PORTS, TIMEOUTS } from "../constants";
8
9
  import { escapeHtmlAttr, escapeHtmlText, escapeJsonForInlineScript } from "./escape";
9
10
  import { REACT_INTERNALS_SHIM_SCRIPT } from "./shims";
11
+ import { generateFastRefreshPreamble } from "../bundler/dev";
10
12
 
11
13
  // Re-export streaming SSR utilities
12
14
  export {
@@ -50,6 +52,23 @@ export interface SSROptions {
50
52
  cssPath?: string | false;
51
53
  /** Island 래핑이 이미 React 엘리먼트 레벨에서 완료됨 (중복 래핑 방지) */
52
54
  islandPreWrapped?: boolean;
55
+ /**
56
+ * Phase 7.2 R1 Agent C (H1) — Content-Security-Policy nonce for the
57
+ * Fast Refresh inline preamble. Three accepted shapes:
58
+ * - `true` → auto-generate a fresh 128-bit base64 nonce
59
+ * per-render and insert it as the `nonce`
60
+ * attribute on the preamble `<script>` tag.
61
+ * - non-empty string → use the caller-provided nonce verbatim
62
+ * (e.g. one already produced by the
63
+ * `secure()` middleware and stashed on
64
+ * `ctx.get('csp-nonce')`).
65
+ * - `false` / unset → legacy behavior; no nonce attribute, no
66
+ * CSP header emitted. Also the effective
67
+ * behavior when env `MANDU_CSP_NONCE=0`.
68
+ * Only takes effect in dev mode with a populated `shared.fastRefresh`
69
+ * manifest entry — prod builds never emit the preamble at all.
70
+ */
71
+ cspNonce?: string | boolean;
53
72
  }
54
73
 
55
74
  let projectRenderToString: ((element: ReactElement) => string) | null | undefined;
@@ -181,6 +200,184 @@ export function wrapWithIsland(
181
200
  return `<div data-mandu-island="${escapeHtmlAttr(routeId)}"${srcAttr} data-mandu-priority="${escapeHtmlAttr(priority)}" style="display:contents">${content}</div>`;
182
201
  }
183
202
 
203
+ /**
204
+ * Phase 7.1 R2 Agent D — Fast Refresh preamble emission helper.
205
+ * Phase 7.2 R1 Agent C (H1) — optional CSP nonce injection.
206
+ *
207
+ * Emits the `<script>` block returned by `generateFastRefreshPreamble`
208
+ * ONLY when all three preconditions hold:
209
+ *
210
+ * 1. `isDev === true` — the preamble and its glue import rely on the
211
+ * `_fast-refresh-runtime.js` / `_vendor-react-refresh.js` assets
212
+ * which are only emitted by the dev bundler path.
213
+ * 2. `manifest.shared.fastRefresh` is populated — which happens only
214
+ * in dev mode (see `build.ts:1548`). Missing here implies either a
215
+ * prod manifest, a failed vendor shim build, or a unit test that
216
+ * stubbed the manifest. All three short-circuit to empty output.
217
+ * 3. Both the `glue` and `runtime` fields are non-empty strings —
218
+ * `generateFastRefreshPreamble` itself re-checks and emits a
219
+ * defensive stub comment if either is missing.
220
+ *
221
+ * Returned as a string so the caller can position it inside `<head>`
222
+ * BEFORE any `<script type="module">` runs. This matters because
223
+ * `reactFastRefresh: true`-transformed islands call `$RefreshReg$` at
224
+ * the top of the module; the stubs installed inside the preamble must
225
+ * exist before those calls execute, otherwise the island throws a
226
+ * `ReferenceError` during evaluation and never hydrates.
227
+ *
228
+ * Production builds see `fastRefresh` as `undefined` on the manifest
229
+ * (build.ts omits it), so this function returns `""` and the HTML
230
+ * remains byte-identical to pre-7.1 prod output.
231
+ *
232
+ * When `nonce` is a non-empty string, the returned `<script>` opening
233
+ * tag is rewritten to `<script nonce="...">`. This is the single
234
+ * modification point for CSP compliance — the inner body comes verbatim
235
+ * from `generateFastRefreshPreamble` (owned by dev.ts) so the bundler
236
+ * and SSR sides stay decoupled.
237
+ */
238
+ function generateFastRefreshPreambleTag(
239
+ isDev: boolean,
240
+ manifest: BundleManifest | undefined,
241
+ nonce?: string,
242
+ ): string {
243
+ if (!isDev) return "";
244
+ const fr = manifest?.shared?.fastRefresh;
245
+ if (!fr) return "";
246
+ if (!fr.glue || !fr.runtime) return "";
247
+ // Phase 7.2.R3 M-01 — manifest wire-up. `isSafeManduUrl` rejects
248
+ // tampered entries (protocol, traversal, non-/.mandu paths, >2KB).
249
+ // Fail closed: if either URL is suspect, skip the preamble entirely
250
+ // — dev refresh breaks loudly instead of injecting an attacker-
251
+ // controlled <script src> via a tampered manifest.
252
+ if (!isSafeManduUrl(fr.glue) || !isSafeManduUrl(fr.runtime)) return "";
253
+ const raw = generateFastRefreshPreamble(fr.glue, fr.runtime);
254
+ if (!nonce) return raw;
255
+ // Inject nonce attribute onto the first <script> tag ONLY. Matches
256
+ // exactly `<script>` (the shape emitted by `generateFastRefreshPreamble`)
257
+ // to avoid accidentally nonce-ing a `<script src=...>` or malformed
258
+ // variant. If the upstream function changes shape, the regex still
259
+ // fails closed (returns raw) — unit-tested.
260
+ const nonceEscaped = escapeHtmlAttr(nonce);
261
+ return raw.replace(/<script>/, `<script nonce="${nonceEscaped}">`);
262
+ }
263
+
264
+ /**
265
+ * Phase 7.2 R1 Agent C (H1) — Resolve the CSP nonce to use for the Fast
266
+ * Refresh preamble. Three layered precedences (highest first):
267
+ *
268
+ * 1. `MANDU_CSP_NONCE=0` env var forces off (opt-out escape hatch for
269
+ * projects with an existing Content-Security-Policy pipeline that
270
+ * would collide with ours).
271
+ * 2. Explicit `options.cspNonce`:
272
+ * - string → use verbatim
273
+ * - true → auto-generate
274
+ * - false → off
275
+ * 3. No option → off (preserves legacy behavior byte-identical).
276
+ *
277
+ * Auto-generation uses `crypto.getRandomValues` with 16 bytes → 128 bits
278
+ * of entropy, matching OWASP guidance and the existing
279
+ * `@mandujs/core/middleware/secure` CSP nonce generator.
280
+ *
281
+ * Only invoked when we actually intend to emit a preamble (i.e. dev mode
282
+ * + `needsHydration` + populated `shared.fastRefresh`). Returns
283
+ * `undefined` when CSP nonce emission is disabled; the caller short-
284
+ * circuits the `nonce=` attribute and skips the response CSP header.
285
+ */
286
+ function resolveFastRefreshCspNonce(
287
+ opt: SSROptions["cspNonce"],
288
+ ): string | undefined {
289
+ // Opt-out: env var takes absolute precedence. Read lazily because
290
+ // process is not available in some edge runtimes.
291
+ try {
292
+ if (typeof process !== "undefined" && process.env && process.env.MANDU_CSP_NONCE === "0") {
293
+ return undefined;
294
+ }
295
+ } catch {
296
+ /* some runtimes lock down `process` access — treat as unset */
297
+ }
298
+ if (opt === false || opt === undefined) return undefined;
299
+ if (typeof opt === "string" && opt.length > 0) return opt;
300
+ if (opt === true) return generateCspNonce();
301
+ return undefined;
302
+ }
303
+
304
+ /**
305
+ * Phase 7.2 R1 Agent C (H1) — Generate a fresh CSP nonce.
306
+ *
307
+ * 16 bytes (128 bits) of cryptographic entropy, base64-encoded. Matches
308
+ * `packages/core/src/middleware/secure/csp.ts#resolveNonce`. The same
309
+ * encoding keeps the nonce attribute short (≤24 base64 chars) and
310
+ * compatible with OWASP CSP3 guidance.
311
+ *
312
+ * Exported as `_testOnly_generateCspNonce` below for tests to verify
313
+ * entropy and encoding without depending on the specific platform.
314
+ */
315
+ function generateCspNonce(): string {
316
+ const bytes = new Uint8Array(16);
317
+ crypto.getRandomValues(bytes);
318
+ // Bun / Node 20+ both expose btoa + String.fromCharCode; avoid the
319
+ // Buffer dependency for portability. Node has `btoa` since v16.
320
+ let bin = "";
321
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
322
+ return btoa(bin);
323
+ }
324
+
325
+ /**
326
+ * Phase 7.2 R1 Agent C (H1) — Build the `Content-Security-Policy`
327
+ * header value to pair with a nonce-bearing preamble.
328
+ *
329
+ * Minimal and permissive by design — we are specifically writing a
330
+ * DEV-mode header that lets the Fast Refresh preamble + dynamic
331
+ * imports execute. We deliberately do NOT emit a blanket CSP for the
332
+ * entire page; that is the job of `@mandujs/core/middleware/secure` or
333
+ * the project's edge. Our sole concern here is: the inline preamble
334
+ * MUST be authorized without forcing the user to hand-write
335
+ * `'unsafe-inline'` in their policy.
336
+ *
337
+ * Header shape:
338
+ * `script-src 'self' 'nonce-<n>' 'strict-dynamic'`
339
+ *
340
+ * Why `'strict-dynamic'`: once the nonced preamble runs, it loads the
341
+ * Fast Refresh runtime via `import('/.mandu/client/...')`. Without
342
+ * `'strict-dynamic'` the module graph would need to be nonce-tagged
343
+ * end-to-end, which Bun.build does not support today.
344
+ */
345
+ function buildFastRefreshCspHeader(nonce: string): string {
346
+ // Nonces are already URL-safe / base64 but we defensively strip any
347
+ // stray quotes — should be impossible given the generator, but this
348
+ // closes the door on a caller-supplied nonce that slipped a quote in.
349
+ const safe = nonce.replace(/["\\\r\n]/g, "");
350
+ return `script-src 'self' 'nonce-${safe}' 'strict-dynamic'`;
351
+ }
352
+
353
+ /** @internal test helper — exposed only so unit tests can inspect the generator. */
354
+ export const _testOnly_generateCspNonce = generateCspNonce;
355
+ /** @internal test helper — exposed only so unit tests can inspect the resolver. */
356
+ export const _testOnly_resolveFastRefreshCspNonce = resolveFastRefreshCspNonce;
357
+ /** @internal test helper — exposed only so unit tests can inspect the header builder. */
358
+ export const _testOnly_buildFastRefreshCspHeader = buildFastRefreshCspHeader;
359
+ /** @internal test helper — exposed only so unit tests can inspect the preamble tag emitter. */
360
+ export const _testOnly_generateFastRefreshPreambleTag = generateFastRefreshPreambleTag;
361
+
362
+ /**
363
+ * Phase 7.2 R1 Agent C (H1) — Internal WeakMap that ferries the
364
+ * CSP nonce chosen during a `renderToHTML` call back to the caller
365
+ * (`renderSSR` / `renderWithHydration`) so they can emit the matching
366
+ * `Content-Security-Policy` response header.
367
+ *
368
+ * Keyed by the `options` object identity — the only caller that cares
369
+ * reuses the same options instance it handed in. External consumers
370
+ * of `renderToHTML` (tests, advanced users) are unaffected: they
371
+ * simply never look up the map and no memory accumulates because
372
+ * WeakMap entries collect when their key reference dies.
373
+ */
374
+ const OPTIONS_TO_NONCE = new WeakMap<object, string>();
375
+
376
+ /** @internal surface for unit tests — do not use in application code. */
377
+ export function _testOnly_getAttachedCspNonce(options: SSROptions): string | undefined {
378
+ return OPTIONS_TO_NONCE.get(options as object);
379
+ }
380
+
184
381
  export function renderToHTML(element: ReactElement, options: SSROptions = {}): string {
185
382
  const {
186
383
  title = "Mandu App",
@@ -276,6 +473,25 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
276
473
  hmrScript = generateHMRScript(hmrPort);
277
474
  }
278
475
 
476
+ // Phase 7.1 R2 Agent D: Fast Refresh preamble. Must land in <head>
477
+ // BEFORE any island `<script type="module">` evaluates — the stubs it
478
+ // installs for `$RefreshReg$` / `$RefreshSig$` are required by every
479
+ // module the bundler transformed with `reactFastRefresh: true`. Dev
480
+ // mode only; prod manifests omit `shared.fastRefresh` so the helper
481
+ // returns "".
482
+ // Phase 7.2 R1 Agent C (H1): resolve nonce up-front so the same
483
+ // value is reused for the <script> tag AND surfaced to the caller
484
+ // (via `renderToHTMLWithMeta`) for the response CSP header.
485
+ const resolvedCspNonce = needsHydration && isDev
486
+ ? resolveFastRefreshCspNonce(options.cspNonce)
487
+ : undefined;
488
+ if (resolvedCspNonce) {
489
+ OPTIONS_TO_NONCE.set(options as object, resolvedCspNonce);
490
+ }
491
+ const fastRefreshPreamble = needsHydration
492
+ ? generateFastRefreshPreambleTag(isDev, bundleManifest, resolvedCspNonce)
493
+ : "";
494
+
279
495
  // DevTools 번들 로드 (개발 모드)
280
496
  let devtoolsScript = "";
281
497
  if (isDev) {
@@ -303,6 +519,7 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
303
519
  ${hoistedLinkTags}
304
520
  ${headTags}
305
521
  ${collectedHeadTags}
522
+ ${fastRefreshPreamble}
306
523
  </head>
307
524
  <body>
308
525
  <div id="root">${bodyContent}</div>
@@ -363,6 +580,17 @@ function generateClientRouterScript(manifest: BundleManifest): string {
363
580
 
364
581
  /**
365
582
  * HMR 스크립트 생성
583
+ *
584
+ * Phase 7.2 Agent B — HDR (Hot Data Revalidation) extension.
585
+ * When the CLI broadcasts `{type: "vite", payload: {type: "custom",
586
+ * event: "mandu:slot-refetch", data: {routeId, slotPath, ...}}}` (via
587
+ * `hmrServer.broadcastVite`) we handle it here without remounting
588
+ * the React tree: fetch the current URL with `X-Mandu-HDR: 1`,
589
+ * receive JSON loader data, and hand it to the router's
590
+ * `applyHDRUpdate` hook (installed by `initializeRouter`). If the
591
+ * route doesn't match, the router hook is missing, or the fetch
592
+ * fails — we fall back to `location.reload()`. See the detailed
593
+ * design notes in `bundler/dev.ts:generateHMRClientScript` docstring.
366
594
  */
367
595
  function generateHMRScript(port: number): string {
368
596
  const hmrPort = port + PORTS.HMR_OFFSET;
@@ -382,6 +610,66 @@ window.__MANDU_HMR_PORT__ = ${hmrPort};
382
610
  }
383
611
  }
384
612
 
613
+ function hdrCurrentRouteId() {
614
+ var rs = window.__MANDU_ROUTER_STATE__;
615
+ if (rs && rs.currentRoute && rs.currentRoute.id) return String(rs.currentRoute.id);
616
+ var r = window.__MANDU_ROUTE__;
617
+ if (r && r.id) return String(r.id);
618
+ return null;
619
+ }
620
+
621
+ function hdrFallback(reason) {
622
+ console.log('[Mandu HDR] Fallback full reload' + (reason ? ' (' + reason + ')' : ''));
623
+ location.reload();
624
+ }
625
+
626
+ function hdrMark(name, data) {
627
+ try {
628
+ if (window.__MANDU_HDR__ && typeof window.__MANDU_HDR__.perfMark === 'function') {
629
+ window.__MANDU_HDR__.perfMark(name, data);
630
+ }
631
+ } catch (_) {}
632
+ }
633
+
634
+ function handleSlotRefetch(data) {
635
+ var routeId = data && typeof data.routeId === 'string' ? data.routeId : null;
636
+ if (!routeId) { hdrFallback('no-routeId'); return; }
637
+ if (window.__MANDU_HDR_DISABLED__ === true) { hdrFallback('disabled'); return; }
638
+ var currentId = hdrCurrentRouteId();
639
+ if (currentId !== routeId) {
640
+ console.log('[Mandu HDR] slot-refetch for ' + routeId + ' ignored (current route: ' + currentId + ')');
641
+ return;
642
+ }
643
+ var started = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
644
+ hdrMark('hdr:refetch-start', { routeId: routeId, slotPath: data.slotPath });
645
+ var url = window.location.pathname + window.location.search;
646
+ var sep = url.indexOf('?') >= 0 ? '&' : '?';
647
+ var dataUrl = url + sep + '_data=1';
648
+ fetch(dataUrl, { credentials: 'same-origin', headers: { 'X-Mandu-HDR': '1' } })
649
+ .then(function (res) {
650
+ if (!res.ok) { hdrFallback('status-' + res.status); return null; }
651
+ return res.json();
652
+ })
653
+ .then(function (payload) {
654
+ if (!payload) return;
655
+ var revalidate = window.__MANDU_ROUTER_REVALIDATE__;
656
+ if (typeof revalidate !== 'function') { hdrFallback('no-router'); return; }
657
+ try {
658
+ revalidate(routeId, payload.loaderData);
659
+ var elapsed = (typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now()) - started;
660
+ console.log('[Mandu HDR] Applied loader data for ' + routeId + ' in ' + elapsed.toFixed(0) + 'ms');
661
+ hdrMark('hdr:refetch', { routeId: routeId, slotPath: data.slotPath, elapsed: elapsed });
662
+ } catch (err) {
663
+ console.error('[Mandu HDR] Revalidate threw:', err);
664
+ hdrFallback('revalidate-throw');
665
+ }
666
+ })
667
+ .catch(function (err) {
668
+ console.error('[Mandu HDR] Fetch failed:', err);
669
+ hdrFallback('fetch-failed');
670
+ });
671
+ }
672
+
385
673
  function connect() {
386
674
  try {
387
675
  ws = new WebSocket('ws://' + window.location.hostname + ':${hmrPort}');
@@ -392,9 +680,26 @@ window.__MANDU_HMR_PORT__ = ${hmrPort};
392
680
  ws.onmessage = function(e) {
393
681
  try {
394
682
  var msg = JSON.parse(e.data);
395
- if (msg.type === 'reload' || msg.type === 'island-update') {
683
+ // Vite-compat envelope: custom event for HDR.
684
+ if ((msg.type === 'vite' || msg.type === 'vite-replay') && msg.payload) {
685
+ if (msg.payload.type === 'custom' && msg.payload.event === 'mandu:slot-refetch') {
686
+ handleSlotRefetch(msg.payload.data || {});
687
+ return;
688
+ }
689
+ // Other Vite payloads fall through to the legacy branches
690
+ // when possible.
691
+ if (msg.payload.type === 'full-reload') {
692
+ location.reload();
693
+ return;
694
+ }
695
+ }
696
+ if (msg.type === 'reload' || msg.type === 'island-update' || msg.type === 'full-reload' || msg.type === 'layout-update' || msg.type === 'invalidate') {
396
697
  console.log('[Mandu HMR] Reloading...');
397
698
  location.reload();
699
+ } else if (msg.type === 'slot-refetch') {
700
+ // Legacy Mandu-internal path (not yet used by the server — kept
701
+ // for forward-compat).
702
+ handleSlotRefetch(msg.data || {});
398
703
  } else if (msg.type === 'css-update') {
399
704
  var cssPath = (msg.data && msg.data.cssPath) || '/.mandu/client/globals.css';
400
705
  var links = document.querySelectorAll('link[rel="stylesheet"]');
@@ -431,18 +736,38 @@ function generateDevtoolsScript(): string {
431
736
  return `<script type="module" src="/.mandu/client/_devtools.js"></script>`;
432
737
  }
433
738
 
434
- export function createHTMLResponse(html: string, status: number = 200): Response {
435
- return new Response(html, {
436
- status,
437
- headers: {
438
- "Content-Type": "text/html; charset=utf-8",
439
- },
440
- });
739
+ export function createHTMLResponse(
740
+ html: string,
741
+ status: number = 200,
742
+ /**
743
+ * Phase 7.2 R1 Agent C (H1) — extra headers to merge in alongside
744
+ * the default `Content-Type`. Used for the Fast Refresh CSP header
745
+ * when a nonce was produced; can be repurposed for future
746
+ * per-response metadata without changing callsites.
747
+ */
748
+ extraHeaders?: Record<string, string>,
749
+ ): Response {
750
+ const headers: Record<string, string> = {
751
+ "Content-Type": "text/html; charset=utf-8",
752
+ };
753
+ if (extraHeaders) {
754
+ for (const [k, v] of Object.entries(extraHeaders)) {
755
+ headers[k] = v;
756
+ }
757
+ }
758
+ return new Response(html, { status, headers });
441
759
  }
442
760
 
443
761
  export function renderSSR(element: ReactElement, options: SSROptions = {}): Response {
444
762
  const html = renderToHTML(element, options);
445
- return createHTMLResponse(html);
763
+ // Phase 7.2 R1 Agent C (H1): if a CSP nonce was produced during
764
+ // rendering (dev + fast-refresh path), emit the matching header so
765
+ // the inline preamble is authorized under strict CSP.
766
+ const nonce = _testOnly_getAttachedCspNonce(options);
767
+ const extra = nonce
768
+ ? { "Content-Security-Policy": buildFastRefreshCspHeader(nonce) }
769
+ : undefined;
770
+ return createHTMLResponse(html, 200, extra);
446
771
  }
447
772
 
448
773
  /**
@@ -472,5 +797,10 @@ export async function renderWithHydration(
472
797
  }
473
798
  ): Promise<Response> {
474
799
  const html = renderToHTML(element, options);
475
- return createHTMLResponse(html);
800
+ // Phase 7.2 R1 Agent C (H1) — same CSP header logic as renderSSR.
801
+ const nonce = _testOnly_getAttachedCspNonce(options);
802
+ const extra = nonce
803
+ ? { "Content-Security-Policy": buildFastRefreshCspHeader(nonce) }
804
+ : undefined;
805
+ return createHTMLResponse(html, 200, extra);
476
806
  }