# API.md Base path: `/api`. JSON in, JSON out. Auth via session cookie. ## Response envelope Success: `{ "ok": true, "data": { ... } }` Failure: `{ "ok": false, "error": { "code": "INSUFFICIENT_ENERGY", "message": "Not enough energy." } }` ## Status codes | Code | Use | |---|---| | 200 | success | | 201 | created (register, create hero) | | 400 | validation failed (`VALIDATION_ERROR`) | | 401 | not logged in (`UNAUTHENTICATED`) | | 403 | logged in but not allowed (`FORBIDDEN`, `BANNED`) | | 404 | not found (`NOT_FOUND`) | | 409 | conflict (`USERNAME_TAKEN`, `HERO_EXISTS`, `EXPEDITION_ACTIVE`) | | 422 | rule violation (`INSUFFICIENT_ENERGY`, `INSUFFICIENT_GOLD`, `LEVEL_TOO_LOW`, `NOT_READY`) | | 429 | rate limited | | 500 | unexpected (never leak stack traces) | ## Endpoints ### Auth (public unless noted) | Method | Path | Body | Returns | |---|---|---|---| | POST | `/auth/register` | `{username, password}` | `{user:{id,username}}` + sets session | | POST | `/auth/login` | `{username, password}` | `{user:{id,username}, hasHero}` | | POST | `/auth/logout` 🔒 | – | `{}` | | GET | `/auth/me` 🔒 | – | `{user:{id,username}, hasHero}` | Validation: username 3–20 chars `[A-Za-z0-9_]`; password 8–72 chars. Login failure always returns the same generic message (`INVALID_CREDENTIALS`) — don't reveal whether the username exists. ### Character 🔒 | Method | Path | Body | Returns | |---|---|---|---| | POST | `/character` | `{name, class}` | `{character}` | | GET | `/character` | – | `{character, derived, equipment, energy:{current,max,nextTickInSec}}` | | POST | `/character/allocate` | `{str,agi,int,vit}` (points to add) | `{character, derived}` | | POST | `/character/rest` | – | heals to full for gold cost (see BALANCE) | ### Hunting 🔒 | Method | Path | Body | Returns | |---|---|---|---| | GET | `/zones` | – | zones with `unlocked` flag | | POST | `/hunt` | `{zoneId}` | `{result:{won,rounds[],xp,gold,loot[],leveledUp,newLevel}, character}` | | GET | `/combat-log?limit=20` | – | recent fights | ### Inventory 🔒 | Method | Path | Body | Returns | |---|---|---|---| | GET | `/inventory` | – | `{items:[{inventoryId,item,quantity,equippedSlot}]}` | | POST | `/inventory/equip` | `{inventoryId}` | `{equipment, derived}` | | POST | `/inventory/unequip` | `{slot}` | `{equipment, derived}` | | POST | `/inventory/use` | `{inventoryId}` | `{character, message}` | ### Shop 🔒 | Method | Path | Body | Returns | |---|---|---|---| | GET | `/shop` | – | stock list with prices and `canAfford`, `levelOk` | | POST | `/shop/buy` | `{itemId, quantity}` (1–99) | `{gold, inventory}` | | POST | `/shop/sell` | `{inventoryId, quantity}` | `{gold, inventory}` | ### Expeditions 🔒 | Method | Path | Body | Returns | |---|---|---|---| | GET | `/expeditions` | – | available defs + active one (with `remainingSec`) | | POST | `/expeditions/start` | `{defId}` | `{expedition}` | | POST | `/expeditions/claim` | – | `{rewards, character}` | ### Leaderboard & heroes (public read) | Method | Path | Query | Returns | |---|---|---|---| | GET | `/leaderboard` | `type=level|gold|kills`, `page=1` | top 50 per page | | GET | `/heroes/:name` | – | public profile (name, class, level, kills, equipped item names) | ### Misc | GET | `/health` | – | `{ok:true}` | ## Rules for every route 1. Validate with zod; on failure → 400 `VALIDATION_ERROR` with field messages. 2. Never trust client-supplied ids for ownership: always scope by `req.session.userId` → character. 3. Responses never include `password_hash` or session data. 4. State-changing endpoints are POST. GET must never modify state. 5. Return the *updated* character where relevant so the UI can refresh without a second call.