http

19 modules

  • http/auth_shape.ts

    Canonical auth shape for action specs and route specs.

    Replaces the discriminated 'public' | 'authenticated' | 'keeper' | {role} literal that conflated authentication, account resolution, actor resolution, and authorization into a single value. The flat record names each axis the dispatcher actually walks:

    • account — does the dispatcher require / load / skip the account?
    • actor — does the dispatcher require / load / skip the acting actor?
    • roles — disjunction of permitted roles (any-of); absent = no role check.
    • credential_types — restricts the credential channel (e.g. daemon_token); absent = any authenticated credential.
    • required_scope — the capability an api token's scope must admit to reach this route (rpc:<method> or surface:<name>); route specs only, absent = no scope gate.

    The same shape governs both ActionSpec.auth (in actions/action_spec.ts) and RouteSpec.auth (in http/route_spec.ts). The canonical schema lives here in http/ because that preserves the existing actions → http dependency direction (and http/error_schemas.ts / http/surface.ts consume the type).

    Registry-time invariants 1, 3, and 4 live on the schema's .superRefine so any spec that fails them throws at the Zod parse boundary. Invariant 2 (the `actor !== 'none' ⟺ input or query declares acting?: ActingActor` biconditional) needs introspection of the spec's input/query schemas, so it is enforced at registration time inside the dispatcher loops (apply_route_specs, create_rpc_endpoint, register_action_ws) via the assert_route_auth_acting_biconditional helper exported below.

  • http/body_limit.ts

    Request body size limit — rejects an oversized body with 413, and a Content-Length it cannot read with 400, before auth and validation.

    Wraps Hono's bodyLimit with a header-first check:

    • A Content-Length that is not a plain run of ASCII digits (abc, -1, +5, 1e9, 0x1F4, empty, or a combined 5, 500) is refused with 400 {error: 'invalid_content_length'}. A cap cannot be applied to a length that does not parse, and bodyLimit passes such a request on uncounted. The runtimes' HTTP parsers answer their own bodiless 400 to most of these before the application sees them, so this branch is defense in depth where they do and the refusal where they do not: Node's refuses them all; Deno and Bun hand over a repeated equal header as one combined value (50, 50) and refuse the rest (observed on Node 24.14.1, Deno 2.8.3, Bun 1.4.3). The Rust spine's parser (hyper) refuses a malformed value and differing repeats, and accepts an equal repeat as the one value.
    • A request whose Content-Length exceeds the cap is refused with 413 {error: 'payload_too_large'}, whatever its method. A digit run of any length compares as a number, so one too long to represent exactly is over the cap, never unreadable. Over a socket that branch is reached on Node and Deno, whose parsers hand over any length below 2^64 and refuse a longer one with their own 400. Bun.serve hands none over: with its default options it answers a length past 128 MiB with its own 413, and one of 2^63 - 1 or more with its own 400 (observed on Node 24.14.1, Deno 2.8.3, Bun 1.4.3).
    • Both refusals answer on the header alone — without touching c.req.raw.body — and carry Connection: close.
    • A request with a well-formed Content-Length beside Transfer-Encoding, or with no Content-Length, falls through to bodyLimit, which measures the bytes that arrive: with Transfer-Encoding the declared length does not frame the body. bodyLimit reads the body into memory, up to the cap, before the next middleware runs, so that 413 — sent once more than the cap has been read, with no Connection: close — also comes ahead of the origin check, auth and the handler. The Rust spine counts such a body as its handler reads it instead, so there a refusal made on the headers answers first; which status an over-cap body with no declared length gets when another refusal also applies is implementation-defined across the two (docs/security.md §"Body Size Limiting"). The Node, Deno and Bun parsers refuse a request carrying both headers with their own 400; hyper accepts one, drops its Content-Length, frames the body as chunked, and closes the connection after the response.
    • A well-formed Content-Length within the cap is trusted to frame the body, as the runtime's HTTP parser enforces.

    What the connection does after the header 413 is the runtime's (docs/security.md §"Body Size Limiting"):

    • Node (@hono/node-server) closes at once, the body unread. The Connection: close header is what does it: without one the adapter holds the connection for a drain window and then closes or, the body untouched and arriving within the window, reads it off and serves the next request.
    • Deno closes once the whole declared body has arrived and holds the connection open while a client withholds it; a request pipelined behind the body is not served. Neither the header nor what the handler does with the body changes that (observed on Deno 2.8.3).
    • Bun closes at once, the body unread, honouring the header. Bun 1.4.3 is the tested floor: an older Bun kept the connection after the 413 and served the next request (correctly framed), so run Bun at the floor or later.
  • http/client_ip.ts

    Read the resolved client IP off the Hono context.

    Split from proxy.ts (which pulls hono/utils/ipaddr for trusted-proxy CIDR matching) so dispatch + route modules that only need to *read* the IP stay free of that value import — and the cross-process test surface that imports them stays free of the optional hono peer. The IP itself is set on the context by the trusted-proxy middleware in proxy.ts.

  • http/common_routes.ts

    Common route spec factories for fuz_app consumers.

    Generic HTTP route factories with no auth-domain dependencies. Auth-aware route factories (account status) live in auth/account_routes.ts.

  • http/db_routes.ts

    API route specs for database administration.

    Allowlist-gated PostgreSQL table browser using information_schema — consumers declare browsable_tables and the credential floor (NON_BROWSABLE_TABLES) is always subtracted. Provides: list tables, view columns/rows (paginated, bytea values placeholdered), delete rows by PK (audited as db_admin_row_delete), health check.

  • http/error_schemas.ts

    Standard error response schemas and error code constants for fuz_app routes.

    Defines ERROR_* constants (single source of truth for machine-parseable error codes), Zod schemas for error response shapes, a type for error schema maps, and derive_error_schemas to auto-populate middleware-produced errors from a route's auth requirement and input schema.

    Used in RouteSpec.errors and MiddlewareSpec.errors for surface introspection and DEV-mode validation.

  • http/ip_canonical.ts

    IP address canonicalization — collapse equivalent string forms into a single key per RFC 5952 (IPv6) plus the dotted form for IPv4-mapped IPv6 addresses.

    Why this exists. Without canonicalization, the four representations ::1, ::01, ::0001, and 0:0:0:0:0:0:0:1 are the same IPv6 address but produce four distinct strings — so an attacker rotating equivalent forms behind a trusted-passthrough proxy could defeat per-IP rate limiting (each form gets a fresh bucket) and pollute audit_log.ip forensics. The collision can extend to IPv4-mapped IPv6 forms (::ffff:127.0.0.1 vs 0:0:0:0:0:ffff:7f00:1 vs the bare 127.0.0.1) — three keys for one address.

    Canonicalization runs through canonicalize_ip which:

    1. Lowercases and char-set filters (IP_LITERAL_CHARS) — non-IP strings ('unknown', 'attacker:controlled', '::1\n') pass through unchanged so downstream strict validators can still reject them.
    2. Parses via Hono's convertIPv*ToBinary family.
    3. Re-emits the canonical RFC 5952 string (lowercase hex, longest-zero-run compressed, IPv4-mapped emitted in the dotted form mandated by RFC 5952 §5).
    4. Strips the ::ffff: prefix from dotted IPv4-mapped forms so the bucket collapses to plain IPv4 — the strip moves AFTER canonicalization because the dotted form is the only form the strip can recognize symmetrically.

    Mirrors zzz_server::proxy::normalize_ip (landed 2026-05-16) which uses the same parse-then-canonicalize-then-strip ordering for the same rate-limit-key-poisoning surface.

  • http/jsonrpc_errors.ts

    JSON-RPC error infrastructure for fuz_app routes.

    Provides error types, named constructors, and HTTP status mapping for the throw/catch error pattern used by apply_route_specs. Core error codes (5 standard + 10 general application). Domain-specific codes stay in consumers — add by casting as JsonrpcErrorCode.

    JsonrpcErrorCode and JsonrpcErrorObject types are Zod-inferred from http/jsonrpc.ts — this module re-uses those as the single source of truth.

    Complementary to http/error_schemas.ts: that module is declarative (Zod schemas for surface introspection), this one is runtime (throw + catch + map).

  • http/jsonrpc_helpers.ts

    JSON-RPC message builders, type guards, and converters.

    Used by the SAES runtime (ActionEvent, ActionDispatcher, transports) and the RPC endpoint dispatcher. Complements http/jsonrpc.ts (schemas) and http/jsonrpc_errors.ts (error infrastructure).

  • http/jsonrpc.ts

    JSON-RPC 2.0 envelope schemas for RPC dispatch and SAES transport.

    Params are object-only following MCP (no positional arrays); per-action spec.input narrows further. Result is the full JSON-RPC valid value space per spec §5 ("determined by the method invoked"): object, array, string, number, boolean, or null — the per-action spec.output is the actual contract. The envelope-level loose-object-only constraint inherited from MCP would reject any action with output: z.null() / z.string() / etc. on the wire, so we relax it here while keeping MCP-superset shape on params + _meta.

    MCP _meta types (JsonrpcMcpMeta, JsonrpcRequestParamsMeta) are exported as building blocks for per-action schemas but are NOT validated at the envelope level — _meta content validation belongs in per-action schemas where it produces the correct error code (invalid_params, not invalid_request).

    @source https://github.com/modelcontextprotocol/typescript-sdk @see https://www.jsonrpc.org/specification

  • http/middleware_spec.ts

    Middleware spec type — named middleware layer definition.

    Separated from http/route_spec.ts so middleware modules can import this type without creating an upward dependency on routes.

  • http/origin.ts

    Request source verification middleware for API protection.

    Verifies requests carry an Origin the server allows, or none. CSRF protection is SameSite: strict on session cookies (see auth/session_middleware.ts) plus the CORS simple-request refusal (http/simple_request.ts) — SameSite alone admits same-site pages, which include every other port on the same host. This module provides origin allowlisting for locally-running services — preventing untrusted websites from making requests as the user browses the web — and is the only gate on the WebSocket upgrade, which is never preflighted.

    The allowlist grammar is shared with the Rust spine entry for entry: parse_allowed_origins accepts and refuses the same entries, naming each refusal with the same AllowedOriginsErrorKind string, and an accepted list matches the same origins. The shared conformance table (testing/cross_backend/origin_conformance.json) pins both.

  • http/pending_effects.ts

    Two-queue side-effect machinery for request handlers.

    Handlers register fire-and-forget work in one of two queues, distinguished by their timing contract:

    • pending_effects: Array<Promise<void>> — eager. Producers push pool writes that are already in flight (audit emits, session-touch UPDATE, api-token usage tracking). The pool write is rollback-resilient by virtue of running outside the request transaction; pushing the in-flight handle lets test mode (await_pending_effects: true) await it.
    • post_commit_effects: Array<() => void | Promise<void>> — deferred. Producers go through emit_after_commit(ctx, fn); the flush middleware is the only site that ever invokes the thunk, and it does so after the request handler (and its wrapping db.transaction) returns. Used for WS sends and any work that must observe a committed transaction.

    Discard on rollback. A post_commit_effects thunk is discarded if the handler's transaction rolls back (a thrown handler) — it must not announce state that never committed. Both dispatch sites enforce this by wrapping their handler in dispatch_with_post_commit_rollback (see its TSDoc below for the full two-sided contract and the Rust-twin parity note). The eager pending_effects queue is the deliberate opposite: its pool writes run outside the transaction and intentionally survive rollback (attempt audits).

    The split exists because the two shapes encode different contracts: eager pushers are saying "wait for this work that's already started"; thunk pushers are saying "run this after the handler returns." Burying both behind one Array<PendingEffect> made c.var.pending_effects.push(x) ambiguous at the call site. With separate queues, the field name is the contract.

    Both RouteContext (HTTP routes) and ActionContext (RPC + WS actions) carry both queues by convention, so this module stays in http/ (every transport depends on it).

  • http/proxy.ts

    Trusted proxy configuration and middleware.

    Resolves the client IP from X-Forwarded-For only when the TCP connection originates from a configured trusted proxy. Without this middleware, get_client_ip returns 'unknown'.

  • http/route_spec.ts

    Introspectable route spec system for Hono apps.

    Routes are defined as data (method, path, auth, input/output schemas, handler), then applied to Hono. The attack surface is generated from the specs — always accurate, always complete.

    Input/output schemas align with SAES ActionSpec conventions:

    • input: Zod schema for the request body (z.null() for no body)
    • output: Zod schema for the success response body
    • z.strictObject() for inputs (reject unknown keys)
  • http/schema_helpers.ts

    Shared pure helpers for schema introspection and middleware matching.

    Used by both http/route_spec.ts (input validation) and http/surface.ts (attack surface generation). Extracted to avoid circular dependencies between routes and middleware.

  • http/simple_request.ts

    Refusing CORS simple requests — the cross-origin POSTs a browser sends without asking first.

    A cross-origin fetch or form POST skips the CORS preflight when its Content-Type is absent or one of the three CORS-safelisted types (text/plain, application/x-www-form-urlencoded, multipart/form-data). The browser sends such a request as-is, with whatever cookies apply — only the response is withheld from the page. Any other content type makes the browser send an OPTIONS preflight first, and since fuz_app answers no preflight with CORS headers, the real request is never sent.

    The session cookie's SameSite=Strict doesn't close this on its own: same-site ignores the port, so a page on http://localhost:8000 is same-site with a server on http://localhost:4460 and its requests carry the cookie. The origin allowlist (http/origin.ts) refuses such a page unless a wildcard (http://localhost:*) admits it; this gate refuses it whatever the allowlist says.

    So every POST route carries a content-type check, run first by apply_route_specs (is_post_content_type_refused, 415 unsupported_content_type): a JSON route takes only application/json, bodyless or not, and a raw_body route takes any type that isn't safelisted. Only POST needs it: PUT, PATCH, and DELETE are never simple methods, so the browser always preflights them.

    The check rests on the server answering no preflight. A consumer that adds CORS middleware granting cross-origin requests reopens what this closes.

    Twin of the Rust spine's fuz_http::simple_request: the same predicates, is_json_content_type checked in each spine JSON handler and is_cors_simple_content_type by the simple_request_layer consumers mount over routes with other bodies.

  • http/surface_query.ts

    Pure query functions over AppSurface data.

    Usable in tests, the adversarial auth runner, and future surface explorer UI. Replaces duplicated inline .filter() patterns.

    Categorical filters (filter_authenticated_routes, filter_role_routes, filter_keeper_routes) group the new flat-record RouteAuth shape into the legacy categorical buckets ('authenticated', 'role', 'keeper') for adversarial test runners and the surface explorer. The buckets are derived views over the authority axes (account, actor, roles, credential_types) — see http/auth_shape.ts for the canonical shape.

  • http/surface.ts

    App surface generation — JSON-serializable attack surface from route and middleware specs.

    Pure schema helpers (is_null_schema, schema_to_surface, middleware_applies, middleware_spec_applies, merge_error_schemas) live in http/schema_helpers.ts.