Skip to content

feat(providers): Cursor API provider (crsr_ keys) and Cursor CLI passthrough - #10729

Merged
diegosouzapw merged 3 commits into
diegosouzapw:release/v3.8.50from
dpozimski:feat/cursor-api-key-and-cli-proxy
Aug 20, 2026
Merged

diegosouzapw merged 3 commits into
diegosouzapw:release/v3.8.50from
dpozimski:feat/cursor-api-key-and-cli-proxy

Conversation

@dpozimski

@dpozimski dpozimski commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Two things, both about using Cursor without an IDE session:

  1. A new provider, cursor-api (dashboard card "Cursor API", alias cua). It is an API-key provider that holds a Cursor user API key (crsr_..., from cursor.com/dashboard/api) and reuses the existing Cursor agent executor, wire format and model catalog. The IDE provider (cursor, OAuth/IDE session) is untouched: separate registry entry, separate catalog card, separate executor binding (new CursorExecutor("cursor-api")), so API-key and IDE-session connections never share renewal, quota or dashboard semantics. A crsr_ key is not usable as a Bearer on api2.cursor.sh (it answers 401); cursor-agent POSTs it to /auth/exchange_user_api_key and gets a 1h session JWT whose refreshToken carries the same exp. open-sse/services/cursorApiKeyAuth.ts does that exchange, caches one session token per key, re-exchanges 5 minutes before expiry and drops the cache when Cursor answers 401. CursorExecutor resolves cursor-api connections to the exchanged token right before opening the h2 stream. The connection test for cursor-api is the exchange itself, so the credential-health sweep gets a real verdict instead of "not supported".

  2. A Cursor CLI passthrough at /api/cursor-cli/[...path]. The CLI honours CURSOR_API_ENDPOINT, and with network.useHttp1ForAgent: true in ~/.cursor/cli-config.json every call it makes is plain HTTP/1.1: unary Connect RPCs, agent.v1.AgentService/RunSSE (SSE), BidiService/BidiAppend, OTLP traces and the key exchange. The handler (open-sse/handlers/cursorCliProxy.ts) authenticates /auth/exchange_user_api_key with an OmniRoute API key and answers with an OmniRoute-minted HS256 JWT (1h, JWT_SECRET); every other path verifies that JWT (issuer, audience, expiry, key still active), picks an active cursor-api connection, swaps in the Cursor credential and streams the upstream reply back. Each hop becomes a call_logs row (provider cursor-api, request type cursor-cli, the OmniRoute key and the connection that served it). The prefix is in PUBLIC_API_ROUTE_PREFIXES with handler-enforced auth, same as /api/codex/connect/.

The minted token has to be a real JWT: the CLI reads exp from whatever it gets back and re-exchanges before almost every request when the token is opaque (measured: 37 exchanges for 40 RPCs with an opaque token, 1 exchange with a JWT).

Provider count moves 341 to 342 (generated reference, diagrams, README, llm.txt mirrors, package.json description). Docs: docs/providers/CURSOR-API-KEY-AND-CLI.md.

History note: the first revision of this PR added API-key support as dual-auth on the cursor IDE provider; it was reworked into the separate cursor-api provider after review.

Related Issues

  • None filed; this came out of running the Cursor CLI behind OmniRoute for usage attribution.

Validation

  • Change type: provider
  • Focused tests: node --import tsx/esm --test tests/unit/cursor-api-key-auth.test.ts tests/unit/cursor-cli-proxy.test.ts tests/unit/cursor-apikey-provider.test.ts tests/unit/public-api-routes.test.ts tests/unit/providers-constants-split.test.ts tests/unit/clinepass-provider.test.ts tests/unit/cursor-executor-split.test.ts tests/unit/cursor-streaming.test.ts tests/unit/cursor-registry-claude-families.test.ts tests/unit/deepai-provider.test.ts; npx bun scripts/check/check-provider-consistency.ts
  • npx eslint and npx prettier --check on the changed files; npm run check:docs-all; npm run check:changelog-integrity; npm run gen:provider-reference
  • Based on current release/v3.8.50 head (b754e44)
  • Production-code changes include new automated tests in this PR

Live run (Docker image built from this branch, docker run omniroute:base with REQUIRE_API_KEY=true):

POST /api/providers {"provider":"cursor-api","name":"cursor-api-docker","apiKey":"crsr_..."} -> provider cursor-api, authType apikey
POST /api/providers/{id}/test                                                               -> {"valid":true,...,"diagnosis":{"type":"ok"}}
POST /v1/chat/completions model=cursor-api/auto                                             -> "CURSOR API PROVIDER VIA OMNIROUTE DOCKER WORKS", tokens in 56 / out 38
CURSOR_API_ENDPOINT=http://localhost:20128/api/cursor-cli CURSOR_API_KEY=<omniroute key> \
  agent -p --trust --model auto "..."                                                       -> "CURSOR CLI VIA OMNIROUTE DOCKER WORKS"

/api/usage/call-logs afterwards: 37 cursor-cli rows for the CLI run (1 auth/exchange_user_api_key, 1 agent.v1.AgentService/RunSSE 4.9 s, BidiService/BidiAppend turns, the rest dashboard/config RPCs), all with provider cursor-api, attributed to the OmniRoute key and the connection cursor-api-docker, plus the chat row with requestedModel cursor-api/auto. /v1/models lists the catalog as cursor-api/<id> and cua/<id>. Direct probes that drove the design: GetMe with the raw key gives 401, with the exchanged JWT gives 200.

Tests Added Or Updated

  • tests/unit/cursor-api-key-auth.test.ts (new): exchange request shape, 401/5xx/malformed mapping, cache refresh window, in-flight dedupe, invalidation, bearer resolver for both connection shapes.
  • tests/unit/cursor-cli-proxy.test.ts (new): exchange mint and 401/503/400/405 paths, token verification (missing, expired, foreign audience, tampered, revoked key), raw key rejected on RPC paths, header stripping and Authorization swap, connection cooldown/fallthrough, upstream 401 cache invalidation, SSE streaming with one log row, sanitized 502.
  • tests/unit/cursor-apikey-provider.test.ts (new): cursor-api registry/catalog/executor wiring, cursor stays OAuth-only, executor credential resolution, connection test via exchange.
  • tests/unit/providers-constants-split.test.ts: APIKEY_PROVIDERS count 228 to 229.
  • tests/snapshots/executors/executor-map.json: cursor-api and cua entries.

Coverage Notes

cursorApiKeyAuth.ts, cursorCliProxy.ts and validateCursorApiProvider are covered end to end by the new unit tests through injected dependencies or a stubbed fetch (no DB or network). The executor change is three small paths (resolveExecutionCredentials, buildHeaders, the 401 invalidation) and the first two are asserted directly.

Reviewer Notes

  • /api/cursor-cli/ does not spawn anything; it only calls fetch against api2.cursor.sh, so it is not in the LOCAL_ONLY set.
  • The exchange endpoint mirrors the /v1/* policy when REQUIRE_API_KEY=false (anonymous session). Every other path still needs the minted JWT, so a stale Cursor token from a previous real login gets a 401 and the CLI re-exchanges, which is the behaviour the CLI already has.
  • cursor-api model listing comes from the static Cursor registry (providerUsesExclusiveSyncedListing stays cursor-only), so no cursor-agent is needed on the host. Usage/quota fetch is not wired for cursor-api (the IDE path uses a session cookie); can follow up if wanted.

Cursor user API keys (crsr_...) are not accepted as a Bearer by api2.cursor.sh; cursor-agent exchanges them at /auth/exchange_user_api_key for a 1h session JWT whose refreshToken carries the same exp. This adds:

- open-sse/services/cursorApiKeyAuth.ts: the exchange plus a per-key cache (refresh 5 min before exp, in-flight dedupe, invalidate on upstream 401).
- CursorExecutor resolves apiKey connections to the exchanged token before opening the h2 stream; OAuth/IDE-session connections are untouched.
- cursor joins DUAL_AUTH_PROVIDER_IDS so POST /api/providers admits an API-key connection and the card shows Manual API key. Registry stays authType oauth (resilience profile unchanged). refresh-cursor rejects non-OAuth rows.
- /api/cursor-cli/[...path]: Cursor CLI passthrough. With CURSOR_API_ENDPOINT=http://<omniroute>/api/cursor-cli and CURSOR_API_KEY=<OmniRoute key> (plus network.useHttp1ForAgent), the CLI exchanges its key for an OmniRoute-minted session JWT and every RPC (unary Connect, RunSSE, BidiAppend, traces) is forwarded to Cursor with the connection credential swapped in and a call_logs row written.
…-auth on the IDE provider

Review feedback on diegosouzapw#10729: API-key access to Cursor is now a separate provider, cursor-api (card Cursor API, alias cua), registered next to cursor in the registry, the API-key catalog and the executor map (CursorExecutor bound to its own provider id). The IDE cursor provider goes back to OAuth-only: DUAL_AUTH_PROVIDER_IDS, refresh-cursor and the dual-auth dashboard test are restored to base. The Cursor CLI passthrough resolves cursor-api connections. cursor-api gets a real connection test (the key exchange), so the credential-health sweep no longer marks fresh connections unavailable; CursorExecutor reads the synced catalog for its own provider id; the provider icon aliases to the Cursor SVG; provider count bumps to 342 across the generated reference, diagrams, README, llm.txt mirrors and package.json.
@dpozimski dpozimski changed the title feat(providers): Cursor API-key connections and Cursor CLI passthrough feat(providers): Cursor API provider (crsr_ keys) and Cursor CLI passthrough Aug 19, 2026
@dpozimski
dpozimski changed the base branch from main to release/v3.8.50 August 19, 2026 10:31
@diegosouzapw
diegosouzapw merged commit 3e0afc8 into diegosouzapw:release/v3.8.50 Aug 20, 2026
3 checks passed
diegosouzapw added a commit that referenced this pull request Aug 20, 2026
…llision

#10722 (Token Kiosk) and #10729 (Cursor) each independently bumped the
gateways/specialty-media APIKEY_PROVIDERS count by one in the same merge-train
batch; neither PR could see the other's bump, so the merged tree landed at 230
while the surviving hardcoded assertion still said 229.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
diegosouzapw added a commit that referenced this pull request Aug 20, 2026
Today's merge-train batch1 (#10722 Token Kiosk, #10729 Cursor) each added one
new APIKEY_PROVIDERS entry, bringing the live provider count to 343 — the
hardcoded '342' in README.md, AGENTS.md, llm.txt, package.json's description,
PROVIDER_REFERENCE.md, and 4 hero/comparison SVGs went stale as a result.
check:docs-counts (STRICT) now passes; regenerated PROVIDER_REFERENCE.md via
npm run gen:provider-reference. The v3.8.50 growth-log table row in README.md
(line 66) is left as-is — it's a historical point-in-time snapshot, not a
live claim.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
@dpozimski

Copy link
Copy Markdown
Contributor Author

@diegosouzapw confirmed its working on my teammates' hosts

muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…through (diegosouzapw#10729)

Merged via merge-train (release/v3.8.50, batch1 2026-08-20) — static gates (typecheck/file-size/complexity/cognitive/changelog) green on the combined tree; test:unit reds observed in the boarded run were verified pre-existing on the pure release tip (unrelated flake), not caused by this PR. Thanks for the contribution!
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…llision

diegosouzapw#10722 (Token Kiosk) and diegosouzapw#10729 (Cursor) each independently bumped the
gateways/specialty-media APIKEY_PROVIDERS count by one in the same merge-train
batch; neither PR could see the other's bump, so the merged tree landed at 230
while the surviving hardcoded assertion still said 229.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
Today's merge-train batch1 (diegosouzapw#10722 Token Kiosk, diegosouzapw#10729 Cursor) each added one
new APIKEY_PROVIDERS entry, bringing the live provider count to 343 — the
hardcoded '342' in README.md, AGENTS.md, llm.txt, package.json's description,
PROVIDER_REFERENCE.md, and 4 hero/comparison SVGs went stale as a result.
check:docs-counts (STRICT) now passes; regenerated PROVIDER_REFERENCE.md via
npm run gen:provider-reference. The v3.8.50 growth-log table row in README.md
(line 66) is left as-is — it's a historical point-in-time snapshot, not a
live claim.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
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.

2 participants