API key service
Source: src/security/api-key.service.ts
Three functions, all small. Together they cover the full lifecycle of a tenant’s bearer-style API key.
generateApiKey(): string
Returns a fresh 32-byte (256-bit) random key, base64url-encoded. The output is shown to the user once and never stored — only the hash goes into the database.
hashApiKey(rawKey: string): string
HMAC-SHA256 with API_KEY_HMAC_SECRET (loaded from the secrets store) over the raw key. Returns hex.
Why HMAC and not plain SHA-256? A database-only compromise must not yield usable keys. An attacker who steals the api_keys.key_hash column also needs API_KEY_HMAC_SECRET to brute-force candidate keys against the stored hashes. The HMAC secret lives in the secrets store, not in the database.
verifyApiKey(rawKey, storedHash): boolean
Constant-time comparison via crypto.timingSafeEqual. Length mismatch short-circuits to false — timingSafeEqual throws on mismatched lengths, so we check first. The hash length is fixed (hex of SHA-256) and not secret, so this short-circuit doesn’t leak anything.
Where it’s used
tenant.middleware.ts— every authenticated request hashes the presented key and looks it up.admin-routes.ts— rotation generates + hashes a new key.scripts/seed-tenant.mjs— bootstrap script for the first key. The seed script does its own HMAC inline so it can stay zero-dependency on the build output.
Tests
tests/integration/api-key.test.ts covers:
- key shape (43 base64url chars)
- hash determinism
- verify on match / mismatch / length mismatch (no throw)