Files
product-fpol/docs/application-api.md
T
khodowany 78b2c383f1
ci / frontend (push) Successful in 9m13s
ci / backend (push) Failing after 6m40s
Game Features
2026-10-04 23:20:06 -05:00

59 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Application API
All application responses are JSON `{ "ok": true, "status": 200, "payload": ... }`, or `{ "ok": false, "status": ..., "code": ..., "message": ... }`. The HTTP status agrees with the application status. Health and legacy simulator routes keep Ligo's original envelope. Player APIs return current projections; the development simulator alone exposes snapshot history.
## Authentication
`fpol_session` is a random opaque cookie, HttpOnly, SameSite=Lax, Path=/, with a 30-day lifetime. Only its SHA-256 digest is stored. Production requires `--secure-cookies` and an HTTPS `--origin`. Tokens never appear in HTTP JSON. Every POST needs `Origin` exactly matching `--origin` and `X-Fpol-Request: 1`; authenticated POSTs also need `X-CSRF-Token` from login or `GET /api/auth/me`. There is no cross-origin API/CORS access.
- `POST /api/auth/login`: `username, password`. Returns the user (`id, username, admin, csrf`) and sets the cookie.
- `POST /api/auth/register`: `username, password, invite, team_name`. A valid invitation and available seat are checked again transactionally after hashing; a failed join does not create an orphan account.
- `GET /api/auth/me`: current user and CSRF token.
- `POST /api/auth/logout`: invalidates the session and clears the cookie.
- `POST /api/auth/password`: `current_password, password`. Rotates all sessions, returning a new one.
- `POST /api/auth/reset`: `reset_token, password`. Single-use operator token; revokes all sessions and reset tokens for that account. Sign in afterward.
Usernames have 3–32 ASCII letters, numbers or underscores, case-insensitive. Passwords have 12–256 UTF-8 bytes. Hashes use OpenSSL scrypt N=131072/r=8/p=1, an independent 128-bit salt, and a 512-bit result. Two hashing workers serve at most eight in-flight jobs, off the HTTP loop. Login attempts are transactionally limited to 10 per username and 100 per transport peer in 15 minutes; behind Caddy that peer allowance is shared by all players. Password recovery is CLI-only, with a 30-minute expiry. Database files/backups contain secrets and need private storage.
## Leagues
All routes require a session. League-scoped reads and writes require membership. An administrator has no implicit access to other players' leagues.
| Method / path | Input |
| --- | --- |
| GET `leagues` | none; lists own memberships |
| POST `league/create` | `name, team_name, max_teams`; optional `catalog_id` (latest real catalog by default) |
| POST `league/join` | `invite, team_name` |
| GET `league` | query `league_id`; includes own queue, teams, picks, timer, readiness and recent events |
| POST `team/name` | `league_id, name` |
| POST `league/invite` | commissioner; `league_id`; returns a seven-day bearer invitation; replaces prior invitation |
| POST `league/revoke-invites` | commissioner; `league_id` |
| POST `league/remove` | commissioner; `league_id, team_id`; before draft only; also revokes existing invitations |
| POST `league/name` | commissioner; `league_id, name`; rename without changing frozen rules |
| POST `league/ratings` | commissioner; `league_id, document`; immutable sourced ratings JSON while forming |
| POST `league/settings` | commissioner; `league_id, name, catalog_id, ratings_id` (nullable); before draft only; clears queues |
No public registration, public league directory, or email invitation service exists. An invitation is reusable until the league fills, it expires, it is revoked, or drafting starts. One account owns at most one team per league. The commissioner cannot remove their own team.
## Drafts
- `POST draft/start`, `draft/pause`, `draft/resume`: commissioner, `league_id`.
- `POST draft/queue`: `league_id, candidates` (ordered unique candidate IDs, maximum 200). Other members never receive this queue.
- `POST draft/pick`: `league_id, candidate_id, pick_number, request_id`. Use a unique request ID per attempted pick. Retrying the same body/ID returns the original pick; reusing an ID for another body fails with 409. Candidate and slot exclusivity are database constraints. The draft clock and human picks use the same serialized, transactional path; a pick at or after deadline fails and the timer advances the turn.
The draft starts only with 2+ teams, enough global/per-party supply, ratings for every race, and a complete national catalog plus enabled verified count/outcome coverage for real races. The compiled inventory includes 435 House seats, 35 Senate contests (including FL/OH specials), and 36 governor races. The practice pool bypasses real-data gates only under `--allow-demo`.
## Candidates and standings
- `GET catalogs`: catalog/rating summaries, no snapshot history. Demo summaries require `--allow-demo`.
- `GET candidates`: optional `league_id` (pins frozen catalog/ratings/ownership), `catalog_id`, `search`, `office`, `state`, `affiliation`, `available=true`, `offset`, `limit` (1–100). `items` includes source provenance and rating/owner when applicable.
- `GET results`: optional `league_id` or `catalog_id`; current race counts and separate outcomes. Includes received/source/check timestamps and stale status. No observations are invented for missing races.
- `GET standings`: required `league_id`; team rosters, candidate score components, known subtotal, full total only when all 12 scores are known, reporting coverage, provisional flag, and tied competition ranks. Teams with no known scores are unranked.
- `GET admin/sources`: administrator only; current source health, assigned coverage, 50 recent quarantines.
GET parameters are percent-decoded, `+` becomes a space, and duplicate/malformed parameters are rejected by the opted-in Ligo query parser. HTTP requests are capped at 64 KiB. There is no remote admin import/reset/SQL endpoint. `fpol_admin app-request FILE|-` is a **trusted local test/operator interface** that may return session secrets and accepts an injected clock; it is never an HTTP route.
## Persistence
Migration `001_documents` stays unchanged. `002_leagues` adds accounts, sessions, invites, leagues, teams, draft clocks, picks, idempotency commands, events, source configuration, raw payloads, observations, current projections, coverage and quarantine. SQLite WAL, foreign keys and a busy timeout are enabled. One application DB worker owns each short `BEGIN IMMEDIATE` transaction. Hashing and network polling happen outside these transactions. The ingestion process uses its own connection to the same volume.