@reckona/mreact-router 0.0.203 → 0.0.205

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 (140) hide show
  1. package/README.md +53 -9
  2. package/dist/actions.d.ts +6 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +70 -10
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/aws-lambda.d.ts +15 -4
  7. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  8. package/dist/adapters/aws-lambda.js +301 -63
  9. package/dist/adapters/aws-lambda.js.map +1 -1
  10. package/dist/adapters/cloudflare.d.ts +2 -1
  11. package/dist/adapters/cloudflare.d.ts.map +1 -1
  12. package/dist/adapters/cloudflare.js +18 -10
  13. package/dist/adapters/cloudflare.js.map +1 -1
  14. package/dist/adapters/edge.d.ts +1 -1
  15. package/dist/adapters/edge.d.ts.map +1 -1
  16. package/dist/adapters/edge.js.map +1 -1
  17. package/dist/adapters/node.d.ts +5 -3
  18. package/dist/adapters/node.d.ts.map +1 -1
  19. package/dist/adapters/node.js +2 -1
  20. package/dist/adapters/node.js.map +1 -1
  21. package/dist/adapters/static.d.ts.map +1 -1
  22. package/dist/adapters/static.js +24 -2
  23. package/dist/adapters/static.js.map +1 -1
  24. package/dist/build.d.ts +1 -0
  25. package/dist/build.d.ts.map +1 -1
  26. package/dist/build.js +91 -5
  27. package/dist/build.js.map +1 -1
  28. package/dist/built-runtime.d.ts +2 -0
  29. package/dist/built-runtime.d.ts.map +1 -1
  30. package/dist/built-runtime.js +16 -0
  31. package/dist/built-runtime.js.map +1 -1
  32. package/dist/cache.d.ts +3 -1
  33. package/dist/cache.d.ts.map +1 -1
  34. package/dist/cache.js +211 -13
  35. package/dist/cache.js.map +1 -1
  36. package/dist/cli.js +12 -1
  37. package/dist/cli.js.map +1 -1
  38. package/dist/client.d.ts.map +1 -1
  39. package/dist/client.js +32 -1
  40. package/dist/client.js.map +1 -1
  41. package/dist/config.d.ts +3 -0
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +21 -2
  44. package/dist/config.js.map +1 -1
  45. package/dist/csp.d.ts +1 -0
  46. package/dist/csp.d.ts.map +1 -1
  47. package/dist/csp.js +11 -6
  48. package/dist/csp.js.map +1 -1
  49. package/dist/dehydrate-policy.d.ts +3 -0
  50. package/dist/dehydrate-policy.d.ts.map +1 -0
  51. package/dist/dehydrate-policy.js +16 -0
  52. package/dist/dehydrate-policy.js.map +1 -0
  53. package/dist/dev-server.d.ts +7 -2
  54. package/dist/dev-server.d.ts.map +1 -1
  55. package/dist/dev-server.js +120 -11
  56. package/dist/dev-server.js.map +1 -1
  57. package/dist/http.d.ts +1 -1
  58. package/dist/http.d.ts.map +1 -1
  59. package/dist/http.js +147 -7
  60. package/dist/http.js.map +1 -1
  61. package/dist/index.d.ts +4 -3
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/logger.d.ts +22 -1
  66. package/dist/logger.d.ts.map +1 -1
  67. package/dist/logger.js +22 -8
  68. package/dist/logger.js.map +1 -1
  69. package/dist/metadata.d.ts +8 -0
  70. package/dist/metadata.d.ts.map +1 -1
  71. package/dist/metadata.js +60 -22
  72. package/dist/metadata.js.map +1 -1
  73. package/dist/navigation-marker.d.ts.map +1 -1
  74. package/dist/navigation-marker.js +72 -9
  75. package/dist/navigation-marker.js.map +1 -1
  76. package/dist/navigation.d.ts +1 -0
  77. package/dist/navigation.d.ts.map +1 -1
  78. package/dist/navigation.js +1 -1
  79. package/dist/navigation.js.map +1 -1
  80. package/dist/node-server.d.ts +5 -2
  81. package/dist/node-server.d.ts.map +1 -1
  82. package/dist/node-server.js +75 -6
  83. package/dist/node-server.js.map +1 -1
  84. package/dist/prerender-entry.d.ts +14 -0
  85. package/dist/prerender-entry.d.ts.map +1 -1
  86. package/dist/prerender-entry.js +40 -3
  87. package/dist/prerender-entry.js.map +1 -1
  88. package/dist/render.d.ts +9 -1
  89. package/dist/render.d.ts.map +1 -1
  90. package/dist/render.js +102 -123
  91. package/dist/render.js.map +1 -1
  92. package/dist/serve.d.ts +8 -2
  93. package/dist/serve.d.ts.map +1 -1
  94. package/dist/serve.js +22 -20
  95. package/dist/serve.js.map +1 -1
  96. package/dist/session.d.ts.map +1 -1
  97. package/dist/session.js +52 -19
  98. package/dist/session.js.map +1 -1
  99. package/dist/upgrade.d.ts +59 -2
  100. package/dist/upgrade.d.ts.map +1 -1
  101. package/dist/upgrade.js +282 -1
  102. package/dist/upgrade.js.map +1 -1
  103. package/dist/vite-config.d.ts +2 -0
  104. package/dist/vite-config.d.ts.map +1 -1
  105. package/dist/vite-config.js +2 -0
  106. package/dist/vite-config.js.map +1 -1
  107. package/dist/vite.d.ts +4 -1
  108. package/dist/vite.d.ts.map +1 -1
  109. package/dist/vite.js +117 -6
  110. package/dist/vite.js.map +1 -1
  111. package/package.json +11 -11
  112. package/src/actions.ts +81 -11
  113. package/src/adapters/aws-lambda.ts +426 -115
  114. package/src/adapters/cloudflare.ts +29 -13
  115. package/src/adapters/edge.ts +2 -0
  116. package/src/adapters/node.ts +8 -10
  117. package/src/adapters/static.ts +31 -2
  118. package/src/build.ts +117 -10
  119. package/src/built-runtime.ts +28 -0
  120. package/src/cache.ts +262 -15
  121. package/src/cli.ts +14 -1
  122. package/src/client.ts +32 -1
  123. package/src/config.ts +43 -6
  124. package/src/csp.ts +14 -11
  125. package/src/dehydrate-policy.ts +23 -0
  126. package/src/dev-server.ts +188 -15
  127. package/src/http.ts +172 -6
  128. package/src/index.ts +14 -1
  129. package/src/logger.ts +47 -10
  130. package/src/metadata.ts +125 -60
  131. package/src/navigation-marker.ts +79 -9
  132. package/src/navigation.ts +1 -1
  133. package/src/node-server.ts +128 -22
  134. package/src/prerender-entry.ts +52 -3
  135. package/src/render.ts +807 -753
  136. package/src/serve.ts +49 -56
  137. package/src/session.ts +56 -25
  138. package/src/upgrade.ts +402 -3
  139. package/src/vite-config.ts +6 -0
  140. package/src/vite.ts +157 -5
package/README.md CHANGED
@@ -51,6 +51,8 @@ export default defineConfig({
51
51
 
52
52
  `allowedSourceDirs` can be omitted for the common route roots: `routesDir: "src/app"` defaults to `["src"]`, and `routesDir: "app"` defaults to `["app"]`. Keep it explicit when shared server/client modules live in additional project-root-relative directories.
53
53
 
54
+ Server query data embedded during hydration is visible to the browser. Set `dehydratePolicyModule` to a project-relative server module inside `allowedSourceDirs` when Vite development, prerendering, built Node output, and generated Lambda handlers must share one filter. The module must export a named `dehydrateOptions` object; Mreact bundles it as executable server code and fails closed when the configured module or export is invalid. Programmatic Node and Lambda handlers may instead pass `dehydrateOptions` directly.
55
+
54
56
  `mreact-router build` reads this config. Pass `--target=node` for plain Node/container output, `--target=aws-lambda` for Lambda artifacts with a generated handler and import policy, or `--target=cloudflare` for Workers artifacts with a generated Worker module. Configure `buildTargets: ["aws-lambda"]`, `["cloudflare"]`, or another explicit target list in `mreactRouter()` when one deployment target should be the project default. Without an explicit target, build output includes only the Node-compatible server/client artifacts; Cloudflare and Lambda artifacts are opt-in. The legacy `appDir` shortcut remains available for tests and older direct programmatic usage, but it is deprecated. Use `projectRoot` + `routesDir` for new code. The shortcut is planned for removal after `0.1.0`.
55
57
 
56
58
  Run `mreact-router boundaries` to inspect every route and its statically traceable rendered server and client components without writing build artifacts. The command loads the same Vite project configuration as `build`; pass an explicit app directory for the legacy direct-directory mode, or add `--json` for a deterministic versioned report with project-relative paths.
@@ -134,7 +136,35 @@ Inferred form actions are bound to the rendered form with a hidden token derived
134
136
 
135
137
  Plain `multipart/form-data` route handlers do not automatically run the server-action CSRF guard. For cookie-authenticated upload forms that post directly to `route.ts`, render a hidden field named `formCsrfFieldName`, set `formCsrfCookie(token)` for the same token, and either call `validateFormCsrf(request, formData)` for small buffered forms or read the CSRF field with `parseMultipartStream()` before consuming a large file part. `request.formData()` buffers multipart file parts in memory; `parseMultipartStream(request, { fields })` keeps file parts as `ReadableStream<Uint8Array>` values with per-part and total byte limits, enforces bounded defaults, and exposes both the raw `filename` and a storage-oriented `safeFilename`. Workers APIs such as R2 require known-length streams, so `part.fixedLengthStream(length).readable` is only valid when the multipart part carries a `Content-Length` header or the handler can pass the exact byte length; otherwise use a bounded `arrayBuffer()` fallback for R2. Put the CSRF field before the file field when you want early rejection without reading the file body.
136
138
 
137
- Use `startDevServer()` and `startServer()` when an app needs to attach HTTP upgrade handling to the same Node server as mreact routes. Both APIs accept `onUpgrade(request, socket, head)`, and `startDevServer()` keeps Vite middleware/HMR wired to the underlying server. Production route artifacts are bundled independently, so mutable custom-server state should live in `getServerRuntimeState(key, create)` rather than in a module-scoped `let` singleton.
139
+ Use `startDevServer()` and `startServer()` when an app needs to attach HTTP upgrade handling to the same Node server as mreact routes. Both APIs accept `onUpgrade(request, socket, head, context)`, and `startDevServer()` keeps Vite middleware/HMR wired to the underlying server. WebSocket handshakes are not protected by the browser same-origin policy or CORS, and browsers send matching cookies with the handshake. mreact therefore rejects missing, opaque, malformed, and cross-origin browser upgrades by default before invoking the application handler. Validate `Origin` before reading a session or other credentials, then explicitly accept or decline every upgrade:
140
+
141
+ ```ts
142
+ import { startServer, validateHttpUpgradeOrigin } from "@reckona/mreact-router";
143
+ import { WebSocketServer } from "ws";
144
+
145
+ const allowedOrigins = ["https://app.example.com"];
146
+ const wss = new WebSocketServer({ noServer: true });
147
+
148
+ await startServer({
149
+ outDir: ".mreact",
150
+ port: 3000,
151
+ upgradeOriginPolicy: { allowedOrigins },
152
+ onUpgrade(request, socket, head, context) {
153
+ const origin = validateHttpUpgradeOrigin(request, { allowedOrigins });
154
+ if (!origin.ok || request.url !== "/ws") {
155
+ return context.decline();
156
+ }
157
+
158
+ context.accept();
159
+ // Authenticate only after Origin validation, then transfer the socket.
160
+ wss.handleUpgrade(request, socket, head, (websocket) => {
161
+ wss.emit("connection", websocket, request);
162
+ });
163
+ },
164
+ });
165
+ ```
166
+
167
+ The first three handler arguments remain compatible with synchronous handlers that write a handshake immediately. A handler that delays verification, including `ws` with asynchronous `verifyClient`, must call `context.accept()` synchronously before starting that work; a return with no synchronous socket action is treated as a decline. Thrown errors, rejected handlers, and undecided asynchronous handlers close the socket. Framework-generated structured fields omit request headers and query data, but an application error message is preserved, so handlers must not copy secrets into thrown errors. The default policy compares `Origin` with the validated request authority, so wildcard binds and trusted reverse proxies work when their Host and forwarded-protocol policies are configured correctly. `close()` waits for accepted upgrade sockets up to `upgradeCloseTimeoutMs` and then destroys only those sockets, while ordinary HTTP responses keep their normal drain behavior. Non-browser clients that omit `Origin` require an explicit `{ allowedOrigins, allowMissingOrigin: true }` policy and an alternate authentication boundary; `"unchecked"` is available only as an explicit compatibility escape hatch. Production route artifacts are bundled independently, so mutable custom-server state should live in `getServerRuntimeState(key, create)` rather than in a module-scoped `let` singleton.
138
168
 
139
169
  Client boundary markers have the same SSR behavior. A plain `Foo.tsx` component runs on the server and does not hydrate by itself. Marking a component boundary with either `Foo.client.tsx` or a top-level `"use client";` directive makes that boundary hydrate on the client; SSR emits a `<template data-mreact-client-boundary="...">` placeholder plus serialized props, and the component JSX appears after hydration. When a client boundary wrapper receives server-renderable JSX children, those children remain visible as SSR DOM between the boundary marker and its props payload so the initial response stays paintable and indexable before hydration. Inferred boundaries from plain imported components can also keep an SSR fallback when every browser-global read is guarded by a `typeof window !== "undefined"` style check: a guarded if/ternary branch, a short-circuited logical expression, or statements after a guarded early exit such as `if (typeof window === "undefined") return;`. A guard on one of `window`, `document`, or `localStorage` covers the others. Unguarded reads, and guards the analyzer cannot follow such as an aliased `const isBrowser = typeof window !== "undefined"` condition, still disable fallback eligibility and produce a placeholder-only boundary. Combining `.client.tsx` with `"use client";` is redundant and does not change the boundary behavior. Route-level `"use client";` is separate: when it appears in a page, layout, or template route module, the whole route is emitted as a hydrated client route. In development, app-local client boundary dependencies are transformed through the mreact client compiler before Vite serves them, and generated mreact runtime imports are resolved by the router plugin, so server-rendered layouts and app shells can import hydrated client controls without adding React or mreact compat runtime aliases.
140
170
 
@@ -171,6 +201,8 @@ Route metadata is composed from parent layouts before the matched page. CSP dire
171
201
 
172
202
  When `metadata.csp.nonce` is set, the CSP serializer appends that nonce to `script-src` and `style-src` only when those directives are present. A nonce on `style-src` disables the browser's implicit allowance for inline styles, so JSX-rendered `<style>` blocks must also carry a matching nonce or move to an external stylesheet. Use `metadata.head` for route-owned inline styles that need the framework nonce, for example `{ tag: "style", nonce: true, content: "body{color:red}" }`. If you only need script hardening, set `script-src` and leave `style-src` unset.
173
203
 
204
+ Responses whose enforced or report-only CSP contains a nonce are treated as visitor-dependent: the router marks them `Cache-Control: private, no-store`, excludes them from route caches and prerender manifests, and rejects older persisted entries that contain a nonce. This rule also applies when a final `onResponse` hook adds the CSP header. Hash-based and other nonce-free CSP responses remain eligible for shared caching.
205
+
174
206
  Rendered routes include conservative default response headers: `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, and `Permissions-Policy: camera=(), microphone=(), geolocation=()`. Use `metadata.security` to opt out of a default, override a policy, add `X-Frame-Options`, or enable HSTS for HTTPS requests. Security header values are validated before they are emitted.
175
207
 
176
208
  Root app files can define crawler and install metadata without adding route handlers. `robots.ts`, `sitemap.ts`, and `manifest.ts` serve `/robots.txt`, `/sitemap.xml`, and `/manifest.webmanifest`; static `robots.txt`, `sitemap.xml`, `manifest.webmanifest`, `favicon.ico`, `icon.*`, `apple-icon.*`, and `opengraph-image.*` files are copied as public assets during builds. Route-local `opengraph-image.tsx` / `.ts` / `.jsx` / `.js` modules serve `/<route>/opengraph-image`, receive `{ params, request }`, may return a `Response`, string, or bytes, and are used as the default `og:image` when the page does not set one explicitly. Static files take precedence over generated metadata routes for the same URL, and icon files are also used as fallback route metadata when a page does not define its own icon or OpenGraph image.
@@ -209,7 +241,11 @@ export async function loader(context: LoaderContext<{ id: string }>): Promise<Us
209
241
  }
210
242
 
211
243
  export default definePage<typeof loader>(function UserPage(props) {
212
- return <h1>{props.params.id}: {props.data.name}</h1>;
244
+ return (
245
+ <h1>
246
+ {props.params.id}: {props.data.name}
247
+ </h1>
248
+ );
213
249
  });
214
250
  ```
215
251
 
@@ -260,7 +296,13 @@ Routes that render route-local `<Await>` directly or through app-local server co
260
296
 
261
297
  ```tsx
262
298
  function FeedList(props) {
263
- return <ul>{props.items.map((item) => <li>{item}</li>)}</ul>;
299
+ return (
300
+ <ul>
301
+ {props.items.map((item) => (
302
+ <li>{item}</li>
303
+ ))}
304
+ </ul>
305
+ );
264
306
  }
265
307
 
266
308
  export default function Page() {
@@ -305,9 +347,7 @@ export default function Page() {
305
347
  placeholderAs="div"
306
348
  placeholder={<StorySkeleton count={batch.size} start={batch.start + 1} />}
307
349
  >
308
- {(resolved) => (
309
- <StoryRows stories={resolved.items} start={resolved.start + 1} />
310
- )}
350
+ {(resolved) => <StoryRows stories={resolved.items} start={resolved.start + 1} />}
311
351
  </Await>
312
352
  ))}
313
353
  </ol>
@@ -347,7 +387,11 @@ Server-only pages get the lightweight navigation runtime automatically — witho
347
387
  import { Link } from "@reckona/mreact-router/link";
348
388
 
349
389
  export default function Page() {
350
- return <Link href="/docs" prefetch="viewport">Docs</Link>;
390
+ return (
391
+ <Link href="/docs" prefetch="viewport">
392
+ Docs
393
+ </Link>
394
+ );
351
395
  }
352
396
  ```
353
397
 
@@ -399,11 +443,11 @@ Production adapters enforce the app-router import policy when bundling loaders,
399
443
 
400
444
  For Lambda deployments, build with `mreact-router build --target=aws-lambda` or `buildApp({ targets: ["aws-lambda"] })`. The generated buffered and streaming handlers warm the same middleware policy during module initialization by default and then use request handlers with duplicate preload disabled. The artifact manifest records `mreact-handler.handler` and `mreact-streaming-handler.handler` separately. Select a broader or disabled initialization policy with `--aws-lambda-preload=hot-route-requests|all|none` or `buildApp({ awsLambdaPreload: "all", targets: ["aws-lambda"] })` only after measuring the packaged handler; pair `hot-route-requests` with `--aws-lambda-preload-routes=/,/login` or `awsLambdaPreloadRoutes`. `none` disables generated module preload but still materializes the writable runtime directory during generated handler import. The package command preserves the build-generated policy unless package options explicitly override it. The target writes Node-compatible server/client output, `.mreact/server/import-policy.json`, and generated Lambda entries while skipping `.mreact/cloudflare` route modules, so loaders and server helpers may import Node-only dependencies such as database drivers without being bundled for the Workers runtime.
401
445
 
402
- Package Lambda deployments with `mreact-router package aws-lambda --from .mreact --out .lambda` instead of pointing deployment tooling at the full project checkout. AWS Lambda enforces a 250 MB unzipped deployment package limit, and the runtime only needs `.mreact/`, `mreact-handler.mjs`, `package.json` / lockfiles, and production `node_modules`; `src/`, tests, dev dependencies, build caches, and Vite/Vitest/Playwright tooling are not required. `mreact-router build --target=aws-lambda` keeps compiled server route artifacts in `.mreact/server/server-modules/*.json` instead of embedding them in one large server manifest, writes compiled module bodies as hashed `.mjs` files, and keeps request/control artifacts separate from render artifacts so loader redirects do not read page render bundles. AWS Lambda request/control artifacts bundle the generated import-policy packages they use for loaders, middleware, route handlers, and metadata, reducing first-hit package resolution on sparse Lambda traffic while leaving render-only, server action, custom handler, and adapter dependencies in production `node_modules`. `createAwsLambdaRequestHandler()` treats `outDir` as read-only and materializes generated runtime files under `/tmp/mreact-router/<hash>/runtime` by default, with a `node_modules` symlink back to the deployed package root. Direct `createAwsLambdaRequestHandler()` and `createAwsLambdaStreamingRequestHandler()` handlers start only middleware and shared runtime preload in the background by default, so all-route preload work does not compete with the first user request. If a request arrives before preload finishes, middleware is resolved first, middleware responses or redirects return without loading the matched page artifact, and continuing requests load only the matched route's artifact closure. Static middleware `config.matcher` and `config.id` values are checked before importing the middleware module, so unmatched health checks and route-local middleware skips avoid evaluating heavyweight middleware dependencies. Route request artifacts omit page render exports, and built loader and route metadata artifacts are split, so loader redirects do not evaluate page-only or metadata-only dependencies before render or metadata is needed. Loader redirects settle before page component server transforms and render imports for non-stream routes, stream routes without a loading boundary, and stream loading routes whose loader source contains router control-flow helpers such as `redirect()` or `notFound()`. `createPreloadedAwsLambdaRequestHandler()` validates the first event before it starts full preload. When a deployment intentionally accepts extra Lambda initialization work to reduce first-valid-request latency, call `await warmAwsLambdaRuntime(options)` during controlled initialization before creating the handler. Pass `runtimeDir` only when you need to control that writable cache location. With pnpm, run `pnpm --dir .lambda install --prod --frozen-lockfile --ignore-scripts --config.node-linker=hoisted`. pnpm's default isolated linker is symlink-heavy, so verify the artifact's symlink count with `find .lambda -type l | wc -l` and measure actual file bytes in addition to `du -sh .lambda` before upload. Packages listed in the generated import policy may still be needed by render-only modules, inferred server actions, custom handlers, or adapter code, so keep them installed in that production artifact.
446
+ Package Lambda deployments with `mreact-router package aws-lambda --from .mreact --out .lambda` instead of pointing deployment tooling at the full project checkout. AWS Lambda enforces a 250 MB unzipped deployment package limit, and the runtime only needs `.mreact/`, `mreact-handler.mjs`, `package.json` / lockfiles, and production `node_modules`; `src/`, tests, dev dependencies, build caches, and Vite/Vitest/Playwright tooling are not required. `mreact-router build --target=aws-lambda` keeps compiled server route artifacts in `.mreact/server/server-modules/*.json` instead of embedding them in one large server manifest, writes compiled module bodies as hashed `.mjs` files, and keeps request/control artifacts separate from render artifacts so loader redirects do not read page render bundles. AWS Lambda request/control artifacts bundle the generated import-policy packages they use for loaders, middleware, route handlers, and metadata, reducing first-hit package resolution on sparse Lambda traffic while leaving render-only, server action, custom handler, and adapter dependencies in production `node_modules`. `createAwsLambdaRequestHandler()` treats `outDir` as read-only and materializes generated runtime files under `/tmp/mreact-router/<hash>/runtime` by default, with a `node_modules` symlink back to the deployed package root. Direct `createAwsLambdaRequestHandler()` and `createAwsLambdaStreamingRequestHandler()` handlers start only middleware and shared runtime preload in the background by default, so all-route preload work does not compete with the first user request. If a request arrives before preload finishes, middleware is resolved first, middleware responses or redirects return without loading the matched page artifact, and continuing requests load only the matched route's artifact closure. Static middleware `config.matcher` and `config.id` values are checked before importing the middleware module, so unmatched health checks and route-local middleware skips avoid evaluating heavyweight middleware dependencies. Route request artifacts omit page render exports, and built loader and route metadata artifacts are split, so loader redirects do not evaluate page-only or metadata-only dependencies before render or metadata is needed. Every loader's outer result settles before page component transforms or render imports for non-stream routes and stream routes, including stream routes with a loading boundary and controls reached through imported helpers. Return non-critical promises through `defer()` and render them with `<Await>` when they should continue behind the loading boundary. `createPreloadedAwsLambdaRequestHandler()` validates the first event before it starts full preload. When a deployment intentionally accepts extra Lambda initialization work to reduce first-valid-request latency, call `await warmAwsLambdaRuntime(options)` during controlled initialization before creating the handler. Pass `runtimeDir` only when you need to control that writable cache location. With pnpm, run `pnpm --dir .lambda install --prod --frozen-lockfile --ignore-scripts --config.node-linker=hoisted`. pnpm's default isolated linker is symlink-heavy, so verify the artifact's symlink count with `find .lambda -type l | wc -l` and measure actual file bytes in addition to `du -sh .lambda` before upload. Packages listed in the generated import policy may still be needed by render-only modules, inferred server actions, custom handlers, or adapter code, so keep them installed in that production artifact.
403
447
 
404
448
  When a deployment needs custom adapter options such as `serverActions.authorize`, `onResponse`, `errorHandler`, or `allowedHosts`, create an app handler and package it with `mreact-router package aws-lambda --from .mreact --out .lambda --handler src/lambda.ts`. The package command bundles that handler into `.lambda/mreact-handler.mjs`, resolves app-local TypeScript imports with Vite-style extensionless paths, and keeps package imports external so production `node_modules` remains the runtime dependency boundary. If the handler imports the same app server modules that routes also import, those modules are compiled into the handler bundle separately from the mreact route artifacts; use `getServerRuntimeState()` for state that must be shared through the router runtime instead of relying on module singletons.
405
449
 
406
- Configure `allowedHosts` for production Lambda deployments so absolute URLs, redirects, cache keys, and metadata cannot be derived from attacker-controlled Host input. The Lambda adapter uses `Host` by default and only reads `x-forwarded-host` when `hostPolicy: "trusted-proxy"` is set. It also ignores `x-forwarded-proto` unless `trustForwardedProto: true` is configured, deriving the request scheme from the Lambda event protocol when available and otherwise defaulting to HTTPS.
450
+ Configure `allowedHosts` for production Lambda deployments so absolute URLs, redirects, cache keys, and metadata cannot be derived from attacker-controlled Host input. The Lambda adapter uses `Host` by default and only reads `x-forwarded-host` when `hostPolicy: "trusted-proxy"` is set. API Gateway HTTP API v2 and Lambda Function URL requests default to HTTPS because `requestContext.http.protocol` is an HTTP version such as `HTTP/1.1`, not a URL scheme. The adapter ignores `x-forwarded-proto` unless `trustForwardedProto: true` is configured; enable that option only behind a proxy that overwrites the header.
407
451
 
408
452
  Use the Lambda `preload` option to tune that trade-off. Direct handlers default to `"middleware"` so cold starts do not kick off all route module imports in parallel with the first request. `createPreloadedAwsLambdaRequestHandler()` and `createPreloadedAwsLambdaStreamingRequestHandler()` default to `"all"` after event validation. Call `warmAwsLambdaRuntime()` explicitly when initialization-time full preload is an intentional deployment choice. Set `preload: "none"` to disable background preload, `preload: "all"` only when you have measured that all-route background preload helps the deployed function, `preload: { mode: "hot-routes", routes: ["/", "/dashboard"] }` with `warmAwsLambdaRuntime()` to warm selected route closures including render modules before valid traffic, or `preload: { mode: "hot-route-requests", routes: ["/", "/login"] }` to warm only middleware plus selected route loader/server-route request modules without evaluating page/layout render modules. For direct handlers that cannot use top-level async initialization, add `wait: "before-render"` to an object preload strategy to let page-rendering invocations wait for the already-started preload after loader redirects have had a chance to return; add `wait: "first-request"` only when the first invocation can pay the full preload cost. Lambda request timing reports `first-request` waits as `preloadWaitMs`, and render timing reports `before-render` waits as `preloadWaitMs`.
409
453
 
package/dist/actions.d.ts CHANGED
@@ -19,9 +19,15 @@ declare class BoundedReplayStore {
19
19
  private readonly ttlMs;
20
20
  private readonly maxEntries;
21
21
  private readonly entries;
22
+ private capacityWarningActive;
23
+ private warnedLeaseReclamation;
24
+ private readonly minimumRetentionMs;
22
25
  constructor(ttlMs: number, maxEntries: number);
23
26
  claim(value: string): ServerActionReplayClaim;
27
+ clear(): void;
24
28
  size(): number;
29
+ private warnCapacity;
30
+ private warnLeaseReclamation;
25
31
  }
26
32
  export declare function __clearDefaultReplayStore(): void;
27
33
  export declare function __readDefaultReplayStore(): BoundedReplayStore;
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
package/dist/actions.js CHANGED
@@ -3,12 +3,12 @@ import { access, readdir, readFile } from "node:fs/promises";
3
3
  import { dirname, join, relative, sep } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { collectFormActionExpressionReferences, hasModuleDirective, } from "@reckona/mreact-compiler";
6
- import { createServerActionHandler, } from "@reckona/mreact-server";
6
+ import { createServerActionHandler, ensureServerActionReplayStoreContract, } from "@reckona/mreact-server";
7
7
  import { bundleRouterModule } from "./bundle-pipeline.js";
8
8
  import { withRouteCacheContext } from "./cache.js";
9
9
  import { fileImportMetaUrlPlugin, importAppRouterSourceModule } from "./module-runner.js";
10
10
  import { createAppRouterImportPolicyPlugin } from "./import-policy.js";
11
- import { cookies } from "./navigation.js";
11
+ import { cookies, isSafeInternalRedirect } from "./navigation.js";
12
12
  export { createFormCsrfToken, formCsrfCookie, formCsrfFieldName, serverActionCookie, validateFormCsrf, } from "./csrf.js";
13
13
  import { formCsrfFieldName, readExistingFormCsrfToken, serverActionCookieName, validateFormCsrf, } from "./csrf.js";
14
14
  import { collectRuntimeInferredServerActions, } from "./server-action-inference.js";
@@ -45,6 +45,7 @@ const actionTokenSecret = configuredActionTokenSecret(process.env.MREACT_SERVER_
45
45
  // traffic does not trip false-positive 409s.
46
46
  const DEFAULT_REPLAY_TTL_MS = 10 * 60 * 1000;
47
47
  const DEFAULT_REPLAY_MAX_ENTRIES = 50_000;
48
+ const DEFAULT_REPLAY_MINIMUM_RETENTION_MS = 60_000;
48
49
  const DEFAULT_ACTION_BODY_MAX_BYTES = 10 * 1024 * 1024;
49
50
  const DEFAULT_ACTION_FORM_MAX_FIELDS = 1_000;
50
51
  let warnedUnrestrictedServerActions = false;
@@ -67,27 +68,50 @@ class BoundedReplayStore {
67
68
  ttlMs;
68
69
  maxEntries;
69
70
  entries = new Map();
71
+ capacityWarningActive = false;
72
+ warnedLeaseReclamation = false;
73
+ minimumRetentionMs;
70
74
  constructor(ttlMs, maxEntries) {
71
75
  this.ttlMs = ttlMs;
72
76
  this.maxEntries = maxEntries;
77
+ this.minimumRetentionMs = Math.min(ttlMs, DEFAULT_REPLAY_MINIMUM_RETENTION_MS);
73
78
  }
74
79
  claim(value) {
80
+ const now = Date.now();
81
+ if (this.entries.size < this.maxEntries) {
82
+ this.capacityWarningActive = false;
83
+ }
75
84
  const existing = this.entries.get(value);
76
- if (existing?.state === "completed" && existing.expiresAt < Date.now()) {
85
+ if (existing?.state === "completed" && existing.expiresAt <= now) {
86
+ this.entries.delete(value);
87
+ }
88
+ else if (existing?.state === "in-flight" && existing.leaseExpiresAt <= now) {
77
89
  this.entries.delete(value);
90
+ this.warnLeaseReclamation();
78
91
  }
79
92
  if (this.entries.has(value))
80
93
  return { status: "replay" };
81
94
  if (this.entries.size >= this.maxEntries) {
82
- const now = Date.now();
95
+ this.warnCapacity();
96
+ let reclaimableCompletedKey;
83
97
  for (const [key, entry] of this.entries) {
84
- if (entry.state === "completed" && entry.expiresAt < now)
98
+ if ((entry.state === "completed" && entry.expiresAt <= now) ||
99
+ (entry.state === "in-flight" && entry.leaseExpiresAt <= now)) {
85
100
  this.entries.delete(key);
101
+ }
102
+ else if (reclaimableCompletedKey === undefined &&
103
+ entry.state === "completed" &&
104
+ entry.protectedUntil <= now) {
105
+ reclaimableCompletedKey = key;
106
+ }
107
+ }
108
+ if (this.entries.size >= this.maxEntries && reclaimableCompletedKey !== undefined) {
109
+ this.entries.delete(reclaimableCompletedKey);
86
110
  }
87
111
  if (this.entries.size >= this.maxEntries)
88
112
  return { status: "capacity-exceeded" };
89
113
  }
90
- const entry = { state: "in-flight" };
114
+ const entry = { state: "in-flight", leaseExpiresAt: now + this.ttlMs };
91
115
  this.entries.set(value, entry);
92
116
  let finalized = false;
93
117
  return {
@@ -97,22 +121,54 @@ class BoundedReplayStore {
97
121
  return;
98
122
  finalized = true;
99
123
  if (this.entries.get(value) === entry) {
100
- this.entries.set(value, { state: "completed", expiresAt: Date.now() + this.ttlMs });
124
+ const completedAt = Date.now();
125
+ this.entries.delete(value);
126
+ this.entries.set(value, {
127
+ state: "completed",
128
+ expiresAt: completedAt + this.ttlMs,
129
+ protectedUntil: completedAt + this.minimumRetentionMs,
130
+ });
101
131
  }
102
132
  },
103
133
  };
104
134
  }
105
135
  // Exposed for tests; not part of the ServerActionReplayStore interface.
136
+ clear() {
137
+ this.entries.clear();
138
+ this.capacityWarningActive = false;
139
+ this.warnedLeaseReclamation = false;
140
+ }
141
+ // Exposed for tests; not part of the ServerActionReplayStore interface.
106
142
  size() {
107
143
  return this.entries.size;
108
144
  }
145
+ warnCapacity() {
146
+ if (this.capacityWarningActive)
147
+ return;
148
+ this.capacityWarningActive = true;
149
+ let completed = 0;
150
+ let inFlight = 0;
151
+ for (const entry of this.entries.values()) {
152
+ if (entry.state === "completed")
153
+ completed += 1;
154
+ else
155
+ inFlight += 1;
156
+ }
157
+ console.error(`mreact-router: default server action replay store reached capacity (size=${this.entries.size}, maxEntries=${this.maxEntries}, completed=${completed}, inFlight=${inFlight}, minimumRetentionMs=${this.minimumRetentionMs}). Completed claims remain exact through minimumRetentionMs and may be reclaimed under pressure afterward; expired in-flight leases may also be reclaimed. Applications requiring exact protection for the full action lifetime and retention window must use a shared replay store.`);
158
+ }
159
+ warnLeaseReclamation() {
160
+ if (this.warnedLeaseReclamation)
161
+ return;
162
+ this.warnedLeaseReclamation = true;
163
+ console.error(`mreact-router: default server action replay store reclaimed an expired in-flight claim after leaseMs=${this.ttlMs}; exact duplicate-execution protection no longer applies to that abandoned claim.`);
164
+ }
109
165
  }
110
166
  const usedFormActionNonces = new BoundedReplayStore(DEFAULT_REPLAY_TTL_MS, DEFAULT_REPLAY_MAX_ENTRIES);
111
167
  // Test helpers: drop all entries between cases / expose the bounded store
112
168
  // so tests can drive its eviction semantics directly. Not part of the
113
169
  // public surface (prefixed with `__`).
114
170
  export function __clearDefaultReplayStore() {
115
- usedFormActionNonces.entries.clear();
171
+ usedFormActionNonces.clear();
116
172
  }
117
173
  export function __readDefaultReplayStore() {
118
174
  return usedFormActionNonces;
@@ -522,8 +578,9 @@ function sameOriginRefererPath(request) {
522
578
  try {
523
579
  const requestUrl = new URL(request.url);
524
580
  const refererUrl = new URL(referer, requestUrl);
525
- return refererUrl.origin === requestUrl.origin
526
- ? `${refererUrl.pathname}${refererUrl.search}`
581
+ const location = `${refererUrl.pathname}${refererUrl.search}`;
582
+ return refererUrl.origin === requestUrl.origin && isSafeInternalRedirect(location)
583
+ ? location
527
584
  : undefined;
528
585
  }
529
586
  catch {
@@ -1001,6 +1058,9 @@ function validateServerActionRequestOrigin(request) {
1001
1058
  }
1002
1059
  async function claimFormNonce(nonce, replayStore) {
1003
1060
  let claim;
1061
+ if (!ensureServerActionReplayStoreContract(replayStore)) {
1062
+ return replayStoreUnavailableResponse();
1063
+ }
1004
1064
  try {
1005
1065
  claim = await replayStore.claim(nonce);
1006
1066
  }