Wire-level E2E coverage for scoped project API key enforcement — 17 tests in tests/api/v3/scoped_api_keys.spec.ts + a scoped-api-keys CI matrix entry.
Stacked on #10638 — merge that first; this PR auto-retargets to master and CI runs the suite then (playwright workflow only triggers on master-base PRs). Until then: verified 17/17 against a local CI-equivalent stack on the merged base.
Approach: no API surface can mint a SCOPED key, so keys are seeded as fixture rows in packages/db/prisma/fixtures/client.ts (same pattern as testAccountAPIKey). The spec does no DB access; no new CI secrets.
Coverage
| # | Test | Key | Asserts |
|---|
| 1–2 | list toolkits / get by slug | toolkits:read | 200, param routes match |
| 3 | write-classified route | toolkits:read | 401 on POST /toolkits/multi |
| 4 | ungranted preset | toolkits:read | 401 on GET /tools |
| 5 | granted preset | tools:read | 200 on GET /tools |
| 6 | read_write level | toolkits:read_write | 200 read + 200 write |
| 7 | fail closed, uncataloged | toolkits:read | 401 on /internal/action_execution/fields |
| 9 | tool-router session | sessions:rw vs toolkits:read | 201 vs 401 |
| 10 | MCP server + transport | sessions:rw vs toolkits:read | create 201, initialize 200 vs 401 |
| 11 | multi-preset key | toolkits+tools:read | 200 / 200 / 401 on third preset |
| 12 | tool execution | tool_execution:write vs tools:read | not-401 vs 401 |
| 13 | connected accounts list | full_access vs toolkits:read | not-401 vs 401 |
| 14 | v3.1 proxy execute | full_access vs tools:read | not-401 vs 401 |
| 15 | Bearer header channel | toolkits:read | enforced same as x-api-key |
| 16 | expired key | expired | 401 "API key has expired" |
| 17 | legacy preset | proxy_execute (old format) | keeps DEVELOPER superset |
| 18 | developer key regression guard | testAccountAPIKey | 200 everywhere |
Tests 12–14 assert not-401 on the allow half: isolates the authz gate from credential state (CI creds → 200; the deny half is the security property).
Review changes
| Feedback | Action |
|---|
| direct DB writes in test | replaced with seed fixtures |
| drop all code comments | dropped (only eslint directives remain) |
| DRY the repeated blocks | call() helper, bodies now 1–3 lines |
| redundant mirror assertion | dropped |
/toolkits/multi "writes" nothing | correct — batch read classified write; test renamed, classification flagged on #10638 |
| test most critical flows | added tool execution, connected accounts, proxy execute (12–14) |
| full-access fallback reverted on base | test 8 flipped to pin fail-closed |
| CodeQL password-hash alert | dismissed "used in tests"; code removed by fixtures rework anyway |
🤖 Generated with Claude Code