Files
2026-09-14 08:00:08 +00:00

59 lines
2.2 KiB
TypeScript

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<AppEnv>): void
/** Close transport-owned long-lived connections during server shutdown. */
abstract stop(): Promise<void>
/**
* 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<Record<string, string>>): RequestHeaders {
const normalizedEntries: Array<[string, string]> = [];
const names = new Set<string>();
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));
};
}