Files
netris-nestri/apps/api
Wanjohi 6429ec4ff7 feat(api): record which host holds a Steam token for whom
A host that signs a person into Steam ends up holding a refresh token. The
control plane needs to know that happened — to show it, and so a host that
lost its disk can find out what it is expected to hold — but it must not know
the credential, because the token is bound to the address that obtained it and
a copy anywhere else is the account-theft signal Steam watches for.

So `steam_enrolment` stores the outcome and has no token column, no encrypted
token column, and no column that could hold one later. The safeguard is that
the credential is never sent here at all; a nullable column would be the first
step in undoing it, so a test asserts the column list exactly and fails if one
appears. Three machine-authenticated routes go with it: report a completed
sign-in, report that Steam refused the token, and list what this host should
have. All three take the host from its own credentials, so a box can neither
report onto nor read another box's hardware. Their bodies are strict, so a
host that sends a token is told it is wrong rather than quietly believed —
which also keeps the value out of the request log.

The Steam id is deliberately not unique. One account signed in on two hosts is
two rows and two tokens, and a unique index there would look like hygiene while
refusing somebody their second box.

There is no `pending` state: a sign-in challenge lives about two minutes inside
one process, and nothing outside it needs to know it exists. Nothing revokes
yet, and `last_ok_at` has no writer — a successful logon happens where there is
no credential to report it with — so the column exists with the shape it will
need and stays null rather than being filled with the nearest event that was
easy to observe.
2026-09-06 13:27:51 +03:00
..
2026-08-06 22:13:51 +03:00
2026-08-06 22:13:51 +03:00

apps/api

The public HTTP API for Nestri — a Hono app. One handler, run either as a Cloudflare Worker or as an ordinary HTTP server.

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           # The handler: middleware, routes, error handler, /doc
  server.ts          # The same handler behind a listening socket
  middleware/auth.ts # Bearer JWT + admin shared-secret auth → Actor
  routes/*.ts        # Thin route namespaces (UserApi, SteamApi, ...)
  utils/             # ErrorResponses, Result(), validator wrapping
wrangler.jsonc       # Worker configuration, one environment per stage
Dockerfile           # The container, built from the repository root
test/                # Route tests

Key details

  • Auth: Authorization: Bearer <JWT> verified against @nestri/auth; or the x-nestri-admin-token header carrying ADMIN_SHARED_SECRET, which bypasses JWT verification entirely and is required — it has no default anywhere. It is what authenticates the callers that have no user identity to present: POST /pairing-code/claim (a device being paired has no identity yet, which is the whole point), POST /games, POST /games/sync, POST /library/sync, GET /waitlist, POST /steam/link on behalf of another user, and POST /games/download-state when an operator is repairing state a box reported.
  • Errors: centralized VisibleError → typed JSON responses.
  • Settings arrive as bindings or as environment variables, and two of them have one spelling of each: Postgres is HYPERDRIVE or DATABASE_URL, and the route to the issuer is an AUTH service binding or AUTH_INTERNAL_URL. Nothing here branches on which it got.
  • AUTH_ISSUER_URL is the issuer's public URL and never the internal one, because it is compared literally against the iss claim on every token.

Running

bun run dev      # under the Workers runtime, on :3000
bun run serve    # as a plain process, on $PORT (default 3000)

Needs a Postgres database and a reachable issuer. Full list and deployment steps: docs/deploy.md.