Files
WRNexusJS/packages/rpc/src/http.ts
T
Clintchiz 03d5cb6aa6
Quality / quality (ubuntu-latest) (push) Failing after 10m40s
Quality / quality (windows-latest) (push) Canceled after 0s
fix(gateway): proxy browser server functions to workspace apps
2026-08-18 23:30:42 +05:30

109 lines
3.8 KiB
TypeScript

import { appOrigin } from "@wrnexus/helpers";
import { RPC_ERROR_CODES, failure, isRetryableStatus } from "./errors.ts";
import { RPC_IDENTITY_HEADER } from "./identity.ts";
import type { CallOptions, RpcTarget, Transport } from "./transport.ts";
import type { ServiceResult } from "./types.ts";
export const RPC_PATH_PREFIX = "/__wrnexus/rpc";
export const RPC_INTERNAL_HEADER = "x-wrnexus-internal";
export function rpcPath(service: string, procedure: string): string {
return `${RPC_PATH_PREFIX}/${service}/${procedure}`;
}
function parseOriginMap(value: string | undefined): Record<string, string> {
if (!value) return {};
try {
const parsed: unknown = JSON.parse(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
return parsed as Record<string, string>;
} catch {
return {};
}
}
/**
* Resolve the origin an RPC call to `app` should target.
*
* Prefer `WRNEXUS_INTERNAL_ORIGINS` (loopback origins the gateway hands each
* child before spawning it) over `appOrigin`, which resolves the app's
* PUBLIC origin. The public origin is the wrong target for inter-app RPC: the
* gateway 404s private `/__wrnexus/rpc/<service>/<procedure>` routes. (The
* exact prefix remains the CSRF-protected browser server-function endpoint.)
* That block keeps inter-app calls off the public internet. Falling back to `appOrigin`
* when no internal-origin map is present keeps single-app and test setups
* (which only set `WRNEXUS_WORKSPACE_ORIGINS`) working.
*/
export function resolveAppOrigin(app: string): string {
const internalOrigin = parseOriginMap(process.env.WRNEXUS_INTERNAL_ORIGINS)[app];
if (internalOrigin) {
try {
return new URL(internalOrigin).origin;
} catch {
// Malformed internal-origin entry — fall through to the public origin.
}
}
return appOrigin(app);
}
export interface HttpTransportOptions {
resolveOrigin?: (app: string) => string;
fetch?: typeof fetch;
}
function isServiceResult(value: unknown): value is ServiceResult {
if (!value || typeof value !== "object" || !("ok" in value)) return false;
const result = value as Record<string, unknown>;
return (
result.ok === true ||
(result.ok === false &&
typeof result.code === "string" &&
typeof result.message === "string" &&
typeof result.retryable === "boolean")
);
}
export function httpTransport(options: HttpTransportOptions = {}): Transport {
const resolveOrigin = options.resolveOrigin ?? resolveAppOrigin;
const doFetch = options.fetch ?? fetch;
return {
async call(target: RpcTarget, payload: unknown, callOptions: CallOptions) {
let response: Response;
try {
const headers: Record<string, string> = {
"content-type": "application/json",
[RPC_INTERNAL_HEADER]: "1",
};
if (callOptions.identity) headers[RPC_IDENTITY_HEADER] = callOptions.identity;
response = await doFetch(
`${resolveOrigin(target.app)}${rpcPath(target.service, target.procedure)}`,
{
method: "POST",
headers,
body: JSON.stringify(payload ?? {}),
signal: callOptions.signal,
},
);
} catch {
return failure(RPC_ERROR_CODES.transport, "Service unreachable");
}
if (!response.ok) {
return {
ok: false,
code: RPC_ERROR_CODES.transport,
message: `Service returned ${response.status}`,
retryable: isRetryableStatus(response.status),
};
}
try {
const result: unknown = await response.json();
return isServiceResult(result)
? result
: failure(RPC_ERROR_CODES.malformed, "Malformed service response");
} catch {
return failure(RPC_ERROR_CODES.malformed, "Malformed service response");
}
},
};
}