import { z } from 'zod'; import { toExtendedJson } from '@xo-cash/utils'; import type { RequestHeaders, RouteDefinition, RouteModule } from '../routes/types.ts'; import { ApplicationError, UnauthorizedError } from '../errors/index.ts'; import { ApplicationRouteStream } from './route-stream.ts'; import type { BaseStream } from './stream/base-stream.ts'; import type { AuthSecp256k1 } from '../auth/auth.ts'; /** Canonical request produced by every transport adapter. */ export type ApplicationRequest = { /** Exact application route name. */ path: string; /** Transport-decoded application payload. */ body?: unknown; /** Transport-normalized request headers, keyed by lowercase name. */ headers?: RequestHeaders; /** Optional correlation ID supplied by a multiplexed transport. */ requestId?: string; }; export type ApplicationRouterDependencies = { /** Authentication service. */ auth: AuthSecp256k1; }; const accountSchema = z .object({ 'x-public-key': z.string(), 'x-signature': z.string(), 'x-timestamp': z.coerce.number(), }) .transform((data) => ({ publicKey: data['x-public-key'], signature: data['x-signature'], timestamp: data['x-timestamp'], })); /** Exact-match application routing shared by every wire transport. */ export class ApplicationRouter { /** @param routes - Validated route table keyed by exact path. */ private constructor( private readonly deps: ApplicationRouterDependencies, private readonly routes: ReadonlyMap, ) {} /** * Load and validate the complete route table before accepting traffic. * * @param routeModules - Route modules whose handlers will be registered. * @returns A ready-to-dispatch router instance. */ static async create(deps: ApplicationRouterDependencies, routeModules: RouteModule[]): Promise { const routes = new Map(); // Collect routes from every module and reject duplicates at startup. for (const routeModule of routeModules) { for (const route of await routeModule.getRoutes()) { ApplicationRouter.assertValidPath(route.url); if (routes.has(route.url)) { throw new Error(`Duplicate application route: ${route.url}`); } routes.set(route.url, route); } } return new ApplicationRouter(deps, routes); } /** * Execute one route using a request-scoped facade over the connection. * * @param request - Transport-normalized application request. * @param connection - Shared connection stream for this transport session. */ async dispatch(request: ApplicationRequest, connection: BaseStream): Promise { // Authenticate the headers on the request. const { publicKey, signature, timestamp } = accountSchema.parse(request.headers); // Make sure the request signature is valid and hasnt been used before await this.deps.auth.verifyUniqueRequest(signature); // Ensure that the headers are present. if (!publicKey || !signature || !timestamp) { throw new UnauthorizedError('Missing authentication headers'); } // Compile the signature payload as `Path:Timestamp:Body` const signaturePayload = `${timestamp}:${request.path}:${toExtendedJson(request.body)}`; // Verify the signature of the request. const verified = await this.deps.auth.verifySignature(publicKey, signature, signaturePayload); if (!verified) { throw new UnauthorizedError('Invalid signature'); } // Get the route from the routes map. const route = this.routes.get(request.path); if (!route) { throw new ApplicationError(404, `No route found for ${request.path}`); } const stream = new ApplicationRouteStream(connection, request.body, request.path, request.requestId, request.headers); await route.handler(stream); } /** * Enforce the deliberately small exact-path routing grammar at startup. * * @param path - Candidate route path to validate. */ private static assertValidPath(path: string): void { if (!path.startsWith('/') || path.length === 1 || path.includes(':') || path.includes('?') || path.includes('#')) { throw new Error(`Invalid application route "${path}": routes must be exact paths beginning with /`); } } }