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.
- package/COMPATIBILITY.md +64 -3
- package/E2B.md +1 -1
- package/README.md +36 -2
- package/README.src.md +35 -1
- package/compatibility-lock.json +3 -3
- package/dist/agents/browser.d.ts +12 -0
- package/dist/agents/browser.js +12 -0
- package/dist/agents/index.d.ts +7 -7
- package/dist/agents/index.js +7 -8
- package/dist/agents-cli.d.ts +1 -1
- package/dist/agents-cli.js +250 -3
- package/dist/agents-deploy.d.ts +186 -0
- package/dist/agents-deploy.js +752 -0
- package/dist/agents-file.d.ts +19 -0
- package/dist/agents-file.js +135 -0
- package/dist/apps/index.d.ts +10 -0
- package/dist/apps/index.js +11 -0
- package/dist/apps-cli.d.ts +3 -1
- package/dist/apps-cli.js +285 -38
- package/dist/blaxel/errors.js +2 -1
- package/dist/ci/index.d.ts +10 -0
- package/dist/ci/index.js +11 -0
- package/dist/ci-cli.d.ts +10 -0
- package/dist/ci-cli.js +343 -0
- package/dist/cli.d.ts +18 -7
- package/dist/cli.js +775 -85
- package/dist/daytona/errors.js +2 -1
- package/dist/e2b/errors.js +2 -1
- package/dist/e2b/filesystem.d.ts +1 -0
- package/dist/e2b/filesystem.js +1 -1
- package/dist/e2b/sandbox.d.ts +11 -2
- package/dist/e2b/sandbox.js +22 -6
- package/dist/errors.d.ts +11 -4
- package/dist/errors.js +27 -4
- package/dist/fly/index.d.ts +102 -0
- package/dist/fly/index.js +930 -0
- package/dist/index.d.ts +7 -3
- package/dist/machine.d.ts +10 -6
- package/dist/machine.js +14 -6
- package/dist/modal/index.d.ts +34 -0
- package/dist/modal/index.js +128 -22
- package/dist/products/agents.d.ts +101 -3
- package/dist/products/agents.js +49 -0
- package/dist/products/apps.d.ts +206 -10
- package/dist/products/apps.js +172 -1
- package/dist/products/ci.d.ts +332 -0
- package/dist/products/ci.js +101 -0
- package/dist/products/index.d.ts +8 -0
- package/dist/products/index.js +15 -0
- package/dist/products/jobs.d.ts +105 -5
- package/dist/products/jobs.js +47 -3
- package/dist/products/network.d.ts +13 -0
- package/dist/products/observability.d.ts +1 -1
- package/dist/products/plans.d.ts +131 -0
- package/dist/products/plans.js +24 -0
- package/dist/products/reservations.d.ts +108 -0
- package/dist/products/reservations.js +41 -0
- package/dist/products/resources.d.ts +2 -1
- package/dist/products/secrets.d.ts +90 -6
- package/dist/products/secrets.js +8 -2
- package/dist/proxy.d.ts +3 -3
- package/dist/proxy.js +20 -0
- package/dist/sandbox.d.ts +29 -1
- package/dist/sandbox.js +56 -3
- package/dist/snapshots.d.ts +1 -1
- package/dist/switched-off.d.ts +5 -0
- package/dist/switched-off.js +8 -0
- package/dist/tar.d.ts +1 -1
- package/dist/tar.js +19 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +48 -9
- package/dist/types.d.ts +35 -4
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/vercel/errors.js +2 -1
- package/dist/vercel/snapshot.js +4 -2
- 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),
|
|
14
|
-
*
|
|
15
|
-
* placeholder, and its requests never
|
|
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
|
|
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
|
package/dist/products/secrets.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
20
|
-
*
|
|
21
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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 = {}) {
|
package/dist/snapshots.d.ts
CHANGED
|
@@ -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,
|
|
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(
|
|
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(
|
|
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-`);
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RuntimeError } from "./errors.js";
|
|
2
|
-
export declare const VERSION = "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.
|
|
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
|
|
483
|
-
|
|
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
|
-
|
|
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
|
|
494
|
-
|
|
495
|
-
(
|
|
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
|
|
137
|
-
* otherwise
|
|
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
|
|
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
|
|
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. */
|