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.

view source

Declarations
#

3 declarations

create_serve_cell_fact_route_spec
#

server/serve_fact_route.ts view source

(options: CreateServeFactRouteSpecOptions): RouteSpec import {create_serve_cell_fact_route_spec} from '@fuzdev/fuz_app/server/serve_fact_route.js';

Build the cell-scoped GET /api/cells/:cell_id/facts/:hash RouteSpec — the per-reference read.

Resolves the named cell (404 if missing / soft-deleted), requires can_view_cell(caller, cell) AND cell.refs to include the hash (else 404, masked), then serves the bytes. Authz is scoped to this one (cell, hash) edge — never unioned across the fact's other referrers.

Pure-public auth — the handler builds the per-request RequestContext from c.var.account_id and enforces visibility per-reference.

options

returns

RouteSpec

create_serve_fact_route_spec
#

server/serve_fact_route.ts view source

(options: CreateServeFactRouteSpecOptions): RouteSpec import {create_serve_fact_route_spec} from '@fuzdev/fuz_app/server/serve_fact_route.js';

Build the admin-only bare-hash GET /api/facts/:hash RouteSpec.

An admin's reach already spans every cell, so serving by bare hash grants no escalation — the union concern that made this route a cross-owner leak for non-admins is vacuous for an admin. Non-admin callers are rejected at the auth phase (403) and never reach the handler. Confidential non-admin reads always go through the cell-scoped route above.

Auth is {account: 'required', actor: 'required', roles: ['admin']} — the dispatcher's authorization phase resolves the acting actor and the post-authorization guard enforces the admin role before the handler runs. The handler re-checks has_role(_, admin) as defense-in-depth so a future mounting/auth-shape regression fails closed rather than serving by bare hash to a non-admin.

options

returns

RouteSpec

CreateServeFactRouteSpecOptions
#

server/serve_fact_route.ts view source

CreateServeFactRouteSpecOptions import type {CreateServeFactRouteSpecOptions} from '@fuzdev/fuz_app/server/serve_fact_route.js';

deps

App deps reference. Currently unused at handler time (cell + fact tables are read via RouteContext.db); kept on the factory signature for symmetry with sibling route factories and to give future role_grant-scoped viewer extensions somewhere to read other deps without changing the public shape.

type AppDeps

facts_dir

Absolute path of the facts directory. Used for the dev/test streaming path.

type string

x_accel?

When set, external facts return an X-Accel-Redirect pointing at ${x_accel.redirect_prefix}<shard>/<rest> — nginx's internal facts location serves the bytes. When unset, external facts stream from <facts_dir>/<shard>/<rest> directly. Production sets a validated XAccelConfig (built via create_x_accel_config, which proves the facts location is internal;); tests + dev leave it unset.

type XAccelConfig

log

type Logger

Depends on
#