Files
WRNexusJS/packages/ssr/src/rpc.ts
T
2026-08-01 01:09:58 +05:30

228 lines
8.1 KiB
TypeScript

export interface RpcParameterContract {
name: string;
type: string;
optional: boolean;
}
export interface RpcManifestContract {
function: string;
parameters?: RpcParameterContract[];
returnType?: string;
}
export interface RpcRequestPayload {
component: string;
function: string;
args: unknown[];
}
export interface RpcContext {
request: Request;
user?: unknown;
traceId: string;
}
export interface RpcHandlerOptions {
resolve(component: string): Promise<{
functions: Record<string, (...args: any[]) => any>;
manifest?: RpcManifestContract[];
} | null>;
authenticate?: (request: Request) => Promise<unknown> | unknown;
authorize?: (context: RpcContext, payload: RpcRequestPayload) => Promise<boolean> | boolean;
validateCsrf?: (request: Request) => Promise<boolean> | boolean;
validateInput?: (
payload: RpcRequestPayload,
manifestEntry: RpcManifestContract,
) => Promise<unknown[]> | unknown[];
validateOutput?: (
value: unknown,
manifestEntry: RpcManifestContract,
) => Promise<unknown> | unknown;
}
function json(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store",
},
});
}
function error(
code: string,
message: string,
status: number,
traceId: string,
details?: unknown,
): Response {
return json(
{ ok: false, error: { code, message, traceId, ...(details === undefined ? {} : { details }) } },
status,
);
}
function validName(value: unknown): value is string {
return typeof value === "string" && /^[A-Za-z_$][\w$]*$/.test(value);
}
function removePromise(type: string): string {
const match = /^Promise\s*<([\s\S]+)>$/.exec(type.trim());
return match?.[1]?.trim() ?? type.trim();
}
function splitUnion(type: string): string[] {
const parts: string[] = [];
let depth = 0;
let quote = "";
let start = 0;
for (let index = 0; index < type.length; index++) {
const char = type[index]!;
if (quote) {
if (char === "\\") index++;
else if (char === quote) quote = "";
continue;
}
if (char === '"' || char === "'") quote = char;
else if ("<([{".includes(char)) depth++;
else if (">)]}".includes(char)) depth = Math.max(0, depth - 1);
else if (char === "|" && depth === 0) {
parts.push(type.slice(start, index).trim());
start = index + 1;
}
}
parts.push(type.slice(start).trim());
return parts.filter(Boolean);
}
function matchesRuntimeType(value: unknown, rawType: string): boolean {
const type = removePromise(rawType).trim();
if (!type || ["unknown", "any", "never"].includes(type)) return true;
const union = splitUnion(type);
if (union.length > 1) return union.some((part) => matchesRuntimeType(value, part));
if (type === "undefined" || type === "void") return value === undefined;
if (type === "null") return value === null;
if (/^"[\s\S]*"$|^'[\s\S]*'$/.test(type)) return value === type.slice(1, -1);
if (/^-?\d+(?:\.\d+)?$/.test(type)) return value === Number(type);
if (type === "true") return value === true;
if (type === "false") return value === false;
if (type === "string") return typeof value === "string";
if (type === "number") return typeof value === "number" && Number.isFinite(value);
if (type === "boolean") return typeof value === "boolean";
if (type === "bigint") return typeof value === "bigint";
if (type === "Date")
return value instanceof Date || (typeof value === "string" && !Number.isNaN(Date.parse(value)));
if (type.endsWith("[]"))
return (
Array.isArray(value) && value.every((item) => matchesRuntimeType(item, type.slice(0, -2)))
);
if (/^(?:Readonly)?Array\s*</.test(type)) return Array.isArray(value);
if (/^(?:Record|Map|Set)\s*</.test(type)) return value !== null && typeof value === "object";
if (/^\{[\s\S]*\}$/.test(type))
return value !== null && typeof value === "object" && !Array.isArray(value);
// Named interfaces/types are validated by generated application validators
// when available. Their JSON transport shape must at least be object-like.
if (/^[A-Za-z_$][\w$]*(?:<.*>)?$/.test(type)) return value !== null && typeof value === "object";
return true;
}
function validateArguments(payload: RpcRequestPayload, manifest: RpcManifestContract): unknown[] {
const parameters = manifest.parameters ?? [];
const required = parameters.filter((parameter) => !parameter.optional).length;
if (payload.args.length < required || payload.args.length > parameters.length) {
throw new TypeError(
`WRN-RPC-INPUT: ${manifest.function} expects ${required === parameters.length ? required : `${required}-${parameters.length}`} arguments, received ${payload.args.length}.`,
);
}
parameters.forEach((parameter, index) => {
const value = payload.args[index];
if (value === undefined && parameter.optional) return;
if (!matchesRuntimeType(value, parameter.type)) {
throw new TypeError(
`WRN-RPC-INPUT: argument '${parameter.name}' expected ${parameter.type}.`,
);
}
});
return payload.args;
}
function validateReturn(value: unknown, manifest: RpcManifestContract): unknown {
const type = manifest.returnType ?? "unknown";
if (!matchesRuntimeType(value, type)) {
throw new TypeError(
`WRN-RPC-OUTPUT: ${manifest.function} returned a value incompatible with ${type}.`,
);
}
return value;
}
export function createRpcHandler(
options: RpcHandlerOptions,
): (request: Request) => Promise<Response> {
return async (request) => {
const traceId = request.headers.get("x-request-id") || crypto.randomUUID();
if (request.method !== "POST")
return error("WRN-RPC-METHOD", "RPC requires POST", 405, traceId);
if (options.validateCsrf && !(await options.validateCsrf(request))) {
return error("WRN-RPC-CSRF", "CSRF validation failed", 403, traceId);
}
let payload: RpcRequestPayload;
try {
payload = (await request.json()) as RpcRequestPayload;
} catch {
return error("WRN-RPC-JSON", "Invalid JSON request", 400, traceId);
}
if (
!validName(payload.component) ||
!validName(payload.function) ||
!Array.isArray(payload.args)
) {
return error("WRN-RPC-PAYLOAD", "Invalid RPC payload", 400, traceId);
}
const resolved = await options.resolve(payload.component);
if (!resolved)
return error(
"WRN-RPC-COMPONENT",
`Unknown RPC component '${payload.component}'`,
404,
traceId,
);
const manifestEntry = resolved.manifest?.find((entry) => entry.function === payload.function);
if (!manifestEntry) {
return error(
"WRN-RPC-FUNCTION",
`Server function '${payload.function}' is not remotely exposed`,
404,
traceId,
);
}
const fn = resolved.functions[payload.function];
if (typeof fn !== "function") {
return error(
"WRN-RPC-FUNCTION",
`Server function '${payload.function}' is not available`,
404,
traceId,
);
}
const user = options.authenticate ? await options.authenticate(request) : undefined;
const context: RpcContext = { request, user, traceId };
if (options.authorize && !(await options.authorize(context, payload))) {
return error("WRN-RPC-AUTHZ", "Not authorized", 403, traceId);
}
try {
const args = options.validateInput
? await options.validateInput(payload, manifestEntry)
: validateArguments(payload, manifestEntry);
const value = await fn(...args, context);
const validated = options.validateOutput
? await options.validateOutput(value, manifestEntry)
: validateReturn(value, manifestEntry);
return json({ ok: true, value: validated, traceId });
} catch (cause) {
const message = cause instanceof Error ? cause.message : String(cause);
const code = message.startsWith("WRN-RPC-INPUT")
? "WRN-RPC-INPUT"
: message.startsWith("WRN-RPC-OUTPUT")
? "WRN-RPC-OUTPUT"
: "WRN-RPC-EXECUTION";
return error(code, message, code === "WRN-RPC-EXECUTION" ? 500 : 400, traceId);
}
};
}