fix(dev-server): route server.fn() RPC to a handler in production
server.fn() posts to POST /__wrnexus/rpc. Dev intercepts that path before handlers.fetch and routes it to a createRpcHandler instance built from loadWrnServerModule; createProductionServer/createProductionHandlers had no such route, so the request fell through to the internal-caller-gated inter-app service RPC and 404'd. Add resolveProdServerFunctions(), a synchronous equivalent of dev's resolve that searches the already-statically-imported ProdManifest components/pages/ layouts for __wrnexusServerFunctions + __wrnexusRpcManifest, and wire it into createProductionHandlers with the same validateCsrf + withServerFnRequestContext wrapping dev uses. Move those two helpers into a new rpc-shared.ts so prod.ts can use them without a circular import through index.ts. Add packages/dev-server/test/prod-server-fn-rpc.test.ts covering a successful call, CSRF rejection, and clean 404s for an unknown component/function.
This commit is contained in:
@@ -8,14 +8,7 @@
|
||||
|
||||
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { resolve, dirname, isAbsolute, join } from "node:path";
|
||||
import {
|
||||
createContext,
|
||||
runWithRequestContext,
|
||||
type Middleware,
|
||||
type Mode,
|
||||
type SecurityConfig,
|
||||
type SeoConfig,
|
||||
} from "@wrnexus/core";
|
||||
import { type Middleware, type Mode, type SecurityConfig, type SeoConfig } from "@wrnexus/core";
|
||||
import { buildRouter, type Router } from "@wrnexus/router";
|
||||
import {
|
||||
resolveThemeConfig,
|
||||
@@ -83,36 +76,11 @@ export { getWrnCompileMetrics, resetWrnCompileMetrics } from "./pipeline.ts";
|
||||
export type { WrnCompileMetrics } from "./pipeline.ts";
|
||||
import { resetDevCache } from "./cache.ts";
|
||||
|
||||
export function validateRpcCsrf(request: Request): boolean {
|
||||
const url = new URL(request.url);
|
||||
const origin = request.headers.get("origin");
|
||||
if (origin && origin !== url.origin) return false;
|
||||
const cookieHeader = request.headers.get("cookie") ?? "";
|
||||
const cookieToken =
|
||||
/(?:^|;\s*)wrn-csrf=([^;]+)/.exec(cookieHeader)?.[1] ??
|
||||
/(?:^|;\s*)wrnexus_csrf=([^;]+)/.exec(cookieHeader)?.[1];
|
||||
const headerToken = request.headers.get("x-csrf-token") ?? request.headers.get("x-wrnexus-csrf");
|
||||
return Boolean(cookieToken && headerToken && decodeURIComponent(cookieToken) === headerToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the server-function RPC handler so it runs inside the request's
|
||||
* AsyncLocalStorage context. `/__wrnexus/rpc` is intercepted BEFORE
|
||||
* `handlers.fetch` (fetchHandler) in the dev server, so a server function
|
||||
* called via `server.fn()` from the browser runs entirely outside
|
||||
* fetchHandler's own context wrap. It runs user code directly, so — like
|
||||
* every other entry point that runs user server code — it needs the request
|
||||
* context too.
|
||||
*/
|
||||
export function withServerFnRequestContext(
|
||||
handler: (request: Request) => Promise<Response>,
|
||||
): (request: Request) => Promise<Response> {
|
||||
return (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
const ctx = createContext(request, url);
|
||||
return runWithRequestContext(ctx, () => handler(request));
|
||||
};
|
||||
}
|
||||
// Shared with prod.ts — kept in their own module to avoid a circular import
|
||||
// (index.ts re-exports createProductionServer/createProductionHandlers from
|
||||
// prod.ts, so prod.ts cannot import these back from index.ts).
|
||||
import { validateRpcCsrf, withServerFnRequestContext } from "./rpc-shared.ts";
|
||||
export { validateRpcCsrf, withServerFnRequestContext };
|
||||
|
||||
import type { DevToolbarConfig } from "@wrnexus/dev-toolbar/types";
|
||||
import { createPluginRunner, discoverPlugins, type PluginInput } from "@wrnexus/plugin";
|
||||
|
||||
Reference in New Issue
Block a user