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 theHonoapp), so importing anything from it forceshonoto 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 nothono. Keeping the type here (onlyimport typedependencies, none of which value-importhono) 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_typeemitted verbatim, inline. To stop a fact stored as (or sniffable to)text/htmlfrom 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-Redirectbranch nginx serves the bytes from its internal facts location, and a location with its ownadd_headerstops 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.bytesPG column; external facts (filesystem-backedfile:<shard>/<rest>URLs) either return anX-Accel-Redirectheader pointing into nginx's internal facts location (production) or stream from disk (dev / tests). The runtime mode is selected by the optionalx_accelfactory 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 isinternal;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
factrow. 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 afactrow is invisible to the read check.The cell-scoped route resolves the named cell, requires
can_view_cell(caller, cell), and requirescell.refsto 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
refsderive from itsdata, and any account that can write a cell writes itsdata— 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-admincell_create/cell_update/cell_clonemay introduce a ref only to a fact its acting actor holds — one it put the bytes of, recorded infact_holderon every put, a deduplicated put included — and is refused403 cell_ref_not_heldotherwise, 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 fromc.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 withreq_ctx: nulland are admitted only by a `cell.visibility === 'public'cell whosemoderation` admits an audience (unmoderated or'approved'). Multi-actor accounts fall through withreq_ctx: null— there's noacting?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
datastring 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_typeemitted verbatim, inline. To stop a fact stored as (or sniffable to)text/htmlfrom executing as stored XSS for any reader of a referencing cell, every served-fact response carriesX-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 twinfuz_fact_serving). The embedded and disk-stream branches set them on the response. On theX-Accel-Redirectbranch 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: attachmentis deliberately not added — it would force-download and break legitimate inline image rendering.Defense-in-depth
external_urlis re-validated against thefile:<shard>/<rest>shape before it addresses the filesystem or anX-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 thatPgFactStore.getreports for it asnull.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:
.htmlfallback 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-Redirectserving path's confidentiality depends on the facts nginx location beinginternal;— 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 ownadd_headeralso 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 everylocationat any depth, in everyserverblock, 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
serverblock — 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 isinclude, 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$uricounts) is refused, in any block at any depth — arewrite,try_files,error_page,index,add_before_body, or aset/mapvalue that a redirect later reads. The only exemptions are eachlocation'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.includeat 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
rootoraliasthat 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-Redirectfrom 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 needsproxy_ignore_headers X-Accel-Redirect;(or thefastcgi_/uwsgi_/scgi_equivalent). - Module code can redirect internally too (ssi
include virtual, njsinternalRedirect, perl, echo), so run trusted modules and content only.
The Rust twin is
fuz_fact_serving'snginx.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')} } }` );- use