# Auth, access rules and rate limits

Who a caller is, what they may reach, and how often.

---

## 1. Keys

A key lives on `user.api_key` (with `user.api_key_created`). Players generate and
revoke their own on `i.cfm?f=option_api`. One key per account; regenerating
replaces the old one immediately.

Callers send it either way:

```
X-API-Key: gccapi_...          preferred
?key=gccapi_...                accepted, but ends up in server logs and browser history
```

`Base.suppliedKey()` reads the header first. `requireKey()` resolves it to
`request.apiUser` (`id`, `nic`, `server`) or answers **401**. Everything
downstream reads the caller through `callerId()` / `callerNic()` /
`callerServer()` — never from a parameter, so a caller can never claim to be
someone else.

### Handling rules

- Never log a key: `safeParams()` strips it before `api_log` sees it.
- Never return a key in a response.
- Treat a key as identifying the **account**, not a session. There is nothing to
  expire and nothing to refresh.

## 2. Access rules

Declared per endpoint as `requires` in `Registry.cfc`:

| Value | Meaning |
|---|---|
| `""` | any valid key |
| `"project:<n>"` | caller must hold game project *n* with `finishflag = 1` |

`dsr.battles` uses `project:5` — Deep Space Radar. The rule mirrors the
in-game entitlement rather than inventing a new one: if the game would not show
it, the API does not either.

**Unknown rule kinds are denied.** Both implementations fail closed:

- `Base.requireAccess` — aborts with 403
- `Meta.canAccess` — returns false, so `meta.whoami` omits the endpoint

Adding a rule kind means updating **both**, or `whoami` will advertise endpoints
that immediately 403.

## 3. Rate limiting

120 requests per minute per key, in `Base.RATE_LIMIT_PER_MIN`. Counters live in
application scope, keyed by caller, and reset on the minute. Over the ceiling is
**429**.

The check runs *after* `requireKey()` — an unauthenticated request is rejected
before it can consume anyone's budget.

Because the counters are in application scope they reset when the app restarts.
That is acceptable: the limit exists to stop runaway polling, not to bill anyone.

## 4. Caching as a courtesy

Every response carries `refresh_secs` and `cache_age_secs` in `meta`. A caller
that respects them will not out-poll the data:

```json
"meta": { "cached": true, "refresh_secs": 60, "cache_age_secs": 12 }
```

`ranks.top50` recomputes once a minute; `planets.types` and `races.list` are
effectively static. Polling faster returns the identical cached body and only
spends rate-limit budget.

## 5. Status codes

| Code | Cause |
|---|---|
| 200 | fine |
| 400 | missing or malformed `action` |
| 401 | missing or unknown key |
| 403 | key is valid but the endpoint's `requires` rule is not met |
| 404 | no such action |
| 405 | not a GET |
| 429 | over the per-key rate limit |
| 500 | handler fault — real reason is in `gcc_player_api.log` only |

## 6. Access logging

`logAccess(status)` writes one `gcc_log.api_log` row per call: caller, action,
status, duration, and `safeParams()` output. This is what answers "who hammered
the API at 3am" — and it is why the key must never leak into the parameter
string.
