Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/api-key-prefix-override.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'e2b': patch
'@e2b/python-sdk': patch
---

Support a custom API key prefix for client-side validation. The prefix defaults to `e2b_` and can be overridden via the `E2B_API_KEY_PREFIX` environment variable or the `apiKeyPrefix` (JS) / `api_key_prefix` (Python) connection option.
23 changes: 15 additions & 8 deletions packages/js-sdk/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@
import { ConnectionConfig } from '../connectionConfig'
import { AuthenticationError, RateLimitError, SandboxError } from '../errors'
import { createApiLogger } from '../logs'
import { escapeRegExp } from '../utils'

const API_KEY_PATTERN = /^e2b_[0-9a-f]+$/
const API_KEY_EXAMPLE = `e2b_${'0'.repeat(40)}`
export const DEFAULT_API_KEY_PREFIX = 'e2b_'

/**
* Validates that an E2B API key has the expected `e2b_` prefix followed by
* hex characters. Throws `AuthenticationError` otherwise.
* Validates that an E2B API key has the expected prefix followed by hex
* characters. Throws `AuthenticationError` otherwise. The prefix defaults to
* `e2b_` and can be overridden via the `E2B_API_KEY_PREFIX` env var or the
* `apiKeyPrefix` connection option.
*/
export function validateApiKey(apiKey: string): void {
if (!API_KEY_PATTERN.test(apiKey)) {
export function validateApiKey(
apiKey: string,
prefix: string = DEFAULT_API_KEY_PREFIX
): void {
const pattern = new RegExp(`^${escapeRegExp(prefix)}[0-9a-f]+$`)
if (!pattern.test(apiKey)) {
const example = `${prefix}${'0'.repeat(40)}`
throw new AuthenticationError(
`Invalid API key format: expected "e2b_" followed by hex characters (e.g. "${API_KEY_EXAMPLE}"). ` +
`Invalid API key format: expected "${prefix}" followed by hex characters (e.g. "${example}"). ` +
'Visit the API Keys tab at https://e2b.dev/dashboard?tab=keys to get your API key.'
)
}
Expand Down Expand Up @@ -58,7 +65,7 @@
}

const message = response.error?.message ?? response.error
return new errorClass(`${response.response.status}: ${message}`, stackTrace)

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (windows-latest)

[unit] tests/volume/file.test.ts > Volume File Operations > getInfo > should get info for a file

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:137:7

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (windows-latest)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write file in nested directory

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.makeDir src/volume/index.ts:322:17 ❯ tests/volume/file.test.ts:124:7

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (windows-latest)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write file with metadata (uid, gid, mode)

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:106:22

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (windows-latest)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should throw VolumeError when writing to existing file with force: false

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:93:9

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (windows-latest)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should overwrite an existing file with force option

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:78:9

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (windows-latest)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write and read an empty file

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:65:7

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (windows-latest)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write and read a file with blob format

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:54:9

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (windows-latest)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write and read binary data

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:41:7

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (windows-latest)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write and read a file with bytes format

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:30:9

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (windows-latest)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write and read a text file

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:11:20

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (ubuntu-22.04)

[unit] tests/volume/file.test.ts > Volume File Operations > getInfo > should get info for a file

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:137:7

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (ubuntu-22.04)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write file in nested directory

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.makeDir src/volume/index.ts:322:17 ❯ tests/volume/file.test.ts:124:7

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (ubuntu-22.04)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write file with metadata (uid, gid, mode)

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:106:22

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (ubuntu-22.04)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should throw VolumeError when writing to existing file with force: false

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:93:9

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (ubuntu-22.04)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should overwrite an existing file with force option

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:78:9

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (ubuntu-22.04)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write and read an empty file

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:65:7

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (ubuntu-22.04)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write and read a file with blob format

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:54:9

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (ubuntu-22.04)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write and read binary data

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:41:7

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (ubuntu-22.04)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write and read a file with bytes format

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:30:9

Check failure on line 68 in packages/js-sdk/src/api/index.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (ubuntu-22.04)

[unit] tests/volume/file.test.ts > Volume File Operations > writeFile and readFile > should write and read a text file

VolumeError: 400: Invalid authorization header ❯ handleApiError src/api/index.ts:68:10 ❯ Volume.writeFile src/volume/index.ts:615:17 ❯ tests/volume/file.test.ts:11:20
}

/**
Expand All @@ -83,7 +90,7 @@
}

if (config.apiKey) {
validateApiKey(config.apiKey)
validateApiKey(config.apiKey, config.apiKeyPrefix)
}

if (opts?.requireAccessToken && !config.accessToken) {
Expand Down
13 changes: 13 additions & 0 deletions packages/js-sdk/src/connectionConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ export interface ConnectionOpts {
* @default E2B_API_KEY // environment variable
*/
apiKey?: string
/**
* Prefix expected on the E2B API key, used by client-side format validation.
* Override this when your deployment issues API keys with a non-default prefix.
*
* @default E2B_API_KEY_PREFIX // environment variable or `e2b_`
*/
apiKeyPrefix?: string
/**
* E2B access token to use for authentication.
*
Expand Down Expand Up @@ -189,12 +196,14 @@ export class ConnectionConfig {
readonly requestTimeoutMs: number

readonly apiKey?: string
readonly apiKeyPrefix: string
readonly accessToken?: string

readonly headers?: Record<string, string>

constructor(opts?: ConnectionOpts) {
this.apiKey = opts?.apiKey || ConnectionConfig.apiKey
this.apiKeyPrefix = opts?.apiKeyPrefix || ConnectionConfig.apiKeyPrefix
Comment thread
mishushakov marked this conversation as resolved.
Dismissed
this.debug = opts?.debug || ConnectionConfig.debug
this.domain = opts?.domain || ConnectionConfig.domain
this.accessToken = opts?.accessToken || ConnectionConfig.accessToken
Expand Down Expand Up @@ -231,6 +240,10 @@ export class ConnectionConfig {
return getEnvVar('E2B_API_KEY')
}

private static get apiKeyPrefix() {
return getEnvVar('E2B_API_KEY_PREFIX') || 'e2b_'
}

private static get accessToken() {
return getEnvVar('E2B_ACCESS_TOKEN')
}
Expand Down
8 changes: 8 additions & 0 deletions packages/js-sdk/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ export function shellQuote(s: string): string {
return "'" + s.replace(/'/g, "'\\''") + "'"
}

/**
* Escape regex metacharacters in a string so it can be safely interpolated
* into a `RegExp` as a literal.
*/
export function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

/**
* Prepare data for upload as a BodyInit, optionally gzip-compressed.
* When gzip is enabled, compresses the data and returns a Blob.
Expand Down
35 changes: 35 additions & 0 deletions packages/js-sdk/tests/api/validateApiKey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,39 @@ describe('validateApiKey', () => {
)
}
})

test('accepts a key with a custom prefix', () => {
assert.doesNotThrow(() =>
validateApiKey('myorg_' + '0'.repeat(40), 'myorg_')
)
})

test('rejects a key whose prefix does not match the custom prefix', () => {
assert.throws(
() => validateApiKey('e2b_' + '0'.repeat(40), 'myorg_'),
AuthenticationError,
/Invalid API key format/
)
})

test('custom prefix appears in the error message and example', () => {
try {
validateApiKey('nope', 'myorg_')
assert.fail('expected validateApiKey to throw')
} catch (err) {
assert.instanceOf(err, AuthenticationError)
assert.match((err as Error).message, /myorg_/)
assert.match((err as Error).message, /myorg_0{40}/)
}
})

test('escapes regex metacharacters in the prefix', () => {
assert.doesNotThrow(() =>
validateApiKey('my.org+' + '0'.repeat(40), 'my.org+')
)
assert.throws(
() => validateApiKey('myXorgY' + '0'.repeat(40), 'my.org+'),
AuthenticationError
)
})
})
22 changes: 22 additions & 0 deletions packages/js-sdk/tests/connectionConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ beforeEach(() => {
E2B_DOMAIN: process.env.E2B_DOMAIN,
E2B_SANDBOX_URL: process.env.E2B_SANDBOX_URL,
E2B_DEBUG: process.env.E2B_DEBUG,
E2B_API_KEY_PREFIX: process.env.E2B_API_KEY_PREFIX,
}
})

Expand Down Expand Up @@ -146,6 +147,27 @@ test('sandbox_url stays localhost in debug mode', () => {
)
})

test('apiKeyPrefix defaults to e2b_', () => {
delete process.env.E2B_API_KEY_PREFIX

const config = new ConnectionConfig()
assert.equal(config.apiKeyPrefix, 'e2b_')
})

test('apiKeyPrefix from env var', () => {
process.env.E2B_API_KEY_PREFIX = 'myorg_'

const config = new ConnectionConfig()
assert.equal(config.apiKeyPrefix, 'myorg_')
})

test('apiKeyPrefix in args has priority over env var', () => {
process.env.E2B_API_KEY_PREFIX = 'fromenv_'

const config = new ConnectionConfig({ apiKeyPrefix: 'fromargs_' })
assert.equal(config.apiKeyPrefix, 'fromargs_')
})

test('getSignal returns user signal when no timeout is set', () => {
const config = new ConnectionConfig({ requestTimeoutMs: 0 })
const controller = new AbortController()
Expand Down
21 changes: 12 additions & 9 deletions packages/python-sdk/e2b/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,21 @@ def status_code(self) -> int: ...
def content(self) -> Union[str, bytes]: ...


_API_KEY_PATTERN = re.compile(r"\Ae2b_[0-9a-f]+\Z")
_API_KEY_EXAMPLE = "e2b_" + "0" * 40
DEFAULT_API_KEY_PREFIX = "e2b_"


def validate_api_key(api_key: str) -> None:
"""Validate that an E2B API key has the expected ``e2b_`` prefix
followed by hex characters. Raises ``AuthenticationException`` otherwise.
def validate_api_key(api_key: str, prefix: str = DEFAULT_API_KEY_PREFIX) -> None:
"""Validate that an E2B API key has the expected prefix followed by hex
characters. Raises ``AuthenticationException`` otherwise. The prefix
defaults to ``e2b_`` and can be overridden via the ``E2B_API_KEY_PREFIX``
env var or the ``api_key_prefix`` connection option.
"""
if not _API_KEY_PATTERN.match(api_key):
pattern = re.compile(rf"\A{re.escape(prefix)}[0-9a-f]+\Z")
if not pattern.match(api_key):
example = f"{prefix}{'0' * 40}"
raise AuthenticationException(
'Invalid API key format: expected "e2b_" followed by hex '
f'characters (e.g. "{_API_KEY_EXAMPLE}"). '
f'Invalid API key format: expected "{prefix}" followed by hex '
f'characters (e.g. "{example}"). '
"Visit the API Keys tab at https://e2b.dev/dashboard?tab=keys to get your API key."
)

Expand Down Expand Up @@ -126,7 +129,7 @@ def __init__(
token = config.api_key

if config.api_key is not None:
validate_api_key(config.api_key)
validate_api_key(config.api_key, config.api_key_prefix)

if require_access_token:
if config.access_token is None:
Expand Down
14 changes: 14 additions & 0 deletions packages/python-sdk/e2b/connection_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ class ApiParams(TypedDict, total=False):
api_key: Optional[str]
"""E2B API Key to use for authentication, defaults to `E2B_API_KEY` environment variable."""

api_key_prefix: Optional[str]
"""Prefix expected on the E2B API key for client-side format validation,
defaults to `E2B_API_KEY_PREFIX` environment variable or `e2b_`."""

domain: Optional[str]
"""E2B domain to use for authentication, defaults to `E2B_DOMAIN` environment variable."""

Expand Down Expand Up @@ -68,6 +72,10 @@ def _debug():
def _api_key():
return os.getenv("E2B_API_KEY")

@staticmethod
def _api_key_prefix():
return os.getenv("E2B_API_KEY_PREFIX") or "e2b_"

@staticmethod
def _api_url():
return os.getenv("E2B_API_URL")
Expand All @@ -85,6 +93,7 @@ def __init__(
domain: Optional[str] = None,
debug: Optional[bool] = None,
api_key: Optional[str] = None,
api_key_prefix: Optional[str] = None,
api_url: Optional[str] = None,
sandbox_url: Optional[str] = None,
access_token: Optional[str] = None,
Expand All @@ -97,6 +106,7 @@ def __init__(
self.domain = domain or ConnectionConfig._domain()
self.debug = debug or ConnectionConfig._debug()
self.api_key = api_key or ConnectionConfig._api_key()
self.api_key_prefix = api_key_prefix or ConnectionConfig._api_key_prefix()
self.access_token = access_token or ConnectionConfig._access_token()
self.headers = {**(headers or {}), **(api_headers or {})}
self.headers["User-Agent"] = f"e2b-python-sdk/{package_version}"
Expand Down Expand Up @@ -191,6 +201,7 @@ def get_api_params(
api_headers = opts.get("api_headers")
request_timeout = opts.get("request_timeout")
api_key = opts.get("api_key")
api_key_prefix = opts.get("api_key_prefix")
api_url = opts.get("api_url")
domain = opts.get("domain")
debug = opts.get("debug")
Expand All @@ -205,6 +216,9 @@ def get_api_params(
return dict(
ApiParams(
api_key=api_key if api_key is not None else self.api_key,
api_key_prefix=api_key_prefix
if api_key_prefix is not None
else self.api_key_prefix,
api_url=api_url if api_url is not None else self.api_url,
domain=domain if domain is not None else self.domain,
debug=debug if debug is not None else self.debug,
Expand Down
21 changes: 21 additions & 0 deletions packages/python-sdk/tests/test_connection_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ def test_api_url_has_correct_priority(monkeypatch):
assert config.api_url == "http://localhost:8080"


def test_api_key_prefix_defaults_to_e2b(monkeypatch):
monkeypatch.delenv("E2B_API_KEY_PREFIX", raising=False)

config = ConnectionConfig()
assert config.api_key_prefix == "e2b_"


def test_api_key_prefix_from_env_var(monkeypatch):
monkeypatch.setenv("E2B_API_KEY_PREFIX", "myorg_")

config = ConnectionConfig()
assert config.api_key_prefix == "myorg_"


def test_api_key_prefix_arg_has_priority_over_env_var(monkeypatch):
monkeypatch.setenv("E2B_API_KEY_PREFIX", "fromenv_")

config = ConnectionConfig(api_key_prefix="fromargs_")
assert config.api_key_prefix == "fromargs_"


def test_sandbox_url_uses_stable_host_for_supported_domain():
config = ConnectionConfig(domain="e2b.app")

Expand Down
22 changes: 22 additions & 0 deletions packages/python-sdk/tests/test_validate_api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,25 @@ def test_error_message_includes_example_token(test_api_key):
with pytest.raises(AuthenticationException) as exc_info:
validate_api_key("nope")
assert test_api_key in str(exc_info.value)


def test_accepts_custom_prefix():
validate_api_key("myorg_" + "0" * 40, prefix="myorg_")


def test_rejects_when_prefix_does_not_match_custom_prefix():
with pytest.raises(AuthenticationException, match=r"Invalid API key format"):
validate_api_key("e2b_" + "0" * 40, prefix="myorg_")


def test_custom_prefix_appears_in_error_message():
with pytest.raises(AuthenticationException) as exc_info:
validate_api_key("nope", prefix="myorg_")
assert "myorg_" in str(exc_info.value)
assert "myorg_" + "0" * 40 in str(exc_info.value)


def test_escapes_regex_metacharacters_in_prefix():
validate_api_key("my.org+" + "0" * 40, prefix="my.org+")
with pytest.raises(AuthenticationException, match=r"Invalid API key format"):
validate_api_key("myXorgY" + "0" * 40, prefix="my.org+")
Loading