/**
 * hosting/doorGuard — what a request has to be before any handler sees it.
 *
 *   const guard = doorGuard({ name: 'auth', allowedHosts: ['bot.corp.example'] });
 *   const refusal = guard.check(req);          // an IncomingMessage works as-is
 *   if (refusal) return answer(refusal.status, refusal.message, refusal.code);
 *
 * ── The threat, in one paragraph ────────────────────────────────────────────
 * A door with no verifier, reachable only inside a company network, looks safe
 * because the network is the fence. It is not: every person inside the fence
 * runs a browser, and every web page they open can make that browser send
 * requests to the door FROM INSIDE. Three ways, all live before this file:
 *
 *   1. **A forged request (CSRF).** A page on any site can make the browser
 *      POST a JSON-shaped body as `text/plain` (or as a form, or with no
 *      content type at all) without asking the server first — a CORS "simple"
 *      request. The door parsed it as JSON and ran a turn: model budget spent,
 *      tools called, a conversation stored. The attacker cannot read the reply;
 *      they do not need to.
 *   2. **DNS rebinding.** A page on an attacker's domain re-points its OWN name
 *      at the door's address. The browser now treats the attacker's page as
 *      same-origin with the door, so it may send anything and READ the answer.
 *   3. **A cross-site WebSocket.** A WebSocket is not bound by the same-origin
 *      policy at all; any page can open one, and only the server can say no.
 *
 * ── The rules, one owner ────────────────────────────────────────────────────
 * `check()` judges a request that CHANGES something — every method but GET,
 * HEAD and OPTIONS, plus every WebSocket upgrade (a GET that opens a channel):
 *
 *   - **Host** (with `allowedHosts` set, or defaulted to the loopback names on
 *     a loopback bind; judged on EVERY method): a name this door was not
 *     configured for is refused, 421. First, because under rebinding the other
 *     two checks pass — the Host header, which no page can set, is the one
 *     fact that still names the attacker. `X-Forwarded-Host` is never read for
 *     it: a rebinding page is same-origin and may set that header itself.
 *   - **Origin**: a browser request from a page this door does not allow is
 *     refused, 403 — including `Origin: null`. A request WITHOUT an Origin is a
 *     script or a server (browsers always send one on these requests) and is
 *     not judged by it; that is what keeps every non-browser client working.
 *   - **Fetch metadata**, as defence in depth: a request the browser itself
 *     marked `Sec-Fetch-Site: cross-site` is refused (403) unless its Origin is
 *     one `allowedOrigins` LISTS — so a proxy that strips `Origin` but forwards
 *     `Sec-Fetch-*` still cannot let a forged request through. For a WebSocket
 *     handshake these two are the whole defence: the browser API cannot set a
 *     header, so there is no preflight to force.
 *   - **Content type** (not for an upgrade — it has no body): anything but
 *     `application/json` is refused, 415. A browser must ask first (a
 *     preflight, which this host never approves) before it sends that type
 *     across origins, so requiring it makes a forged request one the browser
 *     refuses to send. It is the same property a required custom header buys
 *     (RFC 10017 §6.1.3.3.2), without breaking every JSON client that already
 *     sends the header and no other.
 *
 * `checkSessionId()` is the fourth bound, and it is not about browsers: a
 * session id is caller data that rides every stored row, log line and trace
 * span of a conversation, so a door refuses one over
 * {@link MAX_SESSION_ID_LENGTH} or carrying a control character.
 *
 * Where it runs: `httpHost` calls `check()` on every request to its request
 * door and on every conversation upgrade, and `checkSessionId()` on the id each
 * dialect reads. An application's OWN routes beside the door (a sign-in route
 * on `onUnhandled`, say) are the application's, and arrive untouched — so this
 * is exported for them to call, and the rule keeps one spelling.
 *
 * Pure: no socket, no clock, no I/O. The design note is
 * `docs/design/2026-09-door-hardening.md`.
 */
import { HostNotAllowedError, InvalidSessionIdError, OriginNotAllowedError, UnsupportedMediaTypeError } from './errors.js';
/**
 * The longest session id any door carries, in UTF-16 code units — JavaScript's
 * own `string.length`.
 *
 * 400 is the width the reference SQL session store gives its `session_id`
 * column (`NVARCHAR(400)`, which counts the same units), so no id this door
 * admits is one a store refuses AFTER the model was paid for. The id formats in
 * use fit with room to spare — a UUID is 36 — and there is deliberately no
 * knob: a correlation handle wider than this is not one anybody needs.
 */
export declare const MAX_SESSION_ID_LENGTH = 400;
/**
 * The cross-site rules a host enforces at its doors. Every field is optional,
 * and the defaults are the safe ones — see each field for what it costs.
 */
export interface CrossSiteOptions {
    /**
     * The page origins allowed to drive this door from a browser —
     * `['https://app.corp.example']`: scheme, host and port, nothing after.
     *
     * **Unset (the default):** a browser request is served only from a page on
     * this door's own name — its Origin must match the request's `Host`, or an
     * `X-Forwarded-Host` a proxy added. That is every app served from the same
     * host as its door, unchanged; an app on ANOTHER origin must be listed.
     *
     * **Know what the default does not do:** it does not stop DNS rebinding.
     * There, the attacker's page and the request's Host both carry the
     * attacker's name, so they match. {@link CrossSiteOptions.allowedHosts} is
     * the defence for that.
     *
     * A listed origin may be on another SITE by design, so a request from one
     * passes even when the browser marks it `Sec-Fetch-Site: cross-site`. Any
     * other request marked that way is refused — the catch for a proxy that
     * strips `Origin`. A reverse proxy must forward `Origin` and `Sec-Fetch-*`
     * unchanged.
     *
     * `'any'` turns both browser checks off — for a door no browser page can
     * reach with a user's ambient authority. `'null'` and `'*'` are refused at
     * construction: the first is every sandboxed frame on every site, the second
     * would read as a pattern it is not.
     */
    readonly allowedOrigins?: readonly string[] | 'any';
    /**
     * The names this door answers for — `['bot.corp.example']`. A request whose
     * `Host` names anything else is refused with 421, which is what stops DNS
     * rebinding. Matched case-insensitively; an entry without a port matches any
     * port, an entry with one matches only it; IPv6 in brackets (`'[::1]'`).
     *
     * **Unset on a LOOPBACK bind** (`127.0.0.1`, `::1`, `localhost` — a host
     * binding its own socket there): `localhost`, `127.0.0.1` and `[::1]`, any
     * port, silently. Such a socket has no other name, so this refuses nobody
     * and closes rebinding on a developer's own machine.
     *
     * **Unset on any other bind:** every Host is served, and the host prints
     * ONE warning at boot naming this option. A deliberate staged default:
     * refusing at boot would stop existing deployments starting.
     *
     * `'any'` serves every Host on purpose and prints nothing — for a door whose
     * port no browser can reach (a platform's front door in front of it). An
     * empty list is refused at construction: it could never serve anything.
     *
     * Health probes are not judged: a load balancer or kubelet sends the
     * target's ADDRESS as Host, and a probe that failed on it would restart a
     * healthy server.
     */
    readonly allowedHosts?: readonly string[] | 'any';
    /**
     * Refuse a request that changes something unless it says
     * `content-type: application/json` (415). Default `true`.
     *
     * `false` is the named escape hatch for a NON-BROWSER integration that
     * cannot send the header. It removes the half of the defence that does not
     * depend on Origin — so set it only on a door no browser can reach, and
     * keep `allowedOrigins` and `allowedHosts` doing their part.
     */
    readonly requireJsonContentType?: boolean;
}
/** {@link doorGuard}'s options: the rules, plus who is refusing. */
export interface DoorGuardOptions extends CrossSiteOptions {
    /** Who refuses — every refusal names it, as every host refusal does. */
    readonly name: string;
    /**
     * The address this door's socket is bound to, when the door bound it
     * itself. A loopback address with `allowedHosts` unset makes the loopback
     * names the list (see {@link loopbackAllowedHosts}), and a refusal then
     * says the list was defaulted and how to set one. Absent: nothing is
     * inferred.
     */
    readonly bindHost?: string;
}
/**
 * The parts of a request the guard reads. A `node:http` `IncomingMessage` is
 * one as it stands; so is a plain `{ method, headers }`. Header names are
 * matched case-insensitively.
 */
export interface DoorRequest {
    /** The HTTP method. Absent is judged as a request that changes something. */
    readonly method?: string;
    readonly headers: Readonly<Record<string, string | readonly string[] | undefined>>;
}
/** Why a request may not enter — one of three, each carrying its own `status`. */
export type CrossSiteRefusal = UnsupportedMediaTypeError | OriginNotAllowedError | HostNotAllowedError;
/** A guard, built once, asked per request. */
export interface DoorGuard {
    /** The refusal this request earns at the door, or `undefined` to let it in. */
    check(request: DoorRequest): CrossSiteRefusal | undefined;
    /**
     * `true` when `allowedHosts` was left unset — the door answers any Host and
     * the host says so once at boot. `false` for a list or for `'any'`.
     */
    readonly allowedHostsUnset: boolean;
    /**
     * `true` when both browser rules are off (`allowedOrigins: 'any'` and
     * `requireJsonContentType: false`) — the host says so once at boot.
     */
    readonly browserRulesOff: boolean;
}
/**
 * Build a guard. Configuration a door could only ever misread is refused HERE,
 * at construction, by name — never discovered as a mystery 403 in production.
 */
export declare function doorGuard(options: DoorGuardOptions): DoorGuard;
/**
 * The session-id bound — `undefined` for an id a door may carry (or none).
 *
 * A session id is one to {@link MAX_SESSION_ID_LENGTH} characters of VISIBLE
 * ASCII (0x21–0x7E) — the rule MCP gives its own session ids. Why ASCII and
 * not "Unicode minus the dangerous parts":
 *
 *  - **One id, one byte string, from every field.** Node reads a header value
 *    as latin1 and a JSON body as UTF-8, so `café` in `x-session-id` and `café`
 *    in the body were two different stored conversations. With ASCII only,
 *    the two readings cannot differ.
 *  - **Nothing a log, a span or a store can reinterpret.** Controls, line and
 *    paragraph separators (U+2028/9), bidi overrides (U+202E), zero-width
 *    characters and lone surrogates (which any UTF-8 store rewrites to U+FFFD,
 *    merging two ids) are all outside it — no list of exceptions to keep up.
 *  - Every id format in use already fits: UUIDs, platform runtime ids, `conv_…`.
 *
 * The empty string is refused too: every caller that sent `""` shared ONE
 * conversation. The length is judged first, so an over-long id costs one
 * comparison — after a body read that `maxBodyBytes` bounds.
 */
export declare function checkSessionId(sessionId: string | undefined, hostName: string): InvalidSessionIdError | undefined;
/**
 * The names a LOOPBACK-bound door answers to when `allowedHosts` was not set —
 * `localhost`, `127.0.0.1`, `[::1]` (and the bound address itself when it is
 * another `127.x`), any port: the socket already fixes the port, so a name is
 * all a rebinding page can vary. `undefined` for any other bind.
 *
 * It closes DNS rebinding on the classic victim, a developer's own machine,
 * and the MCP SDK's `createMcpExpressApp` does the same for a localhost bind.
 * What it refuses that is legitimate, stated rather than discovered:
 *
 *  - **A reverse proxy on the same machine that forwards the PUBLIC name** in
 *    `Host` (nginx `proxy_set_header Host $host`) — the common case, and the
 *    recommended "bind 127.0.0.1 behind nginx" setup. The proxy's requests
 *    arrive on loopback carrying `bot.corp.example`, which the socket cannot
 *    tell from a rebinding page. List that name in `allowedHosts`; the 421
 *    says so, naming this default (`HostNotAllowedError` · `rule`).
 *  - A hosts-file alias for a loopback address — the same fix.
 *
 * Both fail loudly and in the safe direction, and the fix is the option such
 * a deployment should set anyway.
 *
 * @internal — `doorGuard` asks it with the address a door bound
 * (`DoorGuardOptions.bindHost`).
 */
export declare function loopbackAllowedHosts(bindHost: string | undefined): readonly string[] | undefined;
/**
 * `localhost`, any `127.x` literal, and `::1` or `::ffff:127.x` in ANY IPv6
 * spelling (bracketed or not, `0:0:0:0:0:0:0:1`, leading zeros) — IPv6 is
 * canonicalised by the WHATWG URL parser before it is compared.
 */
export declare function isLoopbackBind(bindHost: string | undefined): boolean;
/**
 * The cross-site rules for a door a PLATFORM fronts — the hosted-runtime
 * adapters' defaults, with whatever the deployment set kept as given.
 *
 * Exempt by default, and why, once for every such adapter: the platform's own
 * front door is the only way to the container's port, and the credential it
 * demands is one no web page holds (a signed request, a bearer the caller
 * attaches) — so a forged browser request cannot authenticate and no browser
 * can reach the port to rebind it. The browser rules would protect nothing
 * there, and which headers the platform forwards (its content type, Origin,
 * Host) is not a fact this library has verified; enforcing them could refuse
 * the platform's own forwarding. Any rule the deployment sets is enforced
 * exactly as on every other door. The session-id bound is NOT exempt: it is
 * not about browsers.
 *
 * @internal — the adapters' shared default, not a public setting.
 */
export declare function platformFrontedDoor(options: CrossSiteOptions, bindHost: string | undefined): CrossSiteOptions;
/**
 * Said once per host name per process, at boot, when a door serves with the
 * browser rules OFF (`allowedOrigins: 'any'` and `requireJsonContentType:
 * false` — the hosted-runtime adapters' default on a non-loopback bind). Those
 * rules are off because a platform's front door is supposed to be the only way
 * in; a self-hoster on `0.0.0.0` needs to hear that it is not.
 *
 * @internal — the words have one owner; `httpHost` decides when.
 */
export declare function warnBrowserRulesOff(hostName: string): void;
/**
 * The staged default, said out loud: a host serving with `allowedHosts` unset
 * prints this ONCE per host name per process, at boot. A name is how every
 * refusal identifies a host, so two hosts sharing one are one door to anyone
 * reading the log — and once is what keeps a warning from becoming noise.
 *
 * @internal — the words have one owner; `httpHost` decides when.
 */
export declare function warnAllowedHostsUnset(hostName: string): void;
