From 9e0a5ec1d5a1519282e9141a7c72f70ba9942119 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:56:48 +0100 Subject: [PATCH 1/2] feat(sdk): accept an Authorization header in place of an API key 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 ` plus `X-Team-ID: ` (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) --- .changeset/sdk-authorization-header.md | 16 +++++++++++++++ packages/js-sdk/src/api/index.ts | 7 ++++++- packages/js-sdk/tests/api/apiKey.test.ts | 24 ++++++++++++++++++++++ packages/python-sdk/e2b/api/__init__.py | 23 +++++++++++++++++---- packages/python-sdk/tests/test_api_key.py | 25 +++++++++++++++++++++++ 5 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 .changeset/sdk-authorization-header.md diff --git a/.changeset/sdk-authorization-header.md b/.changeset/sdk-authorization-header.md new file mode 100644 index 0000000000..f17f7f3d35 --- /dev/null +++ b/.changeset/sdk-authorization-header.md @@ -0,0 +1,16 @@ +--- +'e2b': minor +'@e2b/python-sdk': minor +--- + +The API client accepts an `Authorization` header in place of an API key. Requests can now authenticate with an OAuth access token passed through `apiHeaders` (JS) or `api_headers` (Python), together with the `X-Team-ID` header that selects the team: + +```ts +await Sandbox.create({ + apiHeaders: { Authorization: `Bearer ${accessToken}`, 'X-Team-ID': teamId }, +}) +``` + +```python +Sandbox.create(api_headers={"Authorization": f"Bearer {access_token}", "X-Team-ID": team_id}) +``` diff --git a/packages/js-sdk/src/api/index.ts b/packages/js-sdk/src/api/index.ts index 9264bfe001..96481181fb 100644 --- a/packages/js-sdk/src/api/index.ts +++ b/packages/js-sdk/src/api/index.ts @@ -103,7 +103,12 @@ class ApiClient { requireApiKey?: boolean } = {} ) { - if ((opts.requireApiKey ?? true) && !config.apiKey) { + // An `Authorization` header (e.g. an OAuth access token passed through + // `apiHeaders`) authenticates the request in place of an API key. + const hasAuthorization = Object.keys(config.headers ?? {}).some( + (name) => name.toLowerCase() === 'authorization' + ) + if ((opts.requireApiKey ?? true) && !config.apiKey && !hasAuthorization) { throw new AuthenticationError( 'API key is required, please visit the API Keys tab at https://e2b.dev/dashboard?tab=keys to get your API key. ' + 'You can either set the environment variable `E2B_API_KEY` ' + diff --git a/packages/js-sdk/tests/api/apiKey.test.ts b/packages/js-sdk/tests/api/apiKey.test.ts index 6adfcc782c..93688e411d 100644 --- a/packages/js-sdk/tests/api/apiKey.test.ts +++ b/packages/js-sdk/tests/api/apiKey.test.ts @@ -23,4 +23,28 @@ describe('ApiClient API key requirement', () => { const config = new ConnectionConfig({}) assert.doesNotThrow(() => new ApiClient(config, { requireApiKey: false })) }) + + test.each(['Authorization', 'authorization'])( + 'accepts an %s header in place of an API key', + async (header) => { + vi.stubEnv('E2B_API_KEY', '') + const seen: Headers[] = [] + const config = new ConnectionConfig({ + apiHeaders: { [header]: 'Bearer access-token', 'X-Team-ID': 'team' }, + }) + const client = new ApiClient(config) + await client.api.GET('/teams', { + fetch: async (request: Request) => { + seen.push(request.headers) + return new Response('[]', { + headers: { 'Content-Type': 'application/json' }, + }) + }, + }) + + assert.equal(seen[0].get('Authorization'), 'Bearer access-token') + assert.equal(seen[0].get('X-Team-ID'), 'team') + assert.isNull(seen[0].get('X-API-KEY')) + } + ) }) diff --git a/packages/python-sdk/e2b/api/__init__.py b/packages/python-sdk/e2b/api/__init__.py index 332d237284..069be63869 100644 --- a/packages/python-sdk/e2b/api/__init__.py +++ b/packages/python-sdk/e2b/api/__init__.py @@ -246,15 +246,30 @@ def __init__( ): self._proxy = config.proxy - if config.api_key is None: + # An `Authorization` header (e.g. an OAuth access token passed through + # `api_headers`) authenticates the request in place of an API key. + authorization = next( + ( + (name, value) + for name, value in (config.headers or {}).items() + if name.lower() == "authorization" + ), + None, + ) + + if config.api_key is not None: + token = config.api_key + auth_header_name = "X-API-KEY" + elif authorization is not None: + # The generated client always writes `token` to `auth_header_name`; + # point it at the caller's own header so it isn't sent twice. + auth_header_name, token = authorization + else: raise AuthenticationException( "API key is required, please visit the API Keys tab at https://e2b.dev/dashboard?tab=keys to get your API key. " "You can either set the environment variable `E2B_API_KEY` " 'or you can pass it directly to the method like api_key="e2b_..."', ) - - token = config.api_key - auth_header_name = "X-API-KEY" prefix = "" self._logger = config.logger diff --git a/packages/python-sdk/tests/test_api_key.py b/packages/python-sdk/tests/test_api_key.py index d5169bef54..1d42e0c472 100644 --- a/packages/python-sdk/tests/test_api_key.py +++ b/packages/python-sdk/tests/test_api_key.py @@ -10,3 +10,28 @@ def test_api_client_requires_api_key(monkeypatch): config = ConnectionConfig() with pytest.raises(AuthenticationException, match=r"API key is required"): ApiClient(config) + + +@pytest.mark.parametrize("header", ["Authorization", "authorization"]) +def test_api_client_accepts_authorization_header(monkeypatch, header): + monkeypatch.delenv("E2B_API_KEY", raising=False) + config = ConnectionConfig( + api_headers={header: "Bearer access-token", "X-Team-ID": "team"} + ) + + client = ApiClient(config) + headers = client.get_httpx_client().headers + + assert headers.get_list("Authorization") == ["Bearer access-token"] + assert headers["X-Team-ID"] == "team" + assert "X-API-KEY" not in headers + + +def test_api_client_prefers_api_key(monkeypatch): + monkeypatch.delenv("E2B_API_KEY", raising=False) + config = ConnectionConfig(api_key="e2b_key") + + headers = ApiClient(config).get_httpx_client().headers + + assert headers["X-API-KEY"] == "e2b_key" + assert "Authorization" not in headers From 2f8416204a6b0904b62d966a6499dbe5067229dd Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:49:24 +0100 Subject: [PATCH 2/2] fix(python-sdk): treat an empty E2B_API_KEY as unset with an Authorization 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 --- packages/python-sdk/e2b/api/__init__.py | 3 ++- packages/python-sdk/tests/test_api_key.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/python-sdk/e2b/api/__init__.py b/packages/python-sdk/e2b/api/__init__.py index 069be63869..52c5099a6b 100644 --- a/packages/python-sdk/e2b/api/__init__.py +++ b/packages/python-sdk/e2b/api/__init__.py @@ -257,7 +257,8 @@ def __init__( None, ) - if config.api_key is not None: + # Like the JS SDK, an empty E2B_API_KEY counts as unset. + if config.api_key: token = config.api_key auth_header_name = "X-API-KEY" elif authorization is not None: diff --git a/packages/python-sdk/tests/test_api_key.py b/packages/python-sdk/tests/test_api_key.py index 1d42e0c472..ff190459c9 100644 --- a/packages/python-sdk/tests/test_api_key.py +++ b/packages/python-sdk/tests/test_api_key.py @@ -13,8 +13,12 @@ def test_api_client_requires_api_key(monkeypatch): @pytest.mark.parametrize("header", ["Authorization", "authorization"]) -def test_api_client_accepts_authorization_header(monkeypatch, header): - monkeypatch.delenv("E2B_API_KEY", raising=False) +@pytest.mark.parametrize("env_api_key", [None, ""]) +def test_api_client_accepts_authorization_header(monkeypatch, header, env_api_key): + if env_api_key is None: + monkeypatch.delenv("E2B_API_KEY", raising=False) + else: + monkeypatch.setenv("E2B_API_KEY", env_api_key) config = ConnectionConfig( api_headers={header: "Bearer access-token", "X-Team-ID": "team"} )