withruntime 0.9.0 → 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.
Files changed (90) hide show
  1. package/BLAXEL.md +4 -4
  2. package/DAYTONA.md +13 -7
  3. package/E2B.md +12 -8
  4. package/README.md +15 -7
  5. package/README.src.md +15 -7
  6. package/VERCEL.md +13 -3
  7. package/compatibility-lock.json +84 -8
  8. package/dist/blaxel/errors.js +1 -1
  9. package/dist/blaxel/lookup.d.ts +3 -2
  10. package/dist/blaxel/lookup.js +3 -2
  11. package/dist/blaxel/pagination.d.ts +2 -2
  12. package/dist/blaxel/pagination.js +45 -17
  13. package/dist/blaxel/preview.d.ts +2 -1
  14. package/dist/blaxel/preview.js +15 -13
  15. package/dist/blaxel/process.js +56 -10
  16. package/dist/blaxel/sandbox.d.ts +2 -0
  17. package/dist/blaxel/sandbox.js +2 -0
  18. package/dist/blaxel/snapshot.js +3 -5
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +189 -54
  21. package/dist/client.d.ts +29 -3
  22. package/dist/client.js +40 -11
  23. package/dist/cloudflare/identity.d.ts +7 -0
  24. package/dist/cloudflare/identity.js +13 -0
  25. package/dist/cloudflare/index.d.ts +9 -0
  26. package/dist/cloudflare/index.js +47 -9
  27. package/dist/cloudflare/worker.d.ts +4 -0
  28. package/dist/cloudflare/worker.js +166 -16
  29. package/dist/codesandbox/guest-service.js +57 -20
  30. package/dist/codesandbox/index.js +42 -8
  31. package/dist/compat/core.d.ts +9 -8
  32. package/dist/compat/core.js +60 -21
  33. package/dist/compat/files.js +22 -4
  34. package/dist/credentials.d.ts +6 -1
  35. package/dist/credentials.js +107 -38
  36. package/dist/daytona/daytona.js +11 -4
  37. package/dist/daytona/filesystem.d.ts +17 -4
  38. package/dist/daytona/filesystem.js +121 -38
  39. package/dist/daytona/index.d.ts +1 -1
  40. package/dist/daytona/sandbox.d.ts +1 -1
  41. package/dist/daytona/sandbox.js +16 -7
  42. package/dist/e2b/client.js +1 -1
  43. package/dist/e2b/commands.d.ts +30 -8
  44. package/dist/e2b/commands.js +142 -54
  45. package/dist/e2b/index.d.ts +1 -0
  46. package/dist/e2b/index.js +1 -0
  47. package/dist/e2b/pty.d.ts +24 -0
  48. package/dist/e2b/pty.js +80 -0
  49. package/dist/e2b/sandbox.d.ts +2 -1
  50. package/dist/e2b/sandbox.js +49 -15
  51. package/dist/file-source.d.ts +16 -0
  52. package/dist/file-source.js +162 -0
  53. package/dist/freestyle/index.d.ts +9 -1
  54. package/dist/freestyle/index.js +38 -5
  55. package/dist/index.d.ts +2 -1
  56. package/dist/login.js +16 -10
  57. package/dist/modal/index.js +2 -1
  58. package/dist/network-cli.js +111 -43
  59. package/dist/openai-agents/index.js +9 -45
  60. package/dist/page.d.ts +1 -1
  61. package/dist/page.js +2 -2
  62. package/dist/products/index.d.ts +12 -0
  63. package/dist/products/index.js +10 -0
  64. package/dist/products/observability.d.ts +44 -0
  65. package/dist/products/observability.js +6 -0
  66. package/dist/products/previews.d.ts +8 -2
  67. package/dist/products/previews.js +11 -3
  68. package/dist/products/volumes.d.ts +42 -0
  69. package/dist/products/volumes.js +36 -0
  70. package/dist/products/watch.js +61 -8
  71. package/dist/proxy.js +3 -1
  72. package/dist/runloop/index.d.ts +13 -27
  73. package/dist/runloop/index.js +149 -52
  74. package/dist/sandbox.d.ts +37 -8
  75. package/dist/sandbox.js +432 -68
  76. package/dist/snapshots.d.ts +9 -4
  77. package/dist/sprites/index.js +69 -33
  78. package/dist/tar.d.ts +3 -1
  79. package/dist/tar.js +140 -14
  80. package/dist/transport.d.ts +4 -7
  81. package/dist/transport.js +205 -122
  82. package/dist/vercel/command.d.ts +1 -1
  83. package/dist/vercel/command.js +3 -2
  84. package/dist/vercel/errors.d.ts +1 -1
  85. package/dist/vercel/errors.js +7 -1
  86. package/dist/vercel/sandbox.d.ts +8 -8
  87. package/dist/vercel/sandbox.js +154 -59
  88. package/dist/vercel/snapshot.d.ts +3 -2
  89. package/dist/vercel/snapshot.js +9 -5
  90. package/package.json +2 -1
@@ -1,8 +1,7 @@
1
1
  import { Page } from "./page.js";
2
2
  import type { RequestOptions, Transport } from "./transport.js";
3
- /** A sandbox's whole machine (files, memory, running processes), kept on its
4
- * host to start new sandboxes from, and copied off the host so it survives
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;
@@ -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?.privateAccess)
94
- throw new CompatibilityError("Sprites", "privateAccess roles");
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
- let bytes = 0;
243
+ const maxBuffer = options.maxBuffer || 10 * 1024 * 1024;
244
+ let stdoutBytes = 0;
245
+ let stderrBytes = 0;
231
246
  let failure;
232
- const capture = (target) => (chunk) => {
233
- bytes += chunk.length;
234
- if (bytes > (options.maxBuffer ?? 10 * 1024 * 1024)) {
235
- failure = new Error("maxBuffer exceeded");
236
- cmd.kill("SIGKILL");
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 ? setTimeout(abort, options.timeout) : undefined;
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
- return this.spawn(command, args, { ...options, detachable: true });
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
- only("Sprites URL settings", settings, ["auth"]);
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
- const s = await this.get();
465
- const entries = await s.files.list(this.files.path(path), {
466
- hidden: true,
467
- glob: options.pattern,
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): Promise<void>;
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
- output.on("close", () => input.destroy());
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 = parseInt(text(124, 12).trim() || "0", 8);
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
- await file.write(part);
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
- await merge(root, await realpath(final), await realpath(final));
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();
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.9.0";
2
+ export declare const VERSION = "0.10.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: the API says a body's length
91
- * (x-content-length) and, when small, its SHA-256 (x-content-sha256) before
92
- * sending it, and a body that falls short or differs is read again, twice,
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