withruntime 0.23.0 → 0.33.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 (76) hide show
  1. package/COMPATIBILITY.md +64 -3
  2. package/E2B.md +1 -1
  3. package/README.md +36 -2
  4. package/README.src.md +35 -1
  5. package/compatibility-lock.json +3 -3
  6. package/dist/agents/browser.d.ts +12 -0
  7. package/dist/agents/browser.js +12 -0
  8. package/dist/agents/index.d.ts +7 -7
  9. package/dist/agents/index.js +7 -8
  10. package/dist/agents-cli.d.ts +1 -1
  11. package/dist/agents-cli.js +250 -3
  12. package/dist/agents-deploy.d.ts +186 -0
  13. package/dist/agents-deploy.js +752 -0
  14. package/dist/agents-file.d.ts +19 -0
  15. package/dist/agents-file.js +135 -0
  16. package/dist/apps/index.d.ts +10 -0
  17. package/dist/apps/index.js +11 -0
  18. package/dist/apps-cli.d.ts +3 -1
  19. package/dist/apps-cli.js +285 -38
  20. package/dist/blaxel/errors.js +2 -1
  21. package/dist/ci/index.d.ts +10 -0
  22. package/dist/ci/index.js +11 -0
  23. package/dist/ci-cli.d.ts +10 -0
  24. package/dist/ci-cli.js +343 -0
  25. package/dist/cli.d.ts +18 -7
  26. package/dist/cli.js +775 -85
  27. package/dist/daytona/errors.js +2 -1
  28. package/dist/e2b/errors.js +2 -1
  29. package/dist/e2b/filesystem.d.ts +1 -0
  30. package/dist/e2b/filesystem.js +1 -1
  31. package/dist/e2b/sandbox.d.ts +11 -2
  32. package/dist/e2b/sandbox.js +22 -6
  33. package/dist/errors.d.ts +11 -4
  34. package/dist/errors.js +27 -4
  35. package/dist/fly/index.d.ts +102 -0
  36. package/dist/fly/index.js +930 -0
  37. package/dist/index.d.ts +7 -3
  38. package/dist/machine.d.ts +10 -6
  39. package/dist/machine.js +14 -6
  40. package/dist/modal/index.d.ts +34 -0
  41. package/dist/modal/index.js +128 -22
  42. package/dist/products/agents.d.ts +101 -3
  43. package/dist/products/agents.js +49 -0
  44. package/dist/products/apps.d.ts +206 -10
  45. package/dist/products/apps.js +172 -1
  46. package/dist/products/ci.d.ts +332 -0
  47. package/dist/products/ci.js +101 -0
  48. package/dist/products/index.d.ts +8 -0
  49. package/dist/products/index.js +15 -0
  50. package/dist/products/jobs.d.ts +105 -5
  51. package/dist/products/jobs.js +47 -3
  52. package/dist/products/network.d.ts +13 -0
  53. package/dist/products/observability.d.ts +1 -1
  54. package/dist/products/plans.d.ts +131 -0
  55. package/dist/products/plans.js +24 -0
  56. package/dist/products/reservations.d.ts +108 -0
  57. package/dist/products/reservations.js +41 -0
  58. package/dist/products/resources.d.ts +2 -1
  59. package/dist/products/secrets.d.ts +90 -6
  60. package/dist/products/secrets.js +8 -2
  61. package/dist/proxy.d.ts +3 -3
  62. package/dist/proxy.js +20 -0
  63. package/dist/sandbox.d.ts +29 -1
  64. package/dist/sandbox.js +56 -3
  65. package/dist/snapshots.d.ts +1 -1
  66. package/dist/switched-off.d.ts +5 -0
  67. package/dist/switched-off.js +8 -0
  68. package/dist/tar.d.ts +1 -1
  69. package/dist/tar.js +19 -1
  70. package/dist/transport.d.ts +1 -1
  71. package/dist/transport.js +48 -9
  72. package/dist/types.d.ts +35 -4
  73. package/dist/vercel/errors.d.ts +1 -1
  74. package/dist/vercel/errors.js +2 -1
  75. package/dist/vercel/snapshot.js +4 -2
  76. package/package.json +23 -6
@@ -10,31 +10,99 @@ export type SecretRule = {
10
10
  paths: string[];
11
11
  };
12
12
  /** Which sandboxes get a secret: those whose labels hold every pair of any one
13
- * selector in `labels` (including sandboxes created or labelled later), and
14
- * those named in `sandboxes`. Any other sandbox of the organization sees no
15
- * placeholder, and its requests never carry the value. Absent on a secret:
16
- * every sandbox of the organization. */
13
+ * selector in `labels` (including sandboxes created or labelled later), those
14
+ * named in `sandboxes`, and every copy of the Apps named in `apps`. Any other
15
+ * sandbox of the organization sees no placeholder, and its requests never
16
+ * carry the value. Absent on a secret: every sandbox of the organization. */
17
17
  export type SecretScope = {
18
18
  /** 1 to 8 selectors of 1 to 8 key/value pairs: `[{ role: "hub" }]`. */
19
19
  labels?: Array<Record<string, string>>;
20
20
  /** 1 to 100 sandbox ids of the organization. */
21
21
  sandboxes?: string[];
22
+ /** 1 to 100 App or Function ids: every copy of each, of any version,
23
+ * including copies later deploys, restarts and scaling make. A deploy names
24
+ * the secrets its copies see (`apps.deploy(id, { secrets: [...] })`). */
25
+ apps?: string[];
26
+ };
27
+ /** A GitHub App the servers act as. The secret's value is the App's private
28
+ * key (PEM); the servers make its installation tokens themselves, each for an
29
+ * hour. The key goes into no request, and each request the rules allow carries
30
+ * the current token: `Authorization: Bearer` to the API, and git's
31
+ * `x-access-token` to git over HTTPS. Takes no `header` or `format`. */
32
+ export type SecretGithubApp = {
33
+ /** The App's id. */
34
+ id: number;
35
+ /** The installation's id: where the App is installed. */
36
+ installation: number;
37
+ /** The repositories each token reaches, by name. Absent: every repository of
38
+ * the installation. */
39
+ repositories?: string[];
40
+ /** What each token may do, at most what the App holds: `{ contents: "write" }`.
41
+ * Absent: all the App's permissions. */
42
+ permissions?: Record<string, "read" | "write" | "admin">;
43
+ /** The App's API on GitHub Enterprise Server (`https://<host>/api/v3`); one
44
+ * of `hosts`. Default `https://api.github.com`. */
45
+ api?: string;
46
+ };
47
+ /** What a git push over HTTPS carrying the secret may update. A push to any
48
+ * other ref, a tag, or a delete is refused before it leaves the server, in
49
+ * git's own words (`! [remote rejected]`). */
50
+ export type SecretGit = {
51
+ /** The branches a push may update: `refs/heads/` then a name, with `*` only
52
+ * at the end for any name that begins so, and `{label.<key>}` for the
53
+ * sandbox's own label of that key (a sandbox without it pushes nothing it
54
+ * names): `["refs/heads/task/{label.task}", "refs/heads/task/{label.task}--*"]`.
55
+ * Empty: no push at all. */
56
+ push: string[];
57
+ };
58
+ /** Each sandbox's hour with the secret, the far end's reserve, GraphQL's
59
+ * mutations and shared copies. */
60
+ export type SecretApiPolicy = {
61
+ /** The most requests each sandbox makes with the secret in an hour; past it,
62
+ * 429 with `Retry-After`. */
63
+ callsPerHour?: number;
64
+ /** The most writes (any method but GET, HEAD and OPTIONS; a GraphQL
65
+ * mutation) each sandbox makes in an hour. 0: none. */
66
+ writesPerHour?: number;
67
+ /** Calls the far end keeps for others: none is sent while its
68
+ * `x-ratelimit-remaining` says this many or fewer are left, until its reset. */
69
+ keep?: number;
70
+ /** Reads (GET over HTTP/2) answered from the server's copy while the far end
71
+ * answers 304 to its ETag, shared by every sandbox the secret reaches on that
72
+ * server; past a sandbox's hour, from the copy unasked. Answers carry
73
+ * `x-runtime-copy`. */
74
+ copies?: boolean;
75
+ /** With `copies`: 1 to 300 seconds a copy is answered without asking the far
76
+ * end, GraphQL queries included. */
77
+ fresh?: number;
78
+ /** A GraphQL query always goes; a mutation only when every one is listed. */
79
+ graphql?: {
80
+ /** Default `/graphql`. */
81
+ path?: string;
82
+ /** The mutations a request may make; any other is refused with 403. */
83
+ mutations: string[];
84
+ };
22
85
  };
23
86
  /** A secret your sandboxes use without seeing it. Every sandbox of your
24
- * organization has an environment variable of the secret's name holding
25
- * `placeholder`; the egress proxy sets `header` on every HTTPS request to
87
+ * organization has an environment variable of the secret's name (or `env`)
88
+ * holding `placeholder`; the egress proxy sets `header` on every HTTPS request to
26
89
  * `hosts`, or, without `header`, replaces the placeholder in a credential
27
90
  * header (Authorization, x-api-key, x-goog-api-key, api-key, xi-api-key,
28
91
  * x-subscription-token, private-token). Never in the URL or another header.
29
92
  * The value is never returned. */
30
93
  export type Secret = {
31
94
  name: string;
95
+ /** The environment variable the placeholder is in, when not `name`. */
96
+ env?: string;
32
97
  hosts: string[];
33
98
  header?: string;
34
99
  format?: string;
35
100
  rules?: SecretRule[];
36
101
  /** Absent: every sandbox of the organization. */
37
102
  only?: SecretScope;
103
+ githubApp?: SecretGithubApp;
104
+ git?: SecretGit;
105
+ api?: SecretApiPolicy;
38
106
  placeholder: string;
39
107
  valueBytes: number;
40
108
  createdAt: string;
@@ -62,6 +130,22 @@ export type SetSecret = {
62
130
  * has stays, so a new value never widens who gets it. `{}`: every sandbox of
63
131
  * the organization again. */
64
132
  only?: SecretScope;
133
+ /** The value is a GitHub App's private key (PEM), and the servers make the
134
+ * App's installation tokens themselves. Takes no `header` or `format`. */
135
+ githubApp?: SecretGithubApp;
136
+ /** What a git push over HTTPS carrying the secret may update. Absent: the
137
+ * secret limits no push. */
138
+ git?: SecretGit;
139
+ /** Each sandbox's hour with the secret, the far end's reserve, GraphQL's
140
+ * mutations and shared copies. */
141
+ api?: SecretApiPolicy;
142
+ /** The environment variable sandboxes see it in, when not the secret's
143
+ * name. Several secrets may share one, each scoped (`only`) to sandboxes no
144
+ * other of them reaches, such as one `GITHUB_TOKEN` per repository: a write
145
+ * that would put two in one sandbox is refused (409, conflict). The
146
+ * secret's own name gives the name back. Absent when replacing a secret:
147
+ * the variable it has stays. */
148
+ env?: string;
65
149
  /** Also keep a copy for scheduled jobs, which a job puts into its run's
66
150
  * environment (`jobs.create({ secrets: [...] })`). Unlike the sandboxes'
67
151
  * copy, the jobs copy is yours rather than the account's, versioned, and can
@@ -4,7 +4,9 @@ import { switchedOff } from "./jobs.js";
4
4
  * names what took effect. Calling again with the same arguments is safe. */
5
5
  export class SecretPartlyStoredError extends RuntimeError {
6
6
  }
7
- const unavailable = (error) => error instanceof RuntimeError && (error.code === "unavailable" || error.status === 503);
7
+ /** Secrets for jobs switched off here (501; 503 from an older server). */
8
+ const unavailable = (error) => error instanceof RuntimeError &&
9
+ (error.code === "unavailable" || error.status === 501 || error.status === 503);
8
10
  export function secrets(t) {
9
11
  const off = switchedOff("Secrets for jobs");
10
12
  const store = (call) => t.json(call).catch(off);
@@ -62,7 +64,11 @@ export function secrets(t) {
62
64
  ];
63
65
  for (const replay of replays) {
64
66
  const answer = await replay().catch((again) => {
65
- if (again instanceof RuntimeError && again.status === 409)
67
+ // Not this request's earlier send: a conflict, or its key held by
68
+ // another request (idempotency_conflict, 422; 409 from an older
69
+ // server).
70
+ if (again instanceof RuntimeError &&
71
+ (again.status === 409 || again.code === "idempotency_conflict"))
66
72
  return undefined;
67
73
  throw again;
68
74
  });
package/dist/proxy.d.ts CHANGED
@@ -16,9 +16,9 @@ export type Route = {
16
16
  export declare function exemption(list: string, hostname: string, port: string): string | undefined;
17
17
  /** The route the SDK takes to `target`, read from the environment now. */
18
18
  export declare function routeFor(target: string, env?: Env): Route;
19
- /** Words for a failed connection: the route it took, and what to check.
20
- * `cause` is the error fetch threw, or the status a proxy answered with (Bun's
21
- * fetch hands a refused tunnel back as a response). */
19
+ /** The certificate error under a failed call, if that is why it failed: its
20
+ * code, found anywhere in the cause chain. Pure. */
21
+ export declare function untrustedCertificate(cause: unknown): string | undefined;
22
22
  export declare function describeRoute(target: string, cause: unknown, env?: Env): {
23
23
  via: string;
24
24
  hint?: string;
package/dist/proxy.js CHANGED
@@ -91,6 +91,26 @@ function invalidProxy(name, shown, problem) {
91
91
  /** Words for a failed connection: the route it took, and what to check.
92
92
  * `cause` is the error fetch threw, or the status a proxy answered with (Bun's
93
93
  * fetch hands a refused tunnel back as a response). */
94
+ /** The TLS codes Node gives when a certificate chain is not one it trusts:
95
+ * a proxy or firewall that inspects TLS with its own authority, most often. */
96
+ const UNTRUSTED = new Set([
97
+ "SELF_SIGNED_CERT_IN_CHAIN",
98
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
99
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
100
+ "UNABLE_TO_GET_ISSUER_CERT",
101
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
102
+ ]);
103
+ /** The certificate error under a failed call, if that is why it failed: its
104
+ * code, found anywhere in the cause chain. Pure. */
105
+ export function untrustedCertificate(cause) {
106
+ for (let c = cause, depth = 0; c && typeof c === "object" && depth < 8; depth++) {
107
+ const code = c.code;
108
+ if (typeof code === "string" && UNTRUSTED.has(code))
109
+ return code;
110
+ c = c.cause;
111
+ }
112
+ return undefined;
113
+ }
94
114
  export function describeRoute(target, cause, env = processEnv()) {
95
115
  let route;
96
116
  try {
package/dist/sandbox.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Page } from "./page.js";
2
2
  import { type FileEvent, type WatchHandle, type WatchOptions } from "./products/watch.js";
3
3
  import { Transport, type RequestOptions } from "./transport.js";
4
- import type { CommandResult, DeletedSandbox, ExecOptions, FileEntry, OutputEvent, BinaryOutputEvent, ProcessInfo, SandboxInfo, SandboxSettings } from "./types.js";
4
+ import type { CommandResult, TracedFiles, DeletedSandbox, ExecOptions, FileEntry, FileLink, OutputEvent, BinaryOutputEvent, ProcessInfo, SandboxInfo, SandboxSettings } from "./types.js";
5
5
  import { type SandboxExtensions } from "./products/index.js";
6
6
  import { Tunnel, type PortForward } from "./tunnel.js";
7
7
  import type { StreamUploadSource } from "./file-source.js";
@@ -206,6 +206,9 @@ export declare class Process {
206
206
  } & Pick<RequestOptions, "signal" | "timeoutMs">): Promise<void>;
207
207
  kill(signal?: "SIGTERM" | "SIGKILL" | "SIGINT" | "SIGHUP" | "SIGQUIT" | "SIGUSR1" | "SIGUSR2", options?: RequestOptions): Promise<void>;
208
208
  resize(cols: number, rows: number, options?: RequestOptions): Promise<void>;
209
+ /** What a process spawned with `traceFiles` opened and wrote. Known once it
210
+ * has ended: a running one answers 409 process_running. */
211
+ files(): Promise<TracedFiles>;
209
212
  refresh(): Promise<ProcessInfo>;
210
213
  }
211
214
  export declare class Terminal {
@@ -314,6 +317,31 @@ export declare class Files {
314
317
  rename(from: string, to: string, options?: {
315
318
  overwrite?: boolean;
316
319
  } & RequestOptions): Promise<void>;
320
+ /** A link anyone can open with no API key to download this file, for a
321
+ * browser or another service: Range works, so videos play and downloads
322
+ * resume. One hour by default, at most 7 days (`expiresInSeconds`).
323
+ * `filename` makes it a download with that name; `archive: true` links a
324
+ * folder as a .tar.gz. Revoking the key that made it ends it, as does
325
+ * `revokeLinks()`. */
326
+ downloadUrl(path: string, options?: RequestOptions & {
327
+ expiresInSeconds?: number;
328
+ filename?: string;
329
+ wake?: boolean;
330
+ archive?: boolean;
331
+ }): Promise<FileLink>;
332
+ /** A link anyone can PUT a file's bytes to, with no API key: the body
333
+ * becomes the file at `path`, whole or not at all, up to `maxBytes` (5 GiB
334
+ * at most; the body must arrive within 5 minutes). */
335
+ uploadUrl(path: string, options?: RequestOptions & {
336
+ expiresInSeconds?: number;
337
+ maxBytes?: number;
338
+ wake?: boolean;
339
+ }): Promise<FileLink>;
340
+ /** Ends every link made for this sandbox so far; links made after work. */
341
+ revokeLinks(options?: RequestOptions): Promise<{
342
+ sandboxId: string;
343
+ revokedAt: string;
344
+ }>;
317
345
  /** Packs a directory as archive bytes; `gzip: false` returns plain tar.
318
346
  * Exclusions are relative paths, not globs. `user: root` selects guest root. */
319
347
  archive(path: string, options?: RequestOptions & {
package/dist/sandbox.js CHANGED
@@ -86,6 +86,7 @@ function commandBody(command, options) {
86
86
  ? { stdin: options.stdin }
87
87
  : { stdinBase64: toBase64(options.stdin) }),
88
88
  ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
89
+ ...(options.traceFiles ? { traceFiles: true } : {}),
89
90
  };
90
91
  }
91
92
  /** How long past its timeoutMs a streamed exec waits for an exit that never came. */
@@ -190,6 +191,12 @@ export class Sandbox extends Machine {
190
191
  finally {
191
192
  clearTimeout(timer);
192
193
  }
194
+ // A stream the server ended (continue) is followed to its exit, which
195
+ // names no files: they are the process's own to read.
196
+ const files = exit?.files ??
197
+ (options.traceFiles && processId && exit?.state
198
+ ? await new Process(this.#t, this.id, { id: processId }).files()
199
+ : undefined);
193
200
  result = {
194
201
  exitCode: exit?.exitCode ?? null,
195
202
  ...out,
@@ -198,6 +205,7 @@ export class Sandbox extends Machine {
198
205
  ...(exit?.durationMs === undefined ? {} : { durationMs: exit.durationMs }),
199
206
  ...(processId ? { processId } : {}),
200
207
  ...(exit?.outOfMemory ? { outOfMemory: exit.outOfMemory } : {}),
208
+ ...(files ? { files } : {}),
201
209
  };
202
210
  }
203
211
  if (options.check && (result.exitCode !== 0 || result.timedOut))
@@ -354,6 +362,8 @@ export class Sandbox extends Machine {
354
362
  path: `/v1/sandboxes/${enc(this.id)}:fork`,
355
363
  body,
356
364
  wait: 60,
365
+ // Its key replays a refusal for room or a limit, so it is told at once.
366
+ waitForCapacityMs: 0,
357
367
  ...requestOnly({ idempotencyKey, signal, timeoutMs }),
358
368
  });
359
369
  const sandboxes = reply.sandboxes.map((info) => new Sandbox(this.#t, info));
@@ -410,9 +420,11 @@ function processBytes(encoded) {
410
420
  function cutOff(error) {
411
421
  return (!(error instanceof RuntimeError) ||
412
422
  error instanceof ConnectionError ||
413
- ((error instanceof RateLimitError || error instanceof ServiceUnavailableError) &&
414
- [429, 502, 503, 504].includes(error.status) &&
415
- !DELIBERATE.has(error.code)));
423
+ // The API says whether it passes; an older server is judged by status.
424
+ (error.sentRetryable ??
425
+ ((error instanceof RateLimitError || error instanceof ServiceUnavailableError) &&
426
+ [429, 502, 503, 504].includes(error.status) &&
427
+ !DELIBERATE.has(error.code))));
416
428
  }
417
429
  /** How long a follow keeps reconnecting while every attempt fails. A command
418
430
  * runs for hours whether or not anyone reads it; a few minutes of a deploy, a
@@ -758,6 +770,14 @@ export class Process {
758
770
  ...options,
759
771
  });
760
772
  }
773
+ /** What a process spawned with `traceFiles` opened and wrote. Known once it
774
+ * has ended: a running one answers 409 process_running. */
775
+ files() {
776
+ return this.t.json({
777
+ method: "GET",
778
+ path: `/v1/sandboxes/${enc(this.sandboxId)}/processes/${enc(this.id)}/files`,
779
+ });
780
+ }
761
781
  async refresh() {
762
782
  this.info = await this.t.json({
763
783
  method: "GET",
@@ -817,6 +837,8 @@ export class Terminal {
817
837
  });
818
838
  }
819
839
  write(data) {
840
+ // A view of shared memory is refused by WebSocket.send: the bytes it
841
+ // takes are an ArrayBuffer's, which TypeScript 7's DOM types ask for.
820
842
  this.socket.send(typeof data === "string" ? new TextEncoder().encode(data) : data);
821
843
  }
822
844
  resize(cols, rows) {
@@ -1213,6 +1235,37 @@ export class Files {
1213
1235
  ...request,
1214
1236
  });
1215
1237
  }
1238
+ /** A link anyone can open with no API key to download this file, for a
1239
+ * browser or another service: Range works, so videos play and downloads
1240
+ * resume. One hour by default, at most 7 days (`expiresInSeconds`).
1241
+ * `filename` makes it a download with that name; `archive: true` links a
1242
+ * folder as a .tar.gz. Revoking the key that made it ends it, as does
1243
+ * `revokeLinks()`. */
1244
+ async downloadUrl(path, options = {}) {
1245
+ const { expiresInSeconds, filename, wake, archive, ...request } = options;
1246
+ return this.t.json({
1247
+ method: "POST",
1248
+ path: this.#path("/files:link"),
1249
+ body: { path, method: "GET", expiresInSeconds, filename, wake, archive },
1250
+ ...request,
1251
+ });
1252
+ }
1253
+ /** A link anyone can PUT a file's bytes to, with no API key: the body
1254
+ * becomes the file at `path`, whole or not at all, up to `maxBytes` (5 GiB
1255
+ * at most; the body must arrive within 5 minutes). */
1256
+ async uploadUrl(path, options = {}) {
1257
+ const { expiresInSeconds, maxBytes, wake, ...request } = options;
1258
+ return this.t.json({
1259
+ method: "POST",
1260
+ path: this.#path("/files:link"),
1261
+ body: { path, method: "PUT", expiresInSeconds, maxBytes, wake },
1262
+ ...request,
1263
+ });
1264
+ }
1265
+ /** Ends every link made for this sandbox so far; links made after work. */
1266
+ async revokeLinks(options = {}) {
1267
+ return this.t.json({ method: "POST", path: this.#path("/files:revoke-links"), ...options });
1268
+ }
1216
1269
  /** Packs a directory as archive bytes; `gzip: false` returns plain tar.
1217
1270
  * Exclusions are relative paths, not globs. `user: root` selects guest root. */
1218
1271
  async archive(path, options = {}) {
@@ -17,7 +17,7 @@ export type Snapshot = {
17
17
  diskMiB: number;
18
18
  memoryGuarantee: string;
19
19
  };
20
- /** Null: kept while your account has credit, up to a year from when it was taken. */
20
+ /** Null: kept while your account has credit, with no end date. */
21
21
  retentionDays: number | null;
22
22
  /** True while raw fork memory awaits verified compression. The snapshot
23
23
  * remains ready for starts on its current host, with no storage charge or
@@ -0,0 +1,5 @@
1
+ import { type RuntimeError } from "./errors.js";
2
+ /** A product switched off on this deployment, by its `<product>_unavailable`
3
+ * code: a 501, or a 503 with a deliberate code from an older server (whose
4
+ * host_unavailable and api_unavailable are 503s too, and pass). */
5
+ export declare function productSwitchedOff(error: RuntimeError): boolean;
@@ -0,0 +1,8 @@
1
+ import { DELIBERATE } from "./errors.js";
2
+ /** A product switched off on this deployment, by its `<product>_unavailable`
3
+ * code: a 501, or a 503 with a deliberate code from an older server (whose
4
+ * host_unavailable and api_unavailable are 503s too, and pass). */
5
+ export function productSwitchedOff(error) {
6
+ return (error.code.endsWith("_unavailable") &&
7
+ (error.status === 501 || (error.status === 503 && DELIBERATE.has(error.code))));
8
+ }
package/dist/tar.d.ts CHANGED
@@ -8,7 +8,7 @@ export declare function packDirectory(root: string): Promise<Uint8Array>;
8
8
  * once the whole archive arrived, so an archive cut short leaves nothing
9
9
  * behind that could pass for the folder. A target that exists is merged
10
10
  * into, files of the same name replaced. */
11
- export declare function unpackStream(source: AsyncIterable<Uint8Array> | Iterable<Uint8Array>, target: string, options?: {
11
+ export declare function unpackStream(from: AsyncIterable<Uint8Array> | Iterable<Uint8Array> | ReadableStream<Uint8Array>, target: string, options?: {
12
12
  signal?: AbortSignal;
13
13
  }): Promise<void>;
14
14
  /** Unpacks a whole gzipped tar held in memory, by the same rules. */
package/dist/tar.js CHANGED
@@ -455,14 +455,32 @@ async function merge(from, to, root, signal) {
455
455
  await rename(source, destination);
456
456
  }
457
457
  }
458
+ /** A web stream's chunks, read through its reader. The SDK is built with
459
+ * TypeScript 5, whose DOM types do not make a ReadableStream async iterable
460
+ * (TypeScript 7's do), so a download's body is read this way in both. */
461
+ async function* chunksOf(stream) {
462
+ const reader = stream.getReader();
463
+ try {
464
+ for (;;) {
465
+ const { done, value } = await reader.read();
466
+ if (done)
467
+ return;
468
+ yield value;
469
+ }
470
+ }
471
+ finally {
472
+ reader.releaseLock();
473
+ }
474
+ }
458
475
  /** Unpacks a gzipped tar that arrives as a stream into `target`, refusing any
459
476
  * entry that would land outside it, holding no more than a chunk at a time.
460
477
  * It unpacks into a folder beside the target and moves it into place only
461
478
  * once the whole archive arrived, so an archive cut short leaves nothing
462
479
  * behind that could pass for the folder. A target that exists is merged
463
480
  * into, files of the same name replaced. */
464
- export async function unpackStream(source, target, options = {}) {
481
+ export async function unpackStream(from, target, options = {}) {
465
482
  checkSignal(options.signal);
483
+ const source = from instanceof ReadableStream ? chunksOf(from) : from;
466
484
  const final = resolve(target);
467
485
  await mkdir(dirname(final), { recursive: true });
468
486
  const staging = await mkdtemp(`${final}.runtime-partial-`);
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.23.0";
2
+ export declare const VERSION = "0.33.0";
3
3
  export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
4
4
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
5
5
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
package/dist/transport.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ConnectionError, DELIBERATE, errorFor, NOTHING_RAN, RuntimeError, WAITS_FOR_ROOM, } from "./errors.js";
2
- import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
2
+ import { describeRoute, envFetch, openWebSocket, untrustedCertificate } from "./proxy.js";
3
3
  import { warningText } from "./product-renames.js";
4
- export const VERSION = "0.23.0";
4
+ export const VERSION = "0.33.0";
5
5
  export const DEFAULT_BASE_URL = "https://api.withruntime.com";
6
6
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
7
7
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
@@ -42,6 +42,10 @@ function processEnv() {
42
42
  /** How long an answer that ran nothing (NOTHING_RAN) is retried for a call
43
43
  * that has no deadline of its own. */
44
44
  const NOTHING_RAN_MS = 300_000;
45
+ /** The longest wait the SDK sleeps on for a retry. A refusal that names a
46
+ * longer one (a daily cap, about an hour) is thrown at once, still
47
+ * retryable and with its `retryAfterMs`, for the caller to schedule. */
48
+ const LONGEST_RETRY_WAIT_MS = 60_000;
45
49
  /** How long one attempt waits for its answer to start. The API starts every
46
50
  * answer within 120 s: Caddy answers 503 itself past that, and a longer call
47
51
  * gets its 200 at 90 s and then its JSON (runtime-late-answer). An attempt
@@ -316,6 +320,19 @@ export class Transport {
316
320
  const route = this.#routed && !cancelled
317
321
  ? describeRoute(`${this.baseUrl}${call.path}`, cause)
318
322
  : { via: "" };
323
+ // A certificate this machine does not trust answered, most often a proxy
324
+ // that inspects TLS: said as that, never as "no answer" (18 October 2026,
325
+ // the published CLI on Node behind such a proxy, where Bun trusted it).
326
+ const certificate = cancelled ? undefined : untrustedCertificate(cause);
327
+ if (certificate)
328
+ return new ConnectionError({
329
+ message: `Runtime at ${this.baseUrl}${route.via} could not be reached: the TLS certificate presented is not one this machine trusts (${certificate}).`,
330
+ hint: "A proxy or firewall that inspects TLS signs with its own authority: point NODE_EXTRA_CA_CERTS at its certificate (Node), or add it to the system's trusted certificates.",
331
+ code: "connection_error",
332
+ status: 0,
333
+ ...(call.idempotencyKey ? { idempotencyKey: call.idempotencyKey } : {}),
334
+ cause,
335
+ });
319
336
  return new ConnectionError({
320
337
  message: cancelled
321
338
  ? "The call was cancelled or ran past its deadline."
@@ -453,13 +470,17 @@ export class Transport {
453
470
  parsed = { error: { message: text.slice(0, 500) || `HTTP ${status}` } };
454
471
  }
455
472
  const error = errorFor(status, parsed, key);
473
+ const header = Number(response.headers.get("retry-after"));
474
+ const headerMs = Number.isFinite(header) && header > 0 ? header * 1000 : undefined;
456
475
  // A full house: wait for a slot or for room, then send the same call
457
476
  // again. A request that can never fit (a fork of more copies than the
458
477
  // trial runs) is not waited for.
459
478
  if (retry &&
460
479
  roomMs > 0 &&
461
480
  WAITS_FOR_ROOM.has(error.code) &&
462
- error.details?.field !== "count") {
481
+ error.details?.field !== "count" &&
482
+ // A quota on what is kept says it never clears by waiting.
483
+ error.sentRetryable !== false) {
463
484
  const left = roomUntil - performance.now();
464
485
  if (left <= 0)
465
486
  throw error;
@@ -478,21 +499,32 @@ export class Transport {
478
499
  // still says nothing ran.
479
500
  if ([429, 503].includes(status) &&
480
501
  NOTHING_RAN.has(error.code) &&
502
+ error.sentRetryable !== false &&
481
503
  (!late || error.code === "guest_busy")) {
482
- const header = Number(response.headers.get("retry-after"));
483
- const base = error.retryAfterMs ?? (Number.isFinite(header) && header > 0 ? header * 1000 : 250);
504
+ const named = error.retryAfterMs ?? headerMs;
505
+ if (named !== undefined && named > LONGEST_RETRY_WAIT_MS)
506
+ throw withWait(error, named);
507
+ const base = named ?? 250;
484
508
  const pause = Math.min(30_000, base * 2 ** Math.min(idleAttempt++, 3)) * (0.9 + Math.random() * 0.2);
485
509
  if (pause >= idleUntil() - performance.now())
486
510
  throw error;
487
511
  await this.#retryAfter(error, pause, signal, gate);
488
512
  continue;
489
513
  }
490
- const retryable = [429, 502, 503, 504].includes(status) && !DELIBERATE.has(error.code);
514
+ // The API says whether the same request may succeed; an older
515
+ // server is judged by its status. A refusal for room or for the
516
+ // account's own limit is the room wait's above on a create, wake or
517
+ // restart, which set waitForCapacityMs (0 fails at once); any other
518
+ // call retries what the answer says.
519
+ const retryable = (error.sentRetryable ??
520
+ ([429, 502, 503, 504].includes(status) && !DELIBERATE.has(error.code))) &&
521
+ !(call.waitForCapacityMs !== undefined && WAITS_FOR_ROOM.has(error.code));
491
522
  if (!retry || !retryable || attempt >= this.#maxRetries)
492
523
  throw error;
493
- const header = Number(response.headers.get("retry-after"));
494
- const wait = error.retryAfterMs ??
495
- (Number.isFinite(header) && header > 0 ? header * 1000 : backoff(attempt));
524
+ const named = error.retryAfterMs ?? headerMs;
525
+ if (named !== undefined && named > LONGEST_RETRY_WAIT_MS)
526
+ throw withWait(error, named);
527
+ const wait = named ?? backoff(attempt);
496
528
  await this.#rest(Math.min(30_000, wait) * (0.9 + Math.random() * 0.2), signal, gate);
497
529
  }
498
530
  catch (cause) {
@@ -796,6 +828,13 @@ export function neverSent(cause) {
796
828
  }
797
829
  return false;
798
830
  }
831
+ /** `error` thrown for a wait too long to sleep on, carrying that wait as
832
+ * `retryAfterMs` when only the Retry-After header named it. */
833
+ function withWait(error, ms) {
834
+ if (error.retryAfterMs === undefined)
835
+ error.retryAfterMs = Math.round(ms);
836
+ return error;
837
+ }
799
838
  function backoff(attempt) {
800
839
  return Math.min(8000, 250 * 2 ** attempt) * (0.5 + Math.random());
801
840
  }
package/dist/types.d.ts CHANGED
@@ -133,8 +133,9 @@ export type CreateSandbox = {
133
133
  onLeaseEnd?: "pause" | "stop" | "delete";
134
134
  /** Pause after this many seconds in which nothing happens in it: no
135
135
  * request, no command or terminal running, no open connection, no network
136
- * traffic and no CPU use. Default 60 unless persistent; 0 never;
137
- * otherwise 10 to 86400. A request wakes it again. */
136
+ * traffic and no CPU use. Default 300 unless persistent; 0 never;
137
+ * otherwise 60 to 86400 (a shorter time is raised to 60). A request wakes
138
+ * it again. */
138
139
  idlePauseSeconds?: number;
139
140
  /** A request to a paused sandbox wakes it. Default true. */
140
141
  autoWake?: boolean;
@@ -165,7 +166,7 @@ export type CreateSandbox = {
165
166
  snapshot?: string;
166
167
  /** One to 16 ids of your own sandboxes or snapshots. The new sandbox starts
167
168
  * on a server none of them is on, and stays apart if its start is moved.
168
- * With no other server that has room, the create answers 409 `no_capacity`
169
+ * With no other server that has room, the create answers 503 `no_capacity`
169
170
  * and nothing is made or charged. A copy of a snapshot starts only where that
170
171
  * snapshot is kept, so a `snapshot` with `spreadFrom` naming its server also
171
172
  * answers `no_capacity`. */
@@ -197,6 +198,16 @@ export type OutOfMemory = {
197
198
  /** What to do: a larger memoryMiB, or a program that uses less. */
198
199
  hint: string;
199
200
  };
201
+ /** What a command run with `traceFiles` opened and wrote, until it exited. */
202
+ export type TracedFiles = {
203
+ /** Every file and directory it, or anything it started, opened: sorted
204
+ * absolute paths, links resolved; a directory (a listing) ends in "/". */
205
+ read: string[];
206
+ /** Every file it wrote. */
207
+ written: string[];
208
+ /** Some paths are missing: the lists are not the whole story. */
209
+ truncated: boolean;
210
+ };
200
211
  export type CommandResult = {
201
212
  exitCode: number | null;
202
213
  stdout: string;
@@ -208,6 +219,8 @@ export type CommandResult = {
208
219
  processId?: string;
209
220
  replayed?: boolean;
210
221
  outOfMemory?: OutOfMemory;
222
+ /** With `traceFiles` only. */
223
+ files?: TracedFiles;
211
224
  };
212
225
  export type ExecOptions = {
213
226
  cwd?: string;
@@ -226,6 +239,9 @@ export type ExecOptions = {
226
239
  onStderr?: (text: string) => unknown;
227
240
  /** Throw CommandError when the exit code is not 0. */
228
241
  check?: boolean;
242
+ /** Answer `files`: what the command read and wrote, for keying a cache by
243
+ * what a test really read. */
244
+ traceFiles?: boolean;
229
245
  signal?: AbortSignal;
230
246
  idempotencyKey?: string;
231
247
  };
@@ -265,6 +281,8 @@ export type OutputEvent = {
265
281
  timedOut: boolean;
266
282
  durationMs?: number;
267
283
  outOfMemory?: OutOfMemory;
284
+ /** An exec with `traceFiles`. */
285
+ files?: TracedFiles;
268
286
  } | {
269
287
  type: "truncated";
270
288
  droppedBytes: number;
@@ -302,6 +320,19 @@ export type FileEntry = {
302
320
  group?: string;
303
321
  symlinkTarget?: string;
304
322
  };
323
+ /** A signed link to one file of a sandbox (POST .../files:link): `url` opens
324
+ * with no API key, GET to download (Range works) or PUT to upload, until
325
+ * `expiresAt`. */
326
+ export type FileLink = {
327
+ url: string;
328
+ method: "GET" | "PUT";
329
+ path: string;
330
+ sandboxId: string;
331
+ expiresAt: string;
332
+ filename?: string;
333
+ maxBytes?: number;
334
+ archive?: boolean;
335
+ };
305
336
  /** GET /v1/usage. Money is integer microdollars in strings (1,000,000 = $1),
306
337
  * exact past 2^53: available = credited - spent - expired - held. */
307
338
  export type Usage = {
@@ -457,7 +488,7 @@ export type SandboxSettings = {
457
488
  /** A request to a paused sandbox wakes it. */
458
489
  autoWake?: boolean;
459
490
  /** Pause after this many seconds with no activity, counted from now; 0
460
- * never, otherwise 10 to 86400. */
491
+ * never, otherwise 60 to 86400 (a shorter time is raised to 60). */
461
492
  idlePauseSeconds?: number;
462
493
  /** Keep it running until you stop it, while credit lasts, and keep its disk
463
494
  * after a stop. Paid only. */