Files
netris-nestri/apps/api
Wanjohi 0d8630379b fix(api): a box gets one run, and a stopped run keeps no address
Two invariants the session endpoint stated but did not hold.

A box runs one thing at a time. `POST /session` read `activeForBox` and
refused when something was already running, but the read and the insert
are two statements with nothing between them: two requests that both saw
"nothing is running" each got a row, and the job poll then handed the
host the same box to start twice. Demonstrated at 2 rows and 2 jobs from
one box. That is the failure the state claim exists to prevent, one step
earlier, and it takes the same answer — a partial unique index on the
predicate the read asks about, so the database refuses the second insert.
`Session.request` turns that refusal into the same 409 in the same words,
so a caller cannot tell which of the two caught it.

The migration resolves any existing duplicates before creating the index,
keeping each box's newest unstopped run because that is the one a person
is waiting on, and stopping the rest rather than deleting them.

Separately, a run that reached `ended` or `failed` kept the last ticket
it published. Publishing a new one is already refused, so the stale
address was both the only ticket a client could read for a dead run and
the one nothing was allowed to replace — and a client that polls would
dial it. Terminal transitions now clear it, in `setState` as well as in
the compare-and-set, so the invariant does not depend on which writer
stopped the run.

Seven tests, each checked against the unfixed code first. The published
descriptions for the ticket field and the read endpoint now say that a
stopped run has no address.
2026-09-04 21:58:57 +03:00
..
2026-08-06 22:13:51 +03:00
2026-08-06 22:13:51 +03:00
2026-08-06 22:32:33 +03:00
2026-08-06 22:13:51 +03:00

apps/api

The public HTTP API for Nestri — a Hono app deployed as a Cloudflare Worker.

What it does

Exposes the JSON API consumed by frontends and other clients. Every route is a thin wrapper that validates input, delegates to a domain function in @nestri/core, and returns { data: ... }. All business logic lives in the core package.

Routes:

Prefix Purpose
/ Health check
/user Current user profile, fingerprints, linked accounts
/steam Link / sync / unlink a Steam account
/library Owned games with playtime
/games Game catalog
/pairing-code Device pairing codes
/machine Host machines
/access-token Short-lived access tokens
/doc Generated OpenAPI spec

Structure

app/
  index.ts           # Hono entrypoint: middleware, routes, error handler, /doc
  middleware/auth.ts # Bearer JWT + admin shared-secret auth → Actor
  routes/*.ts        # Thin route namespaces (UserApi, SteamApi, ...)
  utils/             # ErrorResponses, Result(), validator wrapping
test/                # Route tests (Vitest/Bun)

Key details

  • Auth: Authorization: Bearer <JWT> verified against @nestri/auth; or the x-nestri-admin-token header (see ADMIN_SHARED_SECRET).
  • Errors: centralized VisibleError → typed JSON responses.
  • The API worker receives its bindings (AUTH, HYPERDRIVE, STEAM_API_KEY, ADMIN_SHARED_SECRET) from Alchemy — see alchemy.run.ts at the repo root.

Running

Run via the root Alchemy setup (bun alchemy.run.ts --dev). Needs a Postgres database and an auth worker; see the root README for full dev setup.