e2b
Version:
E2B SDK that give agents cloud environments
1 lines • 530 kB
Source Map (JSON)
{"version":3,"sources":["../src/api/index.ts","../src/api/metadata.ts","../package.json","../src/utils.ts","../src/api/inflight.ts","../src/undici.ts","../src/api/http2.ts","../src/errors.ts","../src/logs.ts","../src/connectionConfig.ts","../src/sandbox/signature.ts","../src/sandbox/filesystem/index.ts","../src/envd/api.ts","../src/envd/rpc.ts","../src/envd/versions.ts","../src/envd/filesystem/filesystem_pb.ts","../src/sandbox/filesystem/watchHandle.ts","../src/sandbox/commands/commandHandle.ts","../src/sandbox/network.ts","../src/sandbox/git/utils.ts","../src/sandbox/git/index.ts","../src/volume/client.ts","../src/volume/types.ts","../src/volume/index.ts","../src/sandbox/index.ts","../src/envd/http2.ts","../src/sandbox/commands/index.ts","../src/envd/process/process_pb.ts","../src/sandbox/commands/pty.ts","../src/sandbox/sandboxApi.ts","../src/paginator.ts","../src/template/consts.ts","../src/template/logger.ts","../src/template/utils.ts","../src/template/buildApi.ts","../src/template/dockerfileParser.ts","../src/template/readycmd.ts","../src/template/index.ts","../src/index.ts"],"sourcesContent":["import createClient, { FetchResponse } from 'openapi-fetch'\n\nimport type { components, paths } from './schema.gen'\nimport { defaultHeaders } from './metadata'\nimport { createApiFetch } from './http2'\nimport { ConnectionConfig } from '../connectionConfig'\nimport { AuthenticationError, RateLimitError, SandboxError } from '../errors'\nimport { createApiLogger } from '../logs'\n\nconst API_KEY_PATTERN = /^e2b_[0-9a-f]+$/\nconst API_KEY_EXAMPLE = `e2b_${'0'.repeat(40)}`\n\n/**\n * Validates that an E2B API key has the expected `e2b_` prefix followed by\n * hex characters. Throws `AuthenticationError` otherwise.\n */\nexport function validateApiKey(apiKey: string): void {\n if (!API_KEY_PATTERN.test(apiKey)) {\n throw new AuthenticationError(\n `Invalid API key format: expected \"e2b_\" followed by hex characters (e.g. \"${API_KEY_EXAMPLE}\"). ` +\n 'Visit the API Keys tab at https://e2b.dev/dashboard?tab=keys to get your API key.'\n )\n }\n}\n\nexport function handleApiError(\n response: FetchResponse<any, any, any>,\n errorClass: new (\n message: string,\n stackTrace?: string\n ) => Error = SandboxError,\n stackTrace?: string\n): Error | undefined {\n // openapi-fetch leaves `error` undefined for non-2xx responses with\n // Content-Length: 0, so check the status instead\n if (response.response.ok) {\n return\n }\n\n if (response.response.status === 401) {\n const message = 'Unauthorized, please check your credentials.'\n const content = response.error?.message ?? response.error\n\n if (content) {\n return new AuthenticationError(`${message} - ${content}`)\n }\n return new AuthenticationError(message)\n }\n\n if (response.response.status === 429) {\n const message = 'Rate limit exceeded, please try again later'\n const content = response.error?.message ?? response.error\n\n if (content) {\n return new RateLimitError(`${message} - ${content}`)\n }\n return new RateLimitError(message)\n }\n\n const message =\n response.error?.message || response.error || response.response.statusText\n return new errorClass(`${response.response.status}: ${message}`, stackTrace)\n}\n\n/**\n * Client for interacting with the E2B API.\n */\nclass ApiClient {\n readonly api: ReturnType<typeof createClient<paths>>\n\n constructor(\n config: ConnectionConfig,\n opts: {\n requireApiKey?: boolean\n } = {}\n ) {\n if ((opts.requireApiKey ?? true) && !config.apiKey) {\n throw new AuthenticationError(\n 'API key is required, please visit the API Keys tab at https://e2b.dev/dashboard?tab=keys to get your API key. ' +\n 'You can either set the environment variable `E2B_API_KEY` ' +\n \"or you can pass it directly to the sandbox like Sandbox.create({ apiKey: 'e2b_...' })\"\n )\n }\n\n if (config.apiKey && config.validateApiKey) {\n validateApiKey(config.apiKey)\n }\n\n this.api = createClient<paths>({\n baseUrl: config.apiUrl,\n fetch: createApiFetch(config.proxy),\n // In HTTP 1.1, all connections are considered persistent unless declared otherwise\n // keepalive: true,\n headers: {\n ...defaultHeaders,\n ...(config.apiKey && { 'X-API-KEY': config.apiKey }),\n ...(config.accessToken && {\n Authorization: `Bearer ${config.accessToken}`,\n }),\n ...config.headers,\n },\n querySerializer: {\n array: {\n style: 'form',\n explode: false,\n },\n },\n })\n\n if (config.logger) {\n this.api.use(createApiLogger(config.logger))\n }\n }\n}\n\nexport type { components, paths }\nexport { ApiClient }\n","import platform from 'platform'\n\nimport { version } from '../../package.json'\nimport { runtime, runtimeVersion } from '../utils'\n\nexport { version }\n\nexport const defaultHeaders = {\n browser: (typeof window !== 'undefined' && platform.name) || 'unknown',\n lang: 'js',\n lang_version: runtimeVersion,\n package_version: version,\n publisher: 'e2b',\n sdk_runtime: runtime,\n system: platform.os?.family || 'unknown',\n}\n\nexport function getEnvVar(name: string) {\n if (runtime === 'deno') {\n // @ts-ignore\n return Deno.env.get(name)\n }\n\n if (typeof process === 'undefined') {\n return ''\n }\n\n return process.env[name]\n}\n\n/**\n * Parse an env var as a base-10 integer, falling back to `defaultValue` when\n * the env var is unset. Throws on non-integer input rather than silently\n * falling back so misconfiguration is surfaced loudly.\n */\nexport function parseIntEnv(name: string, defaultValue: number): number {\n const raw = getEnvVar(name)\n if (!raw) return defaultValue\n\n const parsed = Number.parseInt(raw, 10)\n if (!Number.isFinite(parsed)) {\n throw new Error(\n `Invalid ${name}=${JSON.stringify(raw)}: expected an integer.`\n )\n }\n\n return parsed\n}\n\n/**\n * Parse an env var that must be a positive integer (>= 1). Throws on\n * non-positive or non-integer input.\n */\nexport function parsePositiveIntEnv(\n name: string,\n defaultValue: number\n): number {\n const parsed = parseIntEnv(name, defaultValue)\n if (parsed < 1) {\n throw new Error(`Invalid ${name}=${parsed}: expected a positive integer.`)\n }\n\n return parsed\n}\n\n/**\n * Parse an inflight-limit env var. Returns `0` to disable the cap (documented\n * opt-out) or a positive integer to cap concurrency. Throws on non-integer or\n * negative values so misconfiguration is surfaced loudly rather than silently\n * removing the cap. A return value of `0` is recognized by\n * {@link limitConcurrency} as \"no cap\".\n */\nexport function parseInflightLimitEnv(\n name: string,\n defaultValue: number\n): number {\n const parsed = parseIntEnv(name, defaultValue)\n if (parsed < 0) {\n throw new Error(\n `Invalid ${name}=${parsed}: expected a non-negative integer ` +\n '(use 0 to disable the cap).'\n )\n }\n return parsed\n}\n","{\n \"name\": \"e2b\",\n \"version\": \"2.32.0\",\n \"description\": \"E2B SDK that give agents cloud environments\",\n \"homepage\": \"https://e2b.dev\",\n \"license\": \"MIT\",\n \"author\": {\n \"name\": \"FoundryLabs, Inc.\",\n \"email\": \"hello@e2b.dev\",\n \"url\": \"https://e2b.dev\"\n },\n \"bugs\": \"https://github.com/e2b-dev/e2b/issues\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/e2b-dev/e2b\",\n \"directory\": \"packages/js-sdk\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"sideEffects\": false,\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.mjs\",\n \"types\": \"dist/index.d.ts\",\n \"scripts\": {\n \"prepublishOnly\": \"pnpm build\",\n \"build\": \"tsc --noEmit && tsup\",\n \"dev\": \"tsup --watch\",\n \"example\": \"tsx example.mts\",\n \"test\": \"vitest run\",\n \"generate\": \"npm-run-all generate:* && pnpm run format\",\n \"generate:api\": \"python ./../../spec/remove_extra_tags.py sandboxes snapshots templates tags auth volumes && openapi-typescript ../../spec/openapi_generated.yml -x api_key --array-length --alphabetize --default-non-nullable false --output src/api/schema.gen.ts\",\n \"generate:envd\": \"cd ../../spec/envd && buf generate --template buf-js.gen.yaml\\n\",\n \"generate:envd-api\": \"openapi-typescript ../../spec/envd/envd.yaml -x api_key --array-length --alphabetize --output src/envd/schema.gen.ts\",\n \"generate:volume-api\": \"openapi-typescript ../../spec/openapi-volumecontent.yml -x api_key --array-length --alphabetize --output src/volume/schema.gen.ts\",\n \"generate:mcp\": \"json2ts -i ./../../spec/mcp-server.json -o src/sandbox/mcp.d.ts --unreachableDefinitions --style.singleQuote --no-style.semi\",\n \"check-deps\": \"knip\",\n \"pretest\": \"npx playwright install --with-deps chromium\",\n \"postPublish\": \"./scripts/post-publish.sh || true\",\n \"test:bun\": \"bun test tests/runtimes/bun --env-file=.env\",\n \"test:deno\": \"deno test tests/runtimes/deno/ --allow-net --allow-read --allow-env --unstable-sloppy-imports --trace-leaks\",\n \"test:integration\": \"E2B_INTEGRATION_TEST=1 vitest run tests/integration/**\",\n \"typecheck\": \"tsc --noEmit\",\n \"lint\": \"oxlint --config ../../.oxlintrc.json src tests\",\n \"format\": \"prettier --write src/ tests/ example.mts\"\n },\n \"devDependencies\": {\n \"@testing-library/react\": \"^16.2.0\",\n \"@types/node\": \"^20.19.19\",\n \"@types/platform\": \"^1.3.6\",\n \"@types/react\": \"^18.3.11\",\n \"@vitejs/plugin-react\": \"^4.3.4\",\n \"@vitest/browser\": \"^4.1.0\",\n \"@vitest/browser-playwright\": \"^4.1.0\",\n \"dotenv\": \"^16.4.5\",\n \"json-schema-to-typescript\": \"^15.0.4\",\n \"knip\": \"^5.43.6\",\n \"msw\": \"^2.12.10\",\n \"npm-run-all\": \"^4.1.5\",\n \"openapi-typescript\": \"^7.9.1\",\n \"playwright\": \"^1.55.1\",\n \"react\": \"^18.3.1\",\n \"tsup\": \"^8.4.0\",\n \"typescript\": \"^5.4.5\",\n \"vitest\": \"^4.1.8\",\n \"vitest-browser-react\": \"^2.2.0\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"package.json\"\n ],\n \"keywords\": [\n \"e2b\",\n \"ai-agents\",\n \"agents\",\n \"ai\",\n \"code-interpreter\",\n \"sandbox\",\n \"code\",\n \"runtime\",\n \"vm\",\n \"nodejs\",\n \"javascript\",\n \"typescript\"\n ],\n \"dependencies\": {\n \"@bufbuild/protobuf\": \"^2.6.2\",\n \"@connectrpc/connect\": \"2.0.0-rc.3\",\n \"@connectrpc/connect-web\": \"2.0.0-rc.3\",\n \"chalk\": \"^5.3.0\",\n \"compare-versions\": \"^6.1.0\",\n \"dockerfile-ast\": \"^0.7.1\",\n \"glob\": \"^11.1.0\",\n \"openapi-fetch\": \"^0.14.1\",\n \"platform\": \"^1.3.6\",\n \"tar\": \"^7.5.16\",\n \"undici\": \"^7.28.0\"\n },\n \"engines\": {\n \"node\": \">=20.18.1\"\n },\n \"browserslist\": [\n \"defaults\"\n ]\n}\n","import platform from 'platform'\n\ndeclare let window: any\n\ntype Runtime =\n | 'node'\n | 'browser'\n | 'deno'\n | 'bun'\n | 'vercel-edge'\n | 'cloudflare-worker'\n | 'unknown'\n\nfunction getRuntime(): { runtime: Runtime; version: string } {\n // @ts-ignore\n if ((globalThis as any).Bun) {\n // @ts-ignore\n return { runtime: 'bun', version: globalThis.Bun.version }\n }\n\n // @ts-ignore\n if ((globalThis as any).Deno) {\n // @ts-ignore\n return { runtime: 'deno', version: globalThis.Deno.version.deno }\n }\n\n if ((globalThis as any).process?.release?.name === 'node') {\n return { runtime: 'node', version: platform.version || 'unknown' }\n }\n\n // @ts-ignore\n if (typeof EdgeRuntime === 'string') {\n return { runtime: 'vercel-edge', version: 'unknown' }\n }\n\n if ((globalThis as any).navigator?.userAgent === 'Cloudflare-Workers') {\n return { runtime: 'cloudflare-worker', version: 'unknown' }\n }\n\n if (typeof window !== 'undefined') {\n return { runtime: 'browser', version: platform.version || 'unknown' }\n }\n\n return { runtime: 'unknown', version: 'unknown' }\n}\n\nexport const { runtime, version: runtimeVersion } = getRuntime()\n\nexport async function sha256(data: string): Promise<string> {\n // Use WebCrypto API if available\n if (typeof crypto !== 'undefined') {\n const encoder = new TextEncoder()\n const dataBuffer = encoder.encode(data)\n const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer)\n const hashArray = new Uint8Array(hashBuffer)\n return btoa(String.fromCharCode(...hashArray))\n }\n\n // Use Node.js crypto if WebCrypto is not available\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const { createHash } = require('node:crypto')\n const hash = createHash('sha256').update(data, 'utf8').digest()\n return hash.toString('base64')\n}\n\nexport function timeoutToSeconds(timeout: number): number {\n return Math.ceil(timeout / 1000)\n}\n\nexport function dynamicRequire<T>(module: string): T {\n if (runtime === 'browser') {\n throw new Error('Browser runtime is not supported for require')\n }\n\n return require(module)\n}\n\nexport async function dynamicImport<T>(module: string): Promise<T> {\n if (runtime === 'browser') {\n throw new Error('Browser runtime is not supported for dynamic import')\n }\n\n // @ts-ignore\n return await import(module)\n}\n\n// Source: https://github.com/chalk/ansi-regex/blob/main/index.js\nfunction ansiRegex({ onlyFirst = false } = {}) {\n // Valid string terminator sequences are BEL, ESC\\, and 0x9c\n const ST = '(?:\\\\u0007|\\\\u001B\\\\u005C|\\\\u009C)'\n // OSC sequences only: ESC ] ... ST (non-greedy until the first ST)\n const osc = `(?:\\\\u001B\\\\][\\\\s\\\\S]*?${ST})`\n // CSI and related: ESC/C1, optional intermediates, optional params (supports ; and :) then final byte\n const csi =\n '[\\\\u001B\\\\u009B][[\\\\]()#;?]*(?:\\\\d{1,4}(?:[;:]\\\\d{0,4})*)?[\\\\dA-PR-TZcf-nq-uy=><~]'\n\n const pattern = `${osc}|${csi}`\n\n return new RegExp(pattern, onlyFirst ? undefined : 'g')\n}\n\nexport function stripAnsi(text: string): string {\n return text.replace(ansiRegex(), '')\n}\n\n/**\n * Convert data to a Blob, avoiding unnecessary conversions when possible.\n */\nexport function toBlob(\n data: string | ArrayBuffer | Blob | ReadableStream\n): Blob | Promise<Blob> {\n // Already a Blob - use directly\n if (data instanceof Blob) {\n return data\n }\n // String or ArrayBuffer - create Blob\n if (typeof data === 'string' || data instanceof ArrayBuffer) {\n return new Blob([data])\n }\n // ReadableStream - must consume to get Blob\n return new Response(data).blob()\n}\n\n// Characters that are safe to leave unquoted in a POSIX shell, matching the\n// set used by Python's shlex.quote (`[^\\w@%+=:,./-]` is considered unsafe).\nconst UNSAFE_SHELL_CHAR = /[^\\w@%+=:,./-]/\n\n/**\n * Quote a string for safe interpolation into a POSIX shell command.\n *\n * Faithful port of Python's `shlex.quote`: an empty string becomes `''`,\n * values containing only safe characters are returned unchanged (keeping\n * generated commands stable and cache-friendly), and anything else is wrapped\n * in single quotes with embedded single quotes escaped as `'\"'\"'`.\n */\nexport function shellQuote(s: string): string {\n if (s === '') {\n return \"''\"\n }\n if (!UNSAFE_SHELL_CHAR.test(s)) {\n return s\n }\n return \"'\" + s.replace(/'/g, \"'\\\"'\\\"'\") + \"'\"\n}\n\n/**\n * Prepare data for upload as a BodyInit, optionally gzip-compressed.\n *\n * Outside the browser, streams (and gzip-compressed data) are returned as\n * `ReadableStream` so they can be uploaded without buffering in memory.\n * Browsers don't support streaming request bodies, so data is buffered into\n * a Blob there.\n */\nexport async function toUploadBody(\n data: string | ArrayBuffer | Blob | ReadableStream,\n gzip?: boolean\n): Promise<BodyInit> {\n if (gzip) {\n const stream =\n data instanceof ReadableStream\n ? data\n : data instanceof Blob\n ? data.stream()\n : new Blob([data]).stream()\n const compressed = stream.pipeThrough(new CompressionStream('gzip'))\n return runtime === 'browser' ? new Response(compressed).blob() : compressed\n }\n\n if (data instanceof ReadableStream && runtime !== 'browser') {\n return data\n }\n\n return toBlob(data)\n}\n","/**\n * Simple FIFO semaphore used to cap the number of in-flight requests sent\n * through a fetch dispatcher.\n */\nclass Semaphore {\n private active = 0\n private readonly queue: Array<() => void> = []\n\n constructor(private readonly max: number) {}\n\n async acquire(signal?: AbortSignal): Promise<() => void> {\n if (signal?.aborted) throw abortReason(signal)\n if (this.active < this.max) {\n this.active++\n return () => this.release()\n }\n\n return new Promise<() => void>((resolve, reject) => {\n const onAcquire = () => {\n signal?.removeEventListener('abort', onAbort)\n this.active++\n resolve(() => this.release())\n }\n const onAbort = () => {\n const i = this.queue.indexOf(onAcquire)\n if (i >= 0) this.queue.splice(i, 1)\n reject(abortReason(signal))\n }\n this.queue.push(onAcquire)\n signal?.addEventListener('abort', onAbort, { once: true })\n })\n }\n\n private release() {\n this.active--\n const next = this.queue.shift()\n if (next) next()\n }\n}\n\nfunction abortReason(signal: AbortSignal | undefined): unknown {\n return signal?.reason ?? new DOMException('Aborted', 'AbortError')\n}\n\n/**\n * Wrap `fetcher` so at most `max` requests are in-flight at any time.\n * Subsequent requests are FIFO-queued inside the SDK process and dispatched\n * as earlier requests settle.\n *\n * NOTE: the slot is released as soon as `fetcher` resolves with the response\n * headers, not when the response body is fully consumed. This means the\n * effective concurrency can be higher than `max` while bodies are\n * still streaming.\n *\n * TODO: release on body end (consume/cancel/error) so the\n * SDK-level cap aligns with the dispatcher's connection accounting\n */\nexport function limitConcurrency(\n fetcher: typeof fetch,\n max: number\n): typeof fetch {\n if (!Number.isFinite(max) || max <= 0) {\n return fetcher\n }\n\n const sem = new Semaphore(max)\n\n return (async (input, init) => {\n const signal =\n init?.signal ?? (input instanceof Request ? input.signal : undefined)\n const release = await sem.acquire(signal)\n try {\n return await fetcher(input, init)\n } finally {\n release()\n }\n }) as typeof fetch\n}\n","export type UndiciRequestInit = RequestInit & {\n dispatcher?: unknown\n duplex?: 'half'\n}\n\nexport type UndiciModule = {\n Agent: new (options: { allowH2: true; connections?: number }) => unknown\n ProxyAgent: new (options: {\n uri: string\n allowH2: true\n connections?: number\n }) => unknown\n fetch: unknown\n}\n\nexport async function loadUndici(): Promise<UndiciModule | undefined> {\n try {\n // Keep this import opaque to bundlers. It must resolve as a package name\n // from the runtime environment, not as a path relative to this file.\n // eslint-disable-next-line no-new-func\n const importModule = new Function(\n 'moduleName',\n 'return import(moduleName)'\n ) as (moduleName: string) => Promise<UndiciModule>\n\n return await importModule('undici')\n } catch {\n return undefined\n }\n}\n\nexport function toUndiciRequestInput(\n input: RequestInfo | URL,\n init?: RequestInit\n): { input: RequestInfo | URL; init?: RequestInit & { duplex?: 'half' } } {\n if (!(input instanceof Request)) {\n return { input, init }\n }\n\n const requestInit: RequestInit & { duplex?: 'half' } = {\n body: input.body,\n cache: input.cache,\n credentials: input.credentials,\n headers: input.headers,\n integrity: input.integrity,\n keepalive: input.keepalive,\n method: input.method,\n mode: input.mode,\n redirect: input.redirect,\n referrer: input.referrer,\n referrerPolicy: input.referrerPolicy,\n signal: input.signal,\n ...init,\n }\n\n if (requestInit.body) {\n requestInit.duplex = 'half'\n }\n\n return {\n input: input.url,\n init: requestInit,\n }\n}\n","import { runtime } from '../utils'\nimport { parseInflightLimitEnv, parsePositiveIntEnv } from './metadata'\nimport { limitConcurrency } from './inflight'\nimport {\n loadUndici,\n toUndiciRequestInput,\n type UndiciModule,\n type UndiciRequestInit,\n} from '../undici'\n\nconst DEFAULT_API_CONNECTION_LIMIT = 100\n// 1000 = ~10 streams per connection (with the 100-conn default).\n// Override via env if your workload needs different.\nconst DEFAULT_API_INFLIGHT_LIMIT = 1000\n\n// Fetchers are cached per proxy so requests without a proxy keep sharing a\n// single dispatcher while each distinct proxy URL gets its own.\nconst apiFetchers = new Map<string, typeof fetch>()\n\nexport function createApiFetch(proxy?: string): typeof fetch {\n const key = proxy ?? ''\n\n const cached = apiFetchers.get(key)\n if (cached) {\n return cached\n }\n\n const apiFetch = createApiFetchForRuntime(runtime, { proxy })\n apiFetchers.set(key, apiFetch)\n\n return apiFetch\n}\n\nexport function createApiFetchForRuntime(\n currentRuntime = runtime,\n options: {\n connectionLimit?: number\n inflightLimit?: number\n proxy?: string\n loadUndici?: () => Promise<UndiciModule | undefined>\n } = {}\n): typeof fetch {\n if (currentRuntime !== 'node') {\n return fetch\n }\n\n let fetcherPromise: Promise<typeof fetch> | undefined\n\n return (async (input, init) => {\n fetcherPromise ??= buildApiFetcher(options)\n const fetcher = await fetcherPromise\n\n return fetcher(input, init)\n }) as typeof fetch\n}\n\nasync function buildApiFetcher(options: {\n connectionLimit?: number\n inflightLimit?: number\n proxy?: string\n loadUndici?: () => Promise<UndiciModule | undefined>\n}): Promise<typeof fetch> {\n const undici = await (options.loadUndici ?? loadUndici)()\n const inflightLimit = options.inflightLimit ?? getApiInflightLimit()\n\n if (!undici) {\n return limitConcurrency(fetch, inflightLimit)\n }\n\n const { Agent, ProxyAgent, fetch: undiciFetch } = undici\n const connections = options.connectionLimit ?? getApiConnectionLimit()\n const dispatcher = options.proxy\n ? new ProxyAgent({\n uri: options.proxy,\n allowH2: true,\n connections,\n })\n : new Agent({\n allowH2: true,\n connections,\n })\n const fetchWithDispatcher = undiciFetch as unknown as (\n input: RequestInfo | URL,\n init?: UndiciRequestInit\n ) => Promise<Response>\n\n const wrapped: typeof fetch = ((input, init) => {\n const request = toUndiciRequestInput(input, init)\n\n return fetchWithDispatcher(request.input, {\n ...request.init,\n dispatcher,\n })\n }) as typeof fetch\n\n return limitConcurrency(wrapped, inflightLimit)\n}\n\nexport function getApiConnectionLimit(): number {\n return parsePositiveIntEnv(\n 'E2B_API_CONNECTIONS',\n DEFAULT_API_CONNECTION_LIMIT\n )\n}\n\n/**\n * Returns the configured max number of API requests that can be in flight at\n * once, or `0` to disable the cap.\n *\n * Defaults to {@link DEFAULT_API_INFLIGHT_LIMIT} ({@link 1000}). Override via\n * `E2B_API_INFLIGHT_REQUESTS` env var; set to `0` to disable the cap entirely.\n */\nexport function getApiInflightLimit(): number {\n return parseInflightLimitEnv(\n 'E2B_API_INFLIGHT_REQUESTS',\n DEFAULT_API_INFLIGHT_LIMIT\n )\n}\n","// This is the message for the sandbox timeout error when the response code is 502/Unavailable\nexport function formatSandboxTimeoutError(message: string) {\n return new TimeoutError(\n `${message}: This error is likely due to sandbox timeout. You can modify the sandbox timeout by passing 'timeoutMs' when starting the sandbox or calling '.setTimeout' on the sandbox with the desired timeout.`\n )\n}\n\n/**\n * Base class for all sandbox errors.\n *\n * Thrown when general sandbox errors occur.\n */\nexport class SandboxError extends Error {\n constructor(message?: string, stackTrace?: string) {\n super(message)\n this.name = 'SandboxError'\n if (stackTrace) {\n this.stack = stackTrace\n }\n }\n}\n\n/**\n * Thrown when a timeout error occurs.\n *\n * The [unavailable] error type is caused by sandbox timeout.\n *\n * The [canceled] error type is caused by exceeding request timeout.\n *\n * The [deadline_exceeded] error type is caused by exceeding the timeout for command execution, watch, etc.\n *\n * The [unknown] error type is sometimes caused by the sandbox timeout when the request is not processed correctly.\n */\nexport class TimeoutError extends SandboxError {\n constructor(message: string, stackTrace?: string) {\n super(message, stackTrace)\n this.name = 'TimeoutError'\n }\n}\n\n/**\n * Thrown when an invalid argument is provided.\n */\nexport class InvalidArgumentError extends SandboxError {\n constructor(message: string, stackTrace?: string) {\n super(message, stackTrace)\n this.name = 'InvalidArgumentError'\n }\n}\n\n/**\n * Thrown when there is not enough disk space.\n */\nexport class NotEnoughSpaceError extends SandboxError {\n constructor(message: string, stackTrace?: string) {\n super(message, stackTrace)\n this.name = 'NotEnoughSpaceError'\n }\n}\n\n/**\n * Thrown when a resource is not found.\n *\n * @deprecated Use {@link FileNotFoundError} or {@link SandboxNotFoundError} instead. This class will be removed in the next major version.\n */\nexport class NotFoundError extends SandboxError {\n constructor(message: string, stackTrace?: string) {\n super(message, stackTrace)\n this.name = 'NotFoundError'\n }\n}\n\n/**\n * Thrown when a file or directory is not found inside a sandbox.\n */\nexport class FileNotFoundError extends NotFoundError {\n constructor(message: string, stackTrace?: string) {\n super(message, stackTrace)\n this.name = 'FileNotFoundError'\n }\n}\n\n/**\n * Thrown when a sandbox is not found (e.g. it doesn't exist or is no longer running).\n */\nexport class SandboxNotFoundError extends NotFoundError {\n constructor(message: string, stackTrace?: string) {\n super(message, stackTrace)\n this.name = 'SandboxNotFoundError'\n }\n}\n\n/**\n * Thrown when authentication fails.\n */\nexport class AuthenticationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'AuthenticationError'\n }\n}\n\n/**\n * Thrown when git authentication fails.\n */\nexport class GitAuthError extends AuthenticationError {\n constructor(message: string) {\n super(message)\n this.name = 'GitAuthError'\n }\n}\n\n/**\n * Thrown when git upstream tracking is missing.\n */\nexport class GitUpstreamError extends SandboxError {\n constructor(message: string, stackTrace?: string) {\n super(message, stackTrace)\n this.name = 'GitUpstreamError'\n }\n}\n\n/**\n * Thrown when the template uses old envd version. It isn't compatible with the new SDK.\n */\nexport class TemplateError extends SandboxError {\n constructor(message: string, stackTrace?: string) {\n super(message, stackTrace)\n this.name = 'TemplateError'\n }\n}\n\n/**\n * Thrown when the API rate limit is exceeded.\n */\nexport class RateLimitError extends SandboxError {\n constructor(message: string) {\n super(message)\n this.name = 'RateLimitError'\n }\n}\n\n/**\n * Thrown when the build fails.\n */\nexport class BuildError extends Error {\n constructor(message: string, stackTrace?: string) {\n super(message)\n this.name = 'BuildError'\n if (stackTrace) {\n this.stack = stackTrace\n }\n }\n}\n\n/**\n * Thrown when the file upload fails.\n */\nexport class FileUploadError extends BuildError {\n constructor(message: string, stackTrace?: string) {\n super(message, stackTrace)\n this.name = 'FileUploadError'\n }\n}\n\n/**\n * Base class for all volume errors.\n *\n * Thrown when general volume errors occur.\n */\nexport class VolumeError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'VolumeError'\n }\n}\n","import type { Interceptor } from '@connectrpc/connect'\nimport type { Middleware } from 'openapi-fetch'\n\n/**\n * Logger interface compatible with {@link console} used for logging Sandbox messages.\n */\nexport interface Logger {\n /**\n * Debug level logging method.\n */\n debug?: (...args: any[]) => void\n /**\n * Info level logging method.\n */\n info?: (...args: any[]) => void\n /**\n * Warn level logging method.\n */\n warn?: (...args: any[]) => void\n /**\n * Error level logging method.\n */\n error?: (...args: any[]) => void\n}\n\nfunction formatLog(log: any) {\n // Protobuf int64 fields are represented as bigint, which JSON.stringify\n // can't serialize without a replacer.\n return JSON.parse(\n JSON.stringify(log, (_, value) =>\n typeof value === 'bigint' ? value.toString() : value\n )\n )\n}\n\nexport function createRpcLogger(logger: Logger): Interceptor {\n async function* logEach(stream: AsyncIterable<any>) {\n for await (const m of stream) {\n logger.debug?.('Response stream:', formatLog(m))\n yield m\n }\n }\n\n return (next) => async (req) => {\n logger.info?.(`Request: POST ${req.url}`)\n\n const res = await next(req)\n if (res.stream) {\n return {\n ...res,\n message: logEach(res.message),\n }\n } else {\n logger.info?.('Response:', formatLog(res.message))\n }\n\n return res\n }\n}\n\nexport function createApiLogger(logger: Logger): Middleware {\n return {\n async onRequest({ request }) {\n logger.info?.(`Request ${request.method} ${request.url}`)\n return request\n },\n async onResponse({ response }) {\n if (response.status >= 400) {\n logger.error?.('Response:', response.status, response.statusText)\n } else {\n logger.info?.('Response:', response.status, response.statusText)\n }\n\n return response\n },\n }\n}\n","import { Logger } from './logs'\nimport { getEnvVar, version } from './api/metadata'\nimport { runtime } from './utils'\n\n// Remove once all deployments support sandbox subdomains\nconst supportedDomains = ['e2b.app', 'e2b.dev', 'e2b.pro', 'e2b-staging.dev']\n\nexport const REQUEST_TIMEOUT_MS = 60_000 // 60 seconds\nexport const DEFAULT_SANDBOX_TIMEOUT_MS = 300_000 // 300 seconds\nexport const KEEPALIVE_PING_INTERVAL_SEC = 50 // 50 seconds\n\nexport const KEEPALIVE_PING_HEADER = 'Keepalive-Ping-Interval'\n\n/**\n * Connection options for requests to the API.\n */\nexport interface ConnectionOpts {\n /**\n * E2B API key to use for authentication.\n *\n * @default E2B_API_KEY // environment variable\n */\n apiKey?: string\n /**\n * Whether to validate the format of the E2B API key on the client side.\n * Disable this when your deployment issues API keys that don't match the\n * default `e2b_` format.\n *\n * @default E2B_VALIDATE_API_KEY // environment variable or `true`\n */\n validateApiKey?: boolean\n /**\n * E2B access token to use for authentication.\n *\n * @deprecated Pass the token through `apiHeaders` instead, e.g.\n * `apiHeaders: { Authorization: \\`Bearer ${token}\\` }`.\n *\n * @default E2B_ACCESS_TOKEN // environment variable\n */\n accessToken?: string\n /**\n * Domain to use for the API.\n *\n * @default E2B_DOMAIN // environment variable or `e2b.app`\n */\n domain?: string\n /**\n * API Url to use for the API.\n * @internal\n * @default E2B_API_URL // environment variable or `https://api.${domain}`\n */\n apiUrl?: string\n /**\n * Sandbox Url to use for the API.\n * @internal\n * @default E2B_SANDBOX_URL // environment variable, `https://sandbox.${domain}`\n */\n sandboxUrl?: string\n /**\n * If true the SDK starts in the debug mode and connects to the local envd API server.\n * @internal\n * @default E2B_DEBUG // environment variable or `false`\n */\n debug?: boolean\n /**\n * Timeout for requests to the API in **milliseconds**.\n *\n * @default 60_000 // 60 seconds\n */\n requestTimeoutMs?: number\n /**\n * Logger to use for logging messages. It can accept any object that implements `Logger` interface—for example, {@link console}.\n */\n logger?: Logger\n\n /**\n * Additional headers to send with the request.\n *\n * @deprecated Use `apiHeaders` instead.\n */\n headers?: Record<string, string>\n\n /**\n * Proxy URL to use for requests. In case of a sandbox it applies to all\n * requests made to the returned sandbox.\n *\n * @example 'http://user:pass@127.0.0.1:8080'\n */\n proxy?: string\n\n /**\n * Additional headers to send with E2B API requests.\n */\n apiHeaders?: Record<string, string>\n\n /**\n * An optional `AbortSignal` that can be used to cancel the in-flight request.\n * When the signal is aborted, the underlying `fetch` is aborted and the\n * returned promise rejects with an `AbortError`.\n */\n signal?: AbortSignal\n}\n\n/**\n * Options accepted by `ConnectionConfig`.\n */\nexport interface ConnectionConfigOpts extends ConnectionOpts {\n /**\n * Integration wrapping the E2B SDK, appended to the `User-Agent`.\n *\n * @example 'e2b-code-interpreter/0.1.0'\n */\n integration?: string\n}\n\n/**\n * Build an `AbortSignal` that combines an optional request-timeout signal\n * (via `AbortSignal.timeout`) with an optional user-provided signal.\n *\n * Returns `undefined` when neither input would produce a signal.\n *\n * @internal\n */\nexport function buildRequestSignal(\n requestTimeoutMs: number | undefined,\n userSignal: AbortSignal | undefined\n): AbortSignal | undefined {\n // `0` (and `undefined`) disable the request timeout.\n const timeoutSignal = requestTimeoutMs\n ? AbortSignal.timeout(requestTimeoutMs)\n : undefined\n\n if (timeoutSignal && userSignal) {\n return AbortSignal.any([timeoutSignal, userSignal])\n }\n\n return timeoutSignal ?? userSignal\n}\n\n/**\n * Set up an internal `AbortController` for a streaming request.\n *\n * Until `clearStartTimeout` is called, the controller aborts when either\n * - the optional user signal aborts, or\n * - the optional request timeout elapses (used to bound the initial\n * handshake; long-lived streams should call `clearStartTimeout` once\n * the handshake succeeds).\n *\n * The user-signal listener stays attached for the full stream lifetime\n * so the caller can cancel a long-running stream by aborting the signal.\n *\n * `cleanup` is idempotent and detaches the listener, clears the handshake\n * timer (if still pending), and aborts the controller. Call it when the\n * stream finishes or when startup fails.\n *\n * @internal\n */\nexport function setupRequestController(\n requestTimeoutMs: number | undefined,\n userSignal: AbortSignal | undefined\n): {\n controller: AbortController\n clearStartTimeout: () => void\n cleanup: () => void\n} {\n const controller = new AbortController()\n\n const onUserAbort = () => controller.abort(userSignal?.reason)\n if (userSignal) {\n if (userSignal.aborted) {\n controller.abort(userSignal.reason)\n } else {\n userSignal.addEventListener('abort', onUserAbort, { once: true })\n }\n }\n\n let reqTimeout: ReturnType<typeof setTimeout> | undefined = requestTimeoutMs\n ? setTimeout(\n () =>\n controller.abort(\n new DOMException(\n `Request handshake timed out after ${requestTimeoutMs}ms`,\n 'TimeoutError'\n )\n ),\n requestTimeoutMs\n )\n : undefined\n\n const clearStartTimeout = () => {\n if (reqTimeout) {\n clearTimeout(reqTimeout)\n reqTimeout = undefined\n }\n }\n\n let cleaned = false\n const cleanup = () => {\n if (cleaned) return\n cleaned = true\n userSignal?.removeEventListener('abort', onUserAbort)\n clearStartTimeout()\n controller.abort()\n }\n\n return { controller, clearStartTimeout, cleanup }\n}\n\n/**\n * Create a resettable idle-timeout that aborts `controller` when no progress is\n * made within `idleTimeoutMs`. `arm` (re)starts the timer; call it on each\n * chunk. `clear` stops it. `0`/`undefined` disables it (both are no-ops).\n *\n * @internal\n */\nfunction createIdleAbort(\n controller: AbortController,\n idleTimeoutMs: number | undefined,\n label: string\n): { arm: () => void; clear: () => void } {\n let timer: ReturnType<typeof setTimeout> | undefined\n const clear = () => {\n if (timer) {\n clearTimeout(timer)\n timer = undefined\n }\n }\n const arm = () => {\n if (!idleTimeoutMs) return\n clear()\n timer = setTimeout(\n () =>\n controller.abort(\n new DOMException(\n `${label} idle for ${idleTimeoutMs}ms`,\n 'TimeoutError'\n )\n ),\n idleTimeoutMs\n )\n }\n return { arm, clear }\n}\n\n/**\n * Wrap a streaming response body so its pooled connection is released when the\n * stream is fully read, cancelled, errors, or stays idle for too long.\n *\n * Clears the handshake timeout from {@link setupRequestController} (so\n * consuming the body isn't killed by it) and replaces it with an idle-read\n * timeout that bounds only the wire: it's armed while waiting on a network\n * read and cleared the moment a chunk arrives, so a slow or paused consumer\n * never trips it (only a server that stops sending mid-stream does). On expiry\n * it aborts `controller`, tearing down the fetch and releasing the connection.\n * Pass `0`/`undefined` to disable. Call once the handshake has succeeded.\n *\n * @internal\n */\nexport function wrapStreamWithConnectionCleanup(\n body: ReadableStream<Uint8Array> | null,\n {\n clearStartTimeout,\n cleanup,\n controller,\n idleTimeoutMs,\n }: {\n clearStartTimeout: () => void\n cleanup: () => void\n controller: AbortController\n idleTimeoutMs?: number\n }\n): ReadableStream<Uint8Array> {\n clearStartTimeout()\n\n if (!body) {\n cleanup()\n return new Blob([]).stream()\n }\n\n const reader = body.getReader()\n const idle = createIdleAbort(controller, idleTimeoutMs, 'Stream')\n\n // Idempotent: safe to call from multiple stream callbacks.\n const release = () => {\n idle.clear()\n cleanup()\n }\n\n return new ReadableStream<Uint8Array>({\n async pull(streamController) {\n // Bound only the wire: arm before reading from the network and clear the\n // moment a chunk (or EOF) arrives, so a slow or paused consumer never\n // counts against the idle timeout. A consumer that holds the stream but\n // stops reading is never pulled here, so nothing arms—that case is\n // reclaimed server-side, not by this timer.\n idle.arm()\n try {\n const { done, value } = await reader.read()\n idle.clear()\n if (done) {\n release()\n streamController.close()\n } else {\n streamController.enqueue(value)\n }\n } catch (err) {\n release()\n streamController.error(err)\n }\n },\n async cancel(reason) {\n try {\n await reader.cancel(reason)\n } finally {\n release()\n }\n },\n })\n}\n\nfunction buildUserAgent(integration?: string) {\n const userAgentParts = [`e2b-js-sdk/${version}`]\n\n if (integration) {\n userAgentParts.push(integration)\n }\n\n return userAgentParts.join(' ')\n}\n\n/**\n * Configuration for connecting to the API.\n */\nexport class ConnectionConfig {\n public static envdPort = 49983\n\n readonly debug: boolean\n readonly domain: string\n readonly apiUrl: string\n readonly sandboxUrl?: string\n readonly logger?: Logger\n\n readonly requestTimeoutMs: number\n\n readonly apiKey?: string\n readonly validateApiKey: boolean\n /**\n * @deprecated Pass the token through `apiHeaders` instead.\n */\n readonly accessToken?: string\n\n readonly integration?: string\n\n readonly headers?: Record<string, string>\n\n readonly proxy?: string\n\n constructor(opts?: ConnectionConfigOpts) {\n this.apiKey = opts?.apiKey || ConnectionConfig.apiKey\n this.validateApiKey =\n opts?.validateApiKey ?? ConnectionConfig.validateApiKey\n this.debug = opts?.debug ?? ConnectionConfig.debug\n this.domain = opts?.domain || ConnectionConfig.domain\n this.accessToken = opts?.accessToken || ConnectionConfig.accessToken\n this.requestTimeoutMs = opts?.requestTimeoutMs ?? REQUEST_TIMEOUT_MS\n this.logger = opts?.logger\n this.integration = opts?.integration\n this.headers = { ...(opts?.headers ?? {}), ...(opts?.apiHeaders ?? {}) }\n this.headers['User-Agent'] = buildUserAgent(this.integration)\n this.proxy = opts?.proxy\n\n this.apiUrl =\n opts?.apiUrl ||\n ConnectionConfig.apiUrl ||\n (this.debug ? 'http://localhost:3000' : `https://api.${this.domain}`)\n\n this.sandboxUrl = opts?.sandboxUrl || ConnectionConfig.sandboxUrl\n }\n\n private static get domain() {\n return getEnvVar('E2B_DOMAIN') || 'e2b.app'\n }\n\n private static get apiUrl() {\n return getEnvVar('E2B_API_URL')\n }\n\n private static get sandboxUrl() {\n return getEnvVar('E2B_SANDBOX_URL')\n }\n\n private static get debug() {\n return (getEnvVar('E2B_DEBUG') || 'false').toLowerCase() === 'true'\n }\n\n private static get apiKey() {\n return getEnvVar('E2B_API_KEY')\n }\n\n private static get validateApiKey() {\n return (\n (getEnvVar('E2B_VALIDATE_API_KEY') || 'true').toLowerCase() !== 'false'\n )\n }\n\n private static get accessToken() {\n return getEnvVar('E2B_ACCESS_TOKEN')\n }\n\n getSignal(requestTimeoutMs?: number, signal?: AbortSignal) {\n return buildRequestSignal(requestTimeoutMs ?? this.requestTimeoutMs, signal)\n }\n\n getSandboxUrl(\n sandboxId: string,\n opts: { sandboxDomain: string; envdPort: number }\n ) {\n if (this.sandboxUrl) {\n return this.sandboxUrl\n }\n\n if (this.debug) {\n return `http://${this.getHost(sandboxId, opts.envdPort, opts.sandboxDomain)}`\n }\n\n const sandboxDomain = opts.sandboxDomain ?? this.domain\n // The stable sandbox host is only guaranteed for E2B prod; the various other hosted domains may not serve sandbox.<domain> yet and will follow up once those are updated.\n // Issue with cors from browser so holding off on using in browser as well.\n if (runtime !== 'browser' && supportedDomains.includes(sandboxDomain)) {\n return `https://sandbox.${sandboxDomain}`\n }\n\n return `https://${this.getHost(sandboxId, opts.envdPort, sandboxDomain)}`\n }\n\n getSandboxDirectUrl(\n sandboxId: string,\n opts: { sandboxDomain: string; envdPort: number }\n ) {\n if (this.sandboxUrl) {\n return this.sandboxUrl\n }\n\n if (this.debug) {\n return `http://${this.getHost(sandboxId, opts.envdPort, opts.sandboxDomain)}`\n }\n\n return `https://${this.getHost(sandboxId, opts.envdPort, opts.sandboxDomain)}`\n }\n\n getHost(sandboxId: string, port: number, sandboxDomain: string) {\n if (this.debug) {\n return `localhost:${port}`\n }\n\n return `${port}-${sandboxId}.${sandboxDomain ?? this.domain}`\n }\n}\n\n/**\n * User used for the operation in the sandbox.\n */\n\nexport const defaultUsername: Username = 'user'\nexport type Username = string\n","import { sha256 } from '../utils'\n\n/**\n * Get the URL signature for the specified path, operation and user.\n *\n * @param path Path to the file in the sandbox.\n *\n * @param operation File system operation. Can be either `read` or `write`.\n *\n * @param user Sandbox user.\n *\n * @param expirationInSeconds Optional signature expiration time in seconds.\n */\n\ninterface SignatureOpts {\n path: string\n operation: 'read' | 'write'\n user: string | undefined\n expirationInSeconds?: number\n envdAccessToken?: string\n}\n\nexport async function getSignature({\n path,\n operation,\n user,\n expirationInSeconds,\n envdAccessToken,\n}: SignatureOpts): Promise<{ signature: string; expiration: number | null }> {\n if (!envdAccessToken) {\n throw new Error(\n 'Access token is not set and signature cannot be generated!'\n )\n }\n\n // expiration is unix timestamp\n const signatureExpiration =\n expirationInSeconds != null\n ? Math.floor(Date.now() / 1000) + expirationInSeconds\n : null\n let signatureRaw: string\n\n // if user is undefined, set it to empty string to handle default user\n if (user == undefined) {\n user = ''\n }\n\n if (signatureExpiration === null) {\n signatureRaw = `${path}:${operation}:${user}:${envdAccessToken}`\n } else {\n signatureRaw = `${path}:${operation}:${user}:${envdAccessToken}:${signatureExpiration.toString()}`\n }\n\n const hashBase64 = await sha256(signatureRaw)\n const signature = 'v1_' + hashBase64.replace(/=+$/, '')\n\n return {\n signature: signature,\n expiration: signatureExpiration,\n }\n}\n","import {\n Client,\n Code,\n ConnectError,\n createClient,\n Transport,\n} from '@connectrpc/connect'\nimport {\n ConnectionConfig,\n ConnectionOpts,\n defaultUsername,\n KEEPALIVE_PING_HEADER,\n KEEPALIVE_PING_INTERVAL_SEC,\n setupRequestController,\n Username,\n wrapStreamWithConnectionCleanup,\n} from '../../connectionConfig'\n\nimport {\n checkSandboxHealth,\n handleEnvdApiError,\n handleEnvdApiFetchError,\n handleWatchDirStartEvent,\n} from '../../envd/api'\nimport {\n authenticationHeader,\n handleRpcErrorWithHealthCheck,\n SandboxHealthCheck,\n} from '../../envd/rpc'\n\nimport { EnvdApiClient } from '../../envd/api'\nimport {\n EntryInfo as FsEntryInfo,\n Filesystem as FilesystemService,\n FileType as FsFileType,\n} from '../../envd/filesystem/filesystem_pb'\n\nimport { FilesystemEvent, WatchHandle } from './watchHandle'\n\nimport type { Timestamp } from '@bufbuild/protobuf/wkt'\nimport { compareVersions } from 'compare-versions'\nimport {\n ENVD_DEFAULT_USER,\n ENVD_FILE_METADATA,\n ENVD_OCTET_STREAM_UPLOAD,\n ENVD_VERSION_FS_EVENT_ENTRY_INFO,\n ENVD_VERSION_RECURSIVE_WATCH,\n ENVD_VERSION_WATCH_NETWORK_MOUNTS,\n} from '../../envd/versions'\nimport {\n FileNotFoundError,\n InvalidArgumentError,\n TemplateError,\n} from '../../errors'\nimport { runtime, toBlob, toUploadBody } from '../../utils'\n\nconst FILESYSTEM_HTTP_ERROR_MAP: Record<number, (message: string) => Error> = {\n 404: (message: string) => new FileNotFoundError(message),\n}\n\nconst FILESYSTEM_RPC_ERROR_MAP: Partial<\n Record<Code, (message: string) => Error>\n> = {\n [Code.NotFound]: (message: string) => new FileNotFoundError(message),\n}\n\nasync function handleFilesystemRpcError(\n err: unknown,\n checkHealth?: SandboxHealthCheck\n): Promise<Error> {\n return handleRpcErrorWithHealthCheck(\n err,\n checkHealth,\n FILESYSTEM_RPC_ERROR_MAP\n )\n}\n\nfunction handleFilesystemEnvdApiError(res: {\n error?: { message?: string } | string\n response: Response\n}) {\n return handleEnvdApiError(res, FILESYSTEM_HTTP_ERROR_MAP)\n}\n\n/**\n * Sandbox filesystem object information.\n */\nexport interface WriteInfo {\n /**\n * Name of the filesystem object.\n */\n name: string\n /**\n * Type of the filesystem object.\n */\n type?: FileType\n /**\n * Path to the filesystem object.\n */\n path: string\n /**\n * User-defined metadata stored on the file as `user.e2b.*` extended\n * attributes. On writes this reflects the metadata supplied on upload; on\n * reads (`getInfo`, `list`, `rename`) it reflects any `user.e2b.*` xattr on\n * the file, including ones set out-of-band. `undefined` when none is set.\n */\n metadata?: Record<string, string>\n}\n\nexport interface EntryInfo extends WriteInfo {\n /**\n * Size of the filesystem object in bytes.\n */\n size: number\n\n /**\n * File mode and permission bits.\n */\n mode: number\n\n /**\n * String representation of file permissions (e.g. 'rwxr-xr-x').\n */\n permissions: string\n\n /**\n * Owner of the filesystem object.\n */\n owner: string\n\n /**\n * Group owner of the filesystem object.\n */\n group: string\n\n /**\n * Last modification time of the filesystem object.\n */\n modifiedTime?: Date\n\n /**\n * If the filesystem object is a symlink, this is the target of the symlink.\n */\n symlinkTarget?: string\n}\n\n/**\n * Sandbox filesystem object type.\n */\nexport enum FileType {\n /**\n * Filesystem object is a file.\n */\n FILE = 'file',\n /**\n * Filesystem object is a directory.\n */\n DIR = 'dir',\n}\n\nexport type WriteEntry = {\n path: string\n data: string | ArrayBuffer | Blob | ReadableStream\n}\n\nfunction mapFileType(fileType: FsFileType) {\n switch (fileType) {\n case FsFileType.DIRECTORY:\n return FileType.DIR\n case FsFileType.FILE:\n return FileType.FILE\n }\n}\n\nfunction mapModifiedTime(modifiedTime: Timestamp | undefined) {\n if (!modifiedTime) return undefined\n\n return new Date(\n Number(modifiedTime.seconds) * 1000 +\n Math.floor(modifiedTime.nanos / 1_000_000)\n )\n}\n\nfunction mapMetadata(\n metadata: Record<string, string> | undefined\n): Record<string, string> | undefined {\n if (!metadata) return undefined\n return Object.keys(metadata).length === 0 ? undefined : metadata\n}\n\nconst METADATA_HEADER_PREFIX = 'X-Metadata-'\n\n// Metadata keys travel