API keys
Mint, store, verify and revoke Bearer API keys for the REST and MCP tool surfaces — HMAC-peppered storage, scope-gated, audited.
Cartwright exposes its full operational tool surface (catalog, orders, discounts, pages, campaigns, …) over two authenticated programmatic interfaces:
- REST —
POST /api/v1/tools/<name>(one tool per call). - MCP —
POST /api/mcp(Model Context Protocol; see MCP server).
Both authenticate the same way: a Bearer API key in the Authorization header. Source: lib/api-auth.ts, lib/scopes.ts.
Key format
sb_live_<24 random bytes, base64url>The sb_live_ prefix makes keys greppable in logs and recognizable in the admin UI; entropy is randomBytes(24) (192 bits). Keys are created in /admin/api-keys (shop-owner only). The plaintext is shown exactly once at creation — only the hash is persisted.
Storage — the database never holds a usable key
Cartwright stores a keyed hash, not the key:
keyHash = HMAC-SHA256(key_plaintext, pepper = AUTH_SECRET)A database leak alone cannot forge a key: the HMAC pepper is AUTH_SECRET, which lives only in the server environment, never in the DB. Plain SHA-256 was rejected for exactly this reason. AUTH_SECRET is reused deliberately — it's already required by Auth.js, so there's no new secret to rotate.
Verification flow
authenticateApiKey(req) and requireApiScope(req, scope) are the entry points every REST/MCP route calls first:
- Extract the bearer token — missing/malformed →
401. - Prefix check — must start with
sb_live_(cheap reject before any DB hit). - Hash + lookup — HMAC the token, look up
ApiKey.keyHash(unique index). No row →401. - Revocation —
revokedAtset →401 API key revoked. - Last-used stamp —
lastUsedAtupdated fire-and-forget; never fails the request. - Scopes — the key's
scopesJSON is parsed; malformed →401.
The function never throws — it returns { actor } or { error }. requireApiScope then checks the key carries the tool's required scope (hasScope); missing → 403 naming the scope. See Scopes & tools.
Audit identity & revocation
Each authenticated actor is apikey:<id> in the audit log (distinct from user:<id> and storefront-chat:<sid>). Revoke by setting revokedAt via /admin/api-keys; verification rejects revoked keys immediately.
AUTH_SECRET is mandatory — getKeyPepper() throws if it's missing. Rotating it invalidates all existing API keys (and sessions).