@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
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Mandu notFound() helper for SSR loaders and handlers.
3
+ *
4
+ * Symmetric with `redirect()` — both short-circuit the SSR pipeline by
5
+ * returning (or throwing) a marked `Response`. Where `redirect()` tells
6
+ * the runtime "navigate somewhere else", `notFound()` tells the runtime
7
+ * "this resource does not exist; render the 404 surface".
8
+ *
9
+ * ## Usage
10
+ *
11
+ * ```ts
12
+ * import { Mandu, notFound } from "@mandujs/core";
13
+ *
14
+ * export const filling = Mandu.filling().loader(async (ctx) => {
15
+ * const post = await db.post.find(ctx.params.slug);
16
+ * if (!post) return notFound(); // or: throw notFound();
17
+ * return { post };
18
+ * });
19
+ * ```
20
+ *
21
+ * The SSR pipeline (server.ts `loadPageData`) checks each loader result
22
+ * with `isNotFoundResponse()`. On a hit it:
23
+ *
24
+ * 1. Prefers `app/not-found.tsx` (if registered) — rendered as a normal
25
+ * page with status 404 and any pending cookies preserved.
26
+ * 2. Falls back to the framework's built-in 404 JSON error.
27
+ *
28
+ * ## Why a branded Response
29
+ *
30
+ * A bare `new Response(null, { status: 404 })` is NOT treated as a
31
+ * notFound sentinel. That's intentional: a loader that accidentally
32
+ * returns a generic 404 Response (e.g. proxying an upstream fetch) must
33
+ * NOT hijack the page to show our 404 page. Only values minted through
34
+ * `notFound()` carry the internal brand.
35
+ *
36
+ * The brand is a non-enumerable WeakSet membership stamped on the
37
+ * Response object. Never serialised, never visible to clients.
38
+ */
39
+
40
+ /** Internal brand — identifies Response objects minted by `notFound()`. */
41
+ export const NOT_FOUND_BRAND: unique symbol = Symbol.for("@mandujs/core/not-found");
42
+
43
+ /** WeakSet of Response instances tagged as notFound. Avoids property writes. */
44
+ const brandedNotFoundResponses = new WeakSet<Response>();
45
+
46
+ /** Options for tuning a notFound response. */
47
+ export interface NotFoundOptions {
48
+ /** Optional human-readable message. Surfaced to the 404 page via body. */
49
+ message?: string;
50
+ }
51
+
52
+ /**
53
+ * Create a `Response` that signals "not found" to the Mandu SSR pipeline.
54
+ *
55
+ * Returns a real `Response` (status 404) for three reasons:
56
+ *
57
+ * 1. Loaders that `return` or `throw` it are treated identically — no
58
+ * extra plumbing for the "deep call stack wants to bail out" case.
59
+ * 2. Consumers outside an SSR loader (route handlers, middleware) can
60
+ * use the same helper without a separate API.
61
+ * 3. A hostile or buggy loader returning a bare `new Response(null, {status:404})`
62
+ * does NOT trigger the framework's 404 page path — it falls through
63
+ * the existing error channel like any other unexpected Response.
64
+ *
65
+ * @param options - Optional `{ message }`. Message is serialised into the
66
+ * response body as `text/plain; charset=utf-8`. If omitted, the body
67
+ * defaults to `"Not Found"`.
68
+ */
69
+ export function notFound(options: NotFoundOptions = {}): Response {
70
+ const message = typeof options.message === "string" && options.message.length > 0
71
+ ? options.message
72
+ : "Not Found";
73
+
74
+ const response = new Response(message, {
75
+ status: 404,
76
+ headers: { "Content-Type": "text/plain; charset=utf-8" },
77
+ });
78
+ brandedNotFoundResponses.add(response);
79
+ return response;
80
+ }
81
+
82
+ /**
83
+ * True when `value` is a Response produced by `notFound()`.
84
+ *
85
+ * Deliberately strict — only branded responses match. A bare
86
+ * `new Response(null, { status: 404 })` is NOT recognised, nor is a
87
+ * redirect Response (even one with a 404-like status, which would be
88
+ * malformed but shouldn't confuse us).
89
+ */
90
+ export function isNotFoundResponse(value: unknown): value is Response {
91
+ if (!(value instanceof Response)) return false;
92
+ return brandedNotFoundResponses.has(value);
93
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Mandu redirect helper for SSR loaders.
3
+ *
4
+ * ## Usage
5
+ *
6
+ * ```ts
7
+ * import { Mandu, redirect } from "@mandujs/core";
8
+ *
9
+ * export const filling = Mandu.filling().loader(async (ctx) => {
10
+ * const uid = currentUserId(ctx);
11
+ * if (!uid) return redirect("/login"); // short-circuit → 302
12
+ * return { user: await fetchUser(uid) };
13
+ * });
14
+ * ```
15
+ *
16
+ * Both `return redirect(...)` and `throw redirect(...)` are supported — the
17
+ * SSR pipeline recognizes either shape. Thrown is idiomatic for bailing out
18
+ * of a deep call stack without threading the result back up.
19
+ *
20
+ * ## Design
21
+ *
22
+ * `redirect()` returns a real `Response` object (same shape Remix/Next.js
23
+ * use for their own loader-level redirects). This is preferred over a
24
+ * custom sentinel for three reasons:
25
+ *
26
+ * 1. `Response` is already understood by every layer of the runtime
27
+ * (cookies via `applyToResponse`, streaming headers, etc.).
28
+ * 2. A thrown `Response` is trivially distinguishable from a thrown
29
+ * `Error` — `instanceof Response` — so user bugs (`throw new Error(...)`)
30
+ * never get silently converted into redirects.
31
+ * 3. The helper can be used outside loaders (middleware, guards) without
32
+ * additional plumbing.
33
+ *
34
+ * A hidden symbol (`REDIRECT_BRAND`) is stamped on the Response so the
35
+ * runtime can distinguish redirects returned from a loader (for which we
36
+ * short-circuit SSR) from a loader that merely returned an unrelated
37
+ * Response (which would be a user error — loaders must return data). The
38
+ * brand is non-enumerable and scoped to the runtime — clients never see it.
39
+ */
40
+
41
+ export type RedirectStatus = 301 | 302 | 303 | 307 | 308;
42
+
43
+ const DEFAULT_REDIRECT_STATUS: RedirectStatus = 302;
44
+ const VALID_REDIRECT_STATUS = new Set<number>([301, 302, 303, 307, 308]);
45
+
46
+ /** Internal brand — identifies Response objects minted by `redirect()`. */
47
+ export const REDIRECT_BRAND: unique symbol = Symbol.for("@mandujs/core/redirect");
48
+
49
+ /** Options for tuning a redirect response. */
50
+ export interface RedirectOptions {
51
+ /** HTTP status code. Defaults to 302 (Found). */
52
+ status?: RedirectStatus;
53
+ /** Additional headers to merge into the redirect response. */
54
+ headers?: HeadersInit;
55
+ }
56
+
57
+ /**
58
+ * Create a redirect `Response` suitable for returning or throwing from an
59
+ * SSR loader, handler, or middleware.
60
+ *
61
+ * @param url - Destination URL. Absolute or relative. Must be a non-empty
62
+ * string — empty/whitespace throws synchronously (catches silent bugs
63
+ * early rather than producing a useless redirect).
64
+ * @param options - Optional `{ status, headers }`. Status defaults to 302.
65
+ *
66
+ * @throws {TypeError} When `url` is not a non-empty string, or when
67
+ * `options.status` is not one of 301/302/303/307/308.
68
+ */
69
+ export function redirect(url: string, options: RedirectOptions = {}): Response {
70
+ if (typeof url !== "string" || url.trim().length === 0) {
71
+ throw new TypeError(
72
+ `[Mandu] redirect() requires a non-empty URL string (got: ${JSON.stringify(url)})`
73
+ );
74
+ }
75
+
76
+ const status = options.status ?? DEFAULT_REDIRECT_STATUS;
77
+ if (!VALID_REDIRECT_STATUS.has(status)) {
78
+ throw new TypeError(
79
+ `[Mandu] redirect() status must be 301/302/303/307/308 (got: ${status})`
80
+ );
81
+ }
82
+
83
+ const headers = new Headers(options.headers);
84
+ headers.set("Location", url);
85
+
86
+ const response = new Response(null, { status, headers });
87
+ brandResponse(response);
88
+ return response;
89
+ }
90
+
91
+ /**
92
+ * Attach the redirect brand to a Response (non-enumerable).
93
+ * Kept separate so tests / advanced callers that mint their own
94
+ * redirect-status Responses can opt in.
95
+ */
96
+ function brandResponse(response: Response): void {
97
+ Object.defineProperty(response, REDIRECT_BRAND, {
98
+ value: true,
99
+ enumerable: false,
100
+ configurable: false,
101
+ writable: false,
102
+ });
103
+ }
104
+
105
+ /** True when `value` is a Response minted by `redirect()`. */
106
+ export function isManduRedirect(value: unknown): boolean {
107
+ return (
108
+ value instanceof Response &&
109
+ (value as unknown as { [REDIRECT_BRAND]?: boolean })[REDIRECT_BRAND] === true
110
+ );
111
+ }
112
+
113
+ /**
114
+ * True when `value` is any Response with a redirect-range status code.
115
+ *
116
+ * Covers both:
117
+ * - `redirect("/x")` from this module (preferred)
118
+ * - `new Response(null, { status: 302, headers: { Location: ... } })`
119
+ * (idiomatic Remix-style throw; we accept it so users aren't forced
120
+ * into our helper)
121
+ *
122
+ * Deliberately excludes thrown `Error` instances, so a user's
123
+ * `throw new Error("boom")` is never mistaken for a redirect — it falls
124
+ * through to the existing error-handling path.
125
+ */
126
+ export function isRedirectResponse(value: unknown): value is Response {
127
+ if (!(value instanceof Response)) return false;
128
+ if (isManduRedirect(value)) return true;
129
+ if (!VALID_REDIRECT_STATUS.has(value.status)) return false;
130
+ // A redirect without a Location header is malformed — treat as a plain
131
+ // Response so we don't emit a broken 302 to the browser.
132
+ return value.headers.has("Location");
133
+ }