mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 09:15:19 +03:00
A host's hostname was its primary key. That worked and disclosed three things it should not have: ids here are monotonic, so an id in a hostname tells anyone who reads a URL roughly when that machine was registered and where it falls among its owner's others; an id is the primary key, so a name that had to change could only change by re-registering the machine, which is changing its identity to fix its name; and the hostname is also the OAuth audience and the cookie scope, so the id travelled into redirect URLs and browser history. Machines now carry a minted name -- two words and four digits, unique across the fleet, DNS-safe by construction, which an id was not. The words are a curated list rather than a dictionary, because every pair is shown to strangers. Names the fleet's own infrastructure answers on are refused at mint time: one minted onto the edge's own label would take the published key path away from every host at once. Minting retries on the unique index rather than checking first, because two registrations in the same instant both read "free" and both write. Only a name collision retries; a duplicate id or secret means something a new name cannot fix. The migration adds the column in three steps. Generated as a NOT NULL column it fails outright against a populated table, and a default would be worse: every row would share one value on a routing key.
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 thex-nestri-admin-tokenheader carryingADMIN_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/linkon behalf of another user, andPOST /games/download-statewhen 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
HYPERDRIVEorDATABASE_URL, and the route to the issuer is anAUTHservice binding orAUTH_INTERNAL_URL. Nothing here branches on which it got. AUTH_ISSUER_URLis the issuer's public URL and never the internal one, because it is compared literally against theissclaim 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.