API Reference
Authentication and Access
GET and POST /api/auth/* delegate to Better Auth for social sign-in,
callbacks, sessions, and sign-out. GitHub and Google are configured. Do not infer
an enabled email/password registration flow from generic Better Auth endpoints.
Protected routes validate a session and normally return 401 when absent.
Owner/author/admin checks vary: consult the table and route-specific errors.
Admin scheduling requires "user".role = 'admin'.
Documents, Users, and Invitations
| Method | Path | Access | Contract |
|---|---|---|---|
| GET | /api/files | Signed-in owner | Own files with latest version metadata, newest update first |
| POST | /api/files | Signed in | Create file/version 1 from multipart file; optional problemSlug |
| GET | /api/files/shared | Signed in | Files where caller is a contributor, with owner ID |
| GET | /api/files/:fileId | Any signed-in user | Latest bytes for shared-link reading |
| PUT | /api/files/:fileId | Owner | New multipart file version, keeping latest four |
| PATCH | /api/files/:fileId | Owner | Rename with { "filename": "..." } |
| DELETE | /api/files/:fileId | Owner | Delete file/version bytes and metadata; 204 on success |
| GET | /api/files/:fileId/versions | Owner | Versions newest first |
| GET | /api/files/:fileId/versions/:versionId | Owner | Specific stored version bytes |
| GET | /api/users/search?q=... | Signed in | Email partial search; at least two characters, at most ten results |
| GET | /api/users/recent | Signed in | Up to eight recent collaborators on the caller's files |
| GET | /api/files/:fileId/contributors/check | Signed in | { isOwner, isContributor, canCollab } |
| GET | /api/files/:fileId/contributors | Owner or contributor | Current contributor list |
| POST | /api/files/:fileId/contributors | Owner | Direct contributor insertion with { "userId": "..." } |
| DELETE | /api/files/:fileId/contributors | Owner | Remove { "userId": "..." } |
| GET | /api/files/:fileId/invites | Owner | Pending invites with invitee name/email |
| POST | /api/files/:fileId/invites | Owner | Send pending invite with { "userId": "..." }; 201 or duplicate 409 |
| GET | /api/invites | Signed-in invitee | Caller’s pending invitations |
| POST | /api/invites/:inviteId | Pending invitee | { "action": "accept" } or "decline" |
| DELETE | /api/invites/:inviteId | File owner | Cancel pending invite; 204 |
File Constraints and Responses
Uploads require a nonempty File part; the limit is 100 * 1024 * 1024 bytes
(100 MiB). Invalid/empty files return 400; oversized files return 413.
Creation returns 201 with file/version metadata and an explicit file_id.
Saving a version returns its row with 200. Rename validates a nonempty
filename of at most 255 characters.
Downloads return bytes with Content-Type, Content-Disposition, and
X-Version-Number. The latest-version route also returns X-Owned and optional
X-Problem-Slug so a saved solution can reopen its question.
File/version inserts use database transactions after the byte upload. Pruned storage deletion is best-effort after commit. Not every endpoint or external storage operation is part of a single distributed transaction.
Accepting an invite changes its status and grants contributor access atomically. Pending invites are not contributor access. The direct contributor API still exists separately from the invitation acceptance workflow.
Problem Catalog and Grading
| Method | Path | Access | Contract |
|---|---|---|---|
| GET | /api/problems | Public | Catalog with caller's solved/attempt status; anonymous status defaults false/zero |
| GET | /api/problems?mine=1 | Signed in | Caller-authored and built-in catalog entries |
| POST | /api/problems | Signed in | Publish a definition; 201, invalid 400, duplicate slug 409 |
| GET | /api/problems/:slug | Public | Projected definition; non-authors receive only public cases |
| DELETE | /api/problems/:slug | Author, non-built-in | Remove problem; built-ins return 403, success 204 |
| POST | /api/problems/:slug/submit | Public | Grade { "machine": { ... } } against server cases |
Publishing accepts slug, title, difficulty, statement, variant,
tapeCount, starterSource, and cases. A case includes name, input,
expectedOutput, expected, cutoff, and visibility. Validation requires a
2-63 character lowercase/digit/dash slug, 1-20 cases including a public case,
and one of the six supported machine variants. Tape count is 1-8.
Case input/output bounds are 64/128 characters and cutoff is at most 10,000.
Submission validation bounds machine size before execution. Grading returns:
{
"accepted": false,
"passed": 1,
"total": 2,
"cases": [
{ "label": "example", "name": "example", "hidden": false, "passed": true, "reason": "ok", "steps": 3 },
{ "label": "hidden-1", "name": "hidden-1", "hidden": true, "passed": false, "reason": "mismatch", "steps": 4 }
],
"notes": [],
"recorded": false,
"attempts": null
}
This is an illustrative shape, not a captured run. Public case verdicts can
include detail; hidden ones omit expected/actual tape detail. Anonymous work
is graded but not recorded. Signed-in work upserts account progress and
increments attempts. There is no general-purpose /api/tm/parse or
/api/tm/run route in the inspected tree.
Daily Play and Archive
Daily routes use the HttpOnly fla_daily player cookie; sign-in is optional.
The server date uses DAILY_TIMEZONE (default Africa/Johannesburg).
| Method | Path | Contract |
|---|---|---|
| GET | /api/daily/today | Puzzle public view, solved state, probes/answers left, attempts, streak, secondsUntilRollover |
| POST | /api/daily/probe | { "input": "..." }; run a black-box reference, spend a probe, return tape/result/steps and budgets |
| POST | /api/daily/answer | Black-box { "predictions": [{ "tape": "...", "result": "acc" }] } or build { "machine": { ... } } |
| GET | /api/daily/history | Caller’s past puzzle progress/history |
| GET | /api/daily/archive | Past scheduled puzzle archive |
| GET | /api/daily/archive/:date | Past puzzle public view and reveal-policy-controlled reference data |
Unscheduled today returns a 200 object with puzzle: null and a countdown;
probe/answer instead return 404. Probing a build daily returns 405.
Reserved prediction inputs and symbols outside the hidden machine's alphabet
are rejected before spending a probe. The endpoint reports only the offending
typed symbol, not the full hidden alphabet.
Answers are validated before budget spending. Already solved returns 409;
exhausted budgets return 429 with rollover information. Black-box answers
return feedback tiles; build answers use the same authoritative grader as
catalog problems. Final lockout can include a reference reveal for that player.
Current/future archive dates are rejected.
lockout, archive_only, and never govern reference-machine disclosure.
Archive build views also project cases according to the source: generated past
build cases can become visible, while catalog-backed builds still use the
catalog non-author projection. Do not treat a past-date view as permission to
return all catalog private cases.
Daily Administration
| Method | Path | Admin-only behavior |
|---|---|---|
| GET | /api/admin/daily | Calendar rows/statistics and server's today |
| POST | /api/admin/daily | Schedule/upsert a draft date; 201 |
| POST | /api/admin/daily/preview | Compile, generate observations/starter, self-grade, return errors/warnings and transition reachability; no scheduling write |
| PUT | /api/admin/daily/:id | Rewrite/move a scheduled puzzle; date conflict 409 |
| DELETE | /api/admin/daily/:id | Delete schedule/progress under configured cascades; 204 |
Draft fields are date, kind, statement, probeBudget, answerBudget,
revealPolicy, problemSlug, machineSource, inputs, privateInputs, and
hiddenRefs. Black-box mode requires a reference and 1-8 prediction inputs.
Build mode takes a reference-generated starter or catalog source and has zero
probe budget. Answer budget is 1-10; black-box probe budget is 0-20.
Preview reports reached/hidden transitions and generated case/slot counts. Scheduling/replacing an existing date retains earlier attempts; the admin must understand that they still describe the old content.
Groq AI Proxy
POST /api/ai/chat
The current handler does not require a Better Auth session. It uses optional
x-groq-key from the request, otherwise GROQ_API_KEY from the server.
A missing key returns 500 with setup guidance.
{
"messages": [{ "role": "user", "content": "Explain this transition" }],
"editorContent": "setVariant(single-tape); ...",
"smartModel": true
}
The server calls https://api.groq.com/openai/v1/chat/completions with a system
prompt generated from current editor content and the last three supplied
messages. smartModel selects qwen/qwen3.8-27b (4096-token output cap) or
openai/gpt-oss-20b (2048-token cap). These are source-configured identifiers,
not a guarantee of provider availability. Temperature is 0.7.
Success returns { "content": "...", "rateLimited": false }. Invalid JSON or
empty messages return 400; provider limits return 429; provider/network
errors return 502. The server keeps an in-process estimated credit window
(8000 credits per 60 seconds per key), can wait before sending, and accounts
for returned token usage. This is not a persistent/distributed quota.
Data boundary: messages and editor source are sent to Groq. The browser AI
panel stores an optional user key in local storage and sends it to this route.
Do not put key values into documentation or VITE_ build variables, and do not
paste sensitive machine/source content into the assistant without considering
the external service's data handling. No Groq SDK is installed; the integration
uses fetch, the project prompt builder, and Markdown/code rendering.
External File Storage Contract
The application invokes GET, POST, and DELETE
<FILE_SERVER_URL>/internal/:ownerUserId/:storagePath using X-Internal-Key.
Uploads send raw bytes; downloads stream bytes. The storage server source is
not in the inspected application tree, so its implementation, port, health
route, filesystem layout, and path-safety guarantees are not verified here.