@mandujs/core 0.21.0 → 0.22.1

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 +101 -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
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Phase 7.2 R1 Agent C (H1) — CSP nonce tests for Fast Refresh preamble.
3
+ *
4
+ * Three code paths covered:
5
+ *
6
+ * A — nonce resolver (env opt-out, explicit string, auto-generate, off)
7
+ * B — preamble tag emitter (nonce attribute injection, opt-out)
8
+ * C — response header (renderSSR / renderStreamingResponse emit the
9
+ * right `Content-Security-Policy` value when a nonce is in play)
10
+ *
11
+ * Dependencies: none that touch fs.watch / bun:build — tests use
12
+ * in-memory manifests only, so they run in any environment.
13
+ *
14
+ * References:
15
+ * docs/security/phase-7-1-audit.md §2 M-01
16
+ * docs/bun/phase-7-2-team-plan.md §3 Agent C H1
17
+ */
18
+
19
+ import { describe, expect, test, afterEach, beforeEach } from "bun:test";
20
+ import React from "react";
21
+ import type { BundleManifest } from "../types";
22
+ import {
23
+ renderSSR,
24
+ renderToHTML,
25
+ _testOnly_buildFastRefreshCspHeader,
26
+ _testOnly_generateCspNonce,
27
+ _testOnly_generateFastRefreshPreambleTag,
28
+ _testOnly_getAttachedCspNonce,
29
+ _testOnly_resolveFastRefreshCspNonce,
30
+ } from "../../runtime/ssr";
31
+
32
+ /**
33
+ * Minimal dev-mode manifest carrying the same Fast Refresh URLs the
34
+ * bundler emits. We deliberately keep this a literal so unit tests
35
+ * never invoke `Bun.build` — the plumbing under test is purely string
36
+ * manipulation + header emission.
37
+ */
38
+ const DEV_MANIFEST: BundleManifest = {
39
+ version: 1,
40
+ buildTime: "2026-04-19T00:00:00.000Z",
41
+ env: "development",
42
+ bundles: {
43
+ page: {
44
+ js: "/.mandu/client/page.js",
45
+ dependencies: [],
46
+ priority: "visible",
47
+ },
48
+ },
49
+ shared: {
50
+ runtime: "/.mandu/client/_runtime.js",
51
+ vendor: "/.mandu/client/_vendor-react.js",
52
+ fastRefresh: {
53
+ runtime: "/.mandu/client/_fast-refresh-runtime.js",
54
+ glue: "/.mandu/client/_vendor-react-refresh.js",
55
+ },
56
+ },
57
+ };
58
+
59
+ // Preserve env across tests so MANDU_CSP_NONCE toggling doesn't leak.
60
+ let prevEnv: string | undefined;
61
+ beforeEach(() => {
62
+ prevEnv = process.env.MANDU_CSP_NONCE;
63
+ });
64
+ afterEach(() => {
65
+ if (prevEnv === undefined) {
66
+ delete process.env.MANDU_CSP_NONCE;
67
+ } else {
68
+ process.env.MANDU_CSP_NONCE = prevEnv;
69
+ }
70
+ });
71
+
72
+ // ═══════════════════════════════════════════════════════════════════
73
+ // Section A — resolveFastRefreshCspNonce
74
+ // ═══════════════════════════════════════════════════════════════════
75
+
76
+ describe("csp-nonce — resolveFastRefreshCspNonce", () => {
77
+ test("[A1] generates a fresh 128-bit base64 nonce on each call (entropy > 120 unique of 200)", () => {
78
+ const seen = new Set<string>();
79
+ for (let i = 0; i < 200; i++) {
80
+ seen.add(_testOnly_generateCspNonce());
81
+ }
82
+ // 200 draws from 2^128 should produce 200 distinct values; accept
83
+ // ≥120 unique to survive any pathological RNG quirks.
84
+ expect(seen.size).toBeGreaterThan(120);
85
+ const sample = [...seen][0];
86
+ // 16 bytes → 24 base64 chars incl. `=` padding
87
+ expect(sample.length).toBeGreaterThanOrEqual(20);
88
+ expect(sample.length).toBeLessThanOrEqual(32);
89
+ // base64 alphabet only (no slashes / plus are allowed but not typical
90
+ // for nonces; we accept standard base64)
91
+ expect(/^[A-Za-z0-9+/=]+$/.test(sample)).toBe(true);
92
+ });
93
+
94
+ test("[A2] opt-out via MANDU_CSP_NONCE=0 forces undefined even when cspNonce=true", () => {
95
+ process.env.MANDU_CSP_NONCE = "0";
96
+ expect(_testOnly_resolveFastRefreshCspNonce(true)).toBeUndefined();
97
+ expect(_testOnly_resolveFastRefreshCspNonce("fixed")).toBeUndefined();
98
+ expect(_testOnly_resolveFastRefreshCspNonce(false)).toBeUndefined();
99
+ });
100
+
101
+ test("[A3] explicit string value is passed through verbatim", () => {
102
+ delete process.env.MANDU_CSP_NONCE;
103
+ expect(_testOnly_resolveFastRefreshCspNonce("abc123")).toBe("abc123");
104
+ });
105
+
106
+ test("[A4] undefined / false produce undefined (legacy byte-identical path)", () => {
107
+ delete process.env.MANDU_CSP_NONCE;
108
+ expect(_testOnly_resolveFastRefreshCspNonce(undefined)).toBeUndefined();
109
+ expect(_testOnly_resolveFastRefreshCspNonce(false)).toBeUndefined();
110
+ });
111
+ });
112
+
113
+ // ═══════════════════════════════════════════════════════════════════
114
+ // Section B — generateFastRefreshPreambleTag
115
+ // ═══════════════════════════════════════════════════════════════════
116
+
117
+ describe("csp-nonce — generateFastRefreshPreambleTag", () => {
118
+ test("[B1] injects nonce attribute onto <script> tag when nonce supplied", () => {
119
+ const out = _testOnly_generateFastRefreshPreambleTag(true, DEV_MANIFEST, "xyz456");
120
+ expect(out).toContain('<script nonce="xyz456">');
121
+ // The original preamble body must still be present (sanity)
122
+ expect(out).toContain("React Fast Refresh preamble");
123
+ // No bare `<script>` remains before the nonced one
124
+ const firstOpen = out.indexOf("<script");
125
+ expect(out.slice(firstOpen, firstOpen + 20)).toBe('<script nonce="xyz45');
126
+ });
127
+
128
+ test("[B2] returns empty string in prod (isDev=false) regardless of nonce", () => {
129
+ expect(_testOnly_generateFastRefreshPreambleTag(false, DEV_MANIFEST, "x")).toBe("");
130
+ expect(_testOnly_generateFastRefreshPreambleTag(false, DEV_MANIFEST, undefined)).toBe("");
131
+ });
132
+
133
+ test("[B3] returns empty string when manifest has no fastRefresh block (prod manifest)", () => {
134
+ const prodManifest: BundleManifest = {
135
+ ...DEV_MANIFEST,
136
+ env: "production",
137
+ shared: { ...DEV_MANIFEST.shared, fastRefresh: undefined },
138
+ };
139
+ expect(_testOnly_generateFastRefreshPreambleTag(true, prodManifest, "x")).toBe("");
140
+ });
141
+
142
+ test("[B4] without nonce, emits bare <script> (backward compat)", () => {
143
+ const out = _testOnly_generateFastRefreshPreambleTag(true, DEV_MANIFEST, undefined);
144
+ expect(out).toContain("<script>");
145
+ expect(out).not.toContain("<script nonce");
146
+ });
147
+
148
+ test("[B5] nonce attribute value is HTML-escaped (defense in depth)", () => {
149
+ // A pathological nonce containing a `"` must not break out of the
150
+ // attribute. `escapeHtmlAttr` turns it into `&quot;`.
151
+ const out = _testOnly_generateFastRefreshPreambleTag(true, DEV_MANIFEST, 'a"b');
152
+ expect(out).toContain("<script nonce=");
153
+ expect(out).not.toContain('nonce="a"b"'); // would break attribute
154
+ // The &quot; entity is present somewhere in the opening tag
155
+ expect(out).toMatch(/<script nonce="a(?:&quot;|&#34;)b"/);
156
+ });
157
+ });
158
+
159
+ // ═══════════════════════════════════════════════════════════════════
160
+ // Section C — buildFastRefreshCspHeader
161
+ // ═══════════════════════════════════════════════════════════════════
162
+
163
+ describe("csp-nonce — buildFastRefreshCspHeader", () => {
164
+ test("[C1] header contains script-src with nonce and strict-dynamic", () => {
165
+ const h = _testOnly_buildFastRefreshCspHeader("abc123");
166
+ expect(h).toBe("script-src 'self' 'nonce-abc123' 'strict-dynamic'");
167
+ });
168
+
169
+ test("[C2] strips quotes / CR / LF from nonce (defense in depth)", () => {
170
+ const h = _testOnly_buildFastRefreshCspHeader('a"b\nc\rd');
171
+ // All unsafe chars removed — header is a single line
172
+ expect(h).not.toContain("\n");
173
+ expect(h).not.toContain("\r");
174
+ expect(h).not.toContain('"');
175
+ });
176
+ });
177
+
178
+ // ═══════════════════════════════════════════════════════════════════
179
+ // Section D — renderSSR response header integration
180
+ // ═══════════════════════════════════════════════════════════════════
181
+
182
+ describe("csp-nonce — renderSSR integration", () => {
183
+ test("[D1] when cspNonce=true, Response includes Content-Security-Policy header with the same nonce used in preamble", () => {
184
+ delete process.env.MANDU_CSP_NONCE;
185
+ const options = {
186
+ isDev: true,
187
+ routeId: "page",
188
+ serverData: {},
189
+ hydration: { strategy: "island" as const, priority: "visible" as const, preload: false },
190
+ bundleManifest: DEV_MANIFEST,
191
+ cspNonce: true,
192
+ };
193
+ const response = renderSSR(React.createElement("div", null, "hi"), options);
194
+ const header = response.headers.get("Content-Security-Policy");
195
+ expect(header).not.toBeNull();
196
+ expect(header).toMatch(/^script-src 'self' 'nonce-[A-Za-z0-9+/=]+' 'strict-dynamic'$/);
197
+ // The nonce in the header must match the one attached to options
198
+ const attachedNonce = _testOnly_getAttachedCspNonce(options);
199
+ expect(attachedNonce).not.toBeUndefined();
200
+ expect(header).toContain(`'nonce-${attachedNonce}'`);
201
+ });
202
+
203
+ test("[D2] when cspNonce=false (default), NO CSP header is emitted — legacy byte-identical", () => {
204
+ const options = {
205
+ isDev: true,
206
+ routeId: "page",
207
+ serverData: {},
208
+ hydration: { strategy: "island" as const, priority: "visible" as const, preload: false },
209
+ bundleManifest: DEV_MANIFEST,
210
+ };
211
+ const response = renderSSR(React.createElement("div", null, "hi"), options);
212
+ expect(response.headers.get("Content-Security-Policy")).toBeNull();
213
+ });
214
+
215
+ test("[D3] MANDU_CSP_NONCE=0 forces no header even with cspNonce=true", () => {
216
+ process.env.MANDU_CSP_NONCE = "0";
217
+ const options = {
218
+ isDev: true,
219
+ routeId: "page",
220
+ serverData: {},
221
+ hydration: { strategy: "island" as const, priority: "visible" as const, preload: false },
222
+ bundleManifest: DEV_MANIFEST,
223
+ cspNonce: true,
224
+ };
225
+ const response = renderSSR(React.createElement("div", null, "hi"), options);
226
+ expect(response.headers.get("Content-Security-Policy")).toBeNull();
227
+ });
228
+
229
+ test("[D4] when cspNonce is a caller-supplied string, that exact string ends up in header AND preamble", () => {
230
+ delete process.env.MANDU_CSP_NONCE;
231
+ const options = {
232
+ isDev: true,
233
+ routeId: "page",
234
+ serverData: {},
235
+ hydration: { strategy: "island" as const, priority: "visible" as const, preload: false },
236
+ bundleManifest: DEV_MANIFEST,
237
+ cspNonce: "fixed-nonce-value",
238
+ };
239
+ const response = renderSSR(React.createElement("div", null, "hi"), options);
240
+ const header = response.headers.get("Content-Security-Policy");
241
+ expect(header).toBe("script-src 'self' 'nonce-fixed-nonce-value' 'strict-dynamic'");
242
+ });
243
+
244
+ test("[D5] renderToHTML embeds nonce into the preamble script tag end-to-end", () => {
245
+ delete process.env.MANDU_CSP_NONCE;
246
+ const options = {
247
+ isDev: true,
248
+ routeId: "page",
249
+ serverData: {},
250
+ hydration: { strategy: "island" as const, priority: "visible" as const, preload: false },
251
+ bundleManifest: DEV_MANIFEST,
252
+ cspNonce: "e2e-token",
253
+ };
254
+ const html = renderToHTML(React.createElement("div", null, "hi"), options);
255
+ expect(html).toContain('<script nonce="e2e-token">');
256
+ // The preamble URLs must still be present (we didn't mangle the body)
257
+ expect(html).toContain("/.mandu/client/_vendor-react-refresh.js");
258
+ });
259
+
260
+ test("[D6] prod manifest (no shared.fastRefresh) produces no CSP header even with cspNonce=true", () => {
261
+ delete process.env.MANDU_CSP_NONCE;
262
+ const prodManifest: BundleManifest = {
263
+ ...DEV_MANIFEST,
264
+ env: "production",
265
+ shared: { ...DEV_MANIFEST.shared, fastRefresh: undefined },
266
+ };
267
+ const options = {
268
+ isDev: false,
269
+ routeId: "page",
270
+ serverData: {},
271
+ hydration: { strategy: "island" as const, priority: "visible" as const, preload: false },
272
+ bundleManifest: prodManifest,
273
+ cspNonce: true,
274
+ };
275
+ const response = renderSSR(React.createElement("div", null, "hi"), options);
276
+ expect(response.headers.get("Content-Security-Policy")).toBeNull();
277
+ });
278
+ });