88 lines
3.7 KiB
Markdown
88 lines
3.7 KiB
Markdown
# 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.
|