withruntime 0.8.1 → 0.8.3
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/BLAXEL.md +44 -42
- package/DAYTONA.md +16 -16
- package/E2B.md +1 -1
- package/README.md +30 -5
- package/README.src.md +194 -0
- package/VERCEL.md +1 -1
- package/dist/blaxel/index.d.ts +1 -1
- package/dist/blaxel/index.js +2 -2
- package/dist/blaxel/sandbox.d.ts +8 -2
- package/dist/blaxel/sandbox.js +16 -4
- package/dist/blaxel/session.d.ts +31 -0
- package/dist/blaxel/session.js +134 -0
- package/dist/blaxel/unsupported.d.ts +2 -4
- package/dist/blaxel/unsupported.js +2 -4
- package/dist/browser-stub.d.ts +1 -0
- package/dist/browser-stub.js +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +414 -25
- package/dist/daytona/daytona.js +7 -3
- package/dist/daytona/sandbox.js +3 -3
- package/dist/e2b/unsupported.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/keys.d.ts +6 -0
- package/dist/keys.js +4 -0
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/jobs.d.ts +213 -0
- package/dist/products/jobs.js +186 -0
- package/dist/products/secrets.d.ts +81 -9
- package/dist/products/secrets.js +214 -19
- package/dist/sandbox.d.ts +63 -1
- package/dist/sandbox.js +120 -10
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +1 -1
- package/dist/types.d.ts +11 -6
- package/dist/vercel/sandbox.js +1 -1
- package/package.json +7 -1
package/dist/products/secrets.js
CHANGED
|
@@ -1,24 +1,219 @@
|
|
|
1
|
+
import { NotFoundError, RuntimeError } from "../errors.js";
|
|
2
|
+
import { switchedOff } from "./jobs.js";
|
|
3
|
+
/** A write that reached one copy of a secret and not the other: `stored`
|
|
4
|
+
* names what took effect. Calling again with the same arguments is safe. */
|
|
5
|
+
export class SecretPartlyStoredError extends RuntimeError {
|
|
6
|
+
}
|
|
7
|
+
const unavailable = (error) => error instanceof RuntimeError && (error.code === "unavailable" || error.status === 503);
|
|
1
8
|
export function secrets(t) {
|
|
9
|
+
const off = switchedOff("Secrets for jobs");
|
|
10
|
+
const store = (call) => t.json(call).catch(off);
|
|
2
11
|
const path = (name) => `/v1/egress-secrets/${encodeURIComponent(name)}`;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const jobPath = (id) => `/v1/secrets/${encodeURIComponent(id)}`;
|
|
13
|
+
/** Every jobs copy this key's person owns, deleted ones included. */
|
|
14
|
+
async function jobCopies(options) {
|
|
15
|
+
const all = [];
|
|
16
|
+
let after;
|
|
17
|
+
for (;;) {
|
|
18
|
+
const page = await store({
|
|
19
|
+
method: "GET",
|
|
20
|
+
path: "/v1/secrets",
|
|
21
|
+
query: { limit: 100, ...(after ? { after } : {}) },
|
|
22
|
+
...options,
|
|
23
|
+
});
|
|
24
|
+
all.push(...page);
|
|
25
|
+
if (page.length < 100)
|
|
26
|
+
return all;
|
|
27
|
+
after = page.at(-1).id;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const liveCopy = async (name, options) => (await jobCopies(options)).find((s) => s.name === name && s.deletedAt === null);
|
|
31
|
+
const list = async (options) => (await t.json({
|
|
32
|
+
method: "GET",
|
|
33
|
+
path: "/v1/egress-secrets",
|
|
34
|
+
...options,
|
|
35
|
+
})).secrets;
|
|
36
|
+
const key = (options, part) => options?.idempotencyKey
|
|
37
|
+
? { ...options, idempotencyKey: `${options.idempotencyKey}:${part}`.slice(0, 128) }
|
|
38
|
+
: options;
|
|
39
|
+
async function setJobCopy(name, value, options) {
|
|
40
|
+
const copies = await jobCopies(options);
|
|
41
|
+
const live = copies.find((s) => s.name === name && s.deletedAt === null);
|
|
42
|
+
if (live)
|
|
43
|
+
return store({
|
|
44
|
+
method: "POST",
|
|
45
|
+
path: `${jobPath(live.id)}:rotate`,
|
|
46
|
+
body: { value, expectedVersion: live.version },
|
|
47
|
+
...options,
|
|
48
|
+
});
|
|
49
|
+
if (copies.some((s) => s.name === name))
|
|
50
|
+
throw new RuntimeError({
|
|
51
|
+
message: `A jobs secret named ${name} was deleted, and a deleted name cannot be used again.`,
|
|
52
|
+
code: "name_conflict",
|
|
53
|
+
status: 409,
|
|
54
|
+
hint: "Store it under another name.",
|
|
55
|
+
details: { field: "name" },
|
|
56
|
+
});
|
|
57
|
+
return store({
|
|
58
|
+
method: "POST",
|
|
59
|
+
path: "/v1/secrets",
|
|
60
|
+
body: { name, value },
|
|
15
61
|
...options,
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function all(options) {
|
|
65
|
+
const [forSandboxes, forJobs] = await Promise.all([
|
|
66
|
+
list(options),
|
|
67
|
+
jobCopies(options).catch((error) => {
|
|
68
|
+
if (unavailable(error))
|
|
69
|
+
return [];
|
|
70
|
+
throw error;
|
|
71
|
+
}),
|
|
72
|
+
]);
|
|
73
|
+
const byName = new Map();
|
|
74
|
+
for (const s of forSandboxes)
|
|
75
|
+
byName.set(s.name, { name: s.name, sandboxes: s, jobs: null });
|
|
76
|
+
for (const j of forJobs.filter((j) => j.deletedAt === null))
|
|
77
|
+
byName.set(j.name, {
|
|
78
|
+
name: j.name,
|
|
79
|
+
sandboxes: byName.get(j.name)?.sandboxes ?? null,
|
|
80
|
+
jobs: j,
|
|
81
|
+
});
|
|
82
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
83
|
+
}
|
|
84
|
+
async function set(name, input, options) {
|
|
85
|
+
const { jobs, value, ...egress } = input;
|
|
86
|
+
if (!egress.hosts?.length && !jobs)
|
|
87
|
+
throw new RuntimeError({
|
|
88
|
+
message: "Say where the value may go: hosts for sandboxes, jobs: true for scheduled jobs, or both.",
|
|
89
|
+
code: "invalid_request",
|
|
90
|
+
status: 0,
|
|
91
|
+
details: { field: "hosts" },
|
|
92
|
+
});
|
|
93
|
+
let sandboxes;
|
|
94
|
+
if (egress.hosts?.length)
|
|
95
|
+
sandboxes = await t.json({
|
|
96
|
+
method: "PUT",
|
|
97
|
+
path: path(name),
|
|
98
|
+
body: { value, ...egress },
|
|
99
|
+
...(jobs ? key(options, "sandboxes") : options),
|
|
100
|
+
});
|
|
101
|
+
if (!jobs)
|
|
102
|
+
return sandboxes;
|
|
103
|
+
try {
|
|
104
|
+
const copy = await setJobCopy(name, value, key(options, "jobs"));
|
|
105
|
+
return { ...(sandboxes ?? { name }), jobs: copy };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (!sandboxes)
|
|
109
|
+
throw error;
|
|
110
|
+
const cause = error instanceof RuntimeError ? error : undefined;
|
|
111
|
+
throw new SecretPartlyStoredError({
|
|
112
|
+
message: `Stored ${name} for sandboxes, but not for jobs: ${cause?.message ?? String(error)}`,
|
|
113
|
+
code: cause?.code ?? "partly_stored",
|
|
114
|
+
status: cause?.status ?? 0,
|
|
115
|
+
hint: "Run the same call again: storing it for sandboxes again changes nothing, and the jobs copy is stored or rotated.",
|
|
116
|
+
details: { stored: ["sandboxes"], field: "jobs" },
|
|
117
|
+
...(cause?.requestId ? { requestId: cause.requestId } : {}),
|
|
118
|
+
cause: error,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
/** Store or replace a secret. With `hosts`, sandboxes see a placeholder
|
|
124
|
+
* and the egress proxy puts the value into HTTPS requests to them
|
|
125
|
+
* (replacing keeps the placeholder). With `jobs: true`, a copy scheduled
|
|
126
|
+
* jobs can put into their environment; setting it again rotates it. Both
|
|
127
|
+
* at once store both; if only the first lands, `SecretPartlyStoredError`
|
|
128
|
+
* says so and the same call again finishes it. */
|
|
129
|
+
set,
|
|
130
|
+
/** The sandboxes' secrets: names, hosts and placeholders. Never values.
|
|
131
|
+
* `all()` includes the jobs copies. */
|
|
132
|
+
list,
|
|
133
|
+
/** Every secret by name, with where its value may go. */
|
|
134
|
+
all,
|
|
135
|
+
/** One secret by name, with where its value may go. */
|
|
136
|
+
get: async (name, options) => {
|
|
137
|
+
const found = (await all(options)).find((s) => s.name === name);
|
|
138
|
+
if (!found)
|
|
139
|
+
throw new NotFoundError({
|
|
140
|
+
message: `No secret named ${name}.`,
|
|
141
|
+
code: "not_found",
|
|
142
|
+
status: 404,
|
|
143
|
+
hint: "List them with runtime.secrets.all().",
|
|
144
|
+
});
|
|
145
|
+
return found;
|
|
146
|
+
},
|
|
147
|
+
/** Replace the jobs copy's value with a new version. Runs already bound to
|
|
148
|
+
* the old version are refused it. Only the jobs copy has versions; to
|
|
149
|
+
* change the sandboxes' copy, `set` it again. */
|
|
150
|
+
rotate: async (name, value, options) => {
|
|
151
|
+
const live = await liveCopy(name, options);
|
|
152
|
+
if (!live)
|
|
153
|
+
throw new NotFoundError({
|
|
154
|
+
message: `${name} has no copy for jobs to rotate.`,
|
|
155
|
+
code: "not_found",
|
|
156
|
+
status: 404,
|
|
157
|
+
hint: "Store one with set(name, { value, jobs: true }). The sandboxes' copy is replaced by set, not rotated.",
|
|
158
|
+
});
|
|
159
|
+
return store({
|
|
160
|
+
method: "POST",
|
|
161
|
+
path: `${jobPath(live.id)}:rotate`,
|
|
162
|
+
body: { value, expectedVersion: live.version },
|
|
163
|
+
...options,
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
/** Read the jobs copy's value back (needs the key's secrets_reveal
|
|
167
|
+
* permission). The sandboxes' copy can never be read back. */
|
|
168
|
+
reveal: async (name, query = {}, options) => {
|
|
169
|
+
const live = await liveCopy(name, options);
|
|
170
|
+
if (!live)
|
|
171
|
+
throw new NotFoundError({
|
|
172
|
+
message: `${name} has no copy for jobs; the sandboxes' copy of a secret can never be read back.`,
|
|
173
|
+
code: "not_found",
|
|
174
|
+
status: 404,
|
|
175
|
+
});
|
|
176
|
+
return store({
|
|
177
|
+
method: "POST",
|
|
178
|
+
path: `${jobPath(live.id)}:reveal`,
|
|
179
|
+
body: query,
|
|
180
|
+
...options,
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
/** Delete a secret everywhere it is kept: the sandboxes' value is erased
|
|
184
|
+
* and its placeholder stops working, and the jobs copy stops being given
|
|
185
|
+
* to runs (its name stays taken). */
|
|
186
|
+
delete: async (name, options) => {
|
|
187
|
+
const live = await liveCopy(name, options).catch((error) => {
|
|
188
|
+
if (unavailable(error))
|
|
189
|
+
return undefined;
|
|
190
|
+
throw error;
|
|
191
|
+
});
|
|
192
|
+
let removed;
|
|
193
|
+
try {
|
|
194
|
+
removed = await t.json({
|
|
195
|
+
method: "DELETE",
|
|
196
|
+
path: path(name),
|
|
197
|
+
...(live ? key(options, "sandboxes") : options),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (!(live && error instanceof NotFoundError))
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
if (live)
|
|
205
|
+
await store({
|
|
206
|
+
method: "POST",
|
|
207
|
+
path: `${jobPath(live.id)}:delete`,
|
|
208
|
+
body: { expectedVersion: live.version },
|
|
209
|
+
...key(options, "jobs"),
|
|
210
|
+
});
|
|
211
|
+
return {
|
|
212
|
+
name,
|
|
213
|
+
deleted: true,
|
|
214
|
+
enforced: removed?.enforced ?? true,
|
|
215
|
+
from: [...(removed ? ["sandboxes"] : []), ...(live ? ["jobs"] : [])],
|
|
216
|
+
};
|
|
217
|
+
},
|
|
23
218
|
};
|
|
24
219
|
}
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Page } from "./page.js";
|
|
2
2
|
import { type FileEvent, type WatchHandle, type WatchOptions } from "./products/watch.js";
|
|
3
|
-
import
|
|
3
|
+
import { Transport, type RequestOptions } from "./transport.js";
|
|
4
4
|
import type { CommandResult, ExecOptions, FileEntry, OutputEvent, KeepAliveOptions, ProcessInfo, SandboxInfo, SandboxSettings } from "./types.js";
|
|
5
5
|
import { type SandboxExtensions } from "./products/index.js";
|
|
6
6
|
import type { Snapshot, SnapshotOptions } from "./snapshots.js";
|
|
@@ -14,8 +14,27 @@ export declare class Sandbox implements AsyncDisposable {
|
|
|
14
14
|
#private;
|
|
15
15
|
readonly files: Files;
|
|
16
16
|
readonly processes: Processes;
|
|
17
|
+
/** Short-lived tokens that let a browser reach this sandbox directly. */
|
|
18
|
+
readonly sessions: SandboxSessions;
|
|
17
19
|
constructor(transport: Transport, info: SandboxInfo);
|
|
18
20
|
get id(): string;
|
|
21
|
+
/** A sandbox reached with a session token instead of an API key, for code in
|
|
22
|
+
* a browser: commands and their streams, processes, files and previews of
|
|
23
|
+
* this one sandbox. Makes no call; `info` holds only the id until
|
|
24
|
+
* `refresh()`. Your backend makes the session with `sbx.sessions.create()`
|
|
25
|
+
* and hands the page `token` and `sandboxId`.
|
|
26
|
+
*
|
|
27
|
+
* const sbx = Sandbox.fromSession({ token, sandboxId });
|
|
28
|
+
* const { stdout } = await sbx.exec("ls /workspace");
|
|
29
|
+
*/
|
|
30
|
+
static fromSession(session: {
|
|
31
|
+
token: string;
|
|
32
|
+
sandboxId: string;
|
|
33
|
+
/** The API's origin, `apiUrl` in the session; https://api.withruntime.com
|
|
34
|
+
* unless you run your own. */
|
|
35
|
+
apiUrl?: string;
|
|
36
|
+
fetch?: typeof fetch;
|
|
37
|
+
}): Sandbox;
|
|
19
38
|
/** What the API last said about this sandbox. `refresh()` asks again. */
|
|
20
39
|
get info(): SandboxInfo;
|
|
21
40
|
get state(): SandboxInfo["state"];
|
|
@@ -124,6 +143,49 @@ export declare class Sandbox implements AsyncDisposable {
|
|
|
124
143
|
[Symbol.asyncDispose](): Promise<void>;
|
|
125
144
|
toJSON(): SandboxInfo;
|
|
126
145
|
}
|
|
146
|
+
/** A sandbox session, as the API answers it. `token` is set only in the
|
|
147
|
+
* answer to `create`. */
|
|
148
|
+
export type SandboxSession = {
|
|
149
|
+
id: string;
|
|
150
|
+
sandboxId: string;
|
|
151
|
+
name: string | null;
|
|
152
|
+
origins: string[];
|
|
153
|
+
/** The agent whose key made it; the session acts as it. */
|
|
154
|
+
createdBy: string;
|
|
155
|
+
createdAt: string;
|
|
156
|
+
expiresAt: string;
|
|
157
|
+
revokedAt: string | null;
|
|
158
|
+
state: "active" | "expired" | "revoked";
|
|
159
|
+
};
|
|
160
|
+
export type CreatedSandboxSession = SandboxSession & {
|
|
161
|
+
/** The bearer token, `rtsess_...`: hand it to the page, never log it. */
|
|
162
|
+
token: string;
|
|
163
|
+
/** The API's origin, for `Sandbox.fromSession({ apiUrl })`. */
|
|
164
|
+
apiUrl: string;
|
|
165
|
+
};
|
|
166
|
+
/** Sessions: a token your backend makes and hands to your own frontend, which
|
|
167
|
+
* then runs commands, reads and writes files and reaches previews in this one
|
|
168
|
+
* sandbox directly, without proxying through your servers. A session cannot
|
|
169
|
+
* stop, pause, extend, fork, snapshot or change the sandbox, create anything,
|
|
170
|
+
* or reach anything else. It lasts an hour unless asked (a day at most), ends
|
|
171
|
+
* when the key that made it is revoked, and is revocable at once. */
|
|
172
|
+
export declare class SandboxSessions {
|
|
173
|
+
private readonly t;
|
|
174
|
+
private readonly sandboxId;
|
|
175
|
+
constructor(t: Transport, sandboxId: string);
|
|
176
|
+
/** A new session. `origins` are the exact pages that will use it
|
|
177
|
+
* (`https://app.example.com`, or `http://localhost:5173` while developing):
|
|
178
|
+
* the API answers CORS for them and refuses any other page. */
|
|
179
|
+
create(input?: {
|
|
180
|
+
ttlSeconds?: number;
|
|
181
|
+
origins?: string[];
|
|
182
|
+
name?: string;
|
|
183
|
+
}, options?: RequestOptions): Promise<CreatedSandboxSession>;
|
|
184
|
+
/** Sessions still active, and those that ended in the last day, newest first. */
|
|
185
|
+
list(options?: RequestOptions): Promise<SandboxSession[]>;
|
|
186
|
+
/** Ends a session now; its next request is refused. */
|
|
187
|
+
revoke(sessionId: string, options?: RequestOptions): Promise<SandboxSession>;
|
|
188
|
+
}
|
|
127
189
|
export declare class Processes {
|
|
128
190
|
private readonly t;
|
|
129
191
|
private readonly sandboxId;
|
package/dist/sandbox.js
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import { CommandError, ConnectionError, NotFoundError, RuntimeError } from "./errors.js";
|
|
2
2
|
import { Page } from "./page.js";
|
|
3
3
|
import { sandboxWatches, watchDirectory, } from "./products/watch.js";
|
|
4
|
+
import { Transport } from "./transport.js";
|
|
4
5
|
import { sandboxFactories } from "./products/index.js";
|
|
5
6
|
import { Tunnel } from "./tunnel.js";
|
|
6
7
|
const CHUNK = 1_048_576;
|
|
7
|
-
|
|
8
|
+
/** Chunks of a large write in flight at once. Each chunk's reply waits on the
|
|
9
|
+
* API and the guest, and the link idles while every chunk in flight waits:
|
|
10
|
+
* from a home link (12.6 MB/s up, 27 September 2026) 100 MB took 17-18 s at
|
|
11
|
+
* four and 11 s at eight, and sixteen was no faster. The guest takes 128
|
|
12
|
+
* connections at once. */
|
|
13
|
+
const PARALLEL = 8;
|
|
8
14
|
const enc = (id) => encodeURIComponent(id);
|
|
9
|
-
|
|
15
|
+
/* Web APIs only, so the sandbox's commands, processes and files work in a
|
|
16
|
+
browser with a session token (Sandbox.fromSession): no Buffer, no
|
|
17
|
+
node:crypto. */
|
|
18
|
+
const utf8 = (data) => typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
19
|
+
function base64(bytes) {
|
|
20
|
+
let text = "";
|
|
21
|
+
for (let i = 0; i < bytes.length; i += 0x8000)
|
|
22
|
+
text += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
23
|
+
return btoa(text);
|
|
24
|
+
}
|
|
25
|
+
const toBase64 = (data) => base64(utf8(data));
|
|
26
|
+
async function sha256Hex(data) {
|
|
27
|
+
const digest = await crypto.subtle.digest("SHA-256", utf8(data));
|
|
28
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
29
|
+
}
|
|
10
30
|
function commandBody(command, options) {
|
|
11
31
|
return {
|
|
12
32
|
...(typeof command === "string" ? { command } : { argv: [...command] }),
|
|
@@ -27,17 +47,44 @@ export class Sandbox {
|
|
|
27
47
|
#t;
|
|
28
48
|
files;
|
|
29
49
|
processes;
|
|
50
|
+
/** Short-lived tokens that let a browser reach this sandbox directly. */
|
|
51
|
+
sessions;
|
|
30
52
|
constructor(transport, info) {
|
|
31
53
|
this.#t = transport;
|
|
32
54
|
this.#info = info;
|
|
33
55
|
this.files = new Files(transport, info.id);
|
|
34
56
|
this.processes = new Processes(transport, info.id);
|
|
57
|
+
this.sessions = new SandboxSessions(transport, info.id);
|
|
35
58
|
for (const [name, make] of sandboxFactories())
|
|
36
59
|
Object.defineProperty(this, name, { value: make(transport, this), enumerable: false });
|
|
37
60
|
}
|
|
38
61
|
get id() {
|
|
39
62
|
return this.#info.id;
|
|
40
63
|
}
|
|
64
|
+
/** A sandbox reached with a session token instead of an API key, for code in
|
|
65
|
+
* a browser: commands and their streams, processes, files and previews of
|
|
66
|
+
* this one sandbox. Makes no call; `info` holds only the id until
|
|
67
|
+
* `refresh()`. Your backend makes the session with `sbx.sessions.create()`
|
|
68
|
+
* and hands the page `token` and `sandboxId`.
|
|
69
|
+
*
|
|
70
|
+
* const sbx = Sandbox.fromSession({ token, sandboxId });
|
|
71
|
+
* const { stdout } = await sbx.exec("ls /workspace");
|
|
72
|
+
*/
|
|
73
|
+
static fromSession(session) {
|
|
74
|
+
if (!/^rtsess_/.test(session.token))
|
|
75
|
+
throw new RuntimeError({
|
|
76
|
+
message: "That is not a sandbox session token (they start rtsess_).",
|
|
77
|
+
code: "invalid_request",
|
|
78
|
+
status: 0,
|
|
79
|
+
hint: "Make one on your backend with sbx.sessions.create() and pass its token.",
|
|
80
|
+
});
|
|
81
|
+
const transport = new Transport({
|
|
82
|
+
apiKey: session.token,
|
|
83
|
+
...(session.apiUrl ? { baseUrl: session.apiUrl } : {}),
|
|
84
|
+
...(session.fetch ? { fetch: session.fetch } : {}),
|
|
85
|
+
});
|
|
86
|
+
return new Sandbox(transport, { id: session.sandboxId });
|
|
87
|
+
}
|
|
41
88
|
/** What the API last said about this sandbox. `refresh()` asks again. */
|
|
42
89
|
get info() {
|
|
43
90
|
return this.#info;
|
|
@@ -434,7 +481,7 @@ class OutputCursor {
|
|
|
434
481
|
droppedBytes: event.offset - this.cursor,
|
|
435
482
|
resumeAt: event.offset,
|
|
436
483
|
};
|
|
437
|
-
this.cursor = Math.max(this.cursor, event.offset +
|
|
484
|
+
this.cursor = Math.max(this.cursor, event.offset + utf8(event.data).byteLength);
|
|
438
485
|
}
|
|
439
486
|
yield event;
|
|
440
487
|
}
|
|
@@ -458,6 +505,69 @@ function pick(options) {
|
|
|
458
505
|
...(options.timeoutMs ? { timeoutMs: options.timeoutMs + 60_000 } : {}),
|
|
459
506
|
};
|
|
460
507
|
}
|
|
508
|
+
/** Sessions: a token your backend makes and hands to your own frontend, which
|
|
509
|
+
* then runs commands, reads and writes files and reaches previews in this one
|
|
510
|
+
* sandbox directly, without proxying through your servers. A session cannot
|
|
511
|
+
* stop, pause, extend, fork, snapshot or change the sandbox, create anything,
|
|
512
|
+
* or reach anything else. It lasts an hour unless asked (a day at most), ends
|
|
513
|
+
* when the key that made it is revoked, and is revocable at once. */
|
|
514
|
+
export class SandboxSessions {
|
|
515
|
+
t;
|
|
516
|
+
sandboxId;
|
|
517
|
+
constructor(t, sandboxId) {
|
|
518
|
+
this.t = t;
|
|
519
|
+
this.sandboxId = sandboxId;
|
|
520
|
+
}
|
|
521
|
+
/** A new session. `origins` are the exact pages that will use it
|
|
522
|
+
* (`https://app.example.com`, or `http://localhost:5173` while developing):
|
|
523
|
+
* the API answers CORS for them and refuses any other page. */
|
|
524
|
+
async create(input = {}, options = {}) {
|
|
525
|
+
const path = `/v1/sandboxes/${enc(this.sandboxId)}/sessions`;
|
|
526
|
+
const made = await this.t.json({
|
|
527
|
+
method: "POST",
|
|
528
|
+
path,
|
|
529
|
+
body: input,
|
|
530
|
+
...options,
|
|
531
|
+
});
|
|
532
|
+
if (made.token)
|
|
533
|
+
return made;
|
|
534
|
+
/* The answer replayed an earlier create whose reply was lost, and a token
|
|
535
|
+
is shown only once: end that session and make a fresh one. */
|
|
536
|
+
await this.revoke(made.id).catch(() => undefined);
|
|
537
|
+
const fresh = await this.t.json({
|
|
538
|
+
method: "POST",
|
|
539
|
+
path,
|
|
540
|
+
body: input,
|
|
541
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
542
|
+
...(options.timeoutMs ? { timeoutMs: options.timeoutMs } : {}),
|
|
543
|
+
});
|
|
544
|
+
if (!fresh.token)
|
|
545
|
+
throw new RuntimeError({
|
|
546
|
+
message: "The session was made but its token did not arrive.",
|
|
547
|
+
code: "session_token_lost",
|
|
548
|
+
status: 0,
|
|
549
|
+
hint: `Revoke session ${fresh.id} and create another.`,
|
|
550
|
+
});
|
|
551
|
+
return fresh;
|
|
552
|
+
}
|
|
553
|
+
/** Sessions still active, and those that ended in the last day, newest first. */
|
|
554
|
+
async list(options = {}) {
|
|
555
|
+
return (await this.t.json({
|
|
556
|
+
method: "GET",
|
|
557
|
+
path: `/v1/sandboxes/${enc(this.sandboxId)}/sessions`,
|
|
558
|
+
...options,
|
|
559
|
+
})).data;
|
|
560
|
+
}
|
|
561
|
+
/** Ends a session now; its next request is refused. */
|
|
562
|
+
async revoke(sessionId, options = {}) {
|
|
563
|
+
return this.t.json({
|
|
564
|
+
method: "POST",
|
|
565
|
+
path: `/v1/sandboxes/${enc(this.sandboxId)}/sessions/${enc(sessionId)}:revoke`,
|
|
566
|
+
body: {},
|
|
567
|
+
...options,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
461
571
|
export class Processes {
|
|
462
572
|
t;
|
|
463
573
|
sandboxId;
|
|
@@ -563,14 +673,14 @@ export class Process {
|
|
|
563
673
|
}
|
|
564
674
|
/** Sends input. Offsets are tracked for you, so a retried write is never typed twice. */
|
|
565
675
|
async write(data, options = {}) {
|
|
566
|
-
const bytes =
|
|
676
|
+
const bytes = utf8(data);
|
|
567
677
|
let sent = 0;
|
|
568
678
|
do {
|
|
569
679
|
const reply = await this.t.json({
|
|
570
680
|
method: "POST",
|
|
571
681
|
path: `/v1/sandboxes/${enc(this.sandboxId)}/processes/${enc(this.id)}:write`,
|
|
572
682
|
body: {
|
|
573
|
-
base64: bytes.subarray(sent)
|
|
683
|
+
base64: base64(bytes.subarray(sent)),
|
|
574
684
|
offset: this.#inputOffset,
|
|
575
685
|
...(options.eof ? { eof: true } : {}),
|
|
576
686
|
},
|
|
@@ -738,16 +848,16 @@ export class Files {
|
|
|
738
848
|
});
|
|
739
849
|
return { path, size: bytes.length };
|
|
740
850
|
}
|
|
741
|
-
const
|
|
742
|
-
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
851
|
+
const sha256 = await sha256Hex(bytes);
|
|
743
852
|
// A guest journals mutations across operations, not per URL. One logical
|
|
744
853
|
// write therefore owns separate stable keys for each mutation phase.
|
|
745
854
|
const uploadKey = options.idempotencyKey ?? crypto.randomUUID();
|
|
855
|
+
const phaseKeys = new Map();
|
|
856
|
+
for (const phase of ["begin", "legacy-begin", "commit", "chmod", "abort"])
|
|
857
|
+
phaseKeys.set(phase, await sha256Hex(JSON.stringify(["files.write", uploadKey, phase])));
|
|
746
858
|
const phaseOptions = (phase) => ({
|
|
747
859
|
...options,
|
|
748
|
-
idempotencyKey:
|
|
749
|
-
.update(JSON.stringify(["files.write", uploadKey, phase]))
|
|
750
|
-
.digest("hex"),
|
|
860
|
+
idempotencyKey: phaseKeys.get(phase),
|
|
751
861
|
});
|
|
752
862
|
let begin;
|
|
753
863
|
try {
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RuntimeError } from "./errors.js";
|
|
2
|
-
export declare const VERSION = "0.8.
|
|
2
|
+
export declare const VERSION = "0.8.3";
|
|
3
3
|
export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
4
4
|
export type RequestOptions = {
|
|
5
5
|
/** Sent as Idempotency-Key. Omit it: the SDK makes one per call and keeps
|
package/dist/transport.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConnectionError, errorFor, RuntimeError, WAITS_FOR_ROOM } from "./errors.js";
|
|
2
2
|
import { describeRoute, envFetch, openWebSocket } from "./proxy.js";
|
|
3
|
-
export const VERSION = "0.8.
|
|
3
|
+
export const VERSION = "0.8.3";
|
|
4
4
|
export const DEFAULT_BASE_URL = "https://api.withruntime.com";
|
|
5
5
|
export function apiOrigin(value) {
|
|
6
6
|
const url = new URL(value);
|
package/dist/types.d.ts
CHANGED
|
@@ -18,7 +18,8 @@ export type SandboxInfo = {
|
|
|
18
18
|
timeoutSeconds: number;
|
|
19
19
|
/** Pauses after this many idle seconds; 0 is never. */
|
|
20
20
|
idlePauseSeconds?: number;
|
|
21
|
-
/** True for
|
|
21
|
+
/** True only for a sandbox created before 27 September 2026 with the old
|
|
22
|
+
* default idle pause, which paused it only while nothing had used it. */
|
|
22
23
|
idlePauseUnusedOnly?: boolean;
|
|
23
24
|
/** A request (exec, files, terminal, a visit to a shared port) wakes it when paused. */
|
|
24
25
|
autoWake?: boolean;
|
|
@@ -66,8 +67,10 @@ export type CreateSandbox = {
|
|
|
66
67
|
pausable?: boolean;
|
|
67
68
|
/** What happens when timeoutSeconds runs out: "pause" (default) or "stop". */
|
|
68
69
|
onLeaseEnd?: "pause" | "stop";
|
|
69
|
-
/** Pause after this many seconds
|
|
70
|
-
*
|
|
70
|
+
/** Pause after this many seconds in which nothing happens in it: no
|
|
71
|
+
* request, no command or terminal running, no open connection, no network
|
|
72
|
+
* traffic and no CPU use. Default 60 for a pausable sandbox; 0 never;
|
|
73
|
+
* otherwise 10 to 86400. A request wakes it again. */
|
|
71
74
|
idlePauseSeconds?: number;
|
|
72
75
|
/** A request to a paused sandbox wakes it. Default true. */
|
|
73
76
|
autoWake?: boolean;
|
|
@@ -202,8 +205,9 @@ export type Usage = {
|
|
|
202
205
|
reservedMs: number;
|
|
203
206
|
availableMs: number;
|
|
204
207
|
} | null;
|
|
205
|
-
/** Outbound traffic this calendar month, UTC: the first `allowanceBytes`
|
|
206
|
-
*
|
|
208
|
+
/** Outbound traffic this calendar month, UTC: the first `allowanceBytes` an
|
|
209
|
+
* account sends are free, and the rest is charged at the rate in the pricing
|
|
210
|
+
* guide (https://withruntime.com/docs/pricing#network-products). */
|
|
207
211
|
outbound?: {
|
|
208
212
|
month: string;
|
|
209
213
|
sentBytes: number;
|
|
@@ -224,7 +228,8 @@ export type SandboxSettings = {
|
|
|
224
228
|
labels?: Record<string, string>;
|
|
225
229
|
/** A request to a paused sandbox wakes it. */
|
|
226
230
|
autoWake?: boolean;
|
|
227
|
-
/** Pause after this many seconds with no activity, counted from now; 0
|
|
231
|
+
/** Pause after this many seconds with no activity, counted from now; 0
|
|
232
|
+
* never, otherwise 10 to 86400. */
|
|
228
233
|
idlePauseSeconds?: number;
|
|
229
234
|
/** Keep it running while credit lasts and keep its disk after a stop. Paid only. */
|
|
230
235
|
persistent?: boolean;
|
package/dist/vercel/sandbox.js
CHANGED
|
@@ -59,7 +59,7 @@ export function networkRules(policy) {
|
|
|
59
59
|
else if (allow) {
|
|
60
60
|
for (const [domain, rules] of Object.entries(allow)) {
|
|
61
61
|
if (rules.length)
|
|
62
|
-
throw new NotSupportedError(`Network rules that transform or forward requests (for ${domain})`,
|
|
62
|
+
throw new NotSupportedError(`Network rules that transform or forward requests (for ${domain})`, `Allow the domain with an empty rule list and store the credential as a Runtime secret for it: \`npx withruntime secrets set NAME --host ${domain}\`. The sandbox sees a placeholder, and the egress proxy adds the value.`);
|
|
63
63
|
domains.push(domain);
|
|
64
64
|
}
|
|
65
65
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "withruntime",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"mcpName": "com.withruntime/runtime",
|
|
6
6
|
"type": "module",
|
|
@@ -126,6 +126,12 @@
|
|
|
126
126
|
},
|
|
127
127
|
"bugs": "https://github.com/withruntime/runtime/issues",
|
|
128
128
|
"sideEffects": false,
|
|
129
|
+
"browser": {
|
|
130
|
+
"./dist/tar.js": "./dist/browser-stub.js",
|
|
131
|
+
"./dist/image-context.js": "./dist/browser-stub.js",
|
|
132
|
+
"./dist/credentials.js": "./dist/browser-stub.js",
|
|
133
|
+
"undici": false
|
|
134
|
+
},
|
|
129
135
|
"keywords": [
|
|
130
136
|
"sandbox",
|
|
131
137
|
"cloud",
|