fix(gateway): proxy browser server functions to workspace apps
This commit is contained in:
@@ -470,19 +470,37 @@ export function stripUntrustedInternalHeaders(headers: Headers): Headers {
|
||||
}
|
||||
|
||||
/**
|
||||
* The reserved inter-app RPC namespace is refused at the gateway edge, before
|
||||
* any proxying — it is only ever mounted by a child app's own dev-server and
|
||||
* must never be reachable from outside the workspace.
|
||||
* Private inter-app RPC routes are refused at the gateway edge. The exact
|
||||
* prefix is the CSRF-protected browser-to-app server-function endpoint and is
|
||||
* intentionally proxied to the selected child app.
|
||||
*/
|
||||
export function isRpcGatewayPath(pathname: string): boolean {
|
||||
return (
|
||||
pathname === RPC_PATH_PREFIX ||
|
||||
pathname.startsWith(`${RPC_PATH_PREFIX}/`) ||
|
||||
pathname === RPC_STREAM_PATH_PREFIX ||
|
||||
pathname.startsWith(`${RPC_STREAM_PATH_PREFIX}/`)
|
||||
);
|
||||
}
|
||||
|
||||
/** Build the trusted internal hop for a browser server-function request. */
|
||||
export function gatewayBrowserRpcHeaders(
|
||||
req: Request,
|
||||
url: URL,
|
||||
ip: string,
|
||||
forwardedHeaders: boolean,
|
||||
backendOrigin: string,
|
||||
): Headers {
|
||||
const headers = stripUntrustedInternalHeaders(
|
||||
gatewayProxyHeaders(req, url, ip, forwardedHeaders),
|
||||
);
|
||||
// The public request already passed the gateway's host and fetch-metadata
|
||||
// checks. Present the internal proxy hop as same-origin to the child while
|
||||
// retaining the double-submit CSRF cookie and header.
|
||||
headers.set("origin", backendOrigin);
|
||||
headers.delete("host");
|
||||
return headers;
|
||||
}
|
||||
|
||||
/** Boot every app as a child process, then route by Host on one gateway port. */
|
||||
export async function startGateway(opts: GatewayOptions): Promise<RunningGateway> {
|
||||
const port = opts.port ?? 3000;
|
||||
@@ -497,7 +515,7 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
|
||||
);
|
||||
// Loopback-only origins, computed up front (ports are assigned by index
|
||||
// before any child spawns) so every child can reach every other child
|
||||
// directly — bypassing the gateway, which 404s the RPC prefix by design.
|
||||
// directly — bypassing the gateway, which 404s private nested RPC routes.
|
||||
const internalOriginsEnv: Readonly<Record<string, string>> = Object.freeze(
|
||||
Object.fromEntries(
|
||||
opts.apps.map((app, i) => [app.name, `http://127.0.0.1:${app.port ?? port + 1 + i}`]),
|
||||
@@ -737,9 +755,10 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
|
||||
}
|
||||
|
||||
// HTTP → reverse-proxy to the app, preserving method/headers/body.
|
||||
const headers = stripUntrustedInternalHeaders(
|
||||
gatewayProxyHeaders(req, url, ip, forwardedHeaders),
|
||||
);
|
||||
const headers =
|
||||
url.pathname === RPC_PATH_PREFIX
|
||||
? gatewayBrowserRpcHeaders(req, url, ip, forwardedHeaders, target.origin)
|
||||
: stripUntrustedInternalHeaders(gatewayProxyHeaders(req, url, ip, forwardedHeaders));
|
||||
const body =
|
||||
req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer();
|
||||
let res: Response;
|
||||
|
||||
Reference in New Issue
Block a user