feat(runtime): API-key generation endpoint — show-once sys_api_key (closes #1629)#1630
Merged
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1629.
What
POST /api/v1/keys— the only path that mints asys_api_key, returning the raw secret exactly once. Phase 1a shipped key verification (#1624) + thegenerateApiKey()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
{ id, name, prefix, key };keyis the raw secret, shown once. Only the sha256 hash is persisted.name(+ optionalexpires_at).Security (zero-tolerance — red-line)
user_idpinned to the caller, never read from the body → no impersonation.key/id/user_id/revoked/prefixare ignored → a caller cannot forge a known-secret key or pre-set state.{ isSystem: true }insert (sys_api_key is protection-locked) with server-controlled row contents.expires_at→ 400; generation happens only after validation.scopesdeliberately 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 viauser_idresolution. Scoped/narrowing keys need subset-enforcement → deferred.Tests (local, all green)
http-dispatcher.keys.test.ts— 11 cases: show-once 201; only the hash is stored (hashApiKey(raw) === row.key, raw ≠ stored); round-trip (minted raw authenticates throughresolveExecutionContext); anonymous → 401 (no row); impersonation blocked (bodyuser_idignored); forgery blocked (bodykey/id/revokedignored); 405; default name; valid/past/unparseableexpires_at(201/400/400); expired key rejected end-to-end by the verify path. Full runtime suite 376 green;tscclean.Follow-up
objectstack-ai/objectui#1550 (connect surface) consumes this endpoint.
🤖 Generated with Claude Code