withruntime 0.9.0 → 0.11.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.
Files changed (124) hide show
  1. package/BLAXEL.md +4 -4
  2. package/DAYTONA.md +52 -40
  3. package/E2B.md +65 -62
  4. package/README.md +15 -7
  5. package/README.src.md +15 -7
  6. package/VERCEL.md +34 -22
  7. package/compatibility-lock.json +16 -16
  8. package/dist/blaxel/errors.js +1 -1
  9. package/dist/blaxel/lookup.d.ts +3 -2
  10. package/dist/blaxel/lookup.js +3 -2
  11. package/dist/blaxel/pagination.d.ts +2 -2
  12. package/dist/blaxel/pagination.js +45 -17
  13. package/dist/blaxel/preview.d.ts +2 -1
  14. package/dist/blaxel/preview.js +15 -13
  15. package/dist/blaxel/process.js +56 -10
  16. package/dist/blaxel/sandbox.d.ts +2 -0
  17. package/dist/blaxel/sandbox.js +2 -0
  18. package/dist/blaxel/snapshot.js +3 -5
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +262 -56
  21. package/dist/client.d.ts +41 -3
  22. package/dist/client.js +67 -11
  23. package/dist/cloudflare/identity.d.ts +7 -0
  24. package/dist/cloudflare/identity.js +13 -0
  25. package/dist/cloudflare/index.d.ts +9 -0
  26. package/dist/cloudflare/index.js +47 -9
  27. package/dist/cloudflare/worker.d.ts +4 -0
  28. package/dist/cloudflare/worker.js +166 -16
  29. package/dist/codesandbox/guest-service.js +57 -20
  30. package/dist/codesandbox/index.js +42 -8
  31. package/dist/compat/core.d.ts +9 -8
  32. package/dist/compat/core.js +60 -21
  33. package/dist/compat/files.js +22 -4
  34. package/dist/compat/lease.d.ts +36 -0
  35. package/dist/compat/lease.js +87 -0
  36. package/dist/credentials.d.ts +6 -1
  37. package/dist/credentials.js +107 -38
  38. package/dist/daytona/computer-use.d.ts +162 -0
  39. package/dist/daytona/computer-use.js +324 -0
  40. package/dist/daytona/context.d.ts +10 -0
  41. package/dist/daytona/context.js +24 -0
  42. package/dist/daytona/daytona.d.ts +20 -0
  43. package/dist/daytona/daytona.js +134 -20
  44. package/dist/daytona/filesystem.d.ts +17 -4
  45. package/dist/daytona/filesystem.js +129 -38
  46. package/dist/daytona/index.d.ts +3 -27
  47. package/dist/daytona/index.js +2 -7
  48. package/dist/daytona/process.d.ts +63 -6
  49. package/dist/daytona/process.js +224 -23
  50. package/dist/daytona/sandbox.d.ts +20 -6
  51. package/dist/daytona/sandbox.js +55 -32
  52. package/dist/e2b/client.d.ts +5 -0
  53. package/dist/e2b/client.js +6 -2
  54. package/dist/e2b/commands.d.ts +43 -12
  55. package/dist/e2b/commands.js +217 -64
  56. package/dist/e2b/errors.d.ts +7 -0
  57. package/dist/e2b/errors.js +20 -0
  58. package/dist/e2b/filesystem.d.ts +2 -1
  59. package/dist/e2b/filesystem.js +43 -6
  60. package/dist/e2b/index.d.ts +3 -2
  61. package/dist/e2b/index.js +2 -1
  62. package/dist/e2b/pty.d.ts +24 -0
  63. package/dist/e2b/pty.js +83 -0
  64. package/dist/e2b/sandbox.d.ts +17 -6
  65. package/dist/e2b/sandbox.js +239 -43
  66. package/dist/e2b/users.d.ts +30 -0
  67. package/dist/e2b/users.js +185 -0
  68. package/dist/file-source.d.ts +16 -0
  69. package/dist/file-source.js +162 -0
  70. package/dist/freestyle/index.d.ts +9 -1
  71. package/dist/freestyle/index.js +38 -5
  72. package/dist/index.d.ts +3 -1
  73. package/dist/login.js +16 -10
  74. package/dist/mcp.d.ts +2 -1
  75. package/dist/mcp.js +6 -1
  76. package/dist/modal/index.js +2 -1
  77. package/dist/network-cli.js +139 -43
  78. package/dist/openai-agents/index.js +9 -45
  79. package/dist/page.d.ts +1 -1
  80. package/dist/page.js +2 -2
  81. package/dist/products/browser.d.ts +60 -0
  82. package/dist/products/browser.js +37 -0
  83. package/dist/products/desktop.d.ts +2 -2
  84. package/dist/products/desktop.js +2 -2
  85. package/dist/products/images.d.ts +3 -1
  86. package/dist/products/images.js +14 -2
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/network-products.d.ts +7 -0
  90. package/dist/products/network-products.js +3 -0
  91. package/dist/products/observability.d.ts +44 -0
  92. package/dist/products/observability.js +6 -0
  93. package/dist/products/previews.d.ts +8 -2
  94. package/dist/products/previews.js +11 -3
  95. package/dist/products/private-network.d.ts +22 -0
  96. package/dist/products/private-network.js +10 -0
  97. package/dist/products/volumes.d.ts +42 -0
  98. package/dist/products/volumes.js +36 -0
  99. package/dist/products/watch.js +61 -8
  100. package/dist/proxy.js +3 -1
  101. package/dist/runloop/index.d.ts +13 -27
  102. package/dist/runloop/index.js +149 -52
  103. package/dist/sandbox.d.ts +59 -8
  104. package/dist/sandbox.js +479 -68
  105. package/dist/snapshots.d.ts +9 -4
  106. package/dist/sprites/index.js +69 -33
  107. package/dist/tar.d.ts +3 -1
  108. package/dist/tar.js +140 -14
  109. package/dist/transport.d.ts +4 -7
  110. package/dist/transport.js +205 -122
  111. package/dist/types.d.ts +5 -2
  112. package/dist/vercel/command.d.ts +1 -1
  113. package/dist/vercel/command.js +3 -2
  114. package/dist/vercel/errors.d.ts +1 -1
  115. package/dist/vercel/errors.js +7 -1
  116. package/dist/vercel/index.d.ts +1 -7
  117. package/dist/vercel/index.js +1 -3
  118. package/dist/vercel/sandbox-user.d.ts +70 -0
  119. package/dist/vercel/sandbox-user.js +206 -0
  120. package/dist/vercel/sandbox.d.ts +55 -20
  121. package/dist/vercel/sandbox.js +353 -99
  122. package/dist/vercel/snapshot.d.ts +3 -2
  123. package/dist/vercel/snapshot.js +9 -5
  124. package/package.json +2 -1
package/dist/transport.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ConnectionError, DELIBERATE, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
2
2
  import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
3
- export const VERSION = "0.9.0";
3
+ export const VERSION = "0.11.0";
4
4
  export const DEFAULT_BASE_URL = "https://api.withruntime.com";
5
5
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
6
6
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
@@ -80,18 +80,57 @@ const sleep = (ms, signal) => new Promise((resolve, reject) => {
80
80
  else
81
81
  signal?.addEventListener("abort", abort, { once: true });
82
82
  });
83
+ /** A credential lookup can finish independently, but must not keep its
84
+ * caller waiting after cancellation or hold that caller's queue slot. */
85
+ function untilAborted(pending, signal) {
86
+ if (!signal)
87
+ return pending;
88
+ return new Promise((resolve, reject) => {
89
+ const abort = () => reject(abortReason(signal));
90
+ if (signal.aborted)
91
+ abort();
92
+ else
93
+ signal.addEventListener("abort", abort, { once: true });
94
+ pending.then((value) => {
95
+ signal.removeEventListener("abort", abort);
96
+ resolve(value);
97
+ }, (cause) => {
98
+ signal.removeEventListener("abort", abort);
99
+ reject(cause instanceof Error ? cause : new Error("Credential lookup failed.", { cause }));
100
+ });
101
+ });
102
+ }
103
+ function abortReason(signal) {
104
+ return signal?.reason instanceof Error
105
+ ? signal.reason
106
+ : new Error("Aborted", { cause: signal?.reason });
107
+ }
83
108
  /** At most `maximum` holders at once; the rest wait their turn, first come
84
109
  * first served. */
85
110
  function slots(maximum) {
86
111
  let active = 0;
87
112
  const waiting = [];
88
113
  return {
89
- async take() {
114
+ async take(signal) {
115
+ signal?.throwIfAborted();
90
116
  if (active < maximum) {
91
117
  active++;
92
118
  return;
93
119
  }
94
- await new Promise((resolve) => waiting.push(resolve));
120
+ await new Promise((resolve, reject) => {
121
+ const ready = () => {
122
+ signal?.removeEventListener("abort", abort);
123
+ resolve();
124
+ };
125
+ const abort = () => {
126
+ const at = waiting.indexOf(ready);
127
+ if (at >= 0)
128
+ waiting.splice(at, 1);
129
+ reject(abortReason(signal));
130
+ };
131
+ waiting.push(ready);
132
+ signal?.addEventListener("abort", abort, { once: true });
133
+ });
95
134
  },
96
135
  give() {
97
136
  const next = waiting.shift();
@@ -170,22 +209,71 @@ export class Transport {
170
209
  * the same key every time, and waits out a full house for a call that sets
171
210
  * waitForCapacityMs. */
172
211
  async send(call) {
173
- const apiKey = await this.#key();
174
- const write = call.method !== "GET";
175
- const key = write ? (call.idempotencyKey ?? crypto.randomUUID()) : undefined;
176
- // Time spent waiting for room is added to the call's deadline, not taken
177
- // from it: once admitted, a create still has its whole timeoutMs.
212
+ return this.#send(this.#prepare(call), async (response) => response);
213
+ }
214
+ /** Start the deadline before waiting for a connection; keep one key and
215
+ * signal through every retry and the complete response body. */
216
+ #prepare(call) {
217
+ const timeoutMs = call.timeoutMs ?? this.#timeoutMs;
218
+ const roomMs = Math.max(0, call.waitForCapacityMs ?? 0);
219
+ const deadline = timeoutMs === 0 ? undefined : AbortSignal.timeout(timeoutMs + roomMs);
220
+ const signal = deadline
221
+ ? call.signal
222
+ ? AbortSignal.any([call.signal, deadline])
223
+ : deadline
224
+ : call.signal;
225
+ return {
226
+ ...call,
227
+ timeoutMs: 0,
228
+ ...(signal ? { signal } : {}),
229
+ ...(call.method !== "GET"
230
+ ? { idempotencyKey: call.idempotencyKey ?? crypto.randomUUID() }
231
+ : {}),
232
+ };
233
+ }
234
+ #connectionError(call, cause) {
235
+ const cancelled = call.signal?.aborted === true;
236
+ const route = this.#routed && !cancelled
237
+ ? describeRoute(`${this.baseUrl}${call.path}`, cause)
238
+ : { via: "" };
239
+ return new ConnectionError({
240
+ message: cancelled
241
+ ? "The call was cancelled or ran past its deadline."
242
+ : call.method !== "GET"
243
+ ? `No answer from Runtime at ${this.baseUrl}${route.via}. The change may have happened; retrying with the same idempotencyKey is safe.`
244
+ : `No answer from Runtime at ${this.baseUrl}${route.via}.`,
245
+ hint: cancelled
246
+ ? "Raise timeoutMs for a long call, or retry with the same idempotencyKey."
247
+ : (route.hint ?? "Check the network, and RUNTIME_API_URL if you set it."),
248
+ code: cancelled ? "timeout" : "connection_error",
249
+ status: 0,
250
+ ...(call.idempotencyKey ? { idempotencyKey: call.idempotencyKey } : {}),
251
+ cause,
252
+ });
253
+ }
254
+ async #send(call, consume) {
255
+ let apiKey;
256
+ try {
257
+ call.signal?.throwIfAborted();
258
+ apiKey = await untilAborted(this.#key(), call.signal);
259
+ }
260
+ catch (cause) {
261
+ if (call.signal?.aborted)
262
+ throw this.#connectionError(call, cause);
263
+ throw cause;
264
+ }
265
+ const key = call.method !== "GET" ? call.idempotencyKey : undefined;
178
266
  const roomMs = Math.max(0, call.waitForCapacityMs ?? 0);
179
267
  const roomUntil = performance.now() + roomMs;
180
268
  let roomAttempt = 0;
181
- const timeoutMs = call.timeoutMs ?? this.#timeoutMs;
182
- const deadline = timeoutMs === 0 ? new AbortController().signal : AbortSignal.timeout(timeoutMs + roomMs);
183
- const signal = call.signal ? AbortSignal.any([call.signal, deadline]) : deadline;
269
+ const signal = call.signal;
184
270
  const url = `${this.baseUrl}${call.path}${encodeQuery(call.query)}`;
185
271
  const body = call.bytes ?? (call.body === undefined ? undefined : JSON.stringify(call.body));
186
272
  const retry = call.retry ?? true;
187
- for (let attempt = 0;; attempt++) {
273
+ let attempt = 0;
274
+ for (;;) {
188
275
  let response;
276
+ let inCallback = false;
189
277
  // Called as a plain function: Cloudflare Workers' fetch throws "Illegal
190
278
  // invocation" when called as a method of another object (this.#fetch()).
191
279
  const send = this.#fetch;
@@ -193,6 +281,7 @@ export class Transport {
193
281
  // "manual", not "error": Workers accept only "follow" and "manual".
194
282
  // The API never redirects, so a redirect is refused here exactly as
195
283
  // "error" refused it: thrown, retried, then a connection_error.
284
+ signal?.throwIfAborted();
196
285
  response = await send(url, {
197
286
  method: call.method,
198
287
  headers: this.#headers(call, key, apiKey),
@@ -205,138 +294,132 @@ export class Transport {
205
294
  await response.body?.cancel().catch(() => { });
206
295
  throw new TypeError(`fetch failed: Runtime answered a redirect (${response.status}).`);
207
296
  }
208
- }
209
- catch (cause) {
210
- // A proxy variable the SDK cannot use: retrying cannot change it.
211
- if (cause instanceof RuntimeError)
212
- throw cause;
213
- if (signal.aborted || !retry || attempt >= this.#maxRetries) {
214
- const route = this.#routed && !signal.aborted ? describeRoute(url, cause) : { via: "" };
297
+ /* An answer that took longer than the API holds its headers (90 s)
298
+ comes as a 200 sent early, and then its JSON: the answer, or
299
+ {"error": ...} when the work failed after the status went
300
+ (runtime-late-answer, api.md). Either way it is read here, so a
301
+ failure is thrown as the error it is, never returned as a success. */
302
+ let late;
303
+ if (response.ok && response.headers.get("runtime-late-answer") === "true") {
304
+ const text = await response.text();
305
+ const failed = lateFailure(text);
306
+ if (failed === undefined)
307
+ return await consume(new Response(text, { headers: response.headers }));
308
+ late = { status: failed, text };
309
+ }
310
+ else if (response.ok) {
311
+ inCallback = true;
312
+ call.onResponse?.(response);
313
+ inCallback = false;
314
+ return await consume(response);
315
+ }
316
+ const status = late?.status ?? response.status;
317
+ // Runtime never answers 407: it is a proxy refusing the tunnel, which
318
+ // Bun's fetch hands back as a response rather than an error.
319
+ if (response.status === 407 && this.#routed) {
320
+ await response.body?.cancel();
321
+ const route = describeRoute(url, 407);
215
322
  throw new ConnectionError({
216
- message: signal.aborted
217
- ? "The call was cancelled or ran past its deadline."
218
- : write
219
- ? `No answer from Runtime at ${this.baseUrl}${route.via}. The change may have happened; retrying with the same idempotencyKey is safe.`
220
- : `No answer from Runtime at ${this.baseUrl}${route.via}.`,
221
- hint: signal.aborted
222
- ? "Raise timeoutMs for a long call, or retry with the same idempotencyKey."
223
- : (route.hint ?? "Check the network, and RUNTIME_API_URL if you set it."),
224
- code: signal.aborted ? "timeout" : "connection_error",
323
+ message: `No answer from Runtime at ${this.baseUrl}${route.via}.`,
324
+ code: "connection_error",
225
325
  status: 0,
326
+ ...(route.hint ? { hint: route.hint } : {}),
226
327
  ...(key ? { idempotencyKey: key } : {}),
227
- cause,
228
328
  });
229
329
  }
230
- await sleep(backoff(attempt), signal);
231
- continue;
232
- }
233
- /* An answer that took longer than the API holds its headers (90 s)
234
- comes as a 200 sent early, and then its JSON: the answer, or
235
- {"error": ...} when the work failed after the status went
236
- (runtime-late-answer, api.md). Either way it is read here, so a
237
- failure is thrown as the error it is, never returned as a success. */
238
- let late;
239
- if (response.ok && response.headers.get("runtime-late-answer") === "true") {
240
- const text = await response.text();
241
- const failed = lateFailure(text);
242
- if (failed === undefined)
243
- return new Response(text, { headers: response.headers });
244
- late = { status: failed, text };
245
- }
246
- else if (response.ok) {
247
- call.onResponse?.(response);
248
- return response;
330
+ const text = late?.text ?? (await response.text());
331
+ let parsed = null;
332
+ try {
333
+ parsed = JSON.parse(text);
334
+ }
335
+ catch {
336
+ parsed = { error: { message: text.slice(0, 500) || `HTTP ${status}` } };
337
+ }
338
+ const error = errorFor(status, parsed, key);
339
+ // A full house: wait for a slot or for room, then send the same call
340
+ // again. A request that can never fit (a fork of more copies than the
341
+ // trial runs) is not waited for.
342
+ if (retry &&
343
+ roomMs > 0 &&
344
+ WAITS_FOR_ROOM.has(error.code) &&
345
+ error.details?.field !== "count") {
346
+ const left = roomUntil - performance.now();
347
+ if (left <= 0)
348
+ throw error;
349
+ const pause = Math.min(left, error.retryAfterMs ?? roomBackoff(roomAttempt++));
350
+ inCallback = true;
351
+ call.onCapacityWait?.(error, pause);
352
+ inCallback = false;
353
+ await sleep(pause, signal);
354
+ continue;
355
+ }
356
+ const retryable = [429, 502, 503, 504].includes(status) && !DELIBERATE.has(error.code);
357
+ if (!retry || !retryable || attempt >= this.#maxRetries)
358
+ throw error;
359
+ const header = Number(response.headers.get("retry-after"));
360
+ const wait = error.retryAfterMs ??
361
+ (Number.isFinite(header) && header > 0 ? header * 1000 : backoff(attempt));
362
+ await sleep(Math.min(30_000, wait) * (0.9 + Math.random() * 0.2), signal);
249
363
  }
250
- const status = late?.status ?? response.status;
251
- // Runtime never answers 407: it is a proxy refusing the tunnel, which
252
- // Bun's fetch hands back as a response rather than an error.
253
- if (response.status === 407 && this.#routed) {
254
- await response.body?.cancel();
255
- const route = describeRoute(url, 407);
256
- throw new ConnectionError({
257
- message: `No answer from Runtime at ${this.baseUrl}${route.via}.`,
258
- code: "connection_error",
259
- status: 0,
260
- ...(route.hint ? { hint: route.hint } : {}),
261
- ...(key ? { idempotencyKey: key } : {}),
364
+ catch (cause) {
365
+ // Deliberate API refusals and unusable proxy settings cannot be
366
+ // fixed by repeating the request. Body failures are transport
367
+ // failures too: a write replays under its original key.
368
+ if (inCallback) {
369
+ await response?.body?.cancel().catch(() => undefined);
370
+ throw cause;
371
+ }
372
+ if (cause instanceof RuntimeError)
373
+ throw cause;
374
+ if (signal?.aborted || !retry || attempt >= this.#maxRetries)
375
+ throw this.#connectionError(call, cause);
376
+ await sleep(backoff(attempt), signal).catch((cause) => {
377
+ throw this.#connectionError(call, cause);
262
378
  });
263
379
  }
264
- const text = late?.text ?? (await response.text());
265
- let parsed = null;
266
- try {
267
- parsed = JSON.parse(text);
268
- }
269
- catch {
270
- parsed = { error: { message: text.slice(0, 500) || `HTTP ${status}` } };
271
- }
272
- const error = errorFor(status, parsed, key);
273
- // A full house: wait for a slot or for room, then send the same call
274
- // again. A request that can never fit (a fork of more copies than the
275
- // trial runs) is not waited for.
276
- if (retry &&
277
- roomMs > 0 &&
278
- WAITS_FOR_ROOM.has(error.code) &&
279
- error.details?.field !== "count") {
280
- const left = roomUntil - performance.now();
281
- if (left <= 0)
282
- throw error;
283
- const pause = Math.min(left, error.retryAfterMs ?? roomBackoff(roomAttempt++));
284
- call.onCapacityWait?.(error, pause);
285
- await sleep(pause, signal);
286
- continue;
287
- }
288
- const retryable = [429, 502, 503, 504].includes(status) && !DELIBERATE.has(error.code);
289
- if (!retry || !retryable || attempt >= this.#maxRetries)
290
- throw error;
291
- const header = Number(response.headers.get("retry-after"));
292
- const wait = error.retryAfterMs ??
293
- (Number.isFinite(header) && header > 0 ? header * 1000 : backoff(attempt));
294
- await sleep(Math.min(30_000, wait) * (0.9 + Math.random() * 0.2), signal);
380
+ attempt++;
295
381
  }
296
382
  }
297
- async json(call) {
298
- await this.#slots.take();
383
+ async #queued(call, consume) {
299
384
  try {
300
- const text = await (await this.send(call)).text();
301
- return (text ? JSON.parse(text) : null);
385
+ await this.#slots.take(call.signal);
302
386
  }
303
- finally {
304
- this.#slots.give();
387
+ catch (cause) {
388
+ throw this.#connectionError(call, cause);
305
389
  }
306
- }
307
- async bytes(call) {
308
- await this.#slots.take();
309
390
  try {
310
- return new Uint8Array(await (await this.send(call)).arrayBuffer());
391
+ return await this.#send(call, consume);
311
392
  }
312
393
  finally {
313
394
  this.#slots.give();
314
395
  }
315
396
  }
316
- /** A file's bytes, read whole and checked: the API says a body's length
317
- * (x-content-length) and, when small, its SHA-256 (x-content-sha256) before
318
- * sending it, and a body that falls short or differs is read again, twice,
319
- * then refused. Nothing between the API and here reliably turns a stream
320
- * cut part way into an error: on 25 September 2026 a 50 MB file came back
321
- * 29 MB long, exit 0. */
397
+ async json(call) {
398
+ return this.#queued(this.#prepare(call), async (response) => {
399
+ const text = await response.text();
400
+ return (text ? JSON.parse(text) : null);
401
+ });
402
+ }
403
+ async bytes(call) {
404
+ return this.#queued(this.#prepare(call), async (response) => new Uint8Array(await response.arrayBuffer()));
405
+ }
406
+ /** A file's bytes, read whole and checked against its promised length and
407
+ * SHA-256. A short or different body is read again twice, within the same
408
+ * call deadline. */
322
409
  async fileBytes(call) {
410
+ call = this.#prepare(call);
323
411
  for (let attempt = 0;; attempt++) {
324
- await this.#slots.take();
325
- let failure;
326
- try {
327
- const response = await this.send(call);
412
+ const { body, problem } = await this.#queued(call, async (response) => {
328
413
  const body = new Uint8Array(await response.arrayBuffer());
329
- const problem = await checkBody(response.headers, body);
330
- if (!problem)
331
- return body;
332
- failure = problem;
333
- }
334
- finally {
335
- this.#slots.give();
336
- }
337
- if (attempt >= 2 || call.signal?.aborted)
338
- throw failure;
339
- await sleep(backoff(attempt), call.signal);
414
+ return { body, problem: await checkBody(response.headers, body) };
415
+ });
416
+ if (!problem)
417
+ return body;
418
+ if (attempt >= 2)
419
+ throw problem;
420
+ await sleep(backoff(attempt), call.signal).catch((cause) => {
421
+ throw this.#connectionError(call, cause);
422
+ });
340
423
  }
341
424
  }
342
425
  /** A file's bytes as they arrive, for files too big to hold. The stream
package/dist/types.d.ts CHANGED
@@ -42,8 +42,9 @@ export type SandboxInfo = {
42
42
  /** The names of its own environment variables (`env`), on a create, an
43
43
  * update and a read of one sandbox. Values are never shown. */
44
44
  envNames?: string[];
45
- /** On a create from an image with a start command: whether it started,
46
- * became ready (readyMs), timed out, or exited first. The create's answer
45
+ /** On a create from an image with a start command: whether it started
46
+ * (and was still running half a second later), became ready (readyMs),
47
+ * timed out, or exited first (exitCode, and stderr's last lines). The create's answer
47
48
  * alone carries it, and the Sandbox that create returned keeps it through
48
49
  * later reads; `sandboxes.get` and `list` have none, because the API keeps
49
50
  * no record of it. */
@@ -52,6 +53,8 @@ export type SandboxInfo = {
52
53
  processId?: string;
53
54
  exitCode?: number | null;
54
55
  readyMs?: number;
56
+ /** With exited: the last lines the start command wrote to stderr. */
57
+ stderr?: string;
55
58
  };
56
59
  /** Present and true when getOrCreate answered a sandbox that already held the name. */
57
60
  reused?: boolean;
@@ -57,7 +57,7 @@ export declare class Command {
57
57
  signal?: AbortSignal;
58
58
  }): Promise<string>;
59
59
  /** Sends a signal, SIGTERM by default. */
60
- kill(signal?: Signal, _opts?: {
60
+ kill(signal?: Signal, opts?: {
61
61
  abortSignal?: AbortSignal;
62
62
  }): Promise<void>;
63
63
  }
@@ -136,13 +136,14 @@ export class Command {
136
136
  return this.output("stderr", opts);
137
137
  }
138
138
  /** Sends a signal, SIGTERM by default. */
139
- async kill(signal = "SIGTERM", _opts = {}) {
139
+ async kill(signal = "SIGTERM", opts = {}) {
140
+ opts.abortSignal?.throwIfAborted();
140
141
  if (!this.init.process)
141
142
  return;
142
143
  const name = typeof signal === "number" ? SIGNAL_NUMBERS[signal] : signal;
143
144
  if (!name)
144
145
  throw new RangeError(`Signal ${signal} has no name Runtime knows; use SIGTERM, SIGKILL, SIGINT, SIGHUP, SIGQUIT, SIGUSR1 or SIGUSR2.`);
145
- await guard(() => this.init.process.kill(name), this.init.sandboxName);
146
+ await guard(() => this.init.process.kill(name, opts.abortSignal ? { signal: opts.abortSignal } : {}), this.init.sandboxName);
146
147
  }
147
148
  }
148
149
  /** A command that has ended; `exitCode` is set. */
@@ -45,7 +45,7 @@ export declare function translate(error: unknown, sandboxName?: string): unknown
45
45
  export declare function guard<T>(work: () => Promise<T>, sandboxName?: string): Promise<T>;
46
46
  /** A node:fs-style error, as Vercel's `sandbox.fs` throws. */
47
47
  export declare function fsError(code: "ENOENT" | "EEXIST" | "EISDIR" | "ENOTDIR" | "EACCES" | "ENOTEMPTY" | "EINVAL", syscall: string, path: string): Error & {
48
- code: "ENOENT" | "EEXIST" | "ENOTDIR" | "EISDIR" | "EACCES" | "ENOTEMPTY" | "EINVAL";
48
+ code: "ENOENT" | "ENOTDIR" | "EEXIST" | "EISDIR" | "EACCES" | "ENOTEMPTY" | "EINVAL";
49
49
  errno: number;
50
50
  syscall: string;
51
51
  path: string;
@@ -73,7 +73,13 @@ export function translate(error, sandboxName) {
73
73
  return new NotSupportedError(error.code.replace(/_unavailable$/, ""), error.hint ?? "", message);
74
74
  if (error.status < 200 || error.status > 599)
75
75
  return error;
76
- const json = { error: { code: error.code, message: error.message } };
76
+ const json = {
77
+ error: {
78
+ code: error.code,
79
+ message: error.message,
80
+ ...(error.details ? { details: error.details } : {}),
81
+ },
82
+ };
77
83
  const out = new APIError(new Response(JSON.stringify(json), {
78
84
  status: error.status,
79
85
  headers: { "content-type": "application/json" },
@@ -1,6 +1,7 @@
1
1
  export { Sandbox, DEFAULT_TIMEOUT_MS, DEFAULT_VCPUS, MEMORY_MIB_PER_VCPU, networkRules, toRuntimePath, type CreateSandboxParams, type RunCommandParams, type NetworkPolicy, type SandboxRoute, } from "./sandbox.js";
2
2
  export { Command, CommandFinished, type CommandOutput, type LogOutputLine, type Signal, } from "./command.js";
3
3
  export { FileSystem, Stats, Dirent } from "./filesystem.js";
4
+ export { SandboxUser, SandboxUserAlreadyExistsError } from "./sandbox-user.js";
4
5
  export { Snapshot } from "./snapshot.js";
5
6
  export { APIError, StreamError, AuthenticationError, NotSupportedError } from "./errors.js";
6
7
  export type { Credentials, WithRuntime } from "./client.js";
@@ -16,13 +17,6 @@ export declare const Session: {
16
17
  new (...args: unknown[]): never;
17
18
  readonly [key: string]: (...args: unknown[]) => never;
18
19
  };
19
- export declare const SandboxUser: {
20
- (...args: unknown[]): never;
21
- new (...args: unknown[]): never;
22
- readonly [key: string]: (...args: unknown[]) => never;
23
- };
24
- export declare class SandboxUserAlreadyExistsError extends Error {
25
- }
26
20
  export declare const defineSandboxProxy: {
27
21
  (...args: unknown[]): never;
28
22
  new (...args: unknown[]): never;
@@ -9,6 +9,7 @@ import { NotSupportedError } from "./errors.js";
9
9
  export { Sandbox, DEFAULT_TIMEOUT_MS, DEFAULT_VCPUS, MEMORY_MIB_PER_VCPU, networkRules, toRuntimePath, } from "./sandbox.js";
10
10
  export { Command, CommandFinished, } from "./command.js";
11
11
  export { FileSystem, Stats, Dirent } from "./filesystem.js";
12
+ export { SandboxUser, SandboxUserAlreadyExistsError } from "./sandbox-user.js";
12
13
  export { Snapshot } from "./snapshot.js";
13
14
  export { APIError, StreamError, AuthenticationError, NotSupportedError } from "./errors.js";
14
15
  /** Vercel's default region; Runtime runs in one US region. */
@@ -32,7 +33,4 @@ function unsupportedExport(name, alternative) {
32
33
  }
33
34
  export const Drive = unsupportedExport("Drive", "Use a Runtime volume (runtime.volumes) and mount it with withruntime: { create: { volumes: [{ volumeId, path }] } }.");
34
35
  export const Session = unsupportedExport("Session", "A Runtime sandbox is its own session: call the methods on the sandbox.");
35
- export const SandboxUser = unsupportedExport("SandboxUser", "Commands run as the sandbox owner with passwordless sudo; create users with `sudo useradd` in runCommand.");
36
- export class SandboxUserAlreadyExistsError extends Error {
37
- }
38
36
  export const defineSandboxProxy = unsupportedExport("sandbox proxy (defineSandboxProxy)", "Share a port with sandbox.withruntime.previews.create(port) instead.");
@@ -0,0 +1,70 @@
1
+ import type { Command, CommandFinished } from "./command.js";
2
+ import type { RunCommandParams, Sandbox } from "./sandbox.js";
3
+ /** A Linux user or group name Vercel accepts, which is also safe to pass to
4
+ * useradd, groupadd and sudo. */
5
+ export declare function validateName(name: string, kind: string): void;
6
+ /** Thrown when `sandbox.createUser` is called for an existing username. */
7
+ export declare class SandboxUserAlreadyExistsError extends Error {
8
+ readonly username: string;
9
+ constructor(username: string);
10
+ }
11
+ type Signal = {
12
+ signal?: AbortSignal;
13
+ };
14
+ /** A user in a sandbox, made with `sandbox.createUser` or found with
15
+ * `sandbox.asUser`. Commands and file calls run as this user. */
16
+ export declare class SandboxUser {
17
+ #private;
18
+ readonly username: string;
19
+ /** `/root` for root, else `/home/<username>`. */
20
+ readonly homeDir: string;
21
+ /** Use sandbox.createUser or sandbox.asUser. */
22
+ constructor({ sandbox, username }: {
23
+ sandbox: Sandbox;
24
+ username: string;
25
+ });
26
+ /** Runs a command as this user. `sudo: true` runs it as root instead. */
27
+ runCommand(command: string, args?: string[], opts?: {
28
+ signal?: AbortSignal;
29
+ timeoutMs?: number;
30
+ }): Promise<CommandFinished>;
31
+ runCommand(params: RunCommandParams & {
32
+ detached: true;
33
+ }): Promise<Command>;
34
+ runCommand(params: RunCommandParams): Promise<CommandFinished>;
35
+ /** Writes files into this user's home (or absolute paths), then gives them
36
+ * and the directories made for them to this user. */
37
+ writeFiles(files: {
38
+ path: string;
39
+ content: string | Uint8Array;
40
+ mode?: number;
41
+ }[], opts?: Signal): Promise<void>;
42
+ /** The file as this user may read it, as a stream; null when missing. */
43
+ readFile(file: {
44
+ path: string;
45
+ cwd?: string;
46
+ }, opts?: Signal): Promise<NodeJS.ReadableStream | null>;
47
+ /** The file as this user may read it; null when missing. */
48
+ readFileToBuffer(file: {
49
+ path: string;
50
+ cwd?: string;
51
+ }, opts?: Signal): Promise<Buffer | null>;
52
+ /** Copies a file this user may read to the local disk; null when missing. */
53
+ downloadFile(src: {
54
+ path: string;
55
+ cwd?: string;
56
+ }, dst: {
57
+ path: string;
58
+ cwd?: string;
59
+ }, opts?: {
60
+ mkdirRecursive?: boolean;
61
+ signal?: AbortSignal;
62
+ }): Promise<string | null>;
63
+ /** Makes a directory owned by this user. */
64
+ mkDir(path: string, opts?: Signal): Promise<void>;
65
+ addToGroup(groupname: string, opts?: Signal): Promise<void>;
66
+ removeFromGroup(groupname: string, opts?: Signal): Promise<void>;
67
+ }
68
+ /** Runs `cmd args` under sudo; a non-zero exit throws `what(): <stderr>`. */
69
+ export declare function rootOrThrow(sandbox: Sandbox, cmd: string, args: string[], signal: AbortSignal | undefined, what: () => string): Promise<CommandFinished>;
70
+ export {};