You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
ADR-0036 Phase 2b. Phase 1a shipped API-key verification (#1624) + the generateApiKey() primitive (@objectstack/mcp/security); Phase 2 shipped the MCP HTTP transport (#1626); package renamed to @objectstack/mcp (#1627); generic Skill generator (#1628). Missing: a way to MINT a key. Without it, a user can't self-serve a sys_api_key to connect an agent — so the whole "connect your app to Claude/Cursor" flow is blocked.
This is security-critical and per the auth red-line must be its own focused PR with a full test matrix (do not fold into unrelated work).
Goal
An authenticated endpoint that generates a sys_api_key, stores only the hash, and returns the raw secret exactly once.
Design
Route:POST /api/v1/keys (dispatcher handleKeys), body { name, scopes?, expires_at? }. (List/revoke already exist via the sys_api_key data API + the revoke_api_key action — this issue is generation only.)
Generate: use generateApiKey() from @objectstack/mcp (security) → { raw, hash, prefix }.
Insert:sys_api_key is protection-locked (isSystem), so insert with an elevated{ context: { isSystem: true } } — BUT pin user_id to the caller'sexecutionContext.userId, never from the request body.
Response:{ id, name, prefix, key: <raw> } — the raw key once. Never re-displayable.
Security constraints (zero-tolerance)
user_id is taken from the resolved principal, never from the request body (no impersonation).
The raw key and its hash never enter logs, error messages, or any response other than the single creation response.
Whitelist inputs; reject unknown/elevated fields in the body (e.g. caller cannot set revoked:false tricks, key, user_id, id).
Fail-closed on anything ambiguous.
Test matrix
authed → returns { id, prefix, key }; key is the raw secret.
hashApiKey(returned.key) === the stored sys_api_key.key (hash, not plaintext).
round-trip: the returned raw key authenticates via resolveExecutionContext (x-api-key / Authorization: ApiKey) → resolves to the caller's principal.
anonymous → 401, no row created.
user_id is the caller's even if the body tries to set a different user_id (impersonation blocked).
body cannot inject key/id/revoked to forge a known-secret key.
(optional) expires_at / scopes from body are honored and round-trip through the verify path.
Acceptance criteria
New endpoint + tests green locally; raw key shown once; nothing secret logged; isolated PR.
Activity
chore: bump objectui to 2df743b02c1e