Skip to content

feat(sdk): accept an Authorization header in place of an API key - #2102

Merged
mishushakov merged 2 commits into
mainfrom
mishushakov/sdk-authorization-header
Nov 10, 2026
Merged

mishushakov merged 2 commits into
mainfrom
mishushakov/sdk-authorization-header

Conversation

@mishushakov

@mishushakov mishushakov commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Both SDKs' ApiClient threw API key is required unless an API key was set, so an OAuth access token passed through apiHeaders (JS) or api_headers (Python) couldn't authenticate a request on its own. They now accept an Authorization header in place of the key.

The E2B API authenticates these requests with Authorization: Bearer <access token> plus X-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.

import { Sandbox } from 'e2b'

const sandbox = await Sandbox.create({
  apiHeaders: { Authorization: `Bearer ${accessToken}`, 'X-Team-ID': teamId },
})
from e2b import Sandbox

sandbox = Sandbox.create(
    api_headers={"Authorization": f"Bearer {access_token}", "X-Team-ID": team_id}
)

Changes

  • JS: ApiClient skips the API key requirement when the config's headers include Authorization (case-insensitive). It already only sent X-API-KEY when a key was set.
  • Python: the generated AuthenticatedClient always writes its token to auth_header_name. Without an API key, that now points at the caller's own Authorization header (same name and value), so the header isn't sent twice and no X-API-KEY is added. AsyncApiClient inherits the change.
  • With neither an API key nor an Authorization header, both still raise the same error. An exported-but-empty E2B_API_KEY counts as unset in both SDKs.

Verified on prod

On api.e2b.app, Authorization: Bearer plus X-Team-ID works on every endpoint the CLI uses: sandbox list/get/create, timeout, logs, metrics, kill, template list/build/delete, snapshots and /teams. A bearer token without X-Team-ID gets 401 Invalid service token everywhere except /teams.

End to end with only an access token (E2B_API_KEY unset): JS Sandbox.list, then Sandbox.create + commands.run('echo hi') + Sandbox.kill; Python Sandbox.list.

Testing

  • JS tests/api/apiKey.test.ts: either header casing passes the requirement, the request carries Authorization + X-Team-ID, and there's no X-API-KEY.
  • Python tests/test_api_key.py: same for both casings (exactly one Authorization header), and API-key-only configs still send only X-API-KEY.
  • Repo-wide lint and typecheck pass.

Part of PRD-119.

🤖 Generated with Claude Code

@cla-bot cla-bot Bot added the cla-signed label Nov 6, 2026
@changeset-bot

changeset-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2f84162

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
e2b Minor
@e2b/python-sdk Minor
@e2b/code-interpreter-python Patch

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

@mishushakov
mishushakov added this pull request to stack #2092 November 6, 2026 17:03
@cursor

cursor Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

PR Summary

Medium Risk
Changes authentication paths in both SDKs; behavior is narrow (header vs key) but security-sensitive and must not weaken or duplicate auth headers.

Overview
Both JS and Python ApiClient instances can authenticate without an API key when callers supply an Authorization header (e.g. Bearer OAuth token), typically with X-Team-ID, via apiHeaders / api_headers.

JS: constructor treats a case-insensitive Authorization in config headers like having an API key, so the existing “API key required” guard no longer blocks token-only setups; X-API-KEY is still added only when a key is present.

Python: auth routing prefers api_key → X-API-KEY; otherwise reuses the caller’s Authorization header name/value via the generated client so the token isn’t duplicated and X-API-KEY isn’t sent.

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.

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from abaedb0. Download artifacts from this workflow run.

JS SDK (e2b@2.63.1-mishushakov-sdk-authorization-header.0, with @e2b/dockerfile-utils@0.1.2-mishushakov-sdk-authorization-header.0):

npm install ./e2b-dockerfile-utils-0.1.2-mishushakov-sdk-authorization-header.0.tgz ./e2b-2.63.1-mishushakov-sdk-authorization-header.0.tgz

CLI (@e2b/cli@2.23.1-mishushakov-sdk-authorization-header.0):

npm install ./e2b-cli-2.23.1-mishushakov-sdk-authorization-header.0.tgz

Code Interpreter JS SDK (@e2b/code-interpreter@2.9.2-mishushakov-sdk-authorization-header.0):

npm install ./e2b-code-interpreter-2.9.2-mishushakov-sdk-authorization-header.0.tgz

Desktop JS SDK (@e2b/desktop@2.5.1-mishushakov-sdk-authorization-header.0):

npm install ./e2b-desktop-2.5.1-mishushakov-sdk-authorization-header.0.tgz

Python SDK (e2b==2.63.0+mishushakov.sdk.authorization.header, with e2b-dockerfile-utils==0.1.1+mishushakov.sdk.authorization.header):

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.whl

Code Interpreter Python SDK (e2b-code-interpreter==2.11.9+mishushakov.sdk.authorization.header):

pip install ./e2b_code_interpreter-2.11.9+mishushakov.sdk.authorization.header-py3-none-any.whl

Desktop Python SDK (e2b-desktop==2.7.2+mishushakov.sdk.authorization.header):

pip install ./e2b_desktop-2.7.2+mishushakov.sdk.authorization.header-py3-none-any.whl

devin-ai-integration[bot]

This comment was marked as resolved.

@linear-code

linear-code Bot commented Nov 6, 2026

Copy link
Copy Markdown

PRD-119

@mishushakov
mishushakov force-pushed the mishushakov/sdk-authorization-header branch from 1aac476 to e8f520a Compare November 6, 2026 18:00
@mishushakov
mishushakov force-pushed the mishushakov/cli-zod-validation branch from 638a8e9 to 5d2155e Compare November 6, 2026 18:00
@mishushakov
mishushakov removed this pull request from stack #2092 November 6, 2026 18:40
@mishushakov
mishushakov added this pull request to stack #2108 November 6, 2026 18:40
@mishushakov
mishushakov force-pushed the mishushakov/sdk-authorization-header branch 2 times, most recently from b4e1200 to fe43924 Compare November 10, 2026 13:52
mishushakov and others added 2 commits November 10, 2026 14:58
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>
@mishushakov
mishushakov removed this pull request from stack #2108 November 10, 2026 14:00
@mishushakov
mishushakov force-pushed the mishushakov/sdk-authorization-header branch from fe43924 to 2f84162 Compare November 10, 2026 14:00
@mishushakov
mishushakov changed the base branch from mishushakov/cli-zod-validation to main November 10, 2026 14:00
@mishushakov
mishushakov marked this pull request as ready for review November 10, 2026 14:03
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T14:06:12.024423Z 2f84162 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@e2b-harness e2b-harness Bot added the review-requested Posted in #code-review-requests on Slack label Nov 10, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Devin Review: 1 flag

Not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 when config.headers has an authorization header in any casing. X-API-KEY is still only sent when a key is set.
  • Python ApiClient: an empty api_key now counts as unset. Without a key, the generated client's auth_header_name/token is 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. AsyncApiClient inherits the change through the shared ApiClient. 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.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review completed

Nothing new to post: everything this review found is already covered by existing comments on this pull request or didn't merit a separate one.

@mishushakov
mishushakov merged commit 106efea into main Nov 10, 2026
54 of 61 checks passed
@mishushakov
mishushakov deleted the mishushakov/sdk-authorization-header branch November 10, 2026 16:44
mishushakov added a commit that referenced this pull request Nov 12, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed review-requested Posted in #code-review-requests on Slack

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants