Problem
ApiKeyGuard.canActivate() (src/auth/guards/api-key.guard.ts, lines 35–114) on EVERY vendor request:
- Computes
sha256(key) and runs .select('*').eq('key_hash', hash).single() against Supabase — two DB round trips per request counting the last_used_at UPDATE (which fires unconditionally, line 104–113). Under vendor traffic this is pure hot-path database load with zero caching, making the guard itself the scalability ceiling and a denial-of-service amplifier (flooding random keys saturates the DB connection pool).
- Performs a direct hash-equality DB match — fine cryptographically — but the surrounding error behavior distinguishes
API_KEY_INVALID vs API_KEY_INACTIVE vs API_KEY_EXPIRED (lines 59–80), letting an attacker enumerate whether a captured key hash corresponds to a revoked vs expired vs nonexistent key. Low severity individually; sloppy combined with the next point.
- Has no per-key rate limiting, lockout, or anomaly tracking: a leaked key can be driven at line rate indefinitely.
permissions array check (lines 87–95) uses .some(includes) — acceptable — but there is no wildcard support or permission hierarchy, pushing consumers toward over-permissioned keys.
- Fires the
updateLastUsed write on every request with no throttling — write amplification scales 1:1 with reads.
Ground Rules
- Read context/architecture-context.md, context/code-standards.md, context/progress-tracker.md in full
- Read the cache-manager usage patterns already established in
src/modules/liquidity/liquidity.service.ts and src/modules/transactions/transactions.service.ts
- Keep the
request.apiKey contract intact for controllers
What To Build
- Cache key records by hash prefix (never store full keys in cache) with a short TTL; invalidate on revocation via the vendors service write path.
- Collapse
last_used_at writes to at-most-once-per-N-minutes-per-key (cache-guarded dirty flag).
- Normalize all failure responses to a single
API_KEY_UNAUTHORIZED code (log details server-side only).
- Add per-key sliding-window rate limits with structured 429 responses, wired through the repo's established middleware/guard patterns.
- Tests: cache hit avoids DB call (mock assertions); revocation invalidates within one TTL; rate limit trips and resets; enumeration uniformity of error codes.
Files To Touch
src/auth/guards/api-key.guard.ts
src/modules/vendors/vendors.service.ts (cache invalidation hook)
- tests
- relevant docs/progress tracker
Acceptance Criteria
Mandatory Checks Before Opening PR
Standard checklist applies. PRs failing any check will be closed without review.
Problem
ApiKeyGuard.canActivate()(src/auth/guards/api-key.guard.ts, lines 35–114) on EVERY vendor request:sha256(key)and runs.select('*').eq('key_hash', hash).single()against Supabase — two DB round trips per request counting thelast_used_atUPDATE (which fires unconditionally, line 104–113). Under vendor traffic this is pure hot-path database load with zero caching, making the guard itself the scalability ceiling and a denial-of-service amplifier (flooding random keys saturates the DB connection pool).API_KEY_INVALIDvsAPI_KEY_INACTIVEvsAPI_KEY_EXPIRED(lines 59–80), letting an attacker enumerate whether a captured key hash corresponds to a revoked vs expired vs nonexistent key. Low severity individually; sloppy combined with the next point.permissionsarray check (lines 87–95) uses.some(includes)— acceptable — but there is no wildcard support or permission hierarchy, pushing consumers toward over-permissioned keys.updateLastUsedwrite on every request with no throttling — write amplification scales 1:1 with reads.Ground Rules
src/modules/liquidity/liquidity.service.tsandsrc/modules/transactions/transactions.service.tsrequest.apiKeycontract intact for controllersWhat To Build
last_used_atwrites to at-most-once-per-N-minutes-per-key (cache-guarded dirty flag).API_KEY_UNAUTHORIZEDcode (log details server-side only).Files To Touch
src/auth/guards/api-key.guard.tssrc/modules/vendors/vendors.service.ts(cache invalidation hook)Acceptance Criteria
Mandatory Checks Before Opening PR
Standard checklist applies. PRs failing any check will be closed without review.