docs: server architecture, API decisions, follow-ups

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 21:17:18 +01:00
co-authored by Claude Opus 5.5
parent 02b7eed5ec
commit 348b04e928
3 changed files with 45 additions and 2 deletions
+27
View File
@@ -4,6 +4,33 @@ Append-only. Never edit or delete an entry. Supersede it with a new one that lin
One entry per decision a future maintainer would otherwise have to reverse-engineer.
No index: `grep -n '^## ' docs/DECISIONS.md` lists every entry (date, title, line number).
## 2026-10-30: HTTP API: auth modes, CSRF header, error mapping, actor
**Status:** accepted
**Context:** Task 16 (`apps/server`).
**Decision:**
- **Auth modes per route:** `none`, `session` (cookie only: tokens, sources create/edit/delete, device auth, bundle import,
admin), `token:<scope>`, `any:<scope>` (cookie or Bearer). A Bearer token valid for another scope gets 403, an unknown or
revoked one 401. App tokens cannot mint tokens or handle source credentials.
- **CSRF:** cookie-authenticated non-GET requests must send `X-Requested-With: sttracker` (403 otherwise). Browsers cannot
add it cross-site without a CORS preflight, which the server never grants. Bearer requests are exempt. Cookie is
`stt_session`, HttpOnly, SameSite=Lax, Secure when `PUBLIC_URL` is https, 30 days.
- **Error mapping:** zod and `ValidationError` → 400 `validation` (`field: reason`); `AuthError` →
401 (`invalid_credentials`, `unauthenticated`), 403 (`forbidden`), 400 with its own code (`invite_required`,
`username_taken`, `invalid_username`); `NotFoundError` → 404 `not_found`; `ImportError` → 400 with its code (409 for
`account_not_empty`, `conflict`); unique violations → 409 `conflict`; body over the cap → 413 `validation`;
rate limit → 429 `rate_limited`; anything else → 500 `internal` with a fixed message, detail logged only.
- **Limits:** JSON bodies 1 MB, multipart 50 MB (counted while streaming, not trusted from Content-Length).
Login and register: 10 per minute per client IP, in process (resets on restart; not shared across instances).
- **Actor:** every API write uses actor `"user"`, including token-authenticated ones, because the change engine only
protects fields whose `field_sources` is `"user"`. A `token:<name>` actor would let the next sync overwrite a script's edit.
- **Daily backup:** queued for the next 03:00 local with lockKey `"backup"` at boot, 5 s after each run (the finishing job
still holds the lock while it runs), and hourly as a safety net. All three are the same idempotent `enqueue`.
- **Deleted connections** (`paused` with no credentials) are hidden from `GET sources` and 404 on every `sources/:id` route.
- **File imports** travel in the job payload as base64 (`{ connectionId, file: { name, base64 } }`); `syncHandler` decodes
them into `runSync`'s `{ file }` mode, recorded as run mode `file`.
- **OpenAPI** 3.1 is generated from the route table (`z.toJSONSchema`, input side, unrepresentable → any): every route
with method, path, summary, security and request schema. Responses are not typed yet.
## 2026-10-30: Job failure semantics and plugin reload limits
**Status:** accepted
**Context:** Task 9 fix round 1.