UNPKG

e2b

Version:

E2B SDK that give agents cloud environments

1 lines 446 kB
{"version":3,"sources":["../src/api/index.ts","../src/api/metadata.ts","../package.json","../src/utils.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/template/logger.ts","../src/template/utils.ts","../src/template/consts.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\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 returns empty string for error when response body is empty,\n // so we check !== undefined instead of truthiness\n if (response.error === undefined) {\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 = response.error?.message ?? response.error\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 requireAccessToken?: boolean\n requireApiKey?: boolean\n } = { requireAccessToken: false, requireApiKey: false }\n ) {\n if (opts?.requireApiKey && !config.apiKey) {\n throw new AuthenticationError(\n 'API key is required, please visit the Team tab at https://e2b.dev/dashboard 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 (opts?.requireAccessToken && !config.accessToken) {\n throw new AuthenticationError(\n 'Access token is required, please visit the Personal tab at https://e2b.dev/dashboard to get your access token. ' +\n 'You can set the environment variable `E2B_ACCESS_TOKEN` or pass the `accessToken` in options.'\n )\n }\n\n this.api = createClient<paths>({\n baseUrl: config.apiUrl,\n fetch: createApiFetch(),\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 \"name\": \"e2b\",\n \"version\": \"2.24.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\": \"eslint 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 \"@typescript-eslint/eslint-plugin\": \"^7.11.0\",\n \"@typescript-eslint/parser\": \"^7.11.0\",\n \"@vitejs/plugin-react\": \"^4.3.4\",\n \"@vitest/browser\": \"^3.2.4\",\n \"dotenv\": \"^16.4.5\",\n \"eslint\": \"^8.57.1\",\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\": \"^3.2.4\",\n \"vitest-browser-react\": \"^0.1.1\"\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.11\",\n \"undici\": \"^7.25.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\nexport async function wait(ms: number) {\n return new Promise((resolve) => setTimeout(resolve, ms))\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/**\n * Escape a string for safe inclusion in a single-quoted shell argument.\n * Equivalent to Python's shlex.quote().\n */\nexport function shellQuote(s: string): string {\n return \"'\" + s.replace(/'/g, \"'\\\\''\") + \"'\"\n}\n\n/**\n * Prepare data for upload as a BodyInit, optionally gzip-compressed.\n * When gzip is enabled, compresses the data and returns a Blob.\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 new Response(compressed).blob()\n }\n\n return toBlob(data)\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 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 {\n loadUndici,\n toUndiciRequestInput,\n type UndiciModule,\n type UndiciRequestInit,\n} from '../undici'\n\nconst API_CONNECTION_LIMIT = 100\nconst API_UNDICI_FALLBACK_WARNING =\n 'Failed to load undici for API HTTP/2 transport; falling back to global fetch.'\n\nlet apiFetch: typeof fetch | undefined\nlet hasWarnedUndiciFallback = false\n\nexport function createApiFetch(): typeof fetch {\n if (apiFetch) {\n return apiFetch\n }\n\n apiFetch = createApiFetchForRuntime(runtime)\n\n return apiFetch\n}\n\nexport function createApiFetchForRuntime(\n currentRuntime = runtime,\n options: {\n connectionLimit?: number\n loadUndici?: () => Promise<UndiciModule | undefined>\n } = { connectionLimit: API_CONNECTION_LIMIT }\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 loadUndici?: () => Promise<UndiciModule | undefined>\n}): Promise<typeof fetch> {\n const undici = await (options.loadUndici ?? loadUndici)()\n\n if (!undici) {\n warnUndiciFallback()\n\n return fetch\n }\n\n const { Agent, fetch: undiciFetch } = undici\n const dispatcher = new Agent({\n allowH2: true,\n connections: options.connectionLimit,\n })\n const fetchWithDispatcher = undiciFetch as unknown as (\n input: RequestInfo | URL,\n init?: UndiciRequestInit\n ) => Promise<Response>\n\n return ((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\nfunction warnUndiciFallback() {\n if (hasWarnedUndiciFallback) {\n return\n }\n\n hasWarnedUndiciFallback = true\n console.warn(API_UNDICI_FALLBACK_WARNING)\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 return JSON.parse(JSON.stringify(log))\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'\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 * E2B access token to use for authentication.\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 or `https://${port}-${sandboxID}.${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 headers?: 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 * 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 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 * 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 accessToken?: string\n\n readonly headers?: Record<string, string>\n\n constructor(opts?: ConnectionOpts) {\n this.apiKey = opts?.apiKey || ConnectionConfig.apiKey\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.headers = opts?.headers || {}\n this.headers['User-Agent'] = `e2b-js-sdk/${version}`\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 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 return `${this.debug ? 'http' : '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 = expirationInSeconds\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} from '../../connectionConfig'\n\nimport { handleEnvdApiError, handleWatchDirStartEvent } from '../../envd/api'\nimport { authenticationHeader, handleRpcError } from '../../envd/rpc'\n\nimport { EnvdApiClient } from '../../envd/api'\nimport {\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_OCTET_STREAM_UPLOAD,\n ENVD_VERSION_RECURSIVE_WATCH,\n} from '../../envd/versions'\nimport {\n FileNotFoundError,\n InvalidArgumentError,\n TemplateError,\n} from '../../errors'\nimport { 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\nfunction handleFilesystemRpcError(err: unknown): Error {\n return handleRpcError(err, FILESYSTEM_RPC_ERROR_MAP)\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\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\n/**\n * Options for the sandbox filesystem operations.\n */\nexport interface FilesystemRequestOpts\n extends Partial<Pick<ConnectionOpts, 'requestTimeoutMs' | 'signal'>> {\n /**\n * User to use for the operation in the sandbox.\n * This affects the resolution of relative paths and ownership of the created filesystem objects.\n */\n user?: Username\n}\n\n/**\n * Options for writing files to the sandbox filesystem.\n */\nexport interface FilesystemWriteOpts extends FilesystemRequestOpts {\n /**\n * When true, the upload will be gzip-compressed.\n */\n gzip?: boolean\n /**\n * When true, the upload uses `application/octet-stream` instead of `multipart/form-data`.\n *\n * Defaults to `false`. Requires envd 0.5.7 or later — when not supported by\n * the sandbox's envd version, the upload falls back to `multipart/form-data`.\n */\n useOctetStream?: boolean\n}\n\n/**\n * Options for reading files from the sandbox filesystem.\n */\nexport interface FilesystemReadOpts extends FilesystemRequestOpts {\n /**\n * When true, the download will request gzip-encoded responses.\n */\n gzip?: boolean\n}\n\nexport interface FilesystemListOpts extends FilesystemRequestOpts {\n /**\n * Depth of the directory to list.\n */\n depth?: number\n}\n\n/**\n * Options for watching a directory.\n */\nexport interface WatchOpts extends FilesystemRequestOpts {\n /**\n * Timeout for the watch operation in **milliseconds**.\n * You can pass `0` to disable the timeout.\n *\n * @default 60_000 // 60 seconds\n */\n timeoutMs?: number\n /**\n * Callback to call when the watch operation stops.\n */\n onExit?: (err?: Error) => void | Promise<void>\n /**\n * Watch the directory recursively\n */\n recursive?: boolean\n}\n\n/**\n * Module for interacting with the sandbox filesystem.\n */\nexport class Filesystem {\n private readonly rpc: Client<typeof FilesystemService>\n\n private readonly defaultWatchTimeout = 60_000 // 60 seconds\n private readonly defaultWatchRecursive = false\n\n constructor(\n transport: Transport,\n private readonly envdApi: EnvdApiClient,\n private readonly connectionConfig: ConnectionConfig\n ) {\n this.rpc = createClient(FilesystemService, transport)\n }\n\n /**\n * Read file content as a `string`.\n *\n * You can pass `text`, `bytes`, `blob`, or `stream` to `opts.format` to change the return type.\n *\n * @param path path to the file.\n * @param opts connection options.\n * @param [opts.format] format of the file content—`text` by default.\n *\n * @returns file content as string\n */\n async read(\n path: string,\n opts?: FilesystemReadOpts & { format?: 'text' }\n ): Promise<string>\n /**\n * Read file content as a `Uint8Array`.\n *\n * You can pass `text`, `bytes`, `blob`, or `stream` to `opts.format` to change the return type.\n *\n * @param path path to the file.\n * @param opts connection options.\n * @param [opts.format] format of the file content—`bytes`.\n *\n * @returns file content as `Uint8Array`\n */\n async read(\n path: string,\n opts?: FilesystemReadOpts & { format: 'bytes' }\n ): Promise<Uint8Array>\n /**\n * Read file content as a `Blob`.\n *\n * You can pass `text`, `bytes`, `blob`, or `stream` to `opts.format` to change the return type.\n *\n * @param path path to the file.\n * @param opts connection options.\n * @param [opts.format] format of the file content—`blob`.\n *\n * @returns file content as `Blob`\n */\n async read(\n path: string,\n opts?: FilesystemReadOpts & { format: 'blob' }\n ): Promise<Blob>\n /**\n * Read file content as a `ReadableStream`.\n *\n * You can pass `text`, `bytes`, `blob`, or `stream` to `opts.format` to change the return type.\n *\n * @param path path to the file.\n * @param opts connection options.\n * @param [opts.format] format of the file content—`stream`.\n *\n * @returns file content as `ReadableStream`\n */\n async read(\n path: string,\n opts?: FilesystemReadOpts & { format: 'stream' }\n ): Promise<ReadableStream<Uint8Array>>\n async read(\n path: string,\n opts?: FilesystemReadOpts & {\n format?: 'text' | 'bytes' | 'blob' | 'stream'\n }\n ): Promise<unknown> {\n const format = opts?.format ?? 'text'\n\n let user = opts?.user\n if (\n user == undefined &&\n compareVersions(this.envdApi.version, ENVD_DEFAULT_USER) < 0\n ) {\n user = defaultUsername\n }\n\n const headers: Record<string, string> = {}\n if (opts?.gzip) {\n headers['Accept-Encoding'] = 'gzip'\n }\n\n const res = await this.envdApi.api.GET('/files', {\n params: {\n query: {\n path,\n username: user,\n },\n },\n parseAs: format === 'bytes' ? 'arrayBuffer' : format,\n signal: this.connectionConfig.getSignal(\n opts?.requestTimeoutMs,\n opts?.signal\n ),\n headers,\n })\n\n const err = await handleFilesystemEnvdApiError(res)\n if (err) {\n throw err\n }\n\n if (format === 'bytes') {\n return new Uint8Array(res.data as ArrayBuffer)\n }\n\n // When the file is empty, res.data is parsed as `{}`. This is a workaround to return an empty string.\n if (res.response.headers.get('content-length') === '0') {\n return ''\n }\n\n return res.data\n }\n\n /**\n * Write content to a file.\n *\n *\n * Writing to a file that doesn't exist creates the file.\n *\n * Writing to a file that already exists overwrites the file.\n *\n * Writing to a file at path that doesn't exist creates the necessary directories.\n *\n * @param path path to file.\n * @param data data to write to the file. Data can be a string, `ArrayBuffer`, `Blob`, or `ReadableStream`.\n * @param opts connection options.\n *\n * @returns information about the written file\n */\n async write(\n path: string,\n data: string | ArrayBuffer | Blob | ReadableStream,\n opts?: FilesystemWriteOpts\n ): Promise<WriteInfo>\n async write(\n files: WriteEntry[],\n opts?: FilesystemWriteOpts\n ): Promise<WriteInfo[]>\n async write(\n pathOrFiles: string | WriteEntry[],\n dataOrOpts?:\n | string\n | ArrayBuffer\n | Blob\n | ReadableStream\n | FilesystemWriteOpts,\n opts?: FilesystemWriteOpts\n ): Promise<WriteInfo | WriteInfo[]> {\n if (typeof pathOrFiles !== 'string' && !Array.isArray(pathOrFiles)) {\n throw new Error('Path or files are required')\n }\n\n if (typeof pathOrFiles === 'string' && Array.isArray(dataOrOpts)) {\n throw new Error(\n 'Cannot specify both path and array of files. You have to specify either path and data for a single file or an array for multiple files.'\n )\n }\n\n const { path, writeOpts, writeFiles } =\n typeof pathOrFiles === 'string'\n ? {\n path: pathOrFiles,\n writeOpts: opts as FilesystemWriteOpts,\n writeFiles: [\n {\n data: dataOrOpts as\n | string\n | ArrayBuffer\n | Blob\n | ReadableStream,\n },\n ],\n }\n : {\n path: undefined,\n writeOpts: dataOrOpts as FilesystemWriteOpts,\n writeFiles: pathOrFiles as WriteEntry[],\n }\n\n if (writeFiles.length === 0) return [] as WriteInfo[]\n\n let user = writeOpts?.user\n if (\n user == undefined &&\n compareVersions(this.envdApi.version, ENVD_DEFAULT_USER) < 0\n ) {\n user = defaultUsername\n }\n\n const supportsOctetStream =\n compareVersions(this.envdApi.version, ENVD_OCTET_STREAM_UPLOAD) >= 0\n const useOctetStream =\n (writeOpts?.useOctetStream ?? false) && supportsOctetStream\n\n const results: WriteInfo[] = []\n\n const useGzip = writeOpts?.gzip === true\n\n if (useOctetStream) {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/octet-stream',\n }\n if (useGzip) {\n headers['Content-Encoding'] = 'gzip'\n }\n\n const uploadResults = await Promise.all(\n writeFiles.map(async (file) => {\n const filePath = path ?? (file as WriteEntry).path\n const body = await toUploadBody(file.data, useGzip)\n\n const res = await this.envdApi.api.POST('/files', {\n params: {\n query: {\n path: filePath,\n username: user,\n },\n },\n bodySerializer: () => body,\n headers,\n signal: this.connectionConfig.getSignal(\n writeOpts?.requestTimeoutMs,\n writeOpts?.signal\n ),\n body: {},\n })\n\n const err = await handleFilesystemEnvdApiError(res)\n if (err) {\n throw err\n }\n\n const files = res.data as WriteInfo[]\n if (!files || files.length === 0) {\n throw new Error(\n 'Expected to receive information about written file'\n )\n }\n\n return files\n })\n )\n\n for (const files of uploadResults) {\n results.push(...files)\n }\n } else {\n const formData = new FormData()\n for (const file of writeFiles) {\n formData.append(\n 'file',\n await toBlob(file.data),\n (file as WriteEntry).path ?? path!\n )\n }\n\n const res = await this.envdApi.api.POST('/files', {\n params: {\n query: {\n path,\n username: user,\n },\n },\n bodySerializer: () => formData,\n signal: this.connectionConfig.getSignal(\n writeOpts?.requestTimeoutMs,\n writeOpts?.signal\n ),\n body: {},\n })\n\n const err = await handleFilesystemEnvdApiError(res)\n if (err) {\n throw err\n }\n\n const files = res.data as WriteInfo[]\n if (!files || files.length === 0) {\n throw new Error('Expected to receive information about written file')\n }\n\n results.push(...files)\n }\n\n return results.length === 1 && path ? results[0] : results\n }\n\n /**\n * Write multiple files.\n *\n *\n * Writing to a file that doesn't exist creates the file.\n *\n * Writing to a file that already exists overwrites the file.\n *\n * Writing to a file at path that doesn't exist creates the necessary directories.\n *\n * @param files list of files to write as `WriteEntry` objects, each containing `path` and `data`.\n * @param opts connection options.\n *\n * @returns information about the written files\n */\n async writeFiles(\n files: WriteEntry[],\n opts?: FilesystemWriteOpts\n ): Promise<WriteInfo[]> {\n return this.write(files, opts) as Promise<WriteInfo[]>\n }\n\n /**\n * List entries in a directory.\n *\n * @param path path to the directory.\n * @param opts connection options.\n *\n * @returns list of entries in the sandbox filesystem directory.\n */\n async list(path: string, opts?: FilesystemListOpts): Promise<EntryInfo[]> {\n if (typeof opts?.depth === 'number' && opts.depth < 1) {\n throw new InvalidArgumentError('depth should be at least one')\n }\n\n try {\n const res = await this.rpc.listDir(\n {\n path,\n depth: opts?.depth ?? 1,\n },\n {\n headers: authenticationHeader(this.envdApi.version, opts?.user),\n signal: this.connectionConfig.getSignal(\n opts?.requestTimeoutMs,\n opts?.signal\n ),\n }\n )\n\n const entries: EntryInfo[] = []\n\n for (const e of res.entries) {\n const type = mapFileType(e.type)\n\n if (type) {\n entries.push({\n name: e.name,\n type,\n path: e.path,\n size: Number(e.size),\n mode: e.mode,\n permissions: e.permissions,\n owner: e.owner,\n group: e.group,\n modifiedTime: mapModifiedTime(e.modifiedTime),\n symlinkTarget: e.symlinkTarget,\n })\n }\n }\n\n return entries\n } catch (err) {\n throw handleFilesystemRpcError(err)\n }\n }\n\n /**\n * Create a new directory and all directories along the way if needed on the specified path.\n *\n * @param path path to a new directory. For example '/dirA/dirB' when creating 'dirB'.\n * @param opts connection options.\n *\n * @returns `true` if the directory was created, `false` if it already exists.\n */\n async makeDir(path: string, opts?: FilesystemRequestOpts): Promise<boolean> {\n try {\n await this.rpc.makeDir(\n { path },\n {\n headers: authenticationHeader(this.envdApi.version, opts?.user),\n signal: this.connectionConfig.getSignal(\n opts?.requestTimeoutMs,\n opts?.signal\n ),\n }\n )\n\n return true\n } catch (err) {\n if (err instanceof ConnectError) {\n if (err.code === Code.AlreadyExists) {\n return false\n }\n }\n\n throw handleFilesystemRpcError(err)\n }\n }\n\n /**\n * Rename a file or directory.\n *\n * @param oldPath path to the file or directory to rename.\n * @param newPath new path for the file or directory.\n * @param opts connection options.\n *\n * @returns information about renamed file or directory.\n */\n async rename(\n oldPath: string,\n newPath: string,\n opts?: FilesystemRequestOpts\n ): Promise<EntryInfo> {\n try {\n const res = await this.rpc.move(\n {\n source: