# Dweller Games API v1

Base URL: `https://dweller.games/api/v1` (local dev: `http://localhost:8080/api/v1`).

All bodies are JSON (`Content-Type: application/json`). Times are RFC 3339 UTC strings.
Errors always look like `{"error": "machine_code", "message": "Human readable"}` with a 4xx/5xx status.

## Authentication

Send `Authorization: Bearer <token>`. Tokens start with `dg_`. Get one by:

* `POST /auth/login` with a username/email and password (launcher sign-in form), or
* the device flow (`POST /auth/device`, then poll `POST /auth/device/token`) used by the CLI and by
  the launcher's "Sign in with browser" button, or
* creating a personal token at `https://dweller.games/settings/tokens` (CI, GitHub Actions).

Endpoints marked 🔒 need a token. Creator endpoints need the account to be an approved creator.

### `POST /auth/login`
```json
{"login": "alice", "password": "hunter22", "device_name": "Alice's PC (launcher)"}
```
→ `200 {"token": "dg_…", "user": User}` · `401 {"error":"invalid_credentials"}` · `403 {"error":"banned"}` · `429` rate limited.

### `POST /auth/logout` 🔒
Revokes the token used for the call. → `204`.

### `POST /auth/device`
```json
{"client": "launcher", "device_name": "Alice's Mac"}
```
→ `200`
```json
{"device_code": "long-secret", "user_code": "WXYZ-2345",
 "verification_uri": "https://dweller.games/activate",
 "verification_uri_complete": "https://dweller.games/activate?code=WXYZ-2345",
 "interval": 3, "expires_in": 600}
```

### `POST /auth/device/token`
```json
{"device_code": "long-secret"}
```
→ `200 {"token": "dg_…", "user": User}` once the user approves in the browser.
→ `428 {"error":"authorization_pending"}` keep polling every `interval` seconds.
→ `400 {"error":"expired_token"}` or `{"error":"access_denied"}` stop.
(Clients should also treat `{"error":"slow_down"}` as "add 2 seconds to the interval".)

### `GET /me` 🔒
→ `200 User`

## Objects

```jsonc
// User
{"id": 12, "username": "alice", "display_name": "Alice", "avatar_url": "https://…" /* or "" */,
 "role": "user" /* user | creator | admin */, "creator": true, "verified": false}

// Creator (embedded)
{"username": "phastings", "display_name": "Dweller Studio", "avatar_url": "", "verified": true}

// GameSummary
{"id": 3, "slug": "drift-kings", "title": "Drift Kings", "tagline": "Open-world night racing…",
 "creator": Creator, "cover_url": "https://cdn…/cover.jpg", "header_url": "https://cdn…/header.jpg",
 "accent": "#ff3d7f", "tags": ["racing", "open world"],
 "platforms": ["windows", "linux", "macos"], "playable_in_browser": false,
 "latest_version": "1.0.0", "updated_at": "2026-11-13T19:00:00Z", "released_at": "2026-11-01T12:00:00Z",
 "rating": {"positive": 10, "total": 12, "percent": 83}, "downloads": 1234,
 "ai_tools": ["Claude Code"], "url": "https://dweller.games/games/drift-kings"}

// Build: one downloadable file for one platform
{"id": 77, "platform": "windows" /* windows | macos | linux | android | web | gba */,
 "arch": "x64" /* x64 | arm64 | universal | any */,
 "kind": "zip" /* zip | exe | apk | rom | web | appimage | dmg | tar.gz */,
 "filename": "DriftKings-1.0.0-windows-x64.zip", "size": 109831632,
 "sha256": "hex…", "exec": "DriftKings.exe",
 "url": "https://cdn.dweller.games/builds/…",            // direct CDN file (use for downloading)
 "download_url": "https://dweller.games/dl/77",           // counted redirect (use in browsers)
 "play_url": ""                                           // set for web/gba builds
}

// Release
{"id": 9, "version": "1.0.0", "channel": "stable", "released_at": "…", "notes_md": "…", "notes_html": "…",
 "builds": [Build]}

// GameDetail = GameSummary + these fields
{"description_md": "…", "description_html": "…", "screenshots": ["https://…"],
 "controls": "…", "engine": "Godot 4.7", "website": "", "github_repo": "owner/name",
 "multiplayer": false, "latest": Release /* or null */, "releases": [Release /* without builds */],
 "in_library": true /* only when called with a token */, "in_wishlist": false}
```

`exec` rules for installers: for `zip`/`tar.gz` builds, extract the archive into the install folder and
run `exec` (a path relative to the install folder, forward slashes). For `exe`/`appimage` builds the
file itself is the program (`exec` equals `filename`); on Linux/macOS mark it executable. For macOS
zips, `exec` points at the `.app` bundle (`Game.app`) – open it with `open -a`. `apk` is for Android
phones (offer a download), `web`/`gba` builds are played at `play_url`.

## Store

### `GET /home`
Everything the launcher's store front page needs in one call.
→ `200 {"featured": [GameSummary], "new": [GameSummary], "updated": [GameSummary],
        "popular": [GameSummary], "browser": [GameSummary /* playable in the browser */],
        "tags": [{"tag": "rpg", "count": 4}], "announcement": ""}`

### `GET /catalog?q=&tag=&platform=&sort=&page=`
`sort` = `popular` (default) | `new` | `updated` | `rating` | `title`. 30 per page, `page` starts at 1.
→ `200 {"games": [GameSummary], "page": 1, "pages": 3, "total": 61}`

### `GET /games/{slug}`
→ `200 GameDetail` · `404`

### `GET /games/{slug}/latest?platform=windows&arch=x64`
Best build for that platform (`arch` falls back to `universal`, then `any`).
→ `200 {"version": "1.0.0", "release_id": 9, "build": Build}` · `404 {"error":"no_build"}`

### `POST /updates/check`
```json
{"platform": "windows", "arch": "x64",
 "installed": [{"slug": "drift-kings", "version": "0.9.0"}, {"slug": "terald", "version": "1.2.0"}]}
```
→ `200 {"updates": [{"slug": "drift-kings", "from": "0.9.0", "to": "1.0.0", "build": Build, "notes_md": "…"}]}`
Only games with a newer release that has a build for the platform are listed.

## Library 🔒 (synced between the website and the launcher)

### `GET /library`
```json
{"games": [{"game": GameSummary, "added_at": "…", "last_played_at": "…" /* or null */,
            "playtime_seconds": 3600, "latest": Release /* with builds, or null */}]}
```
### `PUT /library/{slug}` → `204` (add) · `DELETE /library/{slug}` → `204` (remove)
### `PUT /wishlist/{slug}` → `204` · `DELETE /wishlist/{slug}` → `204` · `GET /wishlist` → `{"games":[GameSummary]}`
### `POST /library/{slug}/played`
`{"seconds": 1200}` → `204`. Adds playtime and sets `last_played_at` (the launcher calls it when a game exits).

## Creator API 🔒

### `GET /creator/games`
→ `{"games": [GameSummary + {"status": "draft|published|unlisted|removed"}]}`

### `POST /creator/games`
`{"title": "My Game", "slug": "my-game" /*optional*/, "tagline": "", "description_md": "", "tags": ["arcade"],
  "ai_tools": ["Claude Code"], "engine": "Godot 4.7"}` → `201 GameDetail`

### `PATCH /creator/games/{slug}`
Any GameDetail editable field, plus `"status": "published"`. → `200 GameDetail`

### `POST /creator/games/{slug}/uploads`
Ask for a direct upload URL for one build file.
```json
{"filename": "MyGame-windows.zip", "size": 123456, "sha256": "hex", "platform": "windows", "arch": "x64"}
```
→ `200 {"upload_id": "u_…", "put_url": "https://…", "headers": {"Content-Type": "application/zip", "x-amz-acl": "public-read"}, "expires_in": 3600}`
`PUT` the raw file bytes to `put_url` with exactly those headers.

### `POST /creator/games/{slug}/releases`
```json
{"version": "1.2.0", "notes_md": "What's new…", "channel": "stable", "publish": true,
 "builds": [{"upload_id": "u_…", "platform": "windows", "arch": "x64", "exec": "MyGame.exe"}]}
```
→ `201 Release`. `kind` is inferred from the file name; `exec` defaults to the only executable in the archive
or to the file itself. Web builds (`platform: "web"`, a zip with `index.html`) are unpacked for browser play.

### `POST /creator/games/{slug}/media` (multipart form: `kind` = `cover|header|screenshot`, `file`)
Images are resized server-side: `header` is cropped to 1280×600 (wide, used for cards, the hero and page backgrounds),
`cover` to 600×900 (portrait library art), screenshots fit within 1920×1080 with a 480×270 thumbnail.
→ `201 {"url": "…"}`

## Launcher self-update

### `GET /launcher/update/{target}/{arch}/{current_version}`
Tauri v2 dynamic update server. `target` = `windows|darwin|linux`, `arch` = `x86_64|aarch64`.
→ `204` when up to date, otherwise
`200 {"version": "0.2.0", "notes": "…", "pub_date": "…", "url": "https://cdn…", "signature": "…"}`

### `GET /launcher/latest`
→ `200 {"version": "0.2.0", "pub_date": "…", "notes": "…", "downloads": {"windows-x64": {"url": "…", "size": 1}, "macos-universal": {…}, "linux-x64": {…}, "linux-x64-deb": {…}}}`

## GitHub

`POST /hooks/github/{link_id}`: GitHub webhook receiver (`release` events, HMAC-SHA256 signed). Set up from
the game's GitHub tab on the creator dashboard.
