import { type ClientAddressResolver, TRUSTED_CLIENT_ADDRESS_HEADER } from "@ekklesia/core/identity"; import type { Http2Bindings, HttpBindings } from "@hono/node-server"; type NodeServerBindings = HttpBindings | Http2Bindings; export { type ClientAddressConfig, ClientAddressConfigError, type ClientAddressConfigErrorCode, type ClientAddressConfigInput, type ClientAddressFailureReason, type ClientAddressMode, type ClientAddressResolution, type ClientAddressResolver, createClientAddressConfig, createClientAddressResolver, type DirectClientAddressConfig, findInvalidTrustedProxies, normalizeClientAddress, type TrustedProxyClientAddressConfig, } from "@ekklesia/core/identity"; /** A fixed, generic diagnostic — never the peer address, forwarded header content, request path, or * any other request detail — logged only when client-address resolution fails closed. */ export const CLIENT_ADDRESS_UNRESOLVED_LOG_MESSAGE = "[client-address] A request's caller address could not be resolved safely; rejecting with 503."; function sanitizedClientAddressUnresolvedResponse(): Response { return new Response(JSON.stringify({ code: "CLIENT_ADDRESS_UNRESOLVED" }), { headers: { "cache-control": "no-store", "content-type": "application/json" }, status: 503, }); } /** * Wraps a `@hono/node-server` fetch callback at the exact Node/API transport boundary — the one * place in this repository below `apps/api` that reads the raw Node `IncomingMessage` — with * Core's framework-agnostic client-address resolution policy (`@ekklesia/core/identity`). Resolves * the caller's trusted address from the real socket peer (`env.incoming.socket.remoteAddress`) * before the wrapped callback — and therefore Better Auth's own rate limiter — ever sees the * request, then overwrites (never merely appends) any externally supplied copy of * `TRUSTED_CLIENT_ADDRESS_HEADER` with that resolved value. A caller can therefore never choose its * own resolved address, whether by spoofing a forwarded header or by sending the internal header * directly. Resolution failure never reaches the wrapped callback at all — no Better Auth logic, no * Core mutation, and no module handler ever runs for a request whose caller address could not be * resolved safely. */ export function wrapFetchWithClientAddress( fetchCallback: (request: Request, env: NodeServerBindings) => Response | Promise, resolver: ClientAddressResolver, ): (request: Request, env: NodeServerBindings) => Promise { return async (request: Request, env: NodeServerBindings): Promise => { const resolution = resolver.resolve({ headers: request.headers, peerAddress: env.incoming.socket.remoteAddress, }); if (!resolution.ok) { console.error(CLIENT_ADDRESS_UNRESOLVED_LOG_MESSAGE); return sanitizedClientAddressUnresolvedResponse(); } const headers = new Headers(request.headers); headers.set(TRUSTED_CLIENT_ADDRESS_HEADER, resolution.address); const sanitizedRequest = new Request(request, { headers }); return fetchCallback(sanitizedRequest, env); }; }