server

11 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_write.ts

    Shared helper for routing fact bytes between embedded (PG bytes column) and external (filesystem + put_ref) storage tiers based on size.

    External writes go through atomic temp+rename so the facts row never references a partial file; idempotence comes from POSIX rename overwrite + INSERT ... ON CONFLICT DO NOTHING in the fact-store queries layer.

  • server/file_fact_fetcher.ts

    Filesystem-backed FactExternalFetcher.

    Resolves file:<shard>/<rest> external URLs against a configured facts directory. The URL shape is the canonical relative-path scheme: 2 hex chars for the shard subdir + 62 hex chars for the rest of the blake3 hash (64 hex chars total). Files live at <facts_dir>/<shard>/<rest> after the writer atomically temp+renames them in.

    Pre-filter regex: ^file:[0-9a-f]{2}/[0-9a-f]{62}$. A .. segment can't match (. isn't in [0-9a-f]); neither can absolute paths, query strings, or any non-hex character. Defense-in-depth in front of path.join — the fetcher never trusts the URL came from a fact row, even though it always does in practice.

    The fetcher does NOT verify hash content — PgFactStore.get calls fact_hash_verify(hash, bytes) after the fetch and returns null on mismatch.

    Runtime: uses node:fs/promises + node:fs createReadStream so the same code works under Deno (via node compat) and vitest.

  • 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) AND cell.refs includes hash. 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 via the filesystem FactExternalFetcher (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;, so X-Accel serving can't be enabled against a public 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 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. 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.

    Content-addressed serving of inline blake3: images (a markdown doc cell with embedded image refs) works through this model: the referencing cell is the doc cell, so serving 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, applied in all three serve branches). The same pair, byte-identical, is set by the Rust twin (fuz_fact_serving::serve). Content-Disposition: attachment is deliberately not added — it would force-download and break legitimate inline image rendering.

    Defense-in-depth

    The external_url regex is re-validated before issuing X-Accel-Redirect even though PgFactStore.put_ref only writes file:<shard>/<rest>-shaped URLs. A future row-injection bug upstream would otherwise hand nginx an attacker-controlled path.

  • 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/validate_nginx.ts

    String-based nginx config validator for fuz_app deploy configs.

    Checks consumer NGINX_CONFIG template strings for required security properties. This is pattern matching on template strings, not a real nginx parser — it catches common security omissions but won't catch all possible misconfigurations.

  • 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. XAccelConfig makes that assertion structural: a consumer can only obtain the redirect prefix by building an XAccelConfig, which runs this check and throws loudly on a missing or non-internal; location — it cannot silently fall closed.

    This is a best-effort string check (brace-matched location blocks), the TS twin of the Rust fuz_fact_serving nginx.rs — not a real nginx parser, but it catches the security-critical omission. Distinct from validate_nginx_config (server/validate_nginx.ts), which validates the /api Authorization-strip + security headers and takes no facts-location path.