server

10 modules

  • server/app_backend.ts

    App backend types and factory — database initialization + auth migrations + deps.

    Provides AppBackend, CreateAppBackendOptions, and create_app_backend().

    Vocabulary:

    • AppDeps — stateless capabilities: injectable, swappable per environment
    • *Options — static values set at startup, per-factory configuration
    • Runtime state — mutable values (e.g., bootstrap_status) — NOT in deps or options
  • server/app_server_context.ts

    The context threaded to route / RPC / WS endpoint factories.

    Lives in its own module — separate from server/app_server.ts — so it can be consumed as a pure type without dragging in the server-assembly machinery. server/app_server.ts value-imports hono (it builds the Hono app), so importing anything from it forces hono to be installed. Contract-only consumers — cross-process test surfaces, Rust-backed servers that reuse the route/RPC spec factories without running the TS server — need AppServerContext but not hono. Keeping the type here (only import type dependencies, none of which value-import hono) lets them import it framework-free.

  • server/app_server.ts

    Server assembly factory.

    Consumers provide a pre-initialized AppBackend and options (session, origins, routes); create_app_server() handles middleware, bootstrap status, surface generation, and Hono app assembly.

  • server/env.ts

    Base server environment schema and validation.

    Provides BaseServerEnv — a shared Zod schema for common server env vars that apps can use directly or extend with app-specific fields.

    Generic env loading lives in env/load.ts.

  • server/fact_security_headers.ts

    The untrusted-content hardening headers every served fact carries.

    Fact bytes are served with a producer-supplied content_type emitted verbatim, inline. To stop a fact stored as (or sniffable to) text/html from executing as stored XSS for any reader of a referencing cell, every fact response carries FACT_SECURITY_HEADERS: X-Content-Type-Options: nosniff + Content-Security-Policy: default-src 'none'; sandbox.

    The embedded and disk-stream branches of server/serve_fact_route.ts set them on the response directly. On the X-Accel-Redirect branch nginx serves the bytes from its internal facts location, and a location with its own add_header stops the server-level headers from applying — so that location must carry the set itself, as the lines fact_security_header_directives renders. The X-Accel validator (server/x_accel.ts) refuses a facts location without them.

    The Rust twin is fuz_fact_serving::fact_security_headers; both pin the same literals.

  • server/serve_fact_route.ts

    Content-addressed fact serving — cell-scoped, per-reference reads.

    Two routes serve fact bytes from the PG-backed fact store:

    • GET /api/cells/:cell_id/facts/:hash — the per-reference read. The request names the referencing cell. Authz is scoped to that one reference: `(can_view_cell(caller, cell) OR the steward arm) AND cell.refs includes hash`, the steward arm being the governing root's manager reading a pending or rejected contribution (auth/cell_steward.ts). This is the path non-admin callers use, and the only path for confidential content.
    • GET /api/facts/:hash — the bare-hash read, restricted to admins.

    Embedded facts stream from the fact.bytes PG column; external facts (filesystem-backed file:<shard>/<rest> URLs) either return an X-Accel-Redirect header pointing into nginx's internal facts location (production) or stream from disk (dev / tests). The runtime mode is selected by the optional x_accel factory option — a validated XAccelConfig (built via create_x_accel_config in server/x_accel.ts) set in prod, unset in dev. The handle can only be obtained by proving the facts nginx location is internal; and carries the fact hardening headers, so X-Accel serving can't be enabled against a public or header-less location.

    REST, not RPC: binary responses don't fit the JSON-RPC envelope.

    Authorization — authz lives on the cell→fact edge, not the hash

    Facts are global, content-addressed, owner-less bytes: identical bytes from different owners dedup to one fact row. Keying access control on the bare hash therefore unions visibility across every owner that references it — A's private bytes leak the instant B references identical bytes from a public cell. The fix is to scope authz to the (cell, hash) edge: a caller reads a fact *through a specific cell it can view that references the hash*. Dedup becomes a pure storage optimization with zero authz consequence — whether two owners' bytes share a fact row is invisible to the read check.

    The cell-scoped route resolves the named cell, requires can_view_cell(caller, cell), and requires cell.refs to include the hash. B publishing identical bytes from B's public cell makes them readable *via B's cell* — it never touches A's private reference.

    The edge is only as safe as what may create it. A cell's refs derive from its data, and any account that can write a cell writes its data — so this route alone would make a known hash a read capability: name it in a cell you own, flip the cell public, and the bytes serve to the world, while a 200 against a 404 says whether the fact exists. What closes that is possession-gated ref admission on the cell verbs (auth/cell_ref_admission.ts): a non-admin cell_create / cell_update / cell_clone may introduce a ref only to a fact its acting actor holds — one it put the bytes of, recorded in fact_holder on every put, a deduplicated put included — and is refused 403 cell_ref_not_held otherwise, the same answer whether or not a fact is stored. Refs a cell already carries stay admitted through updates and clones, so a viewer who clones a cell carries its refs: what a viewer can read through a cell, it can republish — no more than downloading the bytes and uploading them again. A serve is a snapshot of the edge at read time; revoking someone's access to a cell does not withdraw refs they already wrote elsewhere. Writers that bypass the verbs (seeds, raw inserts) are trusted and not admitted.

    The bare-hash route is admin-only: an admin's reach already spans every cell, so serving by bare hash grants no escalation. Non-admin callers are rejected at the auth phase and never reach the handler. (Explicitly-public facts — a producer opting bytes into world-readable status — are a future refinement; there is no such concept today, so the bare-hash route stays strictly admin-gated.)

    Auth shape on the cell-scoped route is {account: 'none', actor: 'none'} — the dispatcher's authorization phase is skipped for pure-public routes, so the handler builds the RequestContext itself from c.var.account_id (populated by the /api/* session middleware) by resolving the caller's single actor and loading their role_grants. Unauthed callers pass through with req_ctx: null and are admitted only by a `cell.visibility === 'public' cell whose moderation` admits an audience (unmoderated or 'approved'). Multi-actor accounts fall through with req_ctx: null — there's no acting? slot on a pure-public route, so multi-actor callers are treated as anonymous.

    404 is the universal "not viewable" response: missing fact, missing or unviewable cell, or the cell doesn't reference the hash. We deliberately don't distinguish 403 from 404 — neither the existence of a fact nor the existence of a cell→fact edge should leak through the public surface.

    A ref is a data string value that is exactly a fact hash (derive_cell_refs) — a hash inside prose or markup is not one. So a doc cell that shows images stores each image's hash as its own string value (an attachment list, say) beside the text that places them; serving then goes view-doc-cell → doc-cell-refs-include-hash → serve.

    Untrusted-content hardening

    Fact bytes are served with a producer-supplied content_type emitted verbatim, inline. To stop a fact stored as (or sniffable to) text/html from executing as stored XSS for any reader of a referencing cell, every served-fact response carries X-Content-Type-Options: nosniff + Content-Security-Policy: default-src 'none'; sandbox (the FACT_SECURITY_HEADERS set from server/fact_security_headers.ts, byte-identical with the Rust twin fuz_fact_serving). The embedded and disk-stream branches set them on the response. On the X-Accel-Redirect branch nginx serves the bytes, and the headers the client sees come from the internal facts location, not this response — that location must carry the lines fact_security_header_directives renders, which create_x_accel_config enforces. Content-Disposition: attachment is deliberately not added — it would force-download and break legitimate inline image rendering.

    Defense-in-depth

    external_url is re-validated against the file:<shard>/<rest> shape before it addresses the filesystem or an X-Accel-Redirect. PgFactStore writes only that shape, but the column is free text and query_put_fact inserts what it is given, so a row written past the store — or a row-injection bug upstream — would otherwise hand nginx an attacker-controlled path. Any other shape is a logged 404, the same unavailable that PgFactStore.get reports for it as null.

  • server/startup.ts

    Composable startup summary helpers.

    Logs a human-readable summary from an AppSurface.

  • server/static.ts

    Static file serving middleware for SvelteKit static builds.

    Multi-step static serving:

    • Step 1: Exact path match (handles /, assets, images)
    • Step 2: .html fallback for clean URLs (/about → /about.html)
    • Step 3 (optional): SPA fallback for client-side routes
  • server/surface_route.ts

    The surface explorer route — GET /api/surface, serving the generated AppSurface as JSON.

    Opt-in on create_app_server (surface_route: true), and admin-only when mounted: the surface is a map of every route, its auth, and its schemas.

  • server/x_accel.ts

    Fail-loud validation of the X-Accel facts nginx location.

    The X-Accel-Redirect serving path's confidentiality depends on the facts nginx location being internal; — only the authz'd handler's redirect (an internal subrequest) may reach it. A *public* facts location would serve any fact's bytes to anyone who guesses the <shard>/<rest> path, bypassing every cell-visibility check. A location with its own add_header also stops the server-level headers from applying, so the facts location must carry the fact hardening headers itself (fact_security_header_directives in server/fact_security_headers.ts).

    XAccelConfig makes these assertions structural: a consumer can only obtain the redirect prefix by building an XAccelConfig, which runs validate_facts_internal_location and throws loudly on any violation — it cannot silently fall closed.

    Rules

    The config is tokenized as nginx reads it (ngx_conf_read_token): whitespace is exactly space, tab, CR and LF; an unquoted word ends only at those or at ; / { (a } inside a word is part of it, and so is a { straight after $); # starts a comment only where a token would start; quoted strings are one word, with nginx's backslash escapes. It is then parsed into nested blocks, and every location at any depth, in every server block, is classified against the prefix (trailing slashes ignored). A location *serves the prefix* when it is a prefix-type location (no modifier, or ^~) whose path is the prefix or below it. Each one must:

    • use ^~ — a plain prefix location is refused;
    • be a direct child of a server block — one nested in another location is refused;
    • carry, as top-level directives of its own body, an exact internal; and each fact_security_header_directives line (token-equal: whitespace and quoting are free, the words are not);
    • hold only simple directives — any nested block (location, if, limit_except, …) is refused, and so is include, since included files are not followed.

    The first two rules are what keep regex locations out: nginx never evaluates regex locations for a path whose longest prefix match is a ^~ location at server level, and because every prefix location at or below the prefix must be such a location, the longest match for any redirected path always is. (A longer plain location below the prefix can't exist — the ^~ rule refuses it.)

    The prefix may appear only in its own location lines and their bodies: internal; admits every internal redirect and subrequest, so any other directive with a word naming the prefix (trailing slash trimmed, so /_facts$uri counts) is refused, in any block at any depth — a rewrite, try_files, error_page, index, add_before_body, or a set / map value that a redirect later reads. The only exemptions are each location's own modifier and path words and the bodies of the locations serving the prefix.

    Also refused: an = location on the prefix or below it (it can't serve the nested paths the redirect emits); a regex location (~ / ~*) whose pattern spells out the prefix (after unescaping \/, ASCII case-folded for ~*) — dead under the rules above, refused for the clearer message; and a config with no location serving the prefix. include at server level is allowed, but its file is not read — keep facts-related locations out of included files.

    The threat model is operator mistakes, not an adversarial config author: the prefix check is textual, so a value assembled from pieces (/_fac$1) evades it. Beyond the config text:

    • The validator checks URL paths only — another root or alias that maps onto the facts directory would serve its files publicly, so keep the facts directory outside every other location's root.
    • nginx honours X-Accel-Redirect from any proxied upstream (proxy_pass, fastcgi_pass, uwsgi_pass, scgi_pass, grpc_pass), so every upstream in the server is trusted to point into the prefix; an untrusted one needs proxy_ignore_headers X-Accel-Redirect; (or the fastcgi_ / uwsgi_ / scgi_ equivalent).
    • Module code can redirect internally too (ssi include virtual, njs internalRedirect, perl, echo), so run trusted modules and content only.

    The Rust twin is fuz_fact_serving's nginx.rs, held to the same rules and the same fixture corpus.

    @example

    const x_accel = create_x_accel_config( '/_facts/', `server { location ^~ /_facts/ { internal; alias /srv/app/facts/; ${fact_security_header_directives().join('\n\t\t')} } }` );