# Admin HTTP API
A second HTTP server on `--admin-addr` (default `:8081`) is the only
network surface that mutates chassis state — rules, tenants, hostnames,
actors. It also serves the admin UI at `/admin/`. Front it with TLS via
your reverse proxy of choice.
The `txco` CLI is the normal client (`txco apply`, `txco auth …`).
Direct API use is supported for CI and custom tooling. Endpoints are
**tenant-scoped**: resources live under `/v1/tenants/{tenant}/…`.
**Note**: the API only sees concrete state. `op://NAME` symbolic
references are resolved client-side by `txco apply` (which also uploads
the compiled wasm to the computes endpoint) before anything is POSTed.
## Authentication
`--auth-mode` is one of three values (default `both`):
- `signed` — every request must carry RFC 9421 signature headers.
- `basic` — HTTP basic auth via `--admin-user` / `--admin-pass`.
- `both` — accept either; signed callers get their registered actor
identity, basic callers get a synthetic `admin:all` context.
With `both` and *neither* basic credentials *nor* enrolled signing
keys, the chassis runs **open-dev**: requests are admitted with an
`admin:all` context and `source: "open"`. Local development only.
### Signed requests
Signed requests carry three signature headers, produced by the CLI
(`chassis/cli/client/signing.go`):
| Header | Value |
| ----------------- | --------------------------------------------------------------------- |
| `Content-Digest` | `sha-256=::` (empty-body digest pinned for GETs) |
| `Signature-Input` | `sig1=("@method" "@path" "@query" "@authority" "content-digest");keyid="key_…";alg="ed25519";created=;nonce="…"` |
| `Signature` | `sig1=::` |
Server-side policy:
- Covered components are fixed: `@method @path @query @authority
content-digest`.
- `created` must be within ±5 minutes of the chassis clock (default
skew window).
- `(key_id, nonce)` must not have been seen within the last 10 minutes
(replay protection).
- The public key for `key_id` must exist in the actor registry and not
be revoked.
### Bootstrapping the first key
`POST /auth/dev/enroll` exchanges a shared secret (sent as the
`X-Txco-Enroll-Secret` header) for an actor + key pair:
- **Auto-bootstrap (default).** With no `--auth-dev-enroll-secret` and
an empty `actors` table, the chassis generates a 4-word secret at
boot and prints it in a `WARN` line. Single-use: once any actor is
enrolled the endpoint returns 404, even to the original secret.
- **Explicit secret.** `--auth-dev-enroll-secret=` (or
`TXCO_AUTH_DEV_ENROLL_SECRET`) stays valid for multiple enrolments;
the operator manages its lifecycle.
```sh
curl -sS -X POST http://localhost:8081/auth/dev/enroll \
-H 'X-Txco-Enroll-Secret: my-dev-secret' \
-H 'Content-Type: application/json' \
-d '{"public_key_b64":"","algorithm":"ed25519","label":"laptop","kind":"human"}'
# → {"actor_id":"actor_…","key_id":"key_…","capabilities":["admin:all"]}
```
In normal workflows the CLI does this: `txco auth bootstrap-local`.
### Teammates: invitations
After the first admin, teammates onboard via invitation tokens.
Create/list/revoke are tenant-scoped; consume is global and unsigned:
| Method · Path | Auth |
|---|---|
| `POST /v1/tenants/{t}/auth/invitations` | signed (`actor:invite`) |
| `GET /v1/tenants/{t}/auth/invitations` | signed (`actor:read`) |
| `POST /v1/tenants/{t}/auth/invitations/{id}/revoke` | signed (`actor:revoke`) |
| `POST /auth/invitations/consume` | unsigned — `{token, public_key_b64, algorithm, …}` |
Tokens are word-list strings (≥96 bits), stored only as a SHA-256
hash, single-use (conditional-update consume), TTL 24h by default and
capped at 7d. Expired, revoked, consumed, and unknown tokens all return
the same `401 invalid_token` — callers can't probe one state to learn
another.
## Endpoint map
Always unauthenticated: `GET /healthz` (returns `ok`),
`POST /auth/dev/enroll`, `POST /auth/invitations/consume`. Everything
else goes through auth middleware.
Global:
| Endpoint | What |
|---|---|
| `GET /auth/whoami` | Echo the caller's auth context (`source`: `signed` \| `basic` \| `open`) |
| `POST /auth/keys/{keyID}/revoke` | Revoke a key (`actor:revoke`) |
| `GET·DELETE /auth/browser/session` | Browser-session introspection / logout (admin UI) |
| `GET·POST /v1/tenants` | List / create tenants |
Tenant-scoped, under `/v1/tenants/{tenant}`:
| Endpoint | What |
|---|---|
| `GET /ops` | List the tenant's compiled rules |
| `GET /stacks` · `GET /stacks/{name}` | List stacks / stack detail |
| `POST /stacks/{name}/draft` | Open a new draft version |
| `PUT·PATCH·DELETE /stacks/{name}/versions/{n}/files` | Edit the draft's files |
| `POST /stacks/{name}/versions/{n}/validate` | Parse-check a version |
| `POST /stacks/{name}/activate` | Atomic pointer flip to a version |
| `GET /stacks/{name}/versions` · `/diff` | History / compare |
| `PUT·HEAD /computes/{alg}/{digest}` | Upload / probe content-addressed wasm |
| `GET·POST /hostnames` · `DELETE /hostnames/{h}` | Hostname bindings ([ingress.md](./protocols/routing.md)) |
| `POST /hostnames/{h}/attach` · `/challenges` | Bind to a stack / start ownership verification |
| `GET·POST /auth/members` · `DELETE /auth/members/{actor}` | Tenant membership |
| `GET /auth/actors` · `POST /auth/actors/{id}/revoke` | Actor list / revoke |
| `GET /traces/requests.json` · `/requests/{rid}.json` · `/traces/stream` | Trace list / detail / live stream ([trace.md](./trace.md)) |
Also present: `POST /v1/cli` (the admin UI's command bridge),
`POST /v1/fleet/resync`, and `GET·PUT /v1/dns/config`.
### Stack versions, not rule imports
Rules change through versioned stacks: open a **draft**, put files,
**validate**, then **activate** — an atomic pointer flip with full
history (`/versions`, `/diff`), which is also the rollback path
(activate a prior version). `txco apply` drives this flow for you.
The pre-tenancy endpoints (`GET /v1/ops`, `POST /v1/ops/import`, and
the global actor/invitation routes) are retired: they return
`410 route_retired` with a JSON pointer to the tenant-scoped
replacement.