Repository navigation
feat(sdk): accept an Authorization header in place of an API key - #2102
Conversation
🦋 Changeset detectedLatest commit: 2f84162 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
PR SummaryMedium Risk Overview JS: constructor treats a case-insensitive Python: auth routing prefers A minor changeset documents the behavior; new tests cover both header casings, header passthrough, and API-key precedence. Reviewed by Cursor Bugbot for commit b4e1200. Bugbot is set up for automated code reviews on this repo. Configure here. |
Package ArtifactsBuilt from abaedb0. Download artifacts from this workflow run. JS SDK ( npm install ./e2b-dockerfile-utils-0.1.2-mishushakov-sdk-authorization-header.0.tgz ./e2b-2.63.1-mishushakov-sdk-authorization-header.0.tgzCLI ( npm install ./e2b-cli-2.23.1-mishushakov-sdk-authorization-header.0.tgzCode Interpreter JS SDK ( npm install ./e2b-code-interpreter-2.9.2-mishushakov-sdk-authorization-header.0.tgzDesktop JS SDK ( npm install ./e2b-desktop-2.5.1-mishushakov-sdk-authorization-header.0.tgzPython SDK ( pip install ./e2b_dockerfile_utils-0.1.1+mishushakov.sdk.authorization.header-py3-none-any.whl ./e2b-2.63.0+mishushakov.sdk.authorization.header-py3-none-any.whlCode Interpreter Python SDK ( pip install ./e2b_code_interpreter-2.11.9+mishushakov.sdk.authorization.header-py3-none-any.whlDesktop Python SDK ( pip install ./e2b_desktop-2.7.2+mishushakov.sdk.authorization.header-py3-none-any.whl |
1aac476 to
e8f520a
Compare
638a8e9 to
5d2155e
Compare
b4e1200 to
fe43924
Compare
ApiClient threw "API key is required" unless an API key was set, so an OAuth access token passed through apiHeaders / api_headers couldn't be used on its own. Both SDKs now accept an Authorization header instead. The API authenticates such requests with `Authorization: Bearer <token>` plus `X-Team-ID: <team>` (verified on every endpoint the CLI uses). In Python the generated AuthenticatedClient always writes its token to `auth_header_name`, so it now points at the caller's own Authorization header instead of adding X-API-KEY. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ation header Matches the JS SDK: an exported-but-empty key no longer sends X-API-KEY next to the bearer token. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fe43924 to
2f84162
Compare
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
🔍 Devin Review: 1 flag
Not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. It changes how both SDKs decide whether a request is authenticated, so a human should still look at it.
What was reviewed:
- JS
ApiClient: the API key requirement is now skipped whenconfig.headershas anauthorizationheader in any casing.X-API-KEYis still only sent when a key is set. - Python
ApiClient: an emptyapi_keynow counts as unset. Without a key, the generated client'sauth_header_name/tokenis set to the caller's own Authorization header. The generated client writes that same header back with the same name and value, so it isn't sent twice. - The no-key, no-header case still raises the same error in both SDKs.
AsyncApiClientinherits the change through the sharedApiClient. Not run here: tests, lint, typecheck.
Extended reasoning...
The change touches the API client authentication path in packages/js-sdk/src/api/index.ts and packages/python-sdk/e2b/api/init.py, adds tests in both SDKs, and adds a changeset with minor bumps. It is security-sensitive: it decides when a request without an API key may go out, and which header carries the credential. Four inline threads from another automated reviewer were resolved only by the PR author. Commits pushed after those threads may address them, but that can't be confirmed from metadata. Because this is an auth path and those threads have no independent resolution, I'm deferring instead of approving.
… key (#2103) ## Summary The CLI now authenticates every request with the login's OAuth access token instead of the project API key it used to store in `~/.e2b/config.json`. It refreshes the token automatically, including during long-running commands and across concurrent `e2b` processes. `E2B_API_KEY` still works and wins, for CI/CD. Needs #2102 (merged 2026-11-10); docs in e2b-dev/docs#636. Bottom of stack #2115: #2103 → #2104 → #2105 → #2124 → #2125; each PR merges into the one below. ## Changes **1. Login URL.** `e2b auth login` opens `https://e2b.dev/auth/cli` (`https://$E2B_DOMAIN/auth/cli` when set). `DOCS_BASE` and the `E2B_DOCS_BASE` override are removed. Both the old `e2b.dev/docs/api/cli` and the new URL already 308 to `console.e2b.dev/auth/cli`, so the browser flow itself is unchanged. **2. Automatic refresh.** `ensureAuth()` replaces `ensureAPIKey()` everywhere: ```ts const auth = await ensureAuth() // { apiKey } from E2B_API_KEY, else: // { apiHeaders: { Authorization: 'Bearer <access token>', 'X-Team-ID': <project> } } await Sandbox.list({ ...auth }) ``` - Tokens are refreshed when they expire within a minute; they live one hour. - **Refresh tokens rotate**, and the old one is rejected once used. Before, two commands refreshing at the same time could get `invalid_grant`, which deletes the config and signs the user out. Refreshes are now serialized with a lock on the config (`~/.e2b/config.json.lock`, managed by [proper-lockfile](https://github.com/moxystudio/node-proper-lockfile): kept fresh while held, released on process exit, taken over once stale after 30 s), through a generic `withFileLock(path, fn, { staleMs, timeoutMs })` in `src/utils/lock.ts` that #2104 also uses for project changes. The config is re-read under the lock, so a process that waited reuses the tokens the other one just wrote. A lock lost mid-refresh (the holder stalled past the stale window) prints a warning instead of crashing the command. - Credentials are resolved per request, not per command. `sandbox logs --follow`, `sandbox metrics --follow` and the attached `sandbox create` keepalive (now the static `Sandbox.setTimeout`, not the instance method that kept the original token) keep working past an hour. - `e2b auth configure` re-reads the config before saving the selected project. Before, it wrote back the tokens it had read before its prompt, which undid any refresh another process made while the prompt was open. - Config writes are atomic (temp file + rename), since refreshes now write in the background of any command. **3. The stored API key plays no role.** - Login and configure don't write `projectApiKey`, and nothing reads it. The schema and the v1 → v2 migration don't know the field: a key an earlier version stored is carried through writes untouched, like any field this version doesn't know about, but the CLI makes no promise about it. - The project for each request comes from `--project`, then `E2B_PROJECT_ID` / `E2B_TEAM_ID`, then the configured project. So `template list --project` now selects the project for logged-in users too. ```console $ e2b auth login # opens https://e2b.dev/auth/cli $ e2b sandbox list # Bearer <access token> + X-Team-ID: <selected project> $ E2B_PROJECT_ID=<other> e2b template list # one-off project override $ E2B_API_KEY=e2b_... e2b template create my-tpl # CI: API key, no login needed ``` ## Notes for review - **Downgrades.** Released CLIs require `projectApiKey` in a v2 config. After a fresh login with this version there is none, so an older CLI reports the deprecated-config message and asks for `e2b auth login`. The changeset calls this out. - **SDK-400 isn't fixed by this alone.** Login and configure still call `GET /teams` here, which mints a new API key for every project server-side; the CLI just stops storing it. #2104, next in the stack, replaces that call with workspace-api `/v1/me` (BKND-155's CLI side; with access tokens its planned `POST /api-keys` step is not needed). - **Known limit.** `Template.build` is a single SDK call that polls build status with the credentials it started with. A build running over an hour would need a fresh token mid-call. Handling that needs a header provider in the SDK; I left it out here. ## Testing - New `tests/auth.test.ts` (10 tests): - `E2B_API_KEY` precedence; - bearer headers and project selection (`--project` > `E2B_PROJECT_ID` > config); - the signed-out exit; - refresh storing the rotated tokens and keeping fields it doesn't know; - **5 concurrent callers → exactly one refresh**; - `invalid_grant` → signed out; - stale-lock recovery; - `tests/utils/lock.test.ts`: one holder at a time, release when the holder throws, stale-lock takeover, timeout while a live holder keeps the lock; - the `/auth/cli` URL with and without `E2B_DOMAIN`. With the lock stubbed out, the concurrency and stale-lock tests fail. - `user_config_permissions`: unknown fields survive the write, and no temp file is left behind. Migration tests are updated. The command tests mock `ensureAuth`. - `pnpm run test` in `packages/cli`: 186 passed, 11 skipped (the `template create` backend suite needs `E2B_API_KEY`). - **Against prod with my login and no `E2B_API_KEY`:** `auth info`, `sandbox list`, `template list`, then `sandbox create --detach` → `info` → `exec` → `logs` → `kill`. All pass. - **Real rotation:** I planted an expired access token in my config (keeping the real refresh token) and ran three commands in parallel. All three succeeded, the refresh happened once, the session stayed valid, and the lock file was removed. - Repo-wide lint and typecheck pass. Part of PRD-119. Related: SDK-400, BKND-155. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
Both SDKs'
ApiClientthrewAPI key is requiredunless an API key was set, so an OAuth access token passed throughapiHeaders(JS) orapi_headers(Python) couldn't authenticate a request on its own. They now accept anAuthorizationheader in place of the key.The E2B API authenticates these requests with
Authorization: Bearer <access token>plusX-Team-ID: <team>, the same pair the Console uses. The CLI uses this in #2103 to authenticate with its refreshed access token instead of a stored API key; that PR needs this one merged first.Changes
ApiClientskips the API key requirement when the config's headers includeAuthorization(case-insensitive). It already only sentX-API-KEYwhen a key was set.AuthenticatedClientalways writes its token toauth_header_name. Without an API key, that now points at the caller's ownAuthorizationheader (same name and value), so the header isn't sent twice and noX-API-KEYis added.AsyncApiClientinherits the change.Authorizationheader, both still raise the same error. An exported-but-emptyE2B_API_KEYcounts as unset in both SDKs.Verified on prod
On
api.e2b.app,Authorization: BearerplusX-Team-IDworks on every endpoint the CLI uses: sandbox list/get/create, timeout, logs, metrics, kill, template list/build/delete, snapshots and/teams. A bearer token withoutX-Team-IDgets401 Invalid service tokeneverywhere except/teams.End to end with only an access token (
E2B_API_KEYunset): JSSandbox.list, thenSandbox.create+commands.run('echo hi')+Sandbox.kill; PythonSandbox.list.Testing
tests/api/apiKey.test.ts: either header casing passes the requirement, the request carriesAuthorization+X-Team-ID, and there's noX-API-KEY.tests/test_api_key.py: same for both casings (exactly oneAuthorizationheader), and API-key-only configs still send onlyX-API-KEY.Part of PRD-119.
🤖 Generated with Claude Code