Skip to content

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

@xuyushun441-sys

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's executionContext.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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions