import { Hono } from "hono"; import { RequestHeaders } from "../../routes/types"; import { ApplicationError } from "../../errors"; import { HTTP_STATUS_CODE_BAD_REQUEST } from "../../constants"; /** RFC 9110 field-name token grammar. */ const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; /** Hono variables populated by transport-boundary middleware. */ export type AppEnv = { Variables: { /** Decoded Extended JSON request body, when present. */ parsedBody?: unknown; /** Raw JSON text preserved for signature verification. */ rawJsonBody?: string; }; }; export abstract class BaseTransport { /** Attach wire endpoints and middleware to the shared Hono application. */ abstract register(app: Hono): void /** Close transport-owned long-lived connections during server shutdown. */ abstract stop(): Promise /** * Validate and normalize request headers at a transport boundary. * * Lowercase names give HTTP and WebSocket routes identical lookup semantics. * Case-insensitive duplicates are rejected instead of selecting an ambiguous * authentication value. Header values may not contain line breaks. */ public static normalizeRequestHeaders(headers: Readonly>): RequestHeaders { const normalizedEntries: Array<[string, string]> = []; const names = new Set(); for (const [ name, value ] of Object.entries(headers)) { if (!HEADER_NAME_PATTERN.test(name)) { throw new ApplicationError(HTTP_STATUS_CODE_BAD_REQUEST, 'Invalid request header name'); } if (value.includes('\r') || value.includes('\n')) { throw new ApplicationError(HTTP_STATUS_CODE_BAD_REQUEST, 'Invalid request header value'); } const normalizedName = name.toLowerCase(); if (names.has(normalizedName)) { throw new ApplicationError(HTTP_STATUS_CODE_BAD_REQUEST, 'Duplicate request header name'); } names.add(normalizedName); normalizedEntries.push([ normalizedName, value ]); } return Object.freeze(Object.fromEntries(normalizedEntries)); }; }