Files
WRNexusJS/packages/dev-server/src/rpc-shared.ts
T
Clintchiz 768074ac0a 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.
2026-08-20 06:41:32 +05:30

47 lines
2.0 KiB
TypeScript

/**
* Shared server-function RPC helpers used by both the dev server (index.ts)
* and the production server (prod.ts). Kept in their own module so prod.ts
* can import them without creating a circular dependency on index.ts (which
* itself re-exports `createProductionServer`/`createProductionHandlers` from
* prod.ts).
*/
import { createContext, runWithRequestContext } from "@wrnexus/core";
/**
* CSRF check for `/__wrnexus/rpc` (the server-function RPC endpoint the
* browser runtime calls for `server.fn()`). Requires a same-origin request
* carrying a matching CSRF cookie + header pair — the standard double-submit
* cookie pattern.
*/
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 both dev and production, 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));
};
}