feat(runtime): harden hand-rolled API-key auth (ADR-0036 Phase 1a)#1624
Merged
Conversation
better-auth 1.6.x ships no apiKey plugin, so the runtime owns the full sys_api_key auth lifecycle. The existing resolveExecutionContext API-key path was insecure/buggy: it looked up by the RAW key (implying plaintext storage) and filtered on a non-existent `active` field, with no expiry check. - Add packages/runtime/src/security/api-key.ts — the single audited source of truth for key crypto: hashApiKey (sha256 hex at-rest), generateApiKey (256-bit base64url secret, raw returned once, never persisted), extractApiKey (X-API-Key / Authorization: ApiKey; Bearer excluded), parseScopes (JSON-string textarea or array), isExpired (fail-open only for unparseable values). - resolveExecutionContext: hash the inbound key and look it up by the indexed at-rest hash only; reject revoked (where + defensive guard) and expired keys; merge JSON-string scopes into ctx.permissions. Resolved principals flow through the same role/permission/RLS path as sessions. - Export the primitives from the security barrel. - Tests: api-key.test.ts (crypto/parse/extract, 24 cases) and resolve-execution-context.test.ts (verify path: valid/revoked/expired/ unknown/plaintext-not-matched/scopes/org/anonymous/Bearer-ignored). Security: raw keys and hashes never enter logs, responses or errors; fail-closed on anything ambiguous; whitelist matching, not blacklist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| * cannot recover the raw key by probing for partial matches. | ||
| */ | ||
| export function hashApiKey(raw: string): string { | ||
| return createHash('sha256').update(raw, 'utf8').digest('hex'); |
| if (x && x.trim()) return x.trim(); | ||
| const auth = readHeader(headers, 'authorization'); | ||
| if (!auth) return undefined; | ||
| const m = auth.match(/^ApiKey\s+(.+)$/i); |
This was referenced Jun 6, 2026
xuyushun441-sys
added a commit
that referenced
this pull request
Jun 6, 2026
…mcp + ADR-0036 amendment (#1627) Drops the legacy `plugin-` prefix and moves the outbound MCP-server package to the top level (`packages/mcp`), parallel to `@objectstack/rest` — both are "your app exposed over a protocol". Inbound MCP stays `@objectstack/connector-mcp`. - packages/plugins/plugin-mcp-server → packages/mcp; name → @objectstack/mcp; internal plugin id → com.objectstack.mcp; build/tsconfig relative paths fixed for the new depth. Exported API unchanged (MCPServerPlugin, MCPServerRuntime, registerObjectTools, McpDataBridge, …). - @objectstack/cli: dependency + dynamic-loader pkg id updated. - Inbound refs (runtime test, spec comment, changeset config, docs) updated. Pre-launch clean break — no compat shim (only cli depended on it internally). - ADR-0036 amendment (2026-06-07): records (A) the rename, (B) granularity = per-environment not per-app (one MCP server per env covers all apps; dynamic apps via live discovery; key-scope for narrowing), (C) distribution = skills + MCP (one generic portable Skill + live MCP, not hand-maintained vendor config snippets) — verified Agent Skills is now an open cross-platform standard. - Status updated: Phase 1a (#1624) + Phase 2 (#1626) shipped; Phase 2b next. Build + typecheck + tests green (mcp 36, runtime mcp 5); lockfile regenerated. 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.
What
Phase 1a of ADR-0036 ("every app is a REST API + MCP server"): harden the hand-rolled API-key auth in the runtime. This is the security foundation that Phase 1b (objectui surfacing) and Phase 2 (MCP transport) build on.
better-auth 1.6.x ships no apiKey plugin, so the runtime owns the full
sys_api_keylifecycle. The pre-existingresolveExecutionContextAPI-key path was insecure/buggy:activefield (real field isrevoked)Changes
packages/runtime/src/security/api-key.ts— single audited source of truth for key crypto:hashApiKey(raw)— sha256 hex at-rest hash (lookup matches the indexed high-entropy hash exactly → constant-effort comparison).generateApiKey(prefix?)— 256-bit base64url secret; returns{ raw, hash, prefix }; raw returned once, never persisted.extractApiKey—X-API-KeyorAuthorization: ApiKey <token>; Bearer deliberately excluded (sessions own Bearer).parseScopes(JSON-string textarea or array) /isExpired(fail-open only for unparseable values).resolveExecutionContext— hash the inbound key, look it up by the at-rest hash only, reject revoked (where-clause + defensive guard) and expired keys, merge scopes intoctx.permissions. Resolved principals flow through the same role/permission/RLS path as sessions.Tests (local, all green)
api-key.test.ts— crypto/parse/extract (determinism, uniqueness, base64url, Bearer-not-matched, scopes, expiry epoch/ISO/Date).resolve-execution-context.test.ts— verify path: valid (x-api-key + ApiKey) / revoked / expired / unknown / plaintext-stored not matched / scopes / org→tenant / anonymous / Bearer-ignored.tsc --noEmit: 0 errors.Security
Raw keys and hashes never enter logs, responses or error messages. Fail-closed on anything ambiguous. Whitelist matching, not blacklist.
Follow-ups (out of scope, per ADR)
generateApiKey(objectui fix(spec): reject unknown top-level keys on ObjectSchema.create() (#1535) #1540 lays the UI groundwork).🤖 Generated with Claude Code