Skip to content

feat(agent-api): API keys + read endpoints so agents can use boop over HTTP - #212

Merged
brianorwhatever merged 10 commits into
mainfrom
feat/agent-api-keys
Jul 27, 2026
Merged

feat(agent-api): API keys + read endpoints so agents can use boop over HTTP#212
brianorwhatever merged 10 commits into
mainfrom
feat/agent-api-keys

Conversation

@brianorwhatever

@brianorwhatever brianorwhatever commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What & why

boop repositioned agent-first (business plan + agent landing page), but agents couldn't actually run against the stack: the only credential was a short-lived email-OTP JWT (no durable auth for unattended agents), and every /api/* route was a POST mutation (no way to read the queue). The full Mission Control layer that once covered this was deliberately removed in 90b6d7b.

This PR adds the minimal agent-access slice — API keys + read endpoints — so agents can use the current stack without resurrecting ~5,900 lines of Mission Control. It also makes API.md honest.

What's included

  • API keys (convex/apiKeys.ts, convex/apiKeysHttp.ts, apiKeys table): JWT-only POST/GET/DELETE /api/v1/keys. Long-lived pa_live_… keys; only the SHA-256 hash is stored, the raw key is returned exactly once on creation. Revoke enforces ownership.
  • Shared resolver (convex/lib/actor.ts): resolveActor() accepts either an X-API-Key header or the existing JWT session and resolves both to a current DID + scope set. The JWT path is unchanged (sessions get scopes: ["*"]).
  • Writes accept keys (convex/itemsHttp.ts, convex/listsHttp.ts): every write handler now goes through resolveActor + requireScope("items:write"). Mutation authorization logic is untouched.
  • Read endpoints (convex/agentReadHttp.ts): GET /api/v1/lists, GET /api/v1/lists/items?listId= — wrap existing queries, require read scopes.
  • Pure helpers + tests (src/lib/apiKeys.ts, scripts/api-keys.test.mjs): key gen/hash/scope logic, tested in the repo's scripts/*.test.mjs assert style.
  • Docs (API.md): documents the real X-API-Key mode + v1 endpoints; removes the documented-but-nonexistent Mission Control section.
  • Plan (plans/): the advisor plan this was built from, for context.

⚠️ CI / merge note

tsc -b and bun run build require convex codegen to regenerate _generated/api.d.ts with the new apiKeys module first. Without it there are exactly 4 Property 'apiKeys' does not exist errors (stale generated types) — no other type errors. Run npx convex dev --once (or ensure the build pipeline runs codegen with Convex credentials) before/at merge. Verified locally: eslint clean, unit tests pass, all api.* references resolve to real exports.

Not in this PR (deliberate follow-ups)

  • Agent attribution (step 5): the apiKeys.agentDid column + actor.actorDid plumbing exist, but item mutations don't yet stamp a distinct agent DID — so agent actions currently record the owner's DID. Needed to make the "prove human-vs-AI" claim real.
  • VC signing: item VCs remain unsigned placeholders (convex/items.ts), a separate DID-signing follow-up.
  • CORS: new GET/DELETE/X-API-Key routes aren't in the preflight allowlist — fine for server-side agents, needs updating for browser calls.
  • Pre-existing API.md drift: an older section still documents /api/agent/* routes that don't exist.

Test plan

  • bun scripts/api-keys.test.mjs → assertions pass
  • bun test full suite → pass; eslint clean on changed files
  • npx convex dev --once && npx tsc -b && bun run build → green (needs Convex creds)
  • Live smoke: create key → read lists with key → add item with key → revoke → 401

🤖 Generated with Claude Code

Note

Add API key management and read endpoints so agents can authenticate over HTTP

  • Adds agentApiKeys table and CRUD endpoints (POST/GET/DELETE /api/v1/keys) for minting, listing, and revoking API keys; raw key is returned once and never stored — only a SHA-256 hash and prefix are persisted.
  • Adds GET /api/v1/lists and GET /api/v1/lists/items?listId=... read endpoints gated by lists:read and items:read scopes.
  • Introduces resolveActor in lib/actor.ts to unify auth: handlers now accept either a JWT session or an X-API-Key header, with requireScope enforcing per-endpoint scope requirements.
  • Updates all existing write endpoints (/api/items/*, /api/lists/*) to also accept API keys with items:write scope, replacing the previous JWT-only flow.
  • Risk: CORS headers are broadened to allow GET, DELETE, and X-API-Key across all routes, which may affect security posture for browser clients.
📊 Macroscope summarized 2a6c93b. 9 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

brianorwhatever and others added 6 commits July 8, 2026 23:13
Key format/prefix/SHA-256-hash/scope helpers plus an assert-based test
following the scripts/*.test.mjs esbuild pattern. Only hashes are stored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the apiKeys schema table (by_hash/by_owner), Convex CRUD functions,
a Convex-side copy of the pure helpers, and JWT-only HTTP handlers to
create/list/revoke keys. Raw key is returned once; only the hash persists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds resolveActor/requireScope (X-API-Key or JWT) and swaps the inline
requireAuth+getUserByTurnkeyId block in item/list write handlers for it.
Writes require items:write; JWT sessions resolve to scopes ["*"]. Drops
legacyDid threading from the new path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GET /api/v1/lists and GET /api/v1/lists/items (query-param listId, since
the Convex router has no :param segments), plus route wiring for the
/api/v1/keys and read endpoints with OPTIONS pairs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the X-API-Key auth mode and the real /api/v1/keys + read endpoints;
removes the unbuilt Mission Control REST v1 section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread convex/agentReadHttp.ts Outdated
Comment thread convex/http.ts
Comment thread convex/itemsHttp.ts
Comment thread convex/apiKeysHttp.ts
Comment thread convex/apiKeys.ts Outdated
Comment thread convex/listsHttp.ts Outdated
Comment thread convex/agentReadHttp.ts
brianorwhatever and others added 2 commits July 9, 2026 01:08
…egacyDid, CORS

- Read endpoints: gate GET /api/v1/lists/items behind a new authorized query
  getListWithItemsForViewer (canUserViewList: owner/legacy or active publication),
  404 on denied — fixes IDOR letting any items:read key read arbitrary lists.
- API keys: sanitizeScopes() strips "*"/unknown scopes at creation so a caller
  cannot mint a wildcard key.
- Migrated users: resolveActor carries legacyDid on the JWT path only and write
  handlers forward it again; API keys stay legacy-free.
- CORS: allow GET/DELETE methods and the X-API-Key header for browser callers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ot agent

Second Macroscope review round:
- Make convex/apiKeys.ts fns internalQuery/internalMutation — they were public,
  so a client could call createKey directly with any ownerDid (mint a key for any
  user) or probe getByHash. Reached only via internal.* from the JWT-authed layer.
  Same for the server-only getListWithItemsForViewer read query.
- Use actor.did (owner) for ownership/authorization in createList + all item/list
  writes; drop actorDid from ResolvedActor so an agent key can't own/authorize as
  the agent identity. Distinct attribution is deferred to Step 5 (needs mutation
  changes to split authz from attribution).
- CORS: fix convex/http.ts's OWN getCorsHeaders (used by the OPTIONS/corsHandler
  path) — the earlier fix only touched lib/httpResponses.ts.
- Tidy a pre-existing unused-args lint error in the health-check handler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread convex/apiKeysHttp.ts
brianorwhatever and others added 2 commits July 9, 2026 01:27
- revokeKey returns boolean; revokeApiKey answers 404 for a missing/non-owned
  key instead of surfacing it as a 500 (Macroscope medium).
- corsHandler (the OPTIONS preflight handler the v1 routes actually use) hardcoded
  its own POST,OPTIONS headers separate from getCorsHeaders — the real reason
  preflight was still broken. Reuse getCorsHeaders so methods/headers live in one
  place (GET/POST/DELETE + X-API-Key).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eaking stack traces

Two issues that would have broken landing this branch:

- `convex deploy` failed schema validation: deployments still hold orphaned
  rows from the Mission Control `apiKeys` table removed in 90b6d7b (old shape
  has `keyPrefix`, no `prefix`). The new table is now `agentApiKeys`; the dead
  rows stay unvalidated and can be dropped separately.
- `convex/_generated/api.d.ts` was stale, so `npx tsc -b` and `bun run build`
  failed on `internal.apiKeys.*`. Regenerated and committed.

Also: every *Http.ts catch echoed `error.message`, so a mutation authorization
throw returned 500 with internal file paths. `handlerErrorResponse()` maps those
to a clean 403 and everything else to a generic 500 (details still logged).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@brianorwhatever
brianorwhatever merged commit 3f7b003 into main Jul 27, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant