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

6.3 KiB
Raw Permalink Blame History

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.