Skip to content

feat(mcp): sys_api_key generation endpoint — show-once key (ADR-0036 Phase 2b, security-critical) #1629

Description

@xuyushun441-sys

Context

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.)
  • Auth: require context.executionContext.userId. Anonymous → 401.
  • 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.
  • ADR-0036 Status updated (key-gen shipped).

Blocks: objectstack-ai/objectui connect-surface issue (Phase 2b UI).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions