Admin routes
Source: src/auth/admin-routes.ts
Routes that operators (not tenants) call to manage tenant credentials and inspect runtime state. Phase 1 shipped one route (API-key rotation); Phase 2 PR-7 adds two more (platform disconnect, cache metrics).
All routes share the same auth gate: X-Admin-Token header, constant-time compared against the Phase 1 placeholder (see § Phase 1 placeholder). 401 on mismatch.
POST /admin/api-keys/rotate
| Header | Required | Notes |
|---|---|---|
X-Admin-Token | yes | Constant-time compared against the dev placeholder (see “Phase 1 placeholder” below). |
Content-Type | yes | application/json |
Body:
{ "tenantId": "<uuid>", "description": "Claude Desktop - prod" }Response (201 Created):
{ "apiKey": "<43-char base64url>", "graceUntil": "<ISO timestamp +24h>" }The raw apiKey is returned once. There is no endpoint to retrieve it later — the database stores only the HMAC hash.
What the rotation does
In one DB transaction:
- Set
expires_at = now + 24hon every existing key for the tenant. The 24-hour grace lets clients update before old keys stop working. - INSERT a new key row with
expires_at = now + 365d.
After 24h, the old keys naturally expire (the auth middleware’s gt(apiKeys.expiresAt, now) clause). No separate revocation pass is needed.
Phase 1 placeholder
The admin token is currently derived from API_KEY_HMAC_SECRET so we don’t yet need a separate ADMIN_TOKEN secret. This is a deliberate Phase 1 simplification. Phase 5 introduces a real ADMIN_TOKEN credential AND moves these routes behind nginx with an IP allow-list (so only operators on a known IP range can hit them).
If you deploy Phase 1 to the public internet without the Phase 5 nginx allow-list, an attacker who learns API_KEY_HMAC_SECRET (e.g. from a backup leak) can rotate any tenant’s key. Don’t do that.
POST /admin/tenants/:tenantId/disconnect/:platform
Phase 2 (PR-7). Disconnects a tenant from a platform: best-effort upstream OAuth revoke (POST to https://oauth2.googleapis.com/revoke) followed by DELETE FROM platform_credentials WHERE tenant_id = $1 AND platform = $2 inside an RLS-scoped transaction.
| Path param | Validator | Notes |
|---|---|---|
tenantId | z.string().uuid() (v4-strict) | Production tenant IDs from gen_random_uuid() pass; nil/placeholder UUIDs in tests must use proper v4 form. |
platform | z.enum(['google','meta','tiktok']) | Phase 3 lifts the 501 for meta/tiktok once their adapters land. |
Response shapes:
- 200
{ status: 'disconnected', platform }— local row gone; upstream revoke attempted (best-effort). - 401 unauthorized — missing or wrong
X-Admin-Token. - 400 invalid_params — malformed tenant UUID or unknown platform.
- 501 unsupported_platform — platform validated but no adapter registered yet.
- 502 revoke_failed — the adapter threw. Local wipe might or might not have happened; audit row carries the failure reason.
Idempotent: returns 200 even when there is no platform_credentials row to delete. Phase 2 leaves the tenant_deks row in place so re-connecting the same platform re-uses the same DEK; call destroyDek(tenantId) separately if you also want to make the tenant’s prior ciphertexts permanently unreadable.
GET /admin/metrics/cache
Phase 2 (PR-7). Returns the process-local cache hit/miss counters as one entry per ${platform}/${reportType}:
{
"google/account_health": { "hit": 3, "miss": 1, "hitRate": 0.75 },
"google/search_term_waste": { "hit": 0, "miss": 2, "hitRate": 0 }
}Empty object {} if no cache activity has happened since process start. Counters reset on restart (the underlying Map lives in src/cache/metrics.ts). Phase 5 replaces this with a Prometheus / OTel exporter.
Phase 1 placeholder
The admin token is currently derived from API_KEY_HMAC_SECRET so we don’t yet need a separate ADMIN_TOKEN secret. This is a deliberate Phase 1 simplification. Phase 5 introduces a real ADMIN_TOKEN credential AND moves these routes behind nginx with an IP allow-list (so only operators on a known IP range can hit them).
If you deploy Phase 1 to the public internet without the Phase 5 nginx allow-list, an attacker who learns API_KEY_HMAC_SECRET (e.g. from a backup leak) can rotate any tenant’s key OR disconnect their platform credentials. Don’t do that.
Audit
| Route | Success event | Failure event |
|---|---|---|
POST /admin/api-keys/rotate | api_key.rotated/success | — (401s aren’t audited in Phase 1) |
POST /admin/tenants/:id/disconnect/:platform | oauth.token_revoked/success | oauth.token_revoked/failure (on adapter throw) |
GET /admin/metrics/cache | — | — |
Failed admin-token checks return 401 silently — they do not write an audit row in Phase 1. Phase 5’s monitoring rule will pick up repeated 401s on /admin/* directly from nginx access logs.
Tests
- Phase 1 §D4 acceptance — rotation grace window (old key works for ≤24h, new key works immediately, old key 401s after 25h via fake clock).
tests/integration/revocation.test.ts— disconnect happy path, idempotency, upstream-unreachable still wipes locally, 401 / 400 / 501 envelopes.tests/integration/cache-metrics.test.ts— empty when no activity, expected shape after 1 miss + 3 hits, 401 without token.