withruntime 0.8.4 → 0.10.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/BLAXEL.md +4 -4
- package/COMPATIBILITY.md +145 -0
- package/DAYTONA.md +13 -7
- package/E2B.md +34 -20
- package/README.md +22 -11
- package/README.src.md +22 -11
- package/VERCEL.md +13 -3
- package/compatibility-lock.json +357 -0
- package/dist/ai-harness/index.js +2 -2
- package/dist/api-defaults.d.ts +17 -0
- package/dist/api-defaults.js +22 -0
- package/dist/blaxel/errors.js +1 -1
- package/dist/blaxel/lookup.d.ts +3 -2
- package/dist/blaxel/lookup.js +3 -2
- package/dist/blaxel/pagination.d.ts +2 -2
- package/dist/blaxel/pagination.js +45 -17
- package/dist/blaxel/preview.d.ts +3 -2
- package/dist/blaxel/preview.js +16 -14
- package/dist/blaxel/process.d.ts +1 -1
- package/dist/blaxel/process.js +66 -11
- package/dist/blaxel/sandbox.d.ts +2 -0
- package/dist/blaxel/sandbox.js +2 -0
- package/dist/blaxel/snapshot.js +3 -5
- package/dist/cli-words.d.ts +27 -0
- package/dist/cli-words.js +135 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +351 -85
- package/dist/client.d.ts +33 -4
- package/dist/client.js +47 -12
- package/dist/cloudflare/identity.d.ts +7 -0
- package/dist/cloudflare/identity.js +13 -0
- package/dist/cloudflare/index.d.ts +303 -0
- package/dist/cloudflare/index.js +592 -0
- package/dist/cloudflare/worker.d.ts +419 -0
- package/dist/cloudflare/worker.js +447 -0
- package/dist/codesandbox/guest-service.d.ts +31 -0
- package/dist/codesandbox/guest-service.js +490 -0
- package/dist/codesandbox/index.d.ts +386 -0
- package/dist/codesandbox/index.js +1347 -0
- package/dist/compat/core.d.ts +40 -0
- package/dist/compat/core.js +238 -0
- package/dist/compat/files.d.ts +26 -0
- package/dist/compat/files.js +133 -0
- package/dist/compat/shell.d.ts +4 -0
- package/dist/compat/shell.js +165 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +107 -38
- package/dist/daytona/context.js +2 -1
- package/dist/daytona/daytona.js +11 -4
- package/dist/daytona/filesystem.d.ts +17 -4
- package/dist/daytona/filesystem.js +121 -38
- package/dist/daytona/index.d.ts +1 -1
- package/dist/daytona/process.d.ts +1 -1
- package/dist/daytona/process.js +27 -4
- package/dist/daytona/sandbox.d.ts +1 -1
- package/dist/daytona/sandbox.js +16 -7
- package/dist/e2b/client.js +1 -1
- package/dist/e2b/code-interpreter.d.ts +4 -5
- package/dist/e2b/code-interpreter.js +54 -56
- package/dist/e2b/commands.d.ts +35 -12
- package/dist/e2b/commands.js +169 -73
- package/dist/e2b/errors.js +1 -1
- package/dist/e2b/filesystem.d.ts +8 -6
- package/dist/e2b/filesystem.js +127 -28
- package/dist/e2b/index.d.ts +1 -0
- package/dist/e2b/index.js +1 -0
- package/dist/e2b/pty.d.ts +24 -0
- package/dist/e2b/pty.js +80 -0
- package/dist/e2b/sandbox.d.ts +6 -4
- package/dist/e2b/sandbox.js +59 -25
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +15 -2
- package/dist/file-source.d.ts +16 -0
- package/dist/file-source.js +162 -0
- package/dist/freestyle/index.d.ts +309 -0
- package/dist/freestyle/index.js +343 -0
- package/dist/index.d.ts +3 -1
- package/dist/login.js +16 -10
- package/dist/mcp.js +2 -2
- package/dist/modal/index.d.ts +205 -0
- package/dist/modal/index.js +486 -0
- package/dist/network-cli.js +112 -44
- package/dist/openai-agents/index.js +18 -71
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -2
- package/dist/products/audit.d.ts +3 -0
- package/dist/products/index.d.ts +14 -0
- package/dist/products/index.js +12 -0
- package/dist/products/interpreter.d.ts +11 -4
- package/dist/products/interpreter.js +33 -9
- package/dist/products/jobs.js +3 -2
- package/dist/products/limits.d.ts +10 -0
- package/dist/products/observability.d.ts +44 -0
- package/dist/products/observability.js +6 -0
- package/dist/products/previews.d.ts +18 -4
- package/dist/products/previews.js +14 -5
- package/dist/products/tailscale.d.ts +57 -0
- package/dist/products/tailscale.js +17 -0
- package/dist/products/volumes.d.ts +42 -0
- package/dist/products/volumes.js +36 -0
- package/dist/products/watch.d.ts +25 -4
- package/dist/products/watch.js +132 -18
- package/dist/proxy.js +3 -1
- package/dist/runloop/index.d.ts +326 -0
- package/dist/runloop/index.js +535 -0
- package/dist/runloop/pagination.d.ts +46 -0
- package/dist/runloop/pagination.js +82 -0
- package/dist/runloop/snapshots.d.ts +52 -0
- package/dist/runloop/snapshots.js +244 -0
- package/dist/sandbox.d.ts +76 -16
- package/dist/sandbox.js +696 -135
- package/dist/snapshots.d.ts +20 -5
- package/dist/snapshots.js +11 -1
- package/dist/sprites/index.d.ts +251 -0
- package/dist/sprites/index.js +588 -0
- package/dist/tar.d.ts +10 -1
- package/dist/tar.js +311 -35
- package/dist/tools/index.js +2 -1
- package/dist/transport.d.ts +23 -8
- package/dist/transport.js +247 -127
- package/dist/types.d.ts +52 -3
- package/dist/vercel/command.d.ts +1 -1
- package/dist/vercel/command.js +15 -3
- package/dist/vercel/errors.d.ts +2 -2
- package/dist/vercel/errors.js +19 -2
- package/dist/vercel/filesystem.d.ts +21 -18
- package/dist/vercel/filesystem.js +95 -48
- package/dist/vercel/sandbox.d.ts +8 -8
- package/dist/vercel/sandbox.js +159 -64
- package/dist/vercel/snapshot.d.ts +3 -2
- package/dist/vercel/snapshot.js +9 -5
- package/package.json +46 -3
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/** Workers conditional entry. The Durable Object coordinates requests and stores
|
|
2
|
+
* configuration; Runtime owns compute. Existing Container/Wrangler declarations
|
|
3
|
+
* still need removal during infrastructure migration. */
|
|
4
|
+
import { DurableObject } from "cloudflare:workers";
|
|
5
|
+
import { only, CompatibilityError } from "../compat/core.js";
|
|
6
|
+
import { Runtime } from "../client.js";
|
|
7
|
+
import { workerSandboxIdentity } from "./identity.js";
|
|
8
|
+
import { Sandbox as RuntimeSandbox, sanitizeSandboxId, validateSandboxConfiguration, } from "./index.js";
|
|
9
|
+
export * from "./index.js";
|
|
10
|
+
const CONFIGURATION_KEY = "runtime.configuration";
|
|
11
|
+
const CONFIGURATION_INTENT_KEY = "runtime.configuration.pending";
|
|
12
|
+
/** Preserve the original failure even when durable recovery also fails. */
|
|
13
|
+
function configurationFailure(error, uncertain, cleanup) {
|
|
14
|
+
if (error !== null && (typeof error === "object" || typeof error === "function")) {
|
|
15
|
+
try {
|
|
16
|
+
if (uncertain)
|
|
17
|
+
Object.defineProperty(error, "configurationUncertain", { value: true, configurable: true });
|
|
18
|
+
if (cleanup !== undefined)
|
|
19
|
+
Object.defineProperty(error, "cause" in error && error.cause !== undefined ? "cleanupError" : "cause", {
|
|
20
|
+
value: cleanup,
|
|
21
|
+
configurable: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// A frozen exception still keeps its original identity and type.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
/** Queue waits share the caller's existing signal and never reset its deadline. */
|
|
31
|
+
async function waitForConfiguration(pending, signal) {
|
|
32
|
+
signal?.throwIfAborted();
|
|
33
|
+
if (!signal)
|
|
34
|
+
return pending;
|
|
35
|
+
let cancel;
|
|
36
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- rejects with the caller's abort reason as is, as the upstream SDK does
|
|
38
|
+
cancel = () => reject(signal.reason);
|
|
39
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
return await Promise.race([pending, aborted]);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
signal.removeEventListener("abort", cancel);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** The official Worker SDK returns plain records with callable RPC properties,
|
|
49
|
+
* so ids/status are values rather than remote-property promises. */
|
|
50
|
+
function sessionTarget(session) {
|
|
51
|
+
function readFile(path, options) {
|
|
52
|
+
return options?.encoding === "none"
|
|
53
|
+
? session.readFile(path, { encoding: "none" })
|
|
54
|
+
: session.readFile(path, options);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
id: session.id,
|
|
58
|
+
exec: (command, options) => session.exec(command, options),
|
|
59
|
+
execStream: (command, options) => session.execStream(command, options),
|
|
60
|
+
startProcess: async (command, options) => processTarget(await session.startProcess(command, options)),
|
|
61
|
+
writeFile: (...args) => session.writeFile(...args),
|
|
62
|
+
readFile,
|
|
63
|
+
mkdir: (...args) => session.mkdir(...args),
|
|
64
|
+
deleteFile: (path) => session.deleteFile(path),
|
|
65
|
+
renameFile: (path, to) => session.renameFile(path, to),
|
|
66
|
+
moveFile: (path, to) => session.renameFile(path, to),
|
|
67
|
+
exists: (path) => session.exists(path),
|
|
68
|
+
listFiles: (...args) => session.listFiles(...args),
|
|
69
|
+
setEnvVars: (...args) => session.setEnvVars(...args),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function processTarget(process) {
|
|
73
|
+
return {
|
|
74
|
+
id: process.id,
|
|
75
|
+
command: process.command,
|
|
76
|
+
status: process.status,
|
|
77
|
+
startTime: process.startTime,
|
|
78
|
+
endTime: process.endTime,
|
|
79
|
+
exitCode: process.exitCode,
|
|
80
|
+
sessionId: process.sessionId,
|
|
81
|
+
kill: (signal) => process.kill(signal),
|
|
82
|
+
getStatus: () => process.getStatus(),
|
|
83
|
+
getLogs: () => process.getLogs(),
|
|
84
|
+
waitForExit: (timeout) => process.waitForExit(timeout),
|
|
85
|
+
waitForLog: (pattern, timeout) => process.waitForLog(pattern, timeout),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export class Sandbox extends DurableObject {
|
|
89
|
+
configuration = {};
|
|
90
|
+
configurationIntent;
|
|
91
|
+
facade;
|
|
92
|
+
configurationTail = Promise.resolve();
|
|
93
|
+
configurationOperation(operation) {
|
|
94
|
+
const result = this.configurationTail.catch(() => undefined).then(operation);
|
|
95
|
+
this.configurationTail = result;
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
previewTail = Promise.resolve();
|
|
99
|
+
previewOperation(operation) {
|
|
100
|
+
const result = this.previewTail.catch(() => undefined).then(operation);
|
|
101
|
+
this.previewTail = result;
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
initialized;
|
|
105
|
+
constructor(ctx, env) {
|
|
106
|
+
super(ctx, env);
|
|
107
|
+
this.initialized = ctx.blockConcurrencyWhile(async () => {
|
|
108
|
+
this.configuration = (await ctx.storage.get(CONFIGURATION_KEY)) ?? {};
|
|
109
|
+
this.configurationIntent =
|
|
110
|
+
await ctx.storage.get(CONFIGURATION_INTENT_KEY);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async sandbox(signal) {
|
|
114
|
+
await waitForConfiguration(this.initialized, signal);
|
|
115
|
+
// A staged intent is transient while an update is still running. Wait for
|
|
116
|
+
// the queued update before deciding whether uncertainty was retained.
|
|
117
|
+
let configurationTail;
|
|
118
|
+
do {
|
|
119
|
+
configurationTail = this.configurationTail;
|
|
120
|
+
await waitForConfiguration(configurationTail.catch(() => undefined), signal);
|
|
121
|
+
} while (configurationTail !== this.configurationTail);
|
|
122
|
+
signal?.throwIfAborted();
|
|
123
|
+
if (this.configurationIntent)
|
|
124
|
+
throw configurationFailure(new Error("Runtime's configuration is uncertain. Call configure() to reconcile it before use."), true);
|
|
125
|
+
if (!this.configuration.sandboxName)
|
|
126
|
+
throw new Error("Call getSandbox(namespace, id) before using the Durable Object");
|
|
127
|
+
if (!this.env.RUNTIME_API_KEY?.startsWith("rtcloud_"))
|
|
128
|
+
throw new Error("Set the RUNTIME_API_KEY Worker secret to a Runtime API key");
|
|
129
|
+
this.facade ??= new RuntimeSandbox(new Runtime({ apiKey: this.env.RUNTIME_API_KEY, baseUrl: this.env.RUNTIME_API_URL }), workerSandboxIdentity(this.ctx.id.toString(), this.configuration), { ...this.configuration });
|
|
130
|
+
return this.facade;
|
|
131
|
+
}
|
|
132
|
+
async configure(configuration) {
|
|
133
|
+
only("Cloudflare Worker configure", configuration, ["sandboxName", "sleepAfter", "keepAlive"]);
|
|
134
|
+
validateSandboxConfiguration(configuration);
|
|
135
|
+
if (configuration.sandboxName !== undefined) {
|
|
136
|
+
only("Cloudflare Worker sandboxName", configuration.sandboxName, ["name", "normalizeId"]);
|
|
137
|
+
sanitizeSandboxId(configuration.sandboxName.name);
|
|
138
|
+
if (configuration.sandboxName.normalizeId !== undefined &&
|
|
139
|
+
typeof configuration.sandboxName.normalizeId !== "boolean")
|
|
140
|
+
throw new TypeError("normalizeId must be a boolean");
|
|
141
|
+
}
|
|
142
|
+
const requested = {
|
|
143
|
+
...(configuration.sandboxName === undefined
|
|
144
|
+
? {}
|
|
145
|
+
: { sandboxName: { ...configuration.sandboxName } }),
|
|
146
|
+
...(configuration.sleepAfter === undefined ? {} : { sleepAfter: configuration.sleepAfter }),
|
|
147
|
+
...(configuration.keepAlive === undefined ? {} : { keepAlive: configuration.keepAlive }),
|
|
148
|
+
};
|
|
149
|
+
return this.configurationOperation(async () => {
|
|
150
|
+
await this.initialized;
|
|
151
|
+
if (this.configuration.sandboxName &&
|
|
152
|
+
requested.sandboxName &&
|
|
153
|
+
this.configuration.sandboxName.name !== requested.sandboxName.name)
|
|
154
|
+
throw new Error("A Durable Object cannot change sandbox identity");
|
|
155
|
+
if (this.configurationIntent) {
|
|
156
|
+
const facade = this.facade ??
|
|
157
|
+
new RuntimeSandbox(new Runtime({ apiKey: this.env.RUNTIME_API_KEY, baseUrl: this.env.RUNTIME_API_URL }), workerSandboxIdentity(this.ctx.id.toString(), this.configuration), { ...this.configuration });
|
|
158
|
+
try {
|
|
159
|
+
// Recovery uses the saved UUID even after a Durable Object restart.
|
|
160
|
+
const actual = await facade.reconcileConfiguration(this.configurationIntent.sandboxId);
|
|
161
|
+
const reconciled = { ...this.configuration, ...actual };
|
|
162
|
+
await this.ctx.storage.put(CONFIGURATION_KEY, reconciled);
|
|
163
|
+
await this.ctx.storage.delete(CONFIGURATION_INTENT_KEY);
|
|
164
|
+
this.configuration = reconciled;
|
|
165
|
+
this.facade = facade;
|
|
166
|
+
this.configurationIntent = undefined;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
throw configurationFailure(error, true);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const previous = this.configuration;
|
|
173
|
+
const stored = await this.ctx.storage.get(CONFIGURATION_KEY);
|
|
174
|
+
const nativeSandboxId = workerSandboxIdentity(this.ctx.id.toString(), previous);
|
|
175
|
+
const next = { ...previous, ...requested, nativeSandboxId };
|
|
176
|
+
if (!this.facade) {
|
|
177
|
+
await this.ctx.storage.put(CONFIGURATION_KEY, next);
|
|
178
|
+
this.configuration = next;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.configurationIntent = { sandboxId: this.facade.existingNativeId(), previous, next };
|
|
182
|
+
try {
|
|
183
|
+
await this.ctx.storage.put(CONFIGURATION_INTENT_KEY, this.configurationIntent);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
let cleanup;
|
|
187
|
+
try {
|
|
188
|
+
// Native mutation has not started. A failed write may still have
|
|
189
|
+
// persisted the intent, so clear this operation's staging record.
|
|
190
|
+
await this.ctx.storage.delete(CONFIGURATION_INTENT_KEY);
|
|
191
|
+
this.configurationIntent = undefined;
|
|
192
|
+
}
|
|
193
|
+
catch (failure) {
|
|
194
|
+
cleanup = failure;
|
|
195
|
+
}
|
|
196
|
+
throw configurationFailure(error, this.configurationIntent !== undefined, cleanup);
|
|
197
|
+
}
|
|
198
|
+
let nativeAttempted = false;
|
|
199
|
+
try {
|
|
200
|
+
await this.ctx.storage.put(CONFIGURATION_KEY, next);
|
|
201
|
+
nativeAttempted = true;
|
|
202
|
+
await this.facade.configure({
|
|
203
|
+
...(requested.sleepAfter === undefined ? {} : { sleepAfter: requested.sleepAfter }),
|
|
204
|
+
...(requested.keepAlive === undefined ? {} : { keepAlive: requested.keepAlive }),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
let cleanup;
|
|
209
|
+
try {
|
|
210
|
+
if (stored === undefined)
|
|
211
|
+
await this.ctx.storage.delete(CONFIGURATION_KEY);
|
|
212
|
+
else
|
|
213
|
+
await this.ctx.storage.put(CONFIGURATION_KEY, stored);
|
|
214
|
+
if (!nativeAttempted) {
|
|
215
|
+
await this.ctx.storage.delete(CONFIGURATION_INTENT_KEY);
|
|
216
|
+
this.configurationIntent = undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (failure) {
|
|
220
|
+
cleanup = failure;
|
|
221
|
+
}
|
|
222
|
+
// A rejected native request may have applied remotely. Keep its intent
|
|
223
|
+
// until an authoritative read succeeds; rollback is only local state.
|
|
224
|
+
throw configurationFailure(error, this.configurationIntent !== undefined, cleanup);
|
|
225
|
+
}
|
|
226
|
+
this.configuration = next;
|
|
227
|
+
try {
|
|
228
|
+
await this.ctx.storage.delete(CONFIGURATION_INTENT_KEY);
|
|
229
|
+
this.configurationIntent = undefined;
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
throw configurationFailure(error, true);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
setSandboxName(name, normalizeId = false) {
|
|
237
|
+
return this.configure({
|
|
238
|
+
sandboxName: { name: normalizeId ? name.toLowerCase() : name, normalizeId },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
setSleepAfter(sleepAfter) {
|
|
242
|
+
return this.configure({ sleepAfter });
|
|
243
|
+
}
|
|
244
|
+
setKeepAlive(keepAlive) {
|
|
245
|
+
return this.configure({ keepAlive });
|
|
246
|
+
}
|
|
247
|
+
async exec(command, options) {
|
|
248
|
+
return (await this.sandbox(options?.signal)).exec(command, options);
|
|
249
|
+
}
|
|
250
|
+
async execStream(command, options) {
|
|
251
|
+
return (await this.sandbox(options?.signal)).execStream(command, options);
|
|
252
|
+
}
|
|
253
|
+
async startProcess(command, options) {
|
|
254
|
+
return processTarget(await (await this.sandbox()).startProcess(command, options));
|
|
255
|
+
}
|
|
256
|
+
async listProcesses() {
|
|
257
|
+
return (await (await this.sandbox()).listProcesses()).map((p) => processTarget(p));
|
|
258
|
+
}
|
|
259
|
+
async getProcess(id) {
|
|
260
|
+
const p = await (await this.sandbox()).getProcess(id);
|
|
261
|
+
return p ? processTarget(p) : null;
|
|
262
|
+
}
|
|
263
|
+
async getProcessLogs(id) {
|
|
264
|
+
return (await this.sandbox()).getProcessLogs(id);
|
|
265
|
+
}
|
|
266
|
+
async killProcess(id, signal) {
|
|
267
|
+
return (await this.sandbox()).killProcess(id, signal);
|
|
268
|
+
}
|
|
269
|
+
async killAllProcesses() {
|
|
270
|
+
return (await this.sandbox()).killAllProcesses();
|
|
271
|
+
}
|
|
272
|
+
async createSession(options) {
|
|
273
|
+
return sessionTarget(await (await this.sandbox()).createSession(options));
|
|
274
|
+
}
|
|
275
|
+
async getSession(id) {
|
|
276
|
+
return sessionTarget(await (await this.sandbox()).getSession(id));
|
|
277
|
+
}
|
|
278
|
+
async deleteSession(id) {
|
|
279
|
+
return (await this.sandbox()).deleteSession(id);
|
|
280
|
+
}
|
|
281
|
+
async writeFile(...args) {
|
|
282
|
+
return (await this.sandbox()).writeFile(...args);
|
|
283
|
+
}
|
|
284
|
+
async readFile(path, options) {
|
|
285
|
+
const s = await this.sandbox();
|
|
286
|
+
return options?.encoding === "none"
|
|
287
|
+
? s.readFile(path, { encoding: "none" })
|
|
288
|
+
: s.readFile(path, options);
|
|
289
|
+
}
|
|
290
|
+
async mkdir(...args) {
|
|
291
|
+
return (await this.sandbox()).mkdir(...args);
|
|
292
|
+
}
|
|
293
|
+
async deleteFile(path) {
|
|
294
|
+
return (await this.sandbox()).deleteFile(path);
|
|
295
|
+
}
|
|
296
|
+
async renameFile(path, to) {
|
|
297
|
+
return (await this.sandbox()).renameFile(path, to);
|
|
298
|
+
}
|
|
299
|
+
async moveFile(path, to) {
|
|
300
|
+
return (await this.sandbox()).moveFile(path, to);
|
|
301
|
+
}
|
|
302
|
+
async listFiles(...args) {
|
|
303
|
+
return (await this.sandbox()).listFiles(...args);
|
|
304
|
+
}
|
|
305
|
+
async exists(path) {
|
|
306
|
+
return (await this.sandbox()).exists(path);
|
|
307
|
+
}
|
|
308
|
+
async setEnvVars(...args) {
|
|
309
|
+
return (await this.sandbox()).setEnvVars(...args);
|
|
310
|
+
}
|
|
311
|
+
exposePort(port, options) {
|
|
312
|
+
return this.previewOperation(() => this.expose(port, options));
|
|
313
|
+
}
|
|
314
|
+
async expose(port, options) {
|
|
315
|
+
only("Cloudflare Worker exposePort", options, ["name", "hostname", "token"]);
|
|
316
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535 || port === 3000)
|
|
317
|
+
throw new TypeError("Preview port must be 1024-65535, excluding 3000");
|
|
318
|
+
if (!/^[a-z0-9.-]+$/i.test(options.hostname) ||
|
|
319
|
+
options.hostname.startsWith(".") ||
|
|
320
|
+
options.hostname.endsWith("."))
|
|
321
|
+
throw new TypeError("Preview hostname must be a DNS hostname");
|
|
322
|
+
if (options.hostname.endsWith(".workers.dev"))
|
|
323
|
+
throw new Error("Port exposure requires a custom domain; .workers.dev does not support wildcard preview subdomains");
|
|
324
|
+
const existing = await this.ctx.storage.get(`runtime.preview.${port}`);
|
|
325
|
+
const token = options.token ?? existing?.token ?? crypto.randomUUID().replaceAll("-", "");
|
|
326
|
+
const ports = await this.ctx.storage.list({
|
|
327
|
+
prefix: "runtime.preview.",
|
|
328
|
+
});
|
|
329
|
+
if ([...ports.values()].some((p) => p.port !== port && p.token === token))
|
|
330
|
+
throw new Error(`Token '${token}' is already in use by another port`);
|
|
331
|
+
if (!/^[a-z0-9_]{1,63}$/.test(token))
|
|
332
|
+
throw new TypeError("Preview token must contain 1-63 lowercase letters, digits or underscores");
|
|
333
|
+
const sandbox = await this.sandbox();
|
|
334
|
+
const name = this.configuration.sandboxName.name;
|
|
335
|
+
if (name !== name.toLowerCase())
|
|
336
|
+
throw new CompatibilityError("Cloudflare", "uppercase sandbox IDs in preview hostnames; use normalizeId:true");
|
|
337
|
+
const native = await (await sandbox.native()).previews.create(port, { visibility: "private" });
|
|
338
|
+
const url = `https://${port}-${name}-${token}.${options.hostname}`;
|
|
339
|
+
await this.ctx.storage.put(`runtime.preview.${port}`, {
|
|
340
|
+
token,
|
|
341
|
+
url,
|
|
342
|
+
target: native.url,
|
|
343
|
+
authToken: native.token,
|
|
344
|
+
authExpiresAt: native.tokenExpiresAt,
|
|
345
|
+
port,
|
|
346
|
+
name: options.name,
|
|
347
|
+
});
|
|
348
|
+
return { url, port, name: options.name };
|
|
349
|
+
}
|
|
350
|
+
unexposePort(port) {
|
|
351
|
+
return this.previewOperation(async () => {
|
|
352
|
+
await (await this.sandbox()).unexposePort(port);
|
|
353
|
+
await this.ctx.storage.delete(`runtime.preview.${port}`);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async isPortExposed(port) {
|
|
357
|
+
return !!(await this.ctx.storage.get(`runtime.preview.${port}`));
|
|
358
|
+
}
|
|
359
|
+
async validatePortToken(port, token) {
|
|
360
|
+
const record = await this.ctx.storage.get(`runtime.preview.${port}`);
|
|
361
|
+
return record?.token === token;
|
|
362
|
+
}
|
|
363
|
+
async getExposedPorts(hostname) {
|
|
364
|
+
return [
|
|
365
|
+
...(await this.ctx.storage.list({
|
|
366
|
+
prefix: "runtime.preview.",
|
|
367
|
+
})).values(),
|
|
368
|
+
].map((p) => ({
|
|
369
|
+
url: `https://${p.port}-${this.configuration.sandboxName.name}-${p.token}.${hostname}`,
|
|
370
|
+
port: p.port,
|
|
371
|
+
status: "active",
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
async fetch(request) {
|
|
375
|
+
return this.forwardPreview(request);
|
|
376
|
+
}
|
|
377
|
+
async forwardPreview(request) {
|
|
378
|
+
await this.initialized;
|
|
379
|
+
const route = previewRoute(new URL(request.url));
|
|
380
|
+
if (!route || route.id !== this.configuration.sandboxName?.name)
|
|
381
|
+
return new Response("Not found", { status: 404 });
|
|
382
|
+
const record = await this.ctx.storage.get(`runtime.preview.${route.port}`);
|
|
383
|
+
if (!record || record.token !== route.token)
|
|
384
|
+
return new Response("Not found", { status: 404 });
|
|
385
|
+
if (!record.authExpiresAt || Date.parse(record.authExpiresAt) <= Date.now() + 60000) {
|
|
386
|
+
const preview = await (await (await this.sandbox()).native()).previews.get(route.port);
|
|
387
|
+
record.target = preview.url;
|
|
388
|
+
record.authToken = preview.token;
|
|
389
|
+
record.authExpiresAt = preview.tokenExpiresAt;
|
|
390
|
+
await this.ctx.storage.put(`runtime.preview.${route.port}`, record);
|
|
391
|
+
}
|
|
392
|
+
if (!record.authToken)
|
|
393
|
+
throw new Error("Private preview did not return an authorization token");
|
|
394
|
+
const incoming = new URL(request.url), target = new URL(record.target);
|
|
395
|
+
target.pathname = incoming.pathname;
|
|
396
|
+
const authorization = [...target.searchParams];
|
|
397
|
+
target.search = incoming.search;
|
|
398
|
+
for (const [key, value] of authorization)
|
|
399
|
+
target.searchParams.set(key, value);
|
|
400
|
+
const headers = new Headers(request.headers);
|
|
401
|
+
headers.set("x-runtime-preview-token", record.authToken);
|
|
402
|
+
headers.delete("host");
|
|
403
|
+
return fetch(new Request(new Request(target, request), { headers, redirect: "manual" }));
|
|
404
|
+
}
|
|
405
|
+
async destroy() {
|
|
406
|
+
await (await this.sandbox()).destroy();
|
|
407
|
+
this.facade = undefined;
|
|
408
|
+
const ports = await this.ctx.storage.list({ prefix: "runtime.preview." });
|
|
409
|
+
await this.ctx.storage.delete([...ports.keys()]);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
export function getSandbox(namespace, id, options = {}) {
|
|
413
|
+
only("Cloudflare Worker getSandbox", options, ["normalizeId", "sleepAfter", "keepAlive"]);
|
|
414
|
+
sanitizeSandboxId(id);
|
|
415
|
+
const name = options.normalizeId ? id.toLowerCase() : id;
|
|
416
|
+
const stub = namespace.get(namespace.idFromName(name));
|
|
417
|
+
const configured = stub.configure({
|
|
418
|
+
sandboxName: { name, normalizeId: options.normalizeId },
|
|
419
|
+
...(options.sleepAfter !== undefined ? { sleepAfter: options.sleepAfter } : {}),
|
|
420
|
+
...(options.keepAlive !== undefined ? { keepAlive: options.keepAlive } : {}),
|
|
421
|
+
});
|
|
422
|
+
// Await configuration for every operation so no request can race name setup.
|
|
423
|
+
return new Proxy(stub, {
|
|
424
|
+
get(target, key) {
|
|
425
|
+
const value = Reflect.get(target, key);
|
|
426
|
+
if (typeof value !== "function" || key === "then")
|
|
427
|
+
return value;
|
|
428
|
+
return (...args) => Promise.resolve(configured).then(() => Reflect.apply(value, target, args));
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function previewRoute(url) {
|
|
433
|
+
const label = url.hostname.split(".")[0] ?? "";
|
|
434
|
+
const match = /^(\d{4,5})-([a-z0-9_-]+)-([a-z0-9_]{1,63})$/.exec(label);
|
|
435
|
+
if (!match)
|
|
436
|
+
return null;
|
|
437
|
+
const port = Number(match[1]);
|
|
438
|
+
if (port < 1024 || port > 65535 || port === 3000)
|
|
439
|
+
return null;
|
|
440
|
+
return { port, id: match[2], token: match[3] };
|
|
441
|
+
}
|
|
442
|
+
export async function proxyToSandbox(request, env) {
|
|
443
|
+
const route = previewRoute(new URL(request.url));
|
|
444
|
+
if (!route)
|
|
445
|
+
return null;
|
|
446
|
+
return getSandbox(env.Sandbox, route.id, { normalizeId: true }).fetch(request);
|
|
447
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Sandbox, Process } from "../sandbox.js";
|
|
2
|
+
export declare const taskServiceSource: string;
|
|
3
|
+
export declare class ManagedTasks {
|
|
4
|
+
private readonly native;
|
|
5
|
+
constructor(native: Sandbox);
|
|
6
|
+
install(tasks: Record<string, {
|
|
7
|
+
command: string;
|
|
8
|
+
runAtStart?: boolean;
|
|
9
|
+
}>, env: Record<string, string>, setup: {
|
|
10
|
+
steps: {
|
|
11
|
+
name: string;
|
|
12
|
+
command: string;
|
|
13
|
+
}[];
|
|
14
|
+
source: string;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
call<T>(request: Record<string, unknown>, signal?: AbortSignal): Promise<T>;
|
|
17
|
+
get(taskId: string): Promise<Process | undefined>;
|
|
18
|
+
boot(signal?: AbortSignal): Promise<{
|
|
19
|
+
state: "starting" | "ready" | "failed";
|
|
20
|
+
config?: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
} | undefined>;
|
|
23
|
+
start(taskId: string): Promise<Process>;
|
|
24
|
+
stop(taskId: string): Promise<Process | undefined>;
|
|
25
|
+
list(signal?: AbortSignal): Promise<{
|
|
26
|
+
taskId: string;
|
|
27
|
+
process: Process;
|
|
28
|
+
}[]>;
|
|
29
|
+
output(id: string): Promise<string>;
|
|
30
|
+
private process;
|
|
31
|
+
}
|