Repository navigation
harden(keys): minting an API key requires an interactive session - #27536
Conversation
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcVkGJizGH7raJHhAvL
… session credential on signed-in fixtures Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcVkGJizGH7raJHhAvL
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017xRBcVkGJizGH7raJHhAvL
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 84cf8d055758861ff78fdeaf51d86dfc6479b625 && git checkout 84cf8d055758861ff78fdeaf51d86dfc6479b625
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 57c51aab4d2f63293676e9412daed8a5fc54c1bc 11fff72f9058bbb34358a7bb958786df07cd7e30 && git checkout -B drift-repro 57c51aab4d2f63293676e9412daed8a5fc54c1bc && git merge --no-ff 11fff72f9058bbb34358a7bb958786df07cd7e30
node scripts/docs-audit/affected-docs.mjs --json 57c51aab4d2f63293676e9412daed8a5fc54c1bc |
|
domain:devx#1 · sunhaolin · session_017xRBcVkGJizGH7raJHhAvL 席位核对ACCEPT at head
Landing: Generated by Claude Code |
Dispatched by the W3-C3b QA run security path (detail withheld)
Clause-②: no
Metadata-protocol: none
维护者速读
POST /api/v1/keys)现在只接受登录会话发起的请求;用 API key 认证的请求一律 403PERMISSION_DENIED,不写任何数据。packages/runtime/src/domains/keys.ts(新增的会话判定,紧跟匿名判定之后、读取请求体之前);packages/runtime/src/domains/keys-mint-interactive-session.pin.test.ts;packages/runtime/src/api-key-lifecycle.pin.test.ts中改写的 control 用例。pnpm --filter @objectstack/runtime exec vitest run --project local src/domains/keys-mint-interactive-session.pin.test.ts,预期 14 个用例全部通过。What changes
POST /api/v1/keysmints only for a principal that a signed-in session admitted.packages/runtime/src/domains/keys.ts: right after the anonymous decision (ADR-0138 D2) and before the body is read, the domain asksHttpProtocolContext.admission.credential, which the dispatcher's identity step writes from the shared resolver (resolveAuthzContext'scredential, viaonAdmission). Anything other than'session'is refused403. That covers an API-key admission and also an absent admission, because a credential the door was never told about is not a session. The check is positive on purpose, so a credential kind added later is refused until it is admitted explicitly.PERMISSION_DENIED. This route already answers it when the caller is not a member of their active organization. No new code, no ledger change.@objectstack/runtimepatch. It names who is affected and tells embedders that callHttpDispatcher.handleKeysdirectly to record the admitting credential.Tests
packages/runtime/src/domains/keys-mint-interactive-session.pin.test.ts(14 cases).dispatch(): session mint is 201 with one row pinned to the session user. An API-key admission in each accepted header spelling (x-api-key,Authorization: ApiKey,Authorization: Bearer osk_…) is refused 403PERMISSION_DENIEDwith nothing written. A cookie beside the key does not lift the refusal. Every verb gets the 403. Controls: a session wrong verb gets 405, anonymous gets 401UNAUTHENTICATED.handleKeys: no admission, or anapi_keyadmission, is refused with the body unread and no service resolved. Asessionadmission mints.admission: { credential: 'session', authGate: undefined }. No assertion moved.the same key, live, is servedinapi-key-lifecycle.pin.test.tsused a live key to mint. It now pins what it exists to prove. The live key passes the identity step:dispatch()resolves, the admission isapi_keyand the principal is the key's owner. The door then answers with its own rule (403PERMISSION_DENIED), never a lifecycle code.srcimports, so no dist rebuild was involved).node scripts/ablation-replace.mjsreplaced the session condition withif (false): anchor 1 to 0, blob2d6f7ad939f2to96dff2464c80. Result: 11 of the 28 cases across the new pin file and the lifecycle pin failed (every refusal case and the rewritten control), and every control stayed green. Restore: blob equal to HEAD andgit diff HEADempty.Runtime suite readings
Test Files 7 failed | 464 passed (471),Tests 32 failed | 7636 passed | 19 skipped. All 32 failures were hand-built signed-in fixtures without an admission, plus the live-key mint control.Test Files 11 passed (11),Tests 502 passed (502), lock VERDICT command-exit 0.pnpm --filter @objectstack/runtime typecheck: exit 0.check:test-typecheckOK: 27 files, 190 errors, 68 pinned signatures, unchanged ledger.11fff72f,pnpm --filter @objectstack/runtime exec vitest run --project localgaveTest Files 472 passed (472)andTests 7682 passed | 19 skipped (7701), lock VERDICT command-exit 0.Acceptance notes
packages/qa/dogfood/test/authz-conformance.matrix.ts: theanonymous-deny-keysrow's note says "a signed-in caller reaches the mint exactly as before", which is still true for a session. This PR adds no row for the session requirement (outside this dispatch's file surface). Carrier: the dispatching seat.content/docs/api/index.mdx,content/docs/ai/connect-mcp.mdx,content/docs/api/client-sdk.mdx) already show session-based minting. None of them says a key cannot mint. A one-line mention there is a follow-up, outside this dispatch's file surface. Carrier: the dispatching seat.ui/change: the Console's mint calls (AgentConnectSection.tsx, thecreate_api_keyaction) send the session cookie.Verification
Local gate families (dispatch-gates, final head)
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 60 commands for the 10 changed paths. All 60 were run. 58 exited 0.node scripts/check-plugin-teardown-shape.mjs --self-testneeds an unshallowed clone. It is checker health only.pnpm check:dual-build-cjs-loadsneedsdist/for seven packages outside this diff's build closure.--ranreconciliation:60 derived famil(ies) accounted for — 58 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3).Generated by Claude Code