Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .changeset/sdk-authorization-header.md
Original file line number Diff line number Diff line change
@@ -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})
```
7 changes: 6 additions & 1 deletion packages/js-sdk/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Comment thread
mishushakov marked this conversation as resolved.
Resolved
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` ' +
Comment thread
mishushakov marked this conversation as resolved.
Resolved
Expand Down
24 changes: 24 additions & 0 deletions packages/js-sdk/tests/api/apiKey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
}
)
})
24 changes: 20 additions & 4 deletions packages/python-sdk/e2b/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,15 +246,31 @@ 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,
)

# 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:
# 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_..."',
Comment thread
mishushakov marked this conversation as resolved.
Resolved
)

token = config.api_key
auth_header_name = "X-API-KEY"
prefix = ""

self._logger = config.logger
Expand Down
29 changes: 29 additions & 0 deletions packages/python-sdk/tests/test_api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,32 @@ 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"])
@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"}
)

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
Loading