Skip to content

feat(runtime): API-key generation endpoint — show-once sys_api_key (closes #1629)#1630

Merged
xuyushun441-sys merged 1 commit into
mainfrom
feat/api-key-generation
Jun 6, 2026
Merged

feat(runtime): API-key generation endpoint — show-once sys_api_key (closes #1629)#1630
xuyushun441-sys merged 1 commit into
mainfrom
feat/api-key-generation

Conversation

@xuyushun441-sys
Copy link
Copy Markdown
Contributor

Closes #1629.

What

POST /api/v1/keys — the only path that mints a sys_api_key, returning the raw secret exactly once. Phase 1a shipped key verification (#1624) + the generateApiKey() primitive; this is the missing generation half that unblocks the self-serve connect flow (ADR-0036 Phase 2b), and the backend for objectui's show-once key UX (objectstack-ai/objectui#1550).

Behavior

  • Authenticated principal required → returns { id, name, prefix, key }; key is the raw secret, shown once. Only the sha256 hash is persisted.
  • Body whitelisted to name (+ optional expires_at).

Security (zero-tolerance — red-line)

  • user_id pinned to the caller, never read from the body → no impersonation.
  • Body key / id / user_id / revoked / prefix are ignored → a caller cannot forge a known-secret key or pre-set state.
  • Raw key + hash never logged, never in error messages, never re-displayable.
  • Elevated { isSystem: true } insert (sys_api_key is protection-locked) with server-controlled row contents.
  • Fail-closed: anonymous → 401; non-POST → 405; past/unparseable expires_at → 400; generation happens only after validation.
  • scopes deliberately NOT accepted from the body in v1 — the verify path adds scopes to the principal's permissions, so honoring arbitrary body scopes would be an escalation vector. A generated key acts exactly as the caller via user_id resolution. Scoped/narrowing keys need subset-enforcement → deferred.

Tests (local, all green)

http-dispatcher.keys.test.ts11 cases: show-once 201; only the hash is stored (hashApiKey(raw) === row.key, raw ≠ stored); round-trip (minted raw authenticates through resolveExecutionContext); anonymous → 401 (no row); impersonation blocked (body user_id ignored); forgery blocked (body key/id/revoked ignored); 405; default name; valid/past/unparseable expires_at (201/400/400); expired key rejected end-to-end by the verify path. Full runtime suite 376 green; tsc clean.

Follow-up

objectstack-ai/objectui#1550 (connect surface) consumes this endpoint.

🤖 Generated with Claude Code

…loses #1629)

Adds POST /api/v1/keys — the only mint path for sys_api_key. Phase 1a shipped
verification + the generateApiKey() primitive; this is the missing generation
half that unblocks the self-serve connect flow (ADR-0036 Phase 2b).

- Authenticated principal required; returns the raw secret EXACTLY ONCE
  ({ id, name, prefix, key }). Only the sha256 hash is persisted — raw key never
  stored, logged, or re-displayable.
- Security (zero-tolerance): user_id pinned to the caller, never from the body
  (no impersonation); body whitelisted to name (+ optional validated future
  expires_at) — body key/id/user_id/revoked ignored (no forgery/escalation);
  elevated { isSystem:true } insert with server-controlled contents (sys_api_key
  is protection-locked). Anonymous→401, non-POST→405, bad expires_at→400.
- scopes NOT accepted from body in v1 (verify path adds scopes to permissions →
  escalation risk); a key acts AS the caller via user_id resolution. Scoped keys
  need subset-enforcement — deferred.

Tests: 11 security cases (show-once, hash-not-raw, round-trip auth via verify
path, impersonation blocked, forgery blocked, 401/405/400, expiry e2e). Full
runtime suite green (376). ADR-0036 Status updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Building Building Preview, Comment Jun 6, 2026 10:23pm

Request Review

@xuyushun441-sys xuyushun441-sys merged commit f68be58 into main Jun 6, 2026
7 of 8 checks passed
@xuyushun441-sys xuyushun441-sys deleted the feat/api-key-generation branch June 6, 2026 22:23
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling and removed size/m labels Jun 6, 2026
xuyushun441-sys added a commit that referenced this pull request Jun 6, 2026
…R-0036) (#1631)

The dispatcher mounts routes explicitly (no catch-all). #1626 (MCP transport)
and #1630 (key-gen) added dispatch() branches but never registered the HTTP
routes, so /api/v1/mcp and /api/v1/keys 404'd at the HTTP layer before reaching
the dispatcher. Unit tests called handlers directly, hiding it; caught in live
staging e2e.

- Register /mcp (GET/POST/DELETE) + /keys (POST) via dispatch() in the
  dispatcher plugin (transport reads the method from the request).
- dispatcher-plugin.routes.test.ts asserts the registrations (the missing
  regression). Full runtime suite 379 green.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants