withruntime 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/proxy.js ADDED
@@ -0,0 +1,178 @@
1
+ /* Proxies named by the environment: HTTPS_PROXY, HTTP_PROXY and NO_PROXY, in
2
+ lower or upper case, the lower winning when both are set (as curl reads
3
+ them). Bun's fetch reads them itself; its WebSocket and Node's fetch and
4
+ WebSocket do not unless NODE_USE_ENV_PROXY was set before the process
5
+ started, which a library cannot do for its host. So every call the SDK and
6
+ the CLI make goes through `envFetch` and `openWebSocket`, which route by the
7
+ same rule on every runtime:
8
+
9
+ - an https:// or wss:// address uses HTTPS_PROXY; an http:// or ws:// one
10
+ uses HTTP_PROXY. HTTP_PROXY is never used for HTTPS (curl, Bun, Python
11
+ and Go agree; Node's own env support would fall back to it). A connection
12
+ error says so when that is why nothing was tried.
13
+ - NO_PROXY is a list of host names split by commas or spaces. A name
14
+ covers its subdomains, a leading dot is allowed, host:port limits it to
15
+ one port, and `*` alone means every host.
16
+ - A proxy address without a scheme is http://. Only http:// and https://
17
+ proxies are spoken to.
18
+
19
+ On Node the proxy is undici's ProxyAgent: undici is Node's own HTTP client
20
+ (the code under its fetch and WebSocket), loaded only when a proxy applies.
21
+ On Bun the proxy is passed to fetch and WebSocket as their `proxy` option. */
22
+ import { RuntimeError } from "./errors.js";
23
+ const processEnv = () => (typeof process === "undefined" ? {} : process.env);
24
+ function variable(env, name) {
25
+ for (const candidate of [name.toLowerCase(), name.toUpperCase()]) {
26
+ const value = env[candidate]?.trim();
27
+ if (value)
28
+ return { value, name: candidate };
29
+ }
30
+ return undefined;
31
+ }
32
+ /** Whether a NO_PROXY list names this host (and port). Returns the entry. */
33
+ export function exemption(list, hostname, port) {
34
+ const host = hostname
35
+ .toLowerCase()
36
+ .replace(/^\[|\]$/g, "")
37
+ .replace(/\.$/, "");
38
+ for (const raw of list.split(/[\s,]+/)) {
39
+ const entry = raw.trim().toLowerCase();
40
+ if (!entry)
41
+ continue;
42
+ if (entry === "*")
43
+ return entry;
44
+ const match = /^(.*?)(?::(\d+))?$/.exec(entry.replace(/^\[(.*)\]/, "$1"));
45
+ const name = match[1].replace(/^\./, "").replace(/\.$/, "");
46
+ if (!name || (match[2] && match[2] !== port))
47
+ continue;
48
+ if (host === name || host.endsWith(`.${name}`))
49
+ return entry;
50
+ }
51
+ return undefined;
52
+ }
53
+ /** The route the SDK takes to `target`, read from the environment now. */
54
+ export function routeFor(target, env = processEnv()) {
55
+ const url = new URL(target);
56
+ const secure = url.protocol === "https:" || url.protocol === "wss:";
57
+ const port = url.port || (secure ? "443" : "80");
58
+ const found = variable(env, secure ? "https_proxy" : "http_proxy");
59
+ if (!found) {
60
+ const http = secure ? variable(env, "http_proxy") : undefined;
61
+ return http ? { httpOnly: http.name } : {};
62
+ }
63
+ const skip = variable(env, "no_proxy");
64
+ const exempt = skip && exemption(skip.value, url.hostname, port);
65
+ if (skip && exempt)
66
+ return { exempt, variable: skip.name };
67
+ const proxy = /^[a-z][a-z0-9+.-]*:\/\//i.test(found.value)
68
+ ? found.value
69
+ : `http://${found.value}`;
70
+ let parsed;
71
+ try {
72
+ parsed = new URL(proxy);
73
+ }
74
+ catch {
75
+ throw invalidProxy(found.name, found.value, "is not an address");
76
+ }
77
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
78
+ throw invalidProxy(found.name, `${parsed.protocol}//${parsed.host}`, "is not an HTTP proxy");
79
+ return { proxy, shown: `${parsed.protocol}//${parsed.host}`, variable: found.name };
80
+ }
81
+ function invalidProxy(name, shown, problem) {
82
+ return new RuntimeError({
83
+ message: `${name} (${shown}) ${problem}; Runtime connects through http:// and https:// proxies.`,
84
+ code: "invalid_proxy",
85
+ status: 0,
86
+ hint: `Set ${name} to the proxy's address, for example http://proxy.example.com:3128, or unset it.`,
87
+ });
88
+ }
89
+ /** Words for a failed connection: the route it took, and what to check.
90
+ * `cause` is the error fetch threw, or the status a proxy answered with (Bun's
91
+ * fetch hands a refused tunnel back as a response). */
92
+ export function describeRoute(target, cause, env = processEnv()) {
93
+ let route;
94
+ try {
95
+ route = routeFor(target, env);
96
+ }
97
+ catch {
98
+ return { via: "" };
99
+ }
100
+ const url = new URL(target);
101
+ const port = url.port || (url.protocol === "https:" ? "443" : "80");
102
+ if (route.proxy) {
103
+ const status = typeof cause === "number" ? cause : proxyStatus(cause);
104
+ return {
105
+ via: ` through the proxy ${route.shown} (${route.variable})${status ? `, which answered HTTP ${status} to the tunnel request` : ""}`,
106
+ hint: status === 407
107
+ ? `The proxy wants credentials: put them in ${route.variable}, as http://user:password@host:port.`
108
+ : `Check that the proxy is running and allows CONNECT to ${url.hostname}:${port}, or list ${url.hostname} in NO_PROXY to connect directly.`,
109
+ };
110
+ }
111
+ if (route.exempt)
112
+ return {
113
+ via: ` (directly: ${route.variable} lists ${route.exempt})`,
114
+ hint: `Check the network, or remove ${route.exempt} from ${route.variable} to go through the proxy.`,
115
+ };
116
+ if (route.httpOnly)
117
+ return {
118
+ via: " (directly: no HTTPS proxy is set)",
119
+ hint: `${route.httpOnly} is set but HTTPS_PROXY is not, and ${url.hostname} is HTTPS. Set HTTPS_PROXY to the proxy to use it.`,
120
+ };
121
+ return { via: "" };
122
+ }
123
+ /** The status a proxy refused a tunnel with, from undici's or Bun's error. */
124
+ function proxyStatus(cause) {
125
+ for (let error = cause, depth = 0; error && depth < 5; depth++) {
126
+ const message = error.message;
127
+ const text = typeof message === "string" ? message : "";
128
+ const found = /proxy response \((\d{3})\)|proxy.*?\b(40[37]|50[0-9])\b/i.exec(text);
129
+ if (found)
130
+ return Number(found[1] ?? found[2]);
131
+ error = error.cause;
132
+ }
133
+ return undefined;
134
+ }
135
+ const isBun = () => typeof process !== "undefined" && !!process.versions?.bun;
136
+ const isNode = () => typeof process !== "undefined" &&
137
+ !!process.versions?.node &&
138
+ !process.versions.bun &&
139
+ !("Deno" in globalThis);
140
+ const agents = new Map();
141
+ /** One undici ProxyAgent per proxy address, so connections are reused. */
142
+ function undici(proxy) {
143
+ let agent = agents.get(proxy);
144
+ if (!agent) {
145
+ agent = import("undici").then((module) => ({
146
+ fetch: module.fetch,
147
+ WebSocket: module.WebSocket,
148
+ dispatcher: new module.ProxyAgent({ uri: proxy }),
149
+ }));
150
+ agent.catch(() => agents.delete(proxy));
151
+ agents.set(proxy, agent);
152
+ }
153
+ return agent;
154
+ }
155
+ /** fetch, through the proxy the environment names for the address. */
156
+ export const envFetch = (async (input, init) => {
157
+ const target = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
158
+ const { proxy } = routeFor(target);
159
+ if (proxy && isBun())
160
+ return fetch(input, { ...init, proxy });
161
+ if (proxy && isNode()) {
162
+ const { fetch: proxied, dispatcher } = await undici(proxy);
163
+ return (await proxied(target, { ...init, dispatcher }));
164
+ }
165
+ return fetch(input, init);
166
+ });
167
+ /** A WebSocket, through the proxy the environment names for the address. */
168
+ export async function openWebSocket(url, headers) {
169
+ const { proxy } = routeFor(url);
170
+ if (proxy && isNode()) {
171
+ const { WebSocket: Proxied, dispatcher } = await undici(proxy);
172
+ return new Proxied(url, { headers, dispatcher });
173
+ }
174
+ const Socket = globalThis.WebSocket;
175
+ if (!Socket)
176
+ throw new Error("This runtime has no WebSocket; use Node 22+ or Bun.");
177
+ return new Socket(url, proxy && isBun() ? { headers, proxy } : { headers });
178
+ }
package/dist/sandbox.d.ts CHANGED
@@ -74,17 +74,20 @@ export declare class Sandbox implements AsyncDisposable {
74
74
  /** Copies of this sandbox as it is now (files, memory, running processes),
75
75
  * each its own sandbox, answered once they run. A running sandbox is paused
76
76
  * for the moment its snapshot takes, then woken. One without `count`; an
77
- * array with it. */
77
+ * array with it. `funding` is what the copies run on, as for a create;
78
+ * omitted, they keep this sandbox's. */
78
79
  fork(options?: {
79
80
  name?: string;
80
81
  labels?: Record<string, string>;
81
82
  keepSnapshot?: boolean;
83
+ funding?: "trial" | "paid";
82
84
  } & RequestOptions): Promise<Sandbox>;
83
85
  fork(options: {
84
86
  count: number;
85
87
  name?: string;
86
88
  labels?: Record<string, string>;
87
89
  keepSnapshot?: boolean;
90
+ funding?: "trial" | "paid";
88
91
  } & RequestOptions): Promise<Sandbox[]>;
89
92
  /** Starts a stopped persistent sandbox again from its disk (memory is not kept). */
90
93
  restart(options?: RequestOptions & {
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.3.1";
2
+ export declare const VERSION = "0.4.0";
3
3
  export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
4
4
  export type RequestOptions = {
5
5
  /** Sent as Idempotency-Key. Omit it: the SDK makes one per call and keeps
package/dist/transport.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ConnectionError, errorFor, RuntimeError } from "./errors.js";
2
- export const VERSION = "0.3.1";
2
+ import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
3
+ export const VERSION = "0.4.0";
3
4
  export const DEFAULT_BASE_URL = "https://api.withruntime.com";
4
5
  export function apiOrigin(value) {
5
6
  const url = new URL(value);
@@ -83,6 +84,8 @@ export class Transport {
83
84
  #apiKey;
84
85
  #findKey;
85
86
  #fetch;
87
+ /** Whether calls go through envFetch, so a failure can name its proxy. */
88
+ #routed;
86
89
  #timeoutMs;
87
90
  #maxRetries;
88
91
  #client;
@@ -95,7 +98,10 @@ export class Transport {
95
98
  else
96
99
  this.#apiKey = options.apiKey;
97
100
  this.baseUrl = apiOrigin(options.baseUrl ?? DEFAULT_BASE_URL);
98
- this.#fetch = options.fetch ?? fetch;
101
+ // The environment's proxy (HTTPS_PROXY, NO_PROXY) on Node and Bun alike;
102
+ // a fetch passed in is used as it is.
103
+ this.#fetch = options.fetch ?? envFetch;
104
+ this.#routed = options.fetch === undefined;
99
105
  this.#timeoutMs = options.timeoutMs ?? 300_000;
100
106
  this.#maxRetries = options.maxRetries ?? 4;
101
107
  this.#client = options.client ?? `sdk-js/${VERSION}`;
@@ -139,36 +145,65 @@ export class Transport {
139
145
  const retry = call.retry ?? true;
140
146
  for (let attempt = 0;; attempt++) {
141
147
  let response;
148
+ // Called as a plain function: Cloudflare Workers' fetch throws "Illegal
149
+ // invocation" when called as a method of another object (this.#fetch()).
150
+ const send = this.#fetch;
142
151
  try {
143
- response = await this.#fetch(url, {
152
+ // "manual", not "error": Workers accept only "follow" and "manual".
153
+ // The API never redirects, so a redirect is refused here exactly as
154
+ // "error" refused it: thrown, retried, then a connection_error.
155
+ response = await send(url, {
144
156
  method: call.method,
145
157
  headers: this.#headers(call, key, apiKey),
146
- redirect: "error",
158
+ redirect: "manual",
147
159
  signal,
148
160
  ...(body === undefined ? {} : { body: body }),
149
161
  });
162
+ if (response.type === "opaqueredirect" ||
163
+ (response.status >= 300 && response.status < 400)) {
164
+ await response.body?.cancel().catch(() => { });
165
+ throw new TypeError(`fetch failed: Runtime answered a redirect (${response.status}).`);
166
+ }
150
167
  }
151
168
  catch (cause) {
152
- if (signal.aborted || !retry || attempt >= this.#maxRetries)
169
+ // A proxy variable the SDK cannot use: retrying cannot change it.
170
+ if (cause instanceof RuntimeError)
171
+ throw cause;
172
+ if (signal.aborted || !retry || attempt >= this.#maxRetries) {
173
+ const route = this.#routed && !signal.aborted ? describeRoute(url, cause) : { via: "" };
153
174
  throw new ConnectionError({
154
175
  message: signal.aborted
155
176
  ? "The call was cancelled or ran past its deadline."
156
177
  : write
157
- ? `No answer from Runtime at ${this.baseUrl}. The change may have happened; retrying with the same idempotencyKey is safe.`
158
- : `No answer from Runtime at ${this.baseUrl}.`,
178
+ ? `No answer from Runtime at ${this.baseUrl}${route.via}. The change may have happened; retrying with the same idempotencyKey is safe.`
179
+ : `No answer from Runtime at ${this.baseUrl}${route.via}.`,
159
180
  hint: signal.aborted
160
181
  ? "Raise timeoutMs for a long call, or retry with the same idempotencyKey."
161
- : "Check the network, and RUNTIME_API_URL if you set it.",
182
+ : (route.hint ?? "Check the network, and RUNTIME_API_URL if you set it."),
162
183
  code: signal.aborted ? "timeout" : "connection_error",
163
184
  status: 0,
164
185
  ...(key ? { idempotencyKey: key } : {}),
165
186
  cause,
166
187
  });
188
+ }
167
189
  await sleep(backoff(attempt), signal);
168
190
  continue;
169
191
  }
170
192
  if (response.ok)
171
193
  return response;
194
+ // Runtime never answers 407: it is a proxy refusing the tunnel, which
195
+ // Bun's fetch hands back as a response rather than an error.
196
+ if (response.status === 407 && this.#routed) {
197
+ await response.body?.cancel();
198
+ const route = describeRoute(url, 407);
199
+ throw new ConnectionError({
200
+ message: `No answer from Runtime at ${this.baseUrl}${route.via}.`,
201
+ code: "connection_error",
202
+ status: 0,
203
+ ...(route.hint ? { hint: route.hint } : {}),
204
+ ...(key ? { idempotencyKey: key } : {}),
205
+ });
206
+ }
172
207
  const text = await response.text();
173
208
  let parsed = null;
174
209
  try {
@@ -239,11 +274,9 @@ export class Transport {
239
274
  async websocket(path, query) {
240
275
  const apiKey = await this.#key();
241
276
  const url = `${this.baseUrl.replace(/^http/, "ws")}${path}${encodeQuery(query)}`;
242
- const Socket = globalThis.WebSocket;
243
- if (!Socket)
244
- throw new Error("This runtime has no WebSocket; use Node 22+ or Bun.");
245
- return new Socket(url, {
246
- headers: { authorization: `Bearer ${apiKey}`, "x-runtime-client": this.#client },
277
+ return openWebSocket(url, {
278
+ authorization: `Bearer ${apiKey}`,
279
+ "x-runtime-client": this.#client,
247
280
  });
248
281
  }
249
282
  }
package/package.json CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "withruntime",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "mcpName": "com.withruntime/runtime",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
8
  "types": "./dist/index.d.ts",
9
9
  "import": "./dist/index.js"
10
+ },
11
+ "./e2b": {
12
+ "types": "./dist/e2b/index.d.ts",
13
+ "import": "./dist/e2b/index.js"
14
+ },
15
+ "./e2b/code-interpreter": {
16
+ "types": "./dist/e2b/code-interpreter.d.ts",
17
+ "import": "./dist/e2b/code-interpreter.js"
10
18
  }
11
19
  },
12
20
  "bin": {
@@ -15,7 +23,8 @@
15
23
  },
16
24
  "files": [
17
25
  "dist",
18
- "README.md"
26
+ "README.md",
27
+ "E2B.md"
19
28
  ],
20
29
  "engines": {
21
30
  "node": ">=22"
@@ -54,5 +63,8 @@
54
63
  "mcp",
55
64
  "microvm",
56
65
  "code-execution"
57
- ]
66
+ ],
67
+ "dependencies": {
68
+ "undici": "^7.29.1"
69
+ }
58
70
  }