import type { Context } from "./context.ts"; export interface SchemaLike { parse(input: unknown): T; } export interface OutputSchemaLike { readonly __output: T; parse(input: unknown): unknown; } export type InferEndpointSchema = TSchema extends OutputSchemaLike ? TValue : never; export interface EndpointErrorBody { code: string; message: string; details?: unknown; } export class EndpointError extends Error { constructor( readonly status: number, readonly code: string, message: string, readonly details?: unknown, ) { super(message); this.name = "EndpointError"; } } export interface EndpointDefinition { input?: SchemaLike | OutputSchemaLike; output?: SchemaLike | OutputSchemaLike; auth?: "optional" | "required"; description?: string; tags?: string[]; handler(input: I, ctx: Context): O | Promise; } export interface DefinedEndpoint { readonly definition: EndpointDefinition; (ctx: Context, input?: unknown): Promise; } function json(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8" }, }); } function schemaValue(schema: SchemaLike | OutputSchemaLike, input: unknown): T { const parsed = schema.parse(input); if ( parsed && typeof parsed === "object" && "ok" in parsed && "value" in parsed && typeof (parsed as { ok?: unknown }).ok === "boolean" ) { const result = parsed as { ok: boolean; value: T; errors?: unknown }; if (!result.ok) throw new EndpointError( 400, "VALIDATION_ERROR", "Endpoint validation failed.", result.errors, ); return result.value; } return parsed as T; } /** Define a validated, typed endpoint that can also drive SDK/OpenAPI generation. */ export function defineEndpoint< InputSchema extends OutputSchemaLike, OutputSchema extends OutputSchemaLike, >( definition: Omit< EndpointDefinition, InferEndpointSchema>, "input" | "output" > & { input: InputSchema; output: OutputSchema; }, ): DefinedEndpoint, InferEndpointSchema>; export function defineEndpoint( definition: EndpointDefinition, ): DefinedEndpoint; export function defineEndpoint( definition: EndpointDefinition, ): DefinedEndpoint { const endpoint = async (ctx: Context, rawInput?: unknown): Promise => { try { if (definition.auth === "required" && !ctx.user) { throw new EndpointError(401, "UNAUTHENTICATED", "Authentication is required."); } const input = definition.input ? schemaValue(definition.input, rawInput) : rawInput; const rawOutput = await definition.handler(input, ctx); const output = definition.output ? schemaValue(definition.output, rawOutput) : rawOutput; return output instanceof Response ? output : json({ data: output }); } catch (error) { if (error instanceof EndpointError) { return json( { error: { code: error.code, message: error.message, details: error.details } }, error.status, ); } return json( { error: { code: "INTERNAL_ERROR", message: "The endpoint failed unexpectedly.", } satisfies EndpointErrorBody, }, 500, ); } }; return Object.assign(endpoint, { definition }); } export interface RpcClientOptions { baseUrl?: string; fetch?: typeof globalThis.fetch; headers?: HeadersInit | (() => HeadersInit | Promise); } /** Create a tiny typed RPC caller for endpoints exposed by a WrNexus app. */ export function createRpcClient(options: RpcClientOptions = {}) { const request = options.fetch ?? globalThis.fetch; return async function call(path: string, input: I): Promise { const headers = typeof options.headers === "function" ? await options.headers() : (options.headers ?? {}); const response = await request(new URL(path, options.baseUrl ?? globalThis.location?.origin), { method: "POST", headers: { "content-type": "application/json", ...Object.fromEntries(new Headers(headers)) }, body: JSON.stringify(input), }); const body = (await response.json()) as { data?: O; error?: EndpointErrorBody }; if (!response.ok || body.error) { throw new EndpointError( response.status, body.error?.code ?? "RPC_ERROR", body.error?.message ?? `RPC request failed with ${response.status}`, body.error?.details, ); } return body.data as O; }; }