withruntime 0.8.2 → 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 +26 -5
- package/README.src.md +26 -5
- 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 +403 -21
- 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/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 +114 -9
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +1 -1
- 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,6 +1,7 @@
|
|
|
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;
|
|
@@ -11,7 +12,21 @@ const CHUNK = 1_048_576;
|
|
|
11
12
|
* connections at once. */
|
|
12
13
|
const PARALLEL = 8;
|
|
13
14
|
const enc = (id) => encodeURIComponent(id);
|
|
14
|
-
|
|
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
|
+
}
|
|
15
30
|
function commandBody(command, options) {
|
|
16
31
|
return {
|
|
17
32
|
...(typeof command === "string" ? { command } : { argv: [...command] }),
|
|
@@ -32,17 +47,44 @@ export class Sandbox {
|
|
|
32
47
|
#t;
|
|
33
48
|
files;
|
|
34
49
|
processes;
|
|
50
|
+
/** Short-lived tokens that let a browser reach this sandbox directly. */
|
|
51
|
+
sessions;
|
|
35
52
|
constructor(transport, info) {
|
|
36
53
|
this.#t = transport;
|
|
37
54
|
this.#info = info;
|
|
38
55
|
this.files = new Files(transport, info.id);
|
|
39
56
|
this.processes = new Processes(transport, info.id);
|
|
57
|
+
this.sessions = new SandboxSessions(transport, info.id);
|
|
40
58
|
for (const [name, make] of sandboxFactories())
|
|
41
59
|
Object.defineProperty(this, name, { value: make(transport, this), enumerable: false });
|
|
42
60
|
}
|
|
43
61
|
get id() {
|
|
44
62
|
return this.#info.id;
|
|
45
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
|
+
}
|
|
46
88
|
/** What the API last said about this sandbox. `refresh()` asks again. */
|
|
47
89
|
get info() {
|
|
48
90
|
return this.#info;
|
|
@@ -439,7 +481,7 @@ class OutputCursor {
|
|
|
439
481
|
droppedBytes: event.offset - this.cursor,
|
|
440
482
|
resumeAt: event.offset,
|
|
441
483
|
};
|
|
442
|
-
this.cursor = Math.max(this.cursor, event.offset +
|
|
484
|
+
this.cursor = Math.max(this.cursor, event.offset + utf8(event.data).byteLength);
|
|
443
485
|
}
|
|
444
486
|
yield event;
|
|
445
487
|
}
|
|
@@ -463,6 +505,69 @@ function pick(options) {
|
|
|
463
505
|
...(options.timeoutMs ? { timeoutMs: options.timeoutMs + 60_000 } : {}),
|
|
464
506
|
};
|
|
465
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
|
+
}
|
|
466
571
|
export class Processes {
|
|
467
572
|
t;
|
|
468
573
|
sandboxId;
|
|
@@ -568,14 +673,14 @@ export class Process {
|
|
|
568
673
|
}
|
|
569
674
|
/** Sends input. Offsets are tracked for you, so a retried write is never typed twice. */
|
|
570
675
|
async write(data, options = {}) {
|
|
571
|
-
const bytes =
|
|
676
|
+
const bytes = utf8(data);
|
|
572
677
|
let sent = 0;
|
|
573
678
|
do {
|
|
574
679
|
const reply = await this.t.json({
|
|
575
680
|
method: "POST",
|
|
576
681
|
path: `/v1/sandboxes/${enc(this.sandboxId)}/processes/${enc(this.id)}:write`,
|
|
577
682
|
body: {
|
|
578
|
-
base64: bytes.subarray(sent)
|
|
683
|
+
base64: base64(bytes.subarray(sent)),
|
|
579
684
|
offset: this.#inputOffset,
|
|
580
685
|
...(options.eof ? { eof: true } : {}),
|
|
581
686
|
},
|
|
@@ -743,16 +848,16 @@ export class Files {
|
|
|
743
848
|
});
|
|
744
849
|
return { path, size: bytes.length };
|
|
745
850
|
}
|
|
746
|
-
const
|
|
747
|
-
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
851
|
+
const sha256 = await sha256Hex(bytes);
|
|
748
852
|
// A guest journals mutations across operations, not per URL. One logical
|
|
749
853
|
// write therefore owns separate stable keys for each mutation phase.
|
|
750
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])));
|
|
751
858
|
const phaseOptions = (phase) => ({
|
|
752
859
|
...options,
|
|
753
|
-
idempotencyKey:
|
|
754
|
-
.update(JSON.stringify(["files.write", uploadKey, phase]))
|
|
755
|
-
.digest("hex"),
|
|
860
|
+
idempotencyKey: phaseKeys.get(phase),
|
|
756
861
|
});
|
|
757
862
|
let begin;
|
|
758
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/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",
|