withruntime 0.9.0 → 0.11.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/DAYTONA.md +52 -40
- package/E2B.md +65 -62
- package/README.md +15 -7
- package/README.src.md +15 -7
- package/VERCEL.md +34 -22
- package/compatibility-lock.json +16 -16
- 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 +2 -1
- package/dist/blaxel/preview.js +15 -13
- package/dist/blaxel/process.js +56 -10
- 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.d.ts +2 -0
- package/dist/cli.js +262 -56
- package/dist/client.d.ts +41 -3
- package/dist/client.js +67 -11
- package/dist/cloudflare/identity.d.ts +7 -0
- package/dist/cloudflare/identity.js +13 -0
- package/dist/cloudflare/index.d.ts +9 -0
- package/dist/cloudflare/index.js +47 -9
- package/dist/cloudflare/worker.d.ts +4 -0
- package/dist/cloudflare/worker.js +166 -16
- package/dist/codesandbox/guest-service.js +57 -20
- package/dist/codesandbox/index.js +42 -8
- package/dist/compat/core.d.ts +9 -8
- package/dist/compat/core.js +60 -21
- package/dist/compat/files.js +22 -4
- package/dist/compat/lease.d.ts +36 -0
- package/dist/compat/lease.js +87 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +107 -38
- package/dist/daytona/computer-use.d.ts +162 -0
- package/dist/daytona/computer-use.js +324 -0
- package/dist/daytona/context.d.ts +10 -0
- package/dist/daytona/context.js +24 -0
- package/dist/daytona/daytona.d.ts +20 -0
- package/dist/daytona/daytona.js +134 -20
- package/dist/daytona/filesystem.d.ts +17 -4
- package/dist/daytona/filesystem.js +129 -38
- package/dist/daytona/index.d.ts +3 -27
- package/dist/daytona/index.js +2 -7
- package/dist/daytona/process.d.ts +63 -6
- package/dist/daytona/process.js +224 -23
- package/dist/daytona/sandbox.d.ts +20 -6
- package/dist/daytona/sandbox.js +55 -32
- package/dist/e2b/client.d.ts +5 -0
- package/dist/e2b/client.js +6 -2
- package/dist/e2b/commands.d.ts +43 -12
- package/dist/e2b/commands.js +217 -64
- package/dist/e2b/errors.d.ts +7 -0
- package/dist/e2b/errors.js +20 -0
- package/dist/e2b/filesystem.d.ts +2 -1
- package/dist/e2b/filesystem.js +43 -6
- package/dist/e2b/index.d.ts +3 -2
- package/dist/e2b/index.js +2 -1
- package/dist/e2b/pty.d.ts +24 -0
- package/dist/e2b/pty.js +83 -0
- package/dist/e2b/sandbox.d.ts +17 -6
- package/dist/e2b/sandbox.js +239 -43
- package/dist/e2b/users.d.ts +30 -0
- package/dist/e2b/users.js +185 -0
- package/dist/file-source.d.ts +16 -0
- package/dist/file-source.js +162 -0
- package/dist/freestyle/index.d.ts +9 -1
- package/dist/freestyle/index.js +38 -5
- package/dist/index.d.ts +3 -1
- package/dist/login.js +16 -10
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +6 -1
- package/dist/modal/index.js +2 -1
- package/dist/network-cli.js +139 -43
- package/dist/openai-agents/index.js +9 -45
- package/dist/page.d.ts +1 -1
- package/dist/page.js +2 -2
- package/dist/products/browser.d.ts +60 -0
- package/dist/products/browser.js +37 -0
- package/dist/products/desktop.d.ts +2 -2
- package/dist/products/desktop.js +2 -2
- package/dist/products/images.d.ts +3 -1
- package/dist/products/images.js +14 -2
- package/dist/products/index.d.ts +14 -0
- package/dist/products/index.js +12 -0
- package/dist/products/network-products.d.ts +7 -0
- package/dist/products/network-products.js +3 -0
- package/dist/products/observability.d.ts +44 -0
- package/dist/products/observability.js +6 -0
- package/dist/products/previews.d.ts +8 -2
- package/dist/products/previews.js +11 -3
- package/dist/products/private-network.d.ts +22 -0
- package/dist/products/private-network.js +10 -0
- package/dist/products/volumes.d.ts +42 -0
- package/dist/products/volumes.js +36 -0
- package/dist/products/watch.js +61 -8
- package/dist/proxy.js +3 -1
- package/dist/runloop/index.d.ts +13 -27
- package/dist/runloop/index.js +149 -52
- package/dist/sandbox.d.ts +59 -8
- package/dist/sandbox.js +479 -68
- package/dist/snapshots.d.ts +9 -4
- package/dist/sprites/index.js +69 -33
- package/dist/tar.d.ts +3 -1
- package/dist/tar.js +140 -14
- package/dist/transport.d.ts +4 -7
- package/dist/transport.js +205 -122
- package/dist/types.d.ts +5 -2
- package/dist/vercel/command.d.ts +1 -1
- package/dist/vercel/command.js +3 -2
- package/dist/vercel/errors.d.ts +1 -1
- package/dist/vercel/errors.js +7 -1
- package/dist/vercel/index.d.ts +1 -7
- package/dist/vercel/index.js +1 -3
- package/dist/vercel/sandbox-user.d.ts +70 -0
- package/dist/vercel/sandbox-user.js +206 -0
- package/dist/vercel/sandbox.d.ts +55 -20
- package/dist/vercel/sandbox.js +353 -99
- package/dist/vercel/snapshot.d.ts +3 -2
- package/dist/vercel/snapshot.js +9 -5
- package/package.json +2 -1
package/dist/snapshots.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Page } from "./page.js";
|
|
2
2
|
import type { RequestOptions, Transport } from "./transport.js";
|
|
3
|
-
/** A sandbox's
|
|
4
|
-
* host
|
|
5
|
-
* losing that host. */
|
|
3
|
+
/** A sandbox's saved machine or disk, used to start new sandboxes. A verified
|
|
4
|
+
* off-host copy protects it from host loss once `backedUp` is true. */
|
|
6
5
|
export type Snapshot = {
|
|
7
6
|
id: string;
|
|
8
7
|
kind: "snapshot";
|
|
@@ -19,13 +18,19 @@ export type Snapshot = {
|
|
|
19
18
|
memoryGuarantee: string;
|
|
20
19
|
};
|
|
21
20
|
retentionDays: number;
|
|
21
|
+
/** True while raw fork memory awaits verified compression. The snapshot
|
|
22
|
+
* remains ready for starts on its current host, with no storage charge or
|
|
23
|
+
* off-host copy until final publication. Absent on older servers. */
|
|
24
|
+
compressionPending?: boolean;
|
|
22
25
|
/** What it stores, and what storage is metered for: the bytes it alone holds. */
|
|
23
26
|
storedBytes: number | null;
|
|
27
|
+
/** Zero while compression is pending; final bytes once verified. */
|
|
24
28
|
meteredBytes: number | null;
|
|
25
29
|
/** Whether its copy off the host is made and checked. */
|
|
26
30
|
backedUp: boolean;
|
|
27
31
|
/** Where that copy stands. `restoring`: its host was lost and it is being
|
|
28
|
-
* restored onto another; forks answer `snapshot_restoring` until it is.
|
|
32
|
+
* restored onto another; forks answer `snapshot_restoring` until it is.
|
|
33
|
+
* `none` also covers temporary fork snapshots and pending compression. */
|
|
29
34
|
durability: {
|
|
30
35
|
state: "none" | "pending" | "durable" | "failed" | "restoring";
|
|
31
36
|
durableAt: string | null;
|
package/dist/sprites/index.js
CHANGED
|
@@ -4,6 +4,16 @@ import { RuntimeError } from "../errors.js";
|
|
|
4
4
|
import { all, client, CompatibilityError, create, destroy, execOptions, lookup, only, ready, region, } from "../compat/core.js";
|
|
5
5
|
import { GuestFiles } from "../compat/files.js";
|
|
6
6
|
export { CompatibilityError };
|
|
7
|
+
function validateURLSettings(settings) {
|
|
8
|
+
only("Sprites URL settings", settings, ["auth"]);
|
|
9
|
+
if (settings.auth !== "public" && settings.auth !== "sprite")
|
|
10
|
+
throw new TypeError("auth must be public or sprite");
|
|
11
|
+
}
|
|
12
|
+
function validateLabels(labels) {
|
|
13
|
+
if (labels !== undefined &&
|
|
14
|
+
(!Array.isArray(labels) || Array.from(labels).some((label) => typeof label !== "string")))
|
|
15
|
+
throw new TypeError("labels must be an array of strings");
|
|
16
|
+
}
|
|
7
17
|
export class FilesystemError extends Error {
|
|
8
18
|
code;
|
|
9
19
|
path;
|
|
@@ -90,8 +100,9 @@ export class SpritesClient {
|
|
|
90
100
|
]);
|
|
91
101
|
if (o.runtime !== undefined)
|
|
92
102
|
throw new CompatibilityError("Sprites", `vendor runtime image ${o.runtime}; import it into Runtime first`);
|
|
93
|
-
if (o.urlSettings
|
|
94
|
-
|
|
103
|
+
if (o.urlSettings !== undefined)
|
|
104
|
+
validateURLSettings(o.urlSettings);
|
|
105
|
+
validateLabels(o.labels);
|
|
95
106
|
const c = o.config ?? {};
|
|
96
107
|
only("Sprites config", c, ["ramMB", "cpus", "region", "storageGB"]);
|
|
97
108
|
const s = await create(this.runtime, "sprites", {
|
|
@@ -102,7 +113,7 @@ export class SpritesClient {
|
|
|
102
113
|
diskMiB: c.storageGB === undefined ? undefined : Math.ceil((c.storageGB * 1e9) / 1048576),
|
|
103
114
|
region: region("Sprites", c.region),
|
|
104
115
|
labels: { "compat.labels": JSON.stringify(o.labels ?? []) },
|
|
105
|
-
}, o.environment);
|
|
116
|
+
}, o.environment, undefined, o.waitForCapacity === false ? { waitForCapacityMs: 0 } : {});
|
|
106
117
|
const sprite = new Sprite(name, this);
|
|
107
118
|
sprite.hydrate(s);
|
|
108
119
|
if (o.urlSettings) {
|
|
@@ -205,6 +216,8 @@ export class Sprite {
|
|
|
205
216
|
}
|
|
206
217
|
async execFile(file, args = [], options = {}) {
|
|
207
218
|
options.signal?.throwIfAborted();
|
|
219
|
+
if (options.timeout !== undefined && (!Number.isFinite(options.timeout) || options.timeout < 0))
|
|
220
|
+
throw new TypeError("timeout must be a non-negative finite number");
|
|
208
221
|
if (options.encoding &&
|
|
209
222
|
options.encoding !== "buffer" &&
|
|
210
223
|
!Buffer.isEncoding(options.encoding))
|
|
@@ -227,29 +240,45 @@ export class Sprite {
|
|
|
227
240
|
throw new CompatibilityError("Sprites", "control-mode wire protocol");
|
|
228
241
|
const cmd = this.spawn(file, args, options);
|
|
229
242
|
const out = [], err = [];
|
|
230
|
-
|
|
243
|
+
const maxBuffer = options.maxBuffer || 10 * 1024 * 1024;
|
|
244
|
+
let stdoutBytes = 0;
|
|
245
|
+
let stderrBytes = 0;
|
|
231
246
|
let failure;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
247
|
+
let rejectFailure = () => undefined;
|
|
248
|
+
const failed = new Promise((_resolve, reject) => {
|
|
249
|
+
rejectFailure = reject;
|
|
250
|
+
});
|
|
251
|
+
const cancel = (error) => {
|
|
252
|
+
if (failure)
|
|
253
|
+
return;
|
|
254
|
+
failure = error;
|
|
255
|
+
cmd.kill("SIGTERM");
|
|
256
|
+
cmd.close();
|
|
257
|
+
rejectFailure(error);
|
|
258
|
+
};
|
|
259
|
+
const capture = (channel, target) => (chunk) => {
|
|
260
|
+
if (failure)
|
|
261
|
+
return;
|
|
262
|
+
if (channel === "stdout")
|
|
263
|
+
stdoutBytes += chunk.length;
|
|
264
|
+
else
|
|
265
|
+
stderrBytes += chunk.length;
|
|
266
|
+
if ((channel === "stdout" ? stdoutBytes : stderrBytes) > maxBuffer)
|
|
267
|
+
cancel(new Error(`${channel} maxBuffer exceeded`));
|
|
238
268
|
else
|
|
239
269
|
target.push(chunk);
|
|
240
270
|
};
|
|
241
|
-
cmd.stdout.on("data", capture(out));
|
|
242
|
-
cmd.stderr.on("data", capture(err));
|
|
243
|
-
const abort = () =>
|
|
244
|
-
failure = new Error("Command aborted");
|
|
245
|
-
cmd.kill("SIGTERM");
|
|
246
|
-
};
|
|
271
|
+
cmd.stdout.on("data", capture("stdout", out));
|
|
272
|
+
cmd.stderr.on("data", capture("stderr", err));
|
|
273
|
+
const abort = () => cancel(new Error("Command aborted"));
|
|
247
274
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
248
275
|
if (options.signal?.aborted)
|
|
249
276
|
abort();
|
|
250
|
-
const timer = options.timeout
|
|
277
|
+
const timer = options.timeout
|
|
278
|
+
? setTimeout(() => cancel(new Error(`Command timed out after ${options.timeout} ms`)), options.timeout)
|
|
279
|
+
: undefined;
|
|
251
280
|
try {
|
|
252
|
-
const exitCode = await cmd.wait();
|
|
281
|
+
const exitCode = await Promise.race([cmd.wait(), failed]);
|
|
253
282
|
if (failure)
|
|
254
283
|
throw failure;
|
|
255
284
|
const stdout = Buffer.concat(out), stderr = Buffer.concat(err);
|
|
@@ -276,7 +305,9 @@ export class Sprite {
|
|
|
276
305
|
return this.execFile(file, args, options);
|
|
277
306
|
}
|
|
278
307
|
createSession(command, args = [], options = {}) {
|
|
279
|
-
|
|
308
|
+
// The pinned SDK's session wrapper forces a terminal, even when a caller
|
|
309
|
+
// supplies tty:false. Attaching an existing session detects its own PTY.
|
|
310
|
+
return this.spawn(command, args, { ...options, detachable: true, tty: true });
|
|
280
311
|
}
|
|
281
312
|
attachSession(sessionId, options = {}) {
|
|
282
313
|
return this.spawn("", [], { ...options, sessionId });
|
|
@@ -315,14 +346,17 @@ export class Sprite {
|
|
|
315
346
|
throw new CompatibilityError("Sprites", "filesystem-only checkpoints");
|
|
316
347
|
}
|
|
317
348
|
async updateURLSettings(settings) {
|
|
318
|
-
|
|
319
|
-
if (settings.auth !== "public" && settings.auth !== "sprite")
|
|
320
|
-
throw new TypeError("auth must be public or sprite");
|
|
349
|
+
validateURLSettings(settings);
|
|
321
350
|
const p = await (await this.native()).previews.create(8080, { visibility: settings.auth === "public" ? "public" : "private" });
|
|
322
351
|
this.url = p.urlWithToken ?? p.url;
|
|
323
352
|
}
|
|
324
353
|
async update(options) {
|
|
325
354
|
only("Sprites update", options, ["labels", "urlSettings"]);
|
|
355
|
+
if (options.labels === undefined && options.urlSettings === undefined)
|
|
356
|
+
throw new TypeError("urlSettings or labels is required");
|
|
357
|
+
if (options.urlSettings !== undefined)
|
|
358
|
+
validateURLSettings(options.urlSettings);
|
|
359
|
+
validateLabels(options.labels);
|
|
326
360
|
const s = await this.native();
|
|
327
361
|
if (options.labels)
|
|
328
362
|
await s.update({
|
|
@@ -461,18 +495,20 @@ export class SpriteFilesystem {
|
|
|
461
495
|
only("Sprites readdir", options, ["withFileTypes", "recursive", "pattern", "encoding"]);
|
|
462
496
|
if (options.encoding && options.encoding !== "utf8")
|
|
463
497
|
throw new CompatibilityError("Sprites", "directory name encodings other than utf8");
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
498
|
+
if (options.recursive && options.pattern)
|
|
499
|
+
throw new CompatibilityError("Sprites", "recursive listing with a glob pattern");
|
|
500
|
+
const entries = await fileOperation(path, "readdir", async () => {
|
|
501
|
+
const s = await this.get();
|
|
502
|
+
const entries = await s.files.list(this.files.path(path), {
|
|
503
|
+
hidden: true,
|
|
504
|
+
glob: options.pattern,
|
|
505
|
+
});
|
|
506
|
+
if (options.recursive)
|
|
507
|
+
for (let i = 0; i < entries.length; i++)
|
|
508
|
+
if (entries[i].type === "directory")
|
|
509
|
+
entries.push(...(await s.files.list(entries[i].path, { hidden: true })));
|
|
510
|
+
return entries;
|
|
468
511
|
});
|
|
469
|
-
if (options.recursive) {
|
|
470
|
-
if (options.pattern)
|
|
471
|
-
throw new CompatibilityError("Sprites", "recursive listing with a glob pattern");
|
|
472
|
-
for (let i = 0; i < entries.length; i++)
|
|
473
|
-
if (entries[i].type === "directory")
|
|
474
|
-
entries.push(...(await s.files.list(entries[i].path, { hidden: true })));
|
|
475
|
-
}
|
|
476
512
|
return entries.map((e) => options.withFileTypes
|
|
477
513
|
? {
|
|
478
514
|
name: e.name,
|
package/dist/tar.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare function packDirectory(root: string): Promise<Uint8Array>;
|
|
|
6
6
|
* once the whole archive arrived, so an archive cut short leaves nothing
|
|
7
7
|
* behind that could pass for the folder. A target that exists is merged
|
|
8
8
|
* into, files of the same name replaced. */
|
|
9
|
-
export declare function unpackStream(source: AsyncIterable<Uint8Array> | Iterable<Uint8Array>, target: string
|
|
9
|
+
export declare function unpackStream(source: AsyncIterable<Uint8Array> | Iterable<Uint8Array>, target: string, options?: {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}): Promise<void>;
|
|
10
12
|
/** Unpacks a whole gzipped tar held in memory, by the same rules. */
|
|
11
13
|
export declare function unpackArchive(archive: Uint8Array, target: string): Promise<void>;
|
package/dist/tar.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmod, lstat, mkdir, mkdtemp, open, readdir, readFile, realpath, rename, rm, symlink, unlink, } from "node:fs/promises";
|
|
1
|
+
import { chmod, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, realpath, rename, rm, symlink, unlink, } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { Readable } from "node:stream";
|
|
4
4
|
import { createGunzip, gzipSync } from "node:zlib";
|
|
@@ -126,13 +126,111 @@ function cutShort(cause) {
|
|
|
126
126
|
...(cause === undefined ? {} : { cause }),
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
|
+
function checkSignal(signal) {
|
|
130
|
+
if (signal?.aborted)
|
|
131
|
+
throw new RuntimeError({
|
|
132
|
+
message: "The download was cancelled or ran past its deadline.",
|
|
133
|
+
code: "timeout",
|
|
134
|
+
status: 0,
|
|
135
|
+
cause: signal.reason,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** Linux path names fit well below this generous metadata-only memory bound. */
|
|
139
|
+
const MAX_LONG_NAME_BYTES = 65_536;
|
|
140
|
+
function archiveSize(field) {
|
|
141
|
+
const text = field.trim();
|
|
142
|
+
if (text !== "" && !/^[0-7]+$/.test(text))
|
|
143
|
+
throw new Error("The archive contains an invalid file size.");
|
|
144
|
+
const size = text === "" ? 0 : parseInt(text, 8);
|
|
145
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > Number.MAX_SAFE_INTEGER - 511)
|
|
146
|
+
throw new Error("The archive contains an invalid file size.");
|
|
147
|
+
return size;
|
|
148
|
+
}
|
|
149
|
+
/** The gzip check protects the compressed bytes; tar's own checksum also
|
|
150
|
+
* protects the header that determines each file's name, kind and length. */
|
|
151
|
+
function checkHeader(header, field) {
|
|
152
|
+
const text = field.trim();
|
|
153
|
+
let unsigned = 0;
|
|
154
|
+
let signed = 0;
|
|
155
|
+
for (let index = 0; index < header.length; index++) {
|
|
156
|
+
const byte = index >= 148 && index < 156 ? 32 : header[index];
|
|
157
|
+
unsigned += byte;
|
|
158
|
+
signed += byte < 128 ? byte : byte - 256;
|
|
159
|
+
}
|
|
160
|
+
const expected = /^[0-7]+$/.test(text) ? parseInt(text, 8) : NaN;
|
|
161
|
+
// Older tar implementations used signed bytes for non-ASCII path names.
|
|
162
|
+
if (expected !== unsigned && expected !== signed)
|
|
163
|
+
throw new Error("The archive contains an invalid header checksum.");
|
|
164
|
+
}
|
|
165
|
+
/** Validate links against the tree a merge will produce. Staged entries
|
|
166
|
+
* take precedence; paths absent there retain the existing target's entry.
|
|
167
|
+
* A link safe in staging may otherwise lead through an old escaping link. */
|
|
168
|
+
async function mergedLinkStaysInside(staging, root, destination, link) {
|
|
169
|
+
let path = dirname(join(root, relative(staging, destination)));
|
|
170
|
+
const pending = link.split("/");
|
|
171
|
+
const followed = new Set();
|
|
172
|
+
while (pending.length) {
|
|
173
|
+
const part = pending.shift();
|
|
174
|
+
if (!part || part === ".")
|
|
175
|
+
continue;
|
|
176
|
+
path = part === ".." ? dirname(path) : join(path, part);
|
|
177
|
+
if (path !== root && !path.startsWith(root + sep))
|
|
178
|
+
return false;
|
|
179
|
+
const staged = join(staging, relative(root, path));
|
|
180
|
+
const stagedKind = await lstat(staged).catch((error) => {
|
|
181
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
182
|
+
return undefined;
|
|
183
|
+
throw error;
|
|
184
|
+
});
|
|
185
|
+
const actual = stagedKind ? staged : path;
|
|
186
|
+
const kind = stagedKind ??
|
|
187
|
+
(await lstat(actual).catch((error) => {
|
|
188
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
189
|
+
return undefined;
|
|
190
|
+
throw error;
|
|
191
|
+
}));
|
|
192
|
+
if (!kind?.isSymbolicLink())
|
|
193
|
+
continue;
|
|
194
|
+
if (followed.has(path))
|
|
195
|
+
return false;
|
|
196
|
+
followed.add(path);
|
|
197
|
+
const next = await readlink(actual);
|
|
198
|
+
if (isAbsolute(next)) {
|
|
199
|
+
// An existing absolute link is safe only when it stays in this root.
|
|
200
|
+
if (next !== root && !next.startsWith(root + sep))
|
|
201
|
+
return false;
|
|
202
|
+
path = root;
|
|
203
|
+
pending.unshift(...relative(root, next).split(sep));
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
path = dirname(path);
|
|
207
|
+
pending.unshift(...next.split("/"));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
129
212
|
/** The gzip stream's bytes, decompressed as they arrive. */
|
|
130
|
-
function gunzipped(source) {
|
|
213
|
+
function gunzipped(source, signal) {
|
|
131
214
|
const input = Readable.from(source, { objectMode: false });
|
|
132
215
|
const output = createGunzip();
|
|
216
|
+
// A filesystem await may precede async iteration; retain early stream
|
|
217
|
+
// errors for that iterator without an unhandled EventEmitter error.
|
|
218
|
+
output.on("error", () => undefined);
|
|
133
219
|
input.on("error", (error) => output.destroy(error));
|
|
134
220
|
input.pipe(output);
|
|
135
|
-
|
|
221
|
+
const abort = () => {
|
|
222
|
+
try {
|
|
223
|
+
checkSignal(signal);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
output.destroy(error);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
230
|
+
output.on("close", () => {
|
|
231
|
+
signal?.removeEventListener("abort", abort);
|
|
232
|
+
input.destroy();
|
|
233
|
+
});
|
|
136
234
|
return output;
|
|
137
235
|
}
|
|
138
236
|
/** Exact byte counts from a stream of chunks, holding at most one chunk. */
|
|
@@ -189,7 +287,7 @@ class Bytes {
|
|
|
189
287
|
/** Unpacks a tar stream into `root`, a fresh folder: files and folders as they
|
|
190
288
|
* arrive, links noted to make at the end. Throws unless the archive's end
|
|
191
289
|
* arrived. */
|
|
192
|
-
async function unpackEntries(bytes, root) {
|
|
290
|
+
async function unpackEntries(bytes, root, signal) {
|
|
193
291
|
const links = [];
|
|
194
292
|
const decode = (slice) => {
|
|
195
293
|
const end = slice.indexOf(0);
|
|
@@ -198,15 +296,20 @@ async function unpackEntries(bytes, root) {
|
|
|
198
296
|
let longName;
|
|
199
297
|
let longLink;
|
|
200
298
|
for (;;) {
|
|
299
|
+
checkSignal(signal);
|
|
201
300
|
const header = await bytes.exactly(512);
|
|
202
301
|
if (!header)
|
|
203
302
|
throw cutShort();
|
|
204
303
|
if (header.every((byte) => byte === 0)) {
|
|
304
|
+
const terminator = await bytes.exactly(512);
|
|
305
|
+
if (!terminator || !terminator.every((byte) => byte === 0))
|
|
306
|
+
throw cutShort();
|
|
205
307
|
await bytes.drain();
|
|
206
308
|
return links;
|
|
207
309
|
}
|
|
208
310
|
const text = (start, length) => decode(header.subarray(start, start + length));
|
|
209
|
-
const size =
|
|
311
|
+
const size = archiveSize(text(124, 12));
|
|
312
|
+
checkHeader(header, text(148, 8));
|
|
210
313
|
const type = String.fromCharCode(header[156] ?? 48);
|
|
211
314
|
const prefix = text(345, 155);
|
|
212
315
|
let name = longName ?? (prefix ? `${prefix}/${text(0, 100)}` : text(0, 100));
|
|
@@ -220,6 +323,8 @@ async function unpackEntries(bytes, root) {
|
|
|
220
323
|
;
|
|
221
324
|
};
|
|
222
325
|
if (type === "L" || type === "K") {
|
|
326
|
+
if (size > MAX_LONG_NAME_BYTES)
|
|
327
|
+
throw new Error("The archive contains oversized path metadata.");
|
|
223
328
|
const parts = [];
|
|
224
329
|
for await (const part of bytes.take(size))
|
|
225
330
|
parts.push(Uint8Array.from(part));
|
|
@@ -251,8 +356,16 @@ async function unpackEntries(bytes, root) {
|
|
|
251
356
|
await mkdir(dirname(destination), { recursive: true });
|
|
252
357
|
const file = await open(destination, "w");
|
|
253
358
|
try {
|
|
254
|
-
for await (const part of bytes.take(size))
|
|
255
|
-
|
|
359
|
+
for await (const part of bytes.take(size)) {
|
|
360
|
+
let written = 0;
|
|
361
|
+
while (written < part.length) {
|
|
362
|
+
checkSignal(signal);
|
|
363
|
+
const result = await file.write(part.subarray(written));
|
|
364
|
+
if (result.bytesWritten === 0)
|
|
365
|
+
throw new Error("The local file accepted no bytes.");
|
|
366
|
+
written += result.bytesWritten;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
256
369
|
}
|
|
257
370
|
finally {
|
|
258
371
|
await file.close();
|
|
@@ -267,8 +380,9 @@ async function unpackEntries(bytes, root) {
|
|
|
267
380
|
}
|
|
268
381
|
/** Moves what was unpacked in `from` into `to`, merging with what is there
|
|
269
382
|
* and replacing files of the same name, never through a link in `to`. */
|
|
270
|
-
async function merge(from, to, root) {
|
|
383
|
+
async function merge(from, to, root, signal) {
|
|
271
384
|
for (const name of await readdir(from)) {
|
|
385
|
+
checkSignal(signal);
|
|
272
386
|
const source = join(from, name);
|
|
273
387
|
const destination = join(to, name);
|
|
274
388
|
await assertPlain(root, destination, relative(root, destination));
|
|
@@ -276,8 +390,9 @@ async function merge(from, to, root) {
|
|
|
276
390
|
lstat(source),
|
|
277
391
|
lstat(destination).catch(() => undefined),
|
|
278
392
|
]);
|
|
393
|
+
checkSignal(signal);
|
|
279
394
|
if (kind.isDirectory() && there?.isDirectory())
|
|
280
|
-
await merge(source, destination, root);
|
|
395
|
+
await merge(source, destination, root, signal);
|
|
281
396
|
else
|
|
282
397
|
await rename(source, destination);
|
|
283
398
|
}
|
|
@@ -288,30 +403,41 @@ async function merge(from, to, root) {
|
|
|
288
403
|
* once the whole archive arrived, so an archive cut short leaves nothing
|
|
289
404
|
* behind that could pass for the folder. A target that exists is merged
|
|
290
405
|
* into, files of the same name replaced. */
|
|
291
|
-
export async function unpackStream(source, target) {
|
|
406
|
+
export async function unpackStream(source, target, options = {}) {
|
|
407
|
+
checkSignal(options.signal);
|
|
292
408
|
const final = resolve(target);
|
|
293
409
|
await mkdir(dirname(final), { recursive: true });
|
|
294
410
|
const staging = await mkdtemp(`${final}.runtime-partial-`);
|
|
295
|
-
const stream = gunzipped(source);
|
|
411
|
+
const stream = gunzipped(source, options.signal);
|
|
296
412
|
try {
|
|
297
413
|
const root = await realpath(staging);
|
|
298
|
-
const links = await unpackEntries(new Bytes(stream), root);
|
|
414
|
+
const links = await unpackEntries(new Bytes(stream), root, options.signal);
|
|
299
415
|
for (const { destination, link, name } of links) {
|
|
416
|
+
checkSignal(options.signal);
|
|
300
417
|
await assertPlain(root, destination, name);
|
|
301
418
|
await mkdir(dirname(destination), { recursive: true });
|
|
302
419
|
await symlink(link, destination).catch(() => undefined);
|
|
303
420
|
}
|
|
304
421
|
for (const { destination, link, name } of links) {
|
|
422
|
+
checkSignal(options.signal);
|
|
305
423
|
if (await linkStaysInside(root, dirname(destination), link))
|
|
306
424
|
continue;
|
|
307
425
|
await unlink(destination).catch(() => undefined);
|
|
308
426
|
throw new Error(`Refusing an archive link that leads outside the target: ${name} -> ${link}`);
|
|
309
427
|
}
|
|
310
428
|
const there = await lstat(final).catch(() => undefined);
|
|
429
|
+
checkSignal(options.signal);
|
|
311
430
|
if (!there)
|
|
312
431
|
await rename(staging, final);
|
|
313
|
-
else
|
|
314
|
-
|
|
432
|
+
else {
|
|
433
|
+
const targetRoot = await realpath(final);
|
|
434
|
+
for (const { destination, link, name } of links) {
|
|
435
|
+
checkSignal(options.signal);
|
|
436
|
+
if (!(await mergedLinkStaysInside(root, targetRoot, destination, link)))
|
|
437
|
+
throw new Error(`Refusing an archive link that leads outside the target: ${name} -> ${link}`);
|
|
438
|
+
}
|
|
439
|
+
await merge(root, targetRoot, targetRoot, options.signal);
|
|
440
|
+
}
|
|
315
441
|
}
|
|
316
442
|
finally {
|
|
317
443
|
stream.destroy();
|
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.11.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
|
|
@@ -87,12 +87,9 @@ export declare class Transport {
|
|
|
87
87
|
send(call: Call): Promise<Response>;
|
|
88
88
|
json<T>(call: Call): Promise<T>;
|
|
89
89
|
bytes(call: Call): Promise<Uint8Array>;
|
|
90
|
-
/** A file's bytes, read whole and checked
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* then refused. Nothing between the API and here reliably turns a stream
|
|
94
|
-
* cut part way into an error: on 25 September 2026 a 50 MB file came back
|
|
95
|
-
* 29 MB long, exit 0. */
|
|
90
|
+
/** A file's bytes, read whole and checked against its promised length and
|
|
91
|
+
* SHA-256. A short or different body is read again twice, within the same
|
|
92
|
+
* call deadline. */
|
|
96
93
|
fileBytes(call: Call): Promise<Uint8Array>;
|
|
97
94
|
/** A file's bytes as they arrive, for files too big to hold. The stream
|
|
98
95
|
* errors with `download_incomplete` if it ends short of the length the API
|