src/auth/scopes.ts (#374) defines a granular UserScope catalog for user API keys — deposit:write, goals:write, recurring_deposits:write, strategies:write, webhooks:manage, vault:write, alerts:manage, fiat:write, etc. — and requireScope() in src/middleware/apiKeyAuth.ts is the middleware meant to enforce it.
In practice requireScope is only wired up on two route files: src/routes/withdraw.ts and src/routes/keys.ts. Every other route that owns one of those named scopes — deposit.ts, goals.ts, recurring-deposits.ts, strategies.ts, webhooks.ts, vault.ts, alerts.ts, fiat.ts — only calls requireAuth, with no scope check at all.
for f in src/routes/*.ts; do grep -q "requireScope" "$f" || echo "$f"; done
Since requireAuth accepts both sessions and API keys, an API key created with DEFAULT_READ_SCOPES (read-only: portfolio:read, transactions:read, vault:read) is not actually prevented from calling these write endpoints — the scope catalog exists but isn't the access boundary it looks like.
Task
- Audit every route under
src/routes against the scope it should require per USER_SCOPES.
- Attach the matching
requireScope(...) guard (pattern already established in withdraw.ts).
- Add regression tests (e.g. extend
tests/unit/middleware/apiKeyAuth.test.ts) asserting a key without the relevant scope gets 403 on each affected route.
Acceptance criteria
- Every write route enforces the scope named after it in
USER_SCOPES.
- A read-only-scoped API key gets 403 on all of them.
src/auth/scopes.ts(#374) defines a granularUserScopecatalog for user API keys —deposit:write,goals:write,recurring_deposits:write,strategies:write,webhooks:manage,vault:write,alerts:manage,fiat:write, etc. — andrequireScope()insrc/middleware/apiKeyAuth.tsis the middleware meant to enforce it.In practice
requireScopeis only wired up on two route files:src/routes/withdraw.tsandsrc/routes/keys.ts. Every other route that owns one of those named scopes —deposit.ts,goals.ts,recurring-deposits.ts,strategies.ts,webhooks.ts,vault.ts,alerts.ts,fiat.ts— only callsrequireAuth, with no scope check at all.Since
requireAuthaccepts both sessions and API keys, an API key created withDEFAULT_READ_SCOPES(read-only:portfolio:read,transactions:read,vault:read) is not actually prevented from calling these write endpoints — the scope catalog exists but isn't the access boundary it looks like.Task
src/routesagainst the scope it should require perUSER_SCOPES.requireScope(...)guard (pattern already established inwithdraw.ts).tests/unit/middleware/apiKeyAuth.test.ts) asserting a key without the relevant scope gets 403 on each affected route.Acceptance criteria
USER_SCOPES.