# Auth and Tokens

One bearer header, two credential kinds. The backend branches on the `pat_` prefix.

## Credential kinds

- **Session JWT**: minted by Google OAuth in the browser (sign in at shanejli.com). Full power: bypasses all scope checks and all per-minute rate limits.
- **Personal access token (PAT)**: `pat_` + 32 random bytes base64url. Scoped and rate limited. This is what agents should use.

Send either as `Authorization: Bearer <token>`.

## Minting a PAT

Easiest: sign in and mint at `https://shanejli.com/settings/tokens`.

Programmatic (requires a session JWT; a PAT can never mint another PAT, by design):

```bash
curl -X POST https://shanebackend-production.up.railway.app/api/auth/tokens \
  -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
  -d '{"name":"my-agent","scopes":["entries:write","comments:write"]}'
# 201 {"id":"<uuid>","token":"pat_..."}   raw token shown exactly once
```

- List: `GET /api/auth/tokens`. Revoke: `DELETE /api/auth/tokens/:id` (204). Both need a session JWT, with one exception: a PAT may revoke itself (its own id). Any other PAT call gets 403.
- PATs never expire; revocation is the only kill switch.
- Who am I: `GET /api/auth/me` returns your user object for either credential kind.

## Scopes

`entries:write`, `suggestions:write`, `comments:write`, `reactions:write`, `knowledge:write`, `trips:write`, `practice:write`

A PAT without the required scope gets 403 `Token missing required scope: <scope>`. A route shared by several features accepts any one of their scopes and says so: journal image uploads take `entries:write`, `suggestions:write`, `comments:write` or `knowledge:write` and answer 403 `Token missing required scope: one of entries:write, suggestions:write, comments:write, knowledge:write` otherwise. Some routes need only authentication, no scope (for example RNG Capitalist).

## Rate limits (PATs only, rolling 60s, 429 + Retry-After)

| Bucket | Limit/min | Covers |
|---|---|---|
| journal-entries-write | 30 | entry create, append, revert, trash |
| journal-suggestions-write | 30 | suggest, approve, reject, withdraw |
| journal-comments-write | 30 | journal comments |
| journal-reactions-write | 60 | journal reactions |
| courses-write | 60 | all courses writes |
| scoreboard-write | 60 | all scoreboard writes |
| tea-entries-write | 30 | tea writes |
| skincare-write | 60 | skincare writes |
| knowledge-notes-single / -batch | 30 / 5 | note ingest |
| knowledge-entries-write | 30 | knowledge entry create, edit, delete, bulk-delete; connection create, delete |
| knowledge-enrich | 10 | knowledge entry enrich (one LLM call each) |
| knowledge-comments-write | 30 | knowledge comments |
| vocabulary-writes / vocabulary-enrich | 30 / 10 | vocabulary writes / enrich |
| practice-* | 30 (sync and vocab reviews 120) | practice writes |
| rng-capitalist-evaluate / -plaid | 10 / 10 | rng evaluate / budget |

RNG Capitalist bank linking (`POST /api/rng/plaid/link-token` and `/plaid/exchange`) needs a session JWT: any PAT gets 403 `Bank linking requires a browser session, not a PAT`. The rest of `/api/rng` accepts any PAT.

The loans ledger (`/api/loans`, Who Owes Me) is browser-session only, reads included: any PAT gets 403 `Loans requires a browser session, not a PAT`.

Tea entries (`/api/tea-entries`) take `entries:write` for reads as well as writes. Without it the list answers 403 `Token missing required scope: entries:write`, and `GET /api/tea-entries/:id` treats the PAT like anyone else: 401 `PIN required` until it sends the right `X-Tea-Pin`, and the entry's PIN is never in the response.

Skincare (`/api/skincare`) is the same: `GET /api/skincare` and `GET /api/skincare/search` need `entries:write`, like every skincare write, and answer 403 `Token missing required scope: entries:write` without it.

Journal image uploads have a separate 100 per rolling 24h per-user quota (applies to JWTs too) with an honest `Retry-After`.

## Blitz sync sessions (browser only)

`POST /api/blitz/sync-session` with a browser JWT returns `{ baseUrl, accessToken, encryptKey, email, expiresAt }`: the SuperSync server URL, a 365-day SuperSync token, and the caller's encryption password (created on first call, identical on every device). PATs get 403. The Blitz app calls it through the popup at `/blitz/connect`; agents have no reason to call it.

Blitz is retired: it is no longer an element on this site. The endpoint stays up only so existing Blitz installs keep syncing until the sync server is shut down, and it will be removed then.

## Admin-gated surfaces

A few routes are JWT-plus-admin-email only (practice settings PATCH, activity ingest). PATs always get 403 there; agents should skip them.
