mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
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.
packages/core
@nestri/core — the domain layer for Nestri. All business logic, database access, and
serialization lives here. The API and auth workers are thin pass-through translation layers on top.
What it contains
| Area | Files | Purpose |
|---|---|---|
| db | db/index.ts, db/types.ts, db/test.ts |
Drizzle + Postgres (Database.use/transaction), ULID column helpers |
| users | user/* |
Users, linked accounts, fingerprints, library |
| teams | team/* |
Teams + membership with roles (team_member) |
| games | game/* |
Game catalog, depot content, per-host downloads |
| steam | steam/index.ts |
Steam API integration & SSH identity resolution |
| auth | auth/subjects.ts |
JWT subjects shared with the auth worker |
| infra | env.ts, context.ts, actor.ts, fn.ts, id.ts, error.ts, examples.ts |
Environment, Actor model, zod-typed fn() wrappers, IDs, error types, examples |
| migrations | migrations/ |
Drizzle-kit SQL migrations for Postgres schema |
Conventions
- Domain namespaces (
user/,team/, ...) expose typedfn()functions that validate input with a Zod schema and serialize DB rows inside the function boundary — the API routes never see raw table rows. - Actor model:
Actor.userID,Actor.type, ... pull the current authenticated identity fromAsyncLocalStorage(set by the API middleware / auth worker) without passing it through call chains. - Soft delete: every table has
time_deleted; queries filter withisNull(table.timeDeleted). - IDs: ULIDs via
Identifier.ascending('user')→usr_.... - Tables are defined in
*.sql.tsfiles (drizzle) with namespaces inindex.ts. - Environment is read through
Env.get(), init by worker bindings.
Structure
src/
├── actor.ts, env.ts, id.ts, fn.ts, error.ts, examples.ts
├── db/
├── auth/
├── user/ (user.sql.ts, linked-account.*, fingerprint.*, library.*, index.ts)
├── team/ (team.sql.ts, member.*, index.ts)
├── game/ (game.sql.ts, depot.*, download.*, index.ts)
├── steam/ (index.ts)
├── pairing-code/
├── access-token/
└── machine/
Scripts
bun run db:push # push schema (drizzle-kit)
bun run db # open drizzle-kit
Usage
import { Team } from '@nestri/core/team/index';
import { Database } from '@nestri/core/db/index';
const team = await Team.fromID('tem_...');