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/sandbox.js
CHANGED
|
@@ -13,6 +13,49 @@ const CHUNK = 1_048_576;
|
|
|
13
13
|
* connections at once. */
|
|
14
14
|
const PARALLEL = 8;
|
|
15
15
|
const enc = (id) => encodeURIComponent(id);
|
|
16
|
+
/** One explicit deadline across a composed operation and its queue/retries. */
|
|
17
|
+
function requestScope(options) {
|
|
18
|
+
const deadline = options.timeoutMs === undefined || options.timeoutMs === 0
|
|
19
|
+
? undefined
|
|
20
|
+
: AbortSignal.timeout(options.timeoutMs);
|
|
21
|
+
const signal = deadline && options.signal
|
|
22
|
+
? AbortSignal.any([deadline, options.signal])
|
|
23
|
+
: (deadline ?? options.signal);
|
|
24
|
+
const request = {
|
|
25
|
+
...options,
|
|
26
|
+
...(deadline ? { timeoutMs: 0 } : {}),
|
|
27
|
+
...(signal ? { signal } : {}),
|
|
28
|
+
};
|
|
29
|
+
const cancelled = () => new ConnectionError({
|
|
30
|
+
message: "The call was cancelled or ran past its deadline.",
|
|
31
|
+
code: "timeout",
|
|
32
|
+
status: 0,
|
|
33
|
+
cause: signal?.reason,
|
|
34
|
+
});
|
|
35
|
+
const check = () => {
|
|
36
|
+
if (signal?.aborted)
|
|
37
|
+
throw cancelled();
|
|
38
|
+
};
|
|
39
|
+
const wait = (pending) => {
|
|
40
|
+
if (!signal)
|
|
41
|
+
return pending;
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const abort = () => reject(cancelled());
|
|
44
|
+
if (signal.aborted)
|
|
45
|
+
abort();
|
|
46
|
+
else
|
|
47
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
48
|
+
pending.then((value) => {
|
|
49
|
+
signal.removeEventListener("abort", abort);
|
|
50
|
+
resolve(value);
|
|
51
|
+
}, (error) => {
|
|
52
|
+
signal.removeEventListener("abort", abort);
|
|
53
|
+
reject(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
return { request, check, wait };
|
|
58
|
+
}
|
|
16
59
|
/** DELETE /v1/sandboxes/{id}, for `sandbox.delete()` and
|
|
17
60
|
* `runtime.sandboxes.delete(id)`. */
|
|
18
61
|
export function deleteSandbox(t, id, options = {}) {
|
|
@@ -268,7 +311,7 @@ export class Sandbox {
|
|
|
268
311
|
* once. `stdin: "pipe"` keeps its input open for write(); `pty` gives it a
|
|
269
312
|
* terminal. */
|
|
270
313
|
async spawn(command, options = {}) {
|
|
271
|
-
const { stdin, pty, outputEncoding, ...rest } = options;
|
|
314
|
+
const { stdin, pty, outputEncoding, request, ...rest } = options;
|
|
272
315
|
const info = await this.#t.json({
|
|
273
316
|
method: "POST",
|
|
274
317
|
path: `/v1/sandboxes/${enc(this.id)}/processes`,
|
|
@@ -279,6 +322,7 @@ export class Sandbox {
|
|
|
279
322
|
...(outputEncoding ? { outputEncoding } : {}),
|
|
280
323
|
},
|
|
281
324
|
...pick(rest),
|
|
325
|
+
...request,
|
|
282
326
|
});
|
|
283
327
|
return new Process(this.#t, this.id, info);
|
|
284
328
|
}
|
|
@@ -383,6 +427,27 @@ export class Sandbox {
|
|
|
383
427
|
}));
|
|
384
428
|
return this;
|
|
385
429
|
}
|
|
430
|
+
/** Gives it more or fewer vCPUs or more or less memory by a restart: its id,
|
|
431
|
+
* whole disk, volumes, environment, name and previews stay, and its
|
|
432
|
+
* programs stop, which `restart: true` says you know (snapshot it first to
|
|
433
|
+
* keep its memory too). A running sandbox is paused, or stopped if it is
|
|
434
|
+
* persistent, and comes back running at the new size on the same server; a
|
|
435
|
+
* paused or stopped one is started. Memory bills on the new size from then.
|
|
436
|
+
* Refused, with nothing changed, when the server has no room
|
|
437
|
+
* (`no_capacity`), above your quota or the trial's 2 vCPU and 4 GiB, or
|
|
438
|
+
* while a snapshot or fork of it is being taken. */
|
|
439
|
+
async resize(size, options) {
|
|
440
|
+
const { restart, ...rest } = options;
|
|
441
|
+
this.#keep(await this.#t.json({
|
|
442
|
+
method: "POST",
|
|
443
|
+
path: `/v1/sandboxes/${enc(this.id)}:resize`,
|
|
444
|
+
body: { ...size, restart },
|
|
445
|
+
// A pause or stop and a cold boot.
|
|
446
|
+
wait: 120,
|
|
447
|
+
...pick(rest),
|
|
448
|
+
}));
|
|
449
|
+
return this;
|
|
450
|
+
}
|
|
386
451
|
/** Keeps a running sandbox's lease ahead of now, in the background, until
|
|
387
452
|
* stop() or the returned function ends it: every `everySeconds` (60) it
|
|
388
453
|
* extends the lease so that `marginSeconds` (600) remain, never more than
|
|
@@ -464,6 +529,9 @@ export class Sandbox {
|
|
|
464
529
|
const running = this.state === "running";
|
|
465
530
|
// Check before entering cleanup: an abort during refresh must not pause or wake.
|
|
466
531
|
const pauseOptions = running ? request() : undefined;
|
|
532
|
+
let captured;
|
|
533
|
+
let primary;
|
|
534
|
+
let failed = false;
|
|
467
535
|
try {
|
|
468
536
|
if (running)
|
|
469
537
|
await this.pause(pauseOptions);
|
|
@@ -475,6 +543,7 @@ export class Sandbox {
|
|
|
475
543
|
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
476
544
|
...request(),
|
|
477
545
|
});
|
|
546
|
+
captured = snapshot;
|
|
478
547
|
// Prefer: wait is bounded on the server. A capture still in progress
|
|
479
548
|
// must keep its source paused, or the worker refuses it after we wake.
|
|
480
549
|
while (snapshot.state === "capturing") {
|
|
@@ -507,12 +576,39 @@ export class Sandbox {
|
|
|
507
576
|
message: "The server did not confirm a disk-only snapshot.",
|
|
508
577
|
details: { snapshotId: snapshot.id },
|
|
509
578
|
});
|
|
510
|
-
|
|
579
|
+
captured = snapshot;
|
|
511
580
|
}
|
|
512
|
-
|
|
513
|
-
|
|
581
|
+
catch (error) {
|
|
582
|
+
failed = true;
|
|
583
|
+
primary = error;
|
|
584
|
+
}
|
|
585
|
+
if (running) {
|
|
586
|
+
try {
|
|
514
587
|
await this.wake();
|
|
588
|
+
}
|
|
589
|
+
catch (wakeError) {
|
|
590
|
+
const recovery = {
|
|
591
|
+
...(captured ? { snapshotId: captured.id } : {}),
|
|
592
|
+
sourceSandboxId: this.id,
|
|
593
|
+
sourceWakeError: {
|
|
594
|
+
...(wakeError instanceof RuntimeError ? { code: wakeError.code } : {}),
|
|
595
|
+
message: wakeError instanceof Error ? wakeError.message : String(wakeError),
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
const reported = failed ? primary : wakeError;
|
|
599
|
+
if (reported instanceof Error && Object.isExtensible(reported)) {
|
|
600
|
+
if (reported instanceof RuntimeError)
|
|
601
|
+
Object.assign(reported, { details: { ...reported.details, ...recovery } });
|
|
602
|
+
else
|
|
603
|
+
Object.assign(reported, recovery);
|
|
604
|
+
}
|
|
605
|
+
if (!failed)
|
|
606
|
+
throw wakeError;
|
|
607
|
+
}
|
|
515
608
|
}
|
|
609
|
+
if (failed)
|
|
610
|
+
throw primary;
|
|
611
|
+
return captured;
|
|
516
612
|
}
|
|
517
613
|
async fork(options = {}) {
|
|
518
614
|
const { idempotencyKey, signal, timeoutMs, ...body } = options;
|
|
@@ -690,16 +786,18 @@ export class Processes {
|
|
|
690
786
|
this.t = t;
|
|
691
787
|
this.sandboxId = sandboxId;
|
|
692
788
|
}
|
|
693
|
-
async list() {
|
|
789
|
+
async list(options = {}) {
|
|
694
790
|
return (await this.t.json({
|
|
695
791
|
method: "GET",
|
|
696
792
|
path: `/v1/sandboxes/${enc(this.sandboxId)}/processes`,
|
|
793
|
+
...options,
|
|
697
794
|
})).data;
|
|
698
795
|
}
|
|
699
|
-
async get(processId) {
|
|
796
|
+
async get(processId, options = {}) {
|
|
700
797
|
const info = await this.t.json({
|
|
701
798
|
method: "GET",
|
|
702
799
|
path: `/v1/sandboxes/${enc(this.sandboxId)}/processes/${enc(processId)}`,
|
|
800
|
+
...options,
|
|
703
801
|
});
|
|
704
802
|
return new Process(this.t, this.sandboxId, info);
|
|
705
803
|
}
|
|
@@ -762,6 +860,26 @@ export class Process {
|
|
|
762
860
|
get id() {
|
|
763
861
|
return this.info.id;
|
|
764
862
|
}
|
|
863
|
+
/** The command line as the sandbox reports it. */
|
|
864
|
+
get command() {
|
|
865
|
+
return this.info.command;
|
|
866
|
+
}
|
|
867
|
+
/** As of the last answer: spawn, `wait()` or `refresh()`. */
|
|
868
|
+
get state() {
|
|
869
|
+
return this.info.state;
|
|
870
|
+
}
|
|
871
|
+
/** null while it runs, and when it was killed or timed out. */
|
|
872
|
+
get exitCode() {
|
|
873
|
+
return this.info.exitCode;
|
|
874
|
+
}
|
|
875
|
+
toJSON() {
|
|
876
|
+
return this.info;
|
|
877
|
+
}
|
|
878
|
+
/** console.log shows what the process is, not the client inside it. */
|
|
879
|
+
[Symbol.for("nodejs.util.inspect.custom")](_depth, options, show) {
|
|
880
|
+
const { id, sandboxId, command, state, exitCode } = this;
|
|
881
|
+
return `Process ${show({ id, sandboxId, command, state, exitCode }, options)}`;
|
|
882
|
+
}
|
|
765
883
|
/** Every output event from the start (or `cursor`) until exit. */
|
|
766
884
|
output(options = {}) {
|
|
767
885
|
return new Processes(this.t, this.sandboxId).follow(this.id, options);
|
|
@@ -807,6 +925,12 @@ export class Process {
|
|
|
807
925
|
if (event.type === "exit")
|
|
808
926
|
exit = event;
|
|
809
927
|
}
|
|
928
|
+
if (exit)
|
|
929
|
+
this.info = {
|
|
930
|
+
...this.info,
|
|
931
|
+
state: exit.state,
|
|
932
|
+
exitCode: exit.exitCode,
|
|
933
|
+
};
|
|
810
934
|
return {
|
|
811
935
|
exitCode: exit?.exitCode ?? null,
|
|
812
936
|
...out,
|
|
@@ -815,16 +939,27 @@ export class Process {
|
|
|
815
939
|
processId: this.id,
|
|
816
940
|
};
|
|
817
941
|
}
|
|
818
|
-
/** Sends input. Offsets
|
|
942
|
+
/** Sends input. Offsets protect transport retries; signal and timeoutMs cover
|
|
943
|
+
* the whole logical write, including its queue and backpressure. */
|
|
819
944
|
write(data, options = {}) {
|
|
945
|
+
if (options.idempotencyKey !== undefined)
|
|
946
|
+
return Promise.reject(new RuntimeError({
|
|
947
|
+
message: "Process input writes do not accept caller idempotency keys; offsets protect transport retries.",
|
|
948
|
+
code: "unsupported_process_write_option",
|
|
949
|
+
status: 400,
|
|
950
|
+
}));
|
|
820
951
|
// Copy before queuing: callers may reuse their buffer while a previous
|
|
821
952
|
// write waits for the guest to drain its pipe.
|
|
822
953
|
const bytes = new Uint8Array(utf8(data));
|
|
823
954
|
const eof = options.eof === true;
|
|
955
|
+
const { eof: _eof, ...requestOptions } = options;
|
|
956
|
+
const scope = requestScope(requestOptions);
|
|
824
957
|
const write = this.#inputWrites.then(async () => {
|
|
825
958
|
let sent = 0;
|
|
959
|
+
let chunkSize = CHUNK;
|
|
826
960
|
do {
|
|
827
|
-
|
|
961
|
+
scope.check();
|
|
962
|
+
const chunk = bytes.subarray(sent, sent + chunkSize);
|
|
828
963
|
const reply = await this.t.json({
|
|
829
964
|
method: "POST",
|
|
830
965
|
path: `/v1/sandboxes/${enc(this.sandboxId)}/processes/${enc(this.id)}:write`,
|
|
@@ -833,6 +968,7 @@ export class Process {
|
|
|
833
968
|
offset: this.#inputOffset,
|
|
834
969
|
...(eof && sent + chunk.length === bytes.length ? { eof: true } : {}),
|
|
835
970
|
},
|
|
971
|
+
...scope.request,
|
|
836
972
|
});
|
|
837
973
|
const accepted = reply.offset - this.#inputOffset;
|
|
838
974
|
if (!Number.isSafeInteger(reply.offset) || accepted < 0 || accepted > chunk.length)
|
|
@@ -843,23 +979,36 @@ export class Process {
|
|
|
843
979
|
});
|
|
844
980
|
sent += accepted;
|
|
845
981
|
this.#inputOffset = reply.offset;
|
|
982
|
+
// A slow pipe may take only part of a chunk. Keep later uploads within
|
|
983
|
+
// that observed capacity instead of resending almost a MiB each time.
|
|
984
|
+
// Zero is legitimate backpressure: retry a bounded probe at the same
|
|
985
|
+
// offset without discarding bytes or inventing progress.
|
|
986
|
+
if (accepted < chunk.length) {
|
|
987
|
+
chunkSize = accepted > 0 ? accepted : Math.min(chunkSize, 65_536);
|
|
988
|
+
// A full pipe must yield even if a proxy answers immediately; don't
|
|
989
|
+
// spin requests while the guest has accepted none of this input.
|
|
990
|
+
if (accepted === 0)
|
|
991
|
+
await scope.wait(new Promise((resolve) => setTimeout(resolve, 100)));
|
|
992
|
+
}
|
|
846
993
|
} while (sent < bytes.length);
|
|
847
994
|
});
|
|
848
995
|
this.#inputWrites = write.catch(() => undefined);
|
|
849
|
-
return write;
|
|
996
|
+
return scope.wait(write);
|
|
850
997
|
}
|
|
851
|
-
async kill(signal = "SIGTERM") {
|
|
998
|
+
async kill(signal = "SIGTERM", options = {}) {
|
|
852
999
|
await this.t.json({
|
|
853
1000
|
method: "POST",
|
|
854
1001
|
path: `/v1/sandboxes/${enc(this.sandboxId)}/processes/${enc(this.id)}:signal`,
|
|
855
1002
|
body: { signal },
|
|
1003
|
+
...options,
|
|
856
1004
|
});
|
|
857
1005
|
}
|
|
858
|
-
async resize(cols, rows) {
|
|
1006
|
+
async resize(cols, rows, options = {}) {
|
|
859
1007
|
await this.t.json({
|
|
860
1008
|
method: "POST",
|
|
861
1009
|
path: `/v1/sandboxes/${enc(this.sandboxId)}/processes/${enc(this.id)}:resize`,
|
|
862
1010
|
body: { cols, rows },
|
|
1011
|
+
...options,
|
|
863
1012
|
});
|
|
864
1013
|
}
|
|
865
1014
|
async refresh() {
|
|
@@ -993,6 +1142,8 @@ export class Files {
|
|
|
993
1142
|
* sets its permissions (0o755 for a program); 0o644 when left out. */
|
|
994
1143
|
async write(path, data, writeOptions = {}) {
|
|
995
1144
|
const { mode, ...options } = writeOptions;
|
|
1145
|
+
const scope = requestScope(options);
|
|
1146
|
+
scope.check();
|
|
996
1147
|
const octal = mode === undefined ? undefined : (mode & 0o777).toString(8).padStart(3, "0");
|
|
997
1148
|
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
998
1149
|
if (bytes.length <= CHUNK) {
|
|
@@ -1003,27 +1154,100 @@ export class Files {
|
|
|
1003
1154
|
path: this.#path("/files/content"),
|
|
1004
1155
|
query: { path, ...(octal ? { mode: octal } : {}) },
|
|
1005
1156
|
bytes,
|
|
1006
|
-
...
|
|
1157
|
+
...scope.request,
|
|
1007
1158
|
});
|
|
1008
1159
|
return { path, size: bytes.length };
|
|
1009
1160
|
}
|
|
1010
|
-
const sha256 = await sha256Hex(bytes);
|
|
1161
|
+
const sha256 = await scope.wait(sha256Hex(bytes));
|
|
1162
|
+
return this.#writeUpload(path, {
|
|
1163
|
+
size: bytes.length,
|
|
1164
|
+
sha256,
|
|
1165
|
+
chunkAt: (offset, length) => bytes.subarray(offset, offset + length),
|
|
1166
|
+
}, octal, scope);
|
|
1167
|
+
}
|
|
1168
|
+
/** Uploads byte streams with bounded memory on Node/Bun. Unknown-length
|
|
1169
|
+
* inputs stage once in a private local temporary file; known local files
|
|
1170
|
+
* and Blobs avoid that extra disk copy. Paths are local file paths. */
|
|
1171
|
+
async writeStream(path, input, writeOptions = {}) {
|
|
1172
|
+
const { mode, onProgress, ...options } = writeOptions;
|
|
1173
|
+
if (input instanceof Uint8Array) {
|
|
1174
|
+
const result = await this.write(path, input, {
|
|
1175
|
+
...options,
|
|
1176
|
+
...(mode === undefined ? {} : { mode }),
|
|
1177
|
+
});
|
|
1178
|
+
onProgress?.({ bytesSent: result.size });
|
|
1179
|
+
return result;
|
|
1180
|
+
}
|
|
1181
|
+
const scope = requestScope(options);
|
|
1182
|
+
scope.check();
|
|
1183
|
+
if (!globalThis.process
|
|
1184
|
+
?.versions?.node)
|
|
1185
|
+
throw new RuntimeError({
|
|
1186
|
+
message: "Streamed file uploads require Node or Bun for incremental hashing and private local staging. Use files.write with byte data in a browser.",
|
|
1187
|
+
code: "unsupported_runtime",
|
|
1188
|
+
status: 0,
|
|
1189
|
+
});
|
|
1190
|
+
const { prepareFileSource } = await import("./file-source.js");
|
|
1191
|
+
const source = await prepareFileSource(input, { ...scope, signal: scope.request.signal });
|
|
1192
|
+
let written;
|
|
1193
|
+
try {
|
|
1194
|
+
if (source.size <= CHUNK) {
|
|
1195
|
+
written = await this.write(path, await scope.wait(source.chunkAt(0, source.size)), {
|
|
1196
|
+
...scope.request,
|
|
1197
|
+
...(mode === undefined ? {} : { mode }),
|
|
1198
|
+
});
|
|
1199
|
+
onProgress?.({ bytesSent: written.size });
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
const octal = mode === undefined ? undefined : (mode & 0o777).toString(8).padStart(3, "0");
|
|
1203
|
+
written = await this.#writeUpload(path, source, octal, scope, onProgress);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
catch (primary) {
|
|
1207
|
+
try {
|
|
1208
|
+
await source.close();
|
|
1209
|
+
}
|
|
1210
|
+
catch (cleanupError) {
|
|
1211
|
+
if (primary === undefined)
|
|
1212
|
+
throw cleanupError;
|
|
1213
|
+
if (primary instanceof Error && Object.isExtensible(primary)) {
|
|
1214
|
+
try {
|
|
1215
|
+
Object.defineProperty(primary, "cleanupError", {
|
|
1216
|
+
value: cleanupError,
|
|
1217
|
+
configurable: true,
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
catch {
|
|
1221
|
+
/* Keep the original upload failure. */
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
throw primary;
|
|
1226
|
+
}
|
|
1227
|
+
// A failed close after a good upload is the call's failure.
|
|
1228
|
+
await source.close();
|
|
1229
|
+
return written;
|
|
1230
|
+
}
|
|
1231
|
+
/** One implementation of atomic, checked chunk uploads for bytes and streams. */
|
|
1232
|
+
async #writeUpload(path, source, octal, scope, onProgress) {
|
|
1233
|
+
const sha256 = source.sha256;
|
|
1011
1234
|
// A guest journals mutations across operations, not per URL. One logical
|
|
1012
1235
|
// write therefore owns separate stable keys for each mutation phase.
|
|
1013
|
-
const uploadKey =
|
|
1236
|
+
const uploadKey = scope.request.idempotencyKey ?? crypto.randomUUID();
|
|
1014
1237
|
const phaseKeys = new Map();
|
|
1015
1238
|
for (const phase of ["begin", "legacy-begin", "commit", "chmod", "abort"])
|
|
1016
1239
|
phaseKeys.set(phase, await sha256Hex(JSON.stringify(["files.write", uploadKey, phase])));
|
|
1017
1240
|
const phaseOptions = (phase) => ({
|
|
1018
|
-
...
|
|
1241
|
+
...scope.request,
|
|
1019
1242
|
idempotencyKey: phaseKeys.get(phase),
|
|
1020
1243
|
});
|
|
1021
1244
|
let begin;
|
|
1245
|
+
scope.check();
|
|
1022
1246
|
try {
|
|
1023
1247
|
begin = await this.t.json({
|
|
1024
1248
|
method: "POST",
|
|
1025
1249
|
path: this.#path("/uploads"),
|
|
1026
|
-
body: { path, size:
|
|
1250
|
+
body: { path, size: source.size, sha256, ...(octal ? { mode: octal } : {}) },
|
|
1027
1251
|
...phaseOptions("begin"),
|
|
1028
1252
|
});
|
|
1029
1253
|
}
|
|
@@ -1032,73 +1256,158 @@ export class Files {
|
|
|
1032
1256
|
throw error;
|
|
1033
1257
|
// The API aborted the unsupported transfer before accepting chunks.
|
|
1034
1258
|
// A different body needs a different key; retain deterministic retries.
|
|
1259
|
+
scope.check();
|
|
1035
1260
|
begin = await this.t.json({
|
|
1036
1261
|
method: "POST",
|
|
1037
1262
|
path: this.#path("/uploads"),
|
|
1038
|
-
body: { path, size:
|
|
1263
|
+
body: { path, size: source.size, sha256 },
|
|
1039
1264
|
...phaseOptions("legacy-begin"),
|
|
1040
1265
|
});
|
|
1041
1266
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1267
|
+
// A malformed response does not prove ownership of any transfer to abort.
|
|
1268
|
+
if (typeof begin.uploadId !== "string" || begin.uploadId.length === 0)
|
|
1269
|
+
throw new ConnectionError({
|
|
1270
|
+
message: "The file upload returned no transfer identifier.",
|
|
1271
|
+
code: "connection_error",
|
|
1272
|
+
status: 0,
|
|
1273
|
+
});
|
|
1274
|
+
const commit = () => {
|
|
1275
|
+
scope.check();
|
|
1276
|
+
return this.t.json({
|
|
1277
|
+
method: "POST",
|
|
1278
|
+
path: this.#path(`/uploads/${enc(begin.uploadId)}:commit`),
|
|
1279
|
+
body: {},
|
|
1280
|
+
...phaseOptions("commit"),
|
|
1281
|
+
});
|
|
1282
|
+
};
|
|
1048
1283
|
let committed = false;
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1284
|
+
let nextOffset = 0;
|
|
1285
|
+
const stopChunks = new AbortController();
|
|
1286
|
+
const chunkSignal = scope.request.signal
|
|
1287
|
+
? AbortSignal.any([scope.request.signal, stopChunks.signal])
|
|
1288
|
+
: stopChunks.signal;
|
|
1289
|
+
const chunkScope = requestScope({ ...scope.request, signal: chunkSignal });
|
|
1290
|
+
let bytesSent = 0;
|
|
1291
|
+
let failed = false;
|
|
1292
|
+
let primaryFailure;
|
|
1293
|
+
const worker = async () => {
|
|
1052
1294
|
try {
|
|
1053
|
-
|
|
1054
|
-
|
|
1295
|
+
while (nextOffset < source.size) {
|
|
1296
|
+
scope.check();
|
|
1297
|
+
if (stopChunks.signal.aborted)
|
|
1298
|
+
return;
|
|
1299
|
+
const offset = nextOffset;
|
|
1300
|
+
nextOffset += begin.chunkBytes;
|
|
1301
|
+
const offered = source.chunkAt(offset, Math.min(begin.chunkBytes, source.size - offset));
|
|
1302
|
+
const chunk = offered instanceof Promise ? await chunkScope.wait(offered) : offered;
|
|
1303
|
+
chunkScope.check();
|
|
1304
|
+
if (chunk.length !== Math.min(begin.chunkBytes, source.size - offset))
|
|
1305
|
+
throw new ConnectionError({
|
|
1306
|
+
message: "The local upload source returned an incomplete chunk.",
|
|
1307
|
+
code: "source_changed",
|
|
1308
|
+
status: 0,
|
|
1309
|
+
});
|
|
1310
|
+
const receipt = await this.t.json({
|
|
1311
|
+
method: "PUT",
|
|
1312
|
+
path: this.#path(`/uploads/${enc(begin.uploadId)}`),
|
|
1313
|
+
query: { offset },
|
|
1314
|
+
bytes: chunk,
|
|
1315
|
+
...scope.request,
|
|
1316
|
+
signal: chunkSignal,
|
|
1317
|
+
// Offsets and byte hashes make chunk replay safe without mutation keys.
|
|
1318
|
+
idempotencyKey: undefined,
|
|
1319
|
+
});
|
|
1320
|
+
// Parallel receipts report cumulative accepted bytes, not this offset.
|
|
1321
|
+
if (!Number.isSafeInteger(receipt.received) ||
|
|
1322
|
+
receipt.received < chunk.length ||
|
|
1323
|
+
receipt.received > source.size)
|
|
1324
|
+
throw new ConnectionError({
|
|
1325
|
+
message: "The file upload returned invalid byte progress.",
|
|
1326
|
+
code: "connection_error",
|
|
1327
|
+
status: 0,
|
|
1328
|
+
});
|
|
1329
|
+
bytesSent += chunk.length;
|
|
1330
|
+
onProgress?.({ bytesSent });
|
|
1331
|
+
}
|
|
1055
1332
|
}
|
|
1056
1333
|
catch (error) {
|
|
1057
|
-
if (!
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
offsets.push(offset);
|
|
1064
|
-
let next = 0;
|
|
1065
|
-
const worker = async () => {
|
|
1066
|
-
while (next < offsets.length) {
|
|
1067
|
-
const offset = offsets[next++];
|
|
1068
|
-
await this.t.bytes({
|
|
1069
|
-
method: "PUT",
|
|
1070
|
-
path: this.#path(`/uploads/${enc(begin.uploadId)}`),
|
|
1071
|
-
query: { offset },
|
|
1072
|
-
bytes: bytes.subarray(offset, offset + begin.chunkBytes),
|
|
1073
|
-
});
|
|
1334
|
+
if (!failed) {
|
|
1335
|
+
failed = true;
|
|
1336
|
+
primaryFailure = error;
|
|
1337
|
+
stopChunks.abort(error);
|
|
1338
|
+
}
|
|
1339
|
+
throw error;
|
|
1074
1340
|
}
|
|
1075
1341
|
};
|
|
1076
1342
|
try {
|
|
1343
|
+
if (!Number.isSafeInteger(begin.chunkBytes) ||
|
|
1344
|
+
begin.chunkBytes <= 0 ||
|
|
1345
|
+
begin.chunkBytes > CHUNK)
|
|
1346
|
+
throw new ConnectionError({
|
|
1347
|
+
message: "The file upload returned an invalid chunk size.",
|
|
1348
|
+
code: "connection_error",
|
|
1349
|
+
status: 0,
|
|
1350
|
+
});
|
|
1351
|
+
if (begin.replayed) {
|
|
1352
|
+
// A completed commit removed the transfer. Ask its journal before
|
|
1353
|
+
// resending chunks; an incomplete transfer alone is safe to continue.
|
|
1354
|
+
try {
|
|
1355
|
+
await commit();
|
|
1356
|
+
committed = true;
|
|
1357
|
+
onProgress?.({ bytesSent: source.size });
|
|
1358
|
+
}
|
|
1359
|
+
catch (error) {
|
|
1360
|
+
if (!(error instanceof RuntimeError) || error.code !== "upload_incomplete")
|
|
1361
|
+
throw error;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1077
1364
|
if (!committed) {
|
|
1078
|
-
|
|
1365
|
+
const workers = Array.from({ length: Math.min(PARALLEL, Math.ceil(source.size / begin.chunkBytes)) }, worker);
|
|
1366
|
+
// Drain every owned in-flight chunk before aborting its transfer.
|
|
1367
|
+
// Promise.all's early rejection could otherwise leave writes racing cleanup.
|
|
1368
|
+
await Promise.allSettled(workers);
|
|
1369
|
+
if (failed)
|
|
1370
|
+
throw primaryFailure;
|
|
1079
1371
|
await commit();
|
|
1080
1372
|
}
|
|
1081
1373
|
// An older sandbox image ignores the mode in the upload; set it after.
|
|
1082
|
-
if (octal && begin.mode !== octal)
|
|
1374
|
+
if (octal && begin.mode !== octal) {
|
|
1375
|
+
scope.check();
|
|
1083
1376
|
await this.t.json({
|
|
1084
1377
|
method: "POST",
|
|
1085
1378
|
path: this.#path("/files:chmod"),
|
|
1086
1379
|
body: { path, mode: octal },
|
|
1087
1380
|
...phaseOptions("chmod"),
|
|
1088
1381
|
});
|
|
1382
|
+
}
|
|
1089
1383
|
}
|
|
1090
1384
|
catch (error) {
|
|
1091
|
-
|
|
1092
|
-
.json({
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1385
|
+
try {
|
|
1386
|
+
await this.t.json({
|
|
1387
|
+
method: "POST",
|
|
1388
|
+
path: this.#path(`/uploads/${enc(begin.uploadId)}:abort`),
|
|
1389
|
+
body: {},
|
|
1390
|
+
idempotencyKey: phaseKeys.get("abort"),
|
|
1391
|
+
timeoutMs: 5000,
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
catch (cleanupError) {
|
|
1395
|
+
// Keep the original failure; retain cleanup evidence when possible.
|
|
1396
|
+
if (error instanceof Error && Object.isExtensible(error)) {
|
|
1397
|
+
try {
|
|
1398
|
+
Object.defineProperty(error, error.cause === undefined ? "cause" : "cleanupError", {
|
|
1399
|
+
value: cleanupError,
|
|
1400
|
+
configurable: true,
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
catch {
|
|
1404
|
+
/* A frozen property cannot replace the primary failure. */
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1099
1408
|
throw error;
|
|
1100
1409
|
}
|
|
1101
|
-
return { path, size:
|
|
1410
|
+
return { path, size: source.size };
|
|
1102
1411
|
}
|
|
1103
1412
|
/** Directory entries; `depth` goes deeper, `glob` filters (e.g. "**\/*.py"). */
|
|
1104
1413
|
async list(path = "/workspace", options = {}) {
|
|
@@ -1157,6 +1466,80 @@ export class Files {
|
|
|
1157
1466
|
...request,
|
|
1158
1467
|
});
|
|
1159
1468
|
}
|
|
1469
|
+
/** Packs a directory as archive bytes; `gzip: false` returns plain tar.
|
|
1470
|
+
* Exclusions are relative paths, not globs. `user: root` selects guest root. */
|
|
1471
|
+
async archive(path, options = {}) {
|
|
1472
|
+
const { gzip = true, exclude, user, ...request } = options;
|
|
1473
|
+
return this.t.fileBytes({
|
|
1474
|
+
method: "GET",
|
|
1475
|
+
path: this.#path("/files/archive"),
|
|
1476
|
+
query: { path, gzip, ...(exclude ? { exclude } : {}), ...(user ? { user } : {}) },
|
|
1477
|
+
accept: gzip ? "application/gzip" : "application/x-tar",
|
|
1478
|
+
...request,
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
/** Unpacks archive bytes in a directory with the sandbox user's rights by
|
|
1482
|
+
* default; `user: root` explicitly selects root inside this guest. */
|
|
1483
|
+
async unarchive(path, bytes, options = {}) {
|
|
1484
|
+
const { gzip = bytes[0] === 0x1f && bytes[1] === 0x8b, user, ...request } = options;
|
|
1485
|
+
const scope = requestScope(request);
|
|
1486
|
+
scope.check();
|
|
1487
|
+
if (bytes.length <= CHUNK) {
|
|
1488
|
+
await this.t.bytes({
|
|
1489
|
+
method: "PUT",
|
|
1490
|
+
path: this.#path("/files/archive"),
|
|
1491
|
+
query: { path, ...(user ? { user } : {}) },
|
|
1492
|
+
bytes,
|
|
1493
|
+
...scope.request,
|
|
1494
|
+
});
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
const key = request.idempotencyKey ?? crypto.randomUUID();
|
|
1498
|
+
const phase = async (name) => ({
|
|
1499
|
+
...scope.request,
|
|
1500
|
+
idempotencyKey: await sha256Hex(JSON.stringify(["files.unarchive", key, name])),
|
|
1501
|
+
});
|
|
1502
|
+
const begin = await this.t.json({
|
|
1503
|
+
method: "POST",
|
|
1504
|
+
path: this.#path("/files/archive/uploads"),
|
|
1505
|
+
body: { path, gzip, ...(user ? { user } : {}) },
|
|
1506
|
+
...(await phase("begin")),
|
|
1507
|
+
});
|
|
1508
|
+
const upload = (suffix = "") => this.#path(`/files/archive/uploads/${enc(begin.uploadId)}${suffix}`);
|
|
1509
|
+
try {
|
|
1510
|
+
if (!Number.isSafeInteger(begin.chunkBytes) || begin.chunkBytes <= 0)
|
|
1511
|
+
throw new ConnectionError({
|
|
1512
|
+
message: "The archive upload returned an invalid chunk size.",
|
|
1513
|
+
code: "connection_error",
|
|
1514
|
+
status: 0,
|
|
1515
|
+
});
|
|
1516
|
+
for (let offset = 0; offset < bytes.length; offset += begin.chunkBytes) {
|
|
1517
|
+
scope.check();
|
|
1518
|
+
await this.t.bytes({
|
|
1519
|
+
method: "PUT",
|
|
1520
|
+
path: upload(),
|
|
1521
|
+
query: { offset },
|
|
1522
|
+
bytes: bytes.subarray(offset, offset + begin.chunkBytes),
|
|
1523
|
+
...(await phase(`chunk:${offset}`)),
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
scope.check();
|
|
1527
|
+
await this.t.json({
|
|
1528
|
+
method: "POST",
|
|
1529
|
+
path: upload(":commit"),
|
|
1530
|
+
body: {},
|
|
1531
|
+
...(await phase("commit")),
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
catch (error) {
|
|
1535
|
+
// Cleanup must still run when the caller's signal/deadline has expired.
|
|
1536
|
+
const idempotencyKey = await sha256Hex(JSON.stringify(["files.unarchive", key, "abort"]));
|
|
1537
|
+
await this.t
|
|
1538
|
+
.json({ method: "POST", path: upload(":abort"), body: {}, idempotencyKey })
|
|
1539
|
+
.catch(() => undefined);
|
|
1540
|
+
throw error;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1160
1543
|
/** Copies a local file or directory into the sandbox. A directory travels as
|
|
1161
1544
|
* one gzipped tar to the API's folder routes, and the sandbox's own `tar`
|
|
1162
1545
|
* unpacks it as it arrives, making the folder. */
|
|
@@ -1217,22 +1600,39 @@ export class Files {
|
|
|
1217
1600
|
* into place only once every byte has arrived: a failed copy never leaves
|
|
1218
1601
|
* a short file under the name asked for. A short stream is tried again,
|
|
1219
1602
|
* twice, as a short `read` is. */
|
|
1220
|
-
async #streamTo(remotePath, localPath) {
|
|
1603
|
+
async #streamTo(remotePath, localPath, options = {}) {
|
|
1604
|
+
const scope = requestScope(options);
|
|
1221
1605
|
const fs = await import("node:fs/promises");
|
|
1222
1606
|
const partial = `${localPath}.runtime-partial-${crypto.randomUUID().slice(0, 8)}`;
|
|
1223
1607
|
for (let attempt = 0;; attempt++) {
|
|
1224
1608
|
try {
|
|
1225
|
-
|
|
1609
|
+
scope.check();
|
|
1610
|
+
const stream = await this.readStream(remotePath, scope.request);
|
|
1226
1611
|
// The handle is closed before anything else touches the file, so a
|
|
1227
1612
|
// failed copy's partial file is really gone when the error returns.
|
|
1228
|
-
const file = await fs.open(partial, "w");
|
|
1229
1613
|
try {
|
|
1230
|
-
|
|
1231
|
-
|
|
1614
|
+
const file = await fs.open(partial, "w");
|
|
1615
|
+
try {
|
|
1616
|
+
for await (const chunk of stream) {
|
|
1617
|
+
let written = 0;
|
|
1618
|
+
while (written < chunk.length) {
|
|
1619
|
+
scope.check();
|
|
1620
|
+
const result = await file.write(chunk.subarray(written));
|
|
1621
|
+
if (result.bytesWritten === 0)
|
|
1622
|
+
throw new Error("The local file accepted no bytes.");
|
|
1623
|
+
written += result.bytesWritten;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
finally {
|
|
1628
|
+
await file.close();
|
|
1629
|
+
}
|
|
1232
1630
|
}
|
|
1233
1631
|
finally {
|
|
1234
|
-
|
|
1632
|
+
// Opening or writing the local file can fail before consumption.
|
|
1633
|
+
await stream.cancel().catch(() => undefined);
|
|
1235
1634
|
}
|
|
1635
|
+
scope.check();
|
|
1236
1636
|
await fs.rename(partial, localPath);
|
|
1237
1637
|
return;
|
|
1238
1638
|
}
|
|
@@ -1248,10 +1648,12 @@ export class Files {
|
|
|
1248
1648
|
/** Copies a file or directory out of the sandbox. A file streams to disk,
|
|
1249
1649
|
* any size, checked against its length; a directory comes as one tar from
|
|
1250
1650
|
* the API's folder route. */
|
|
1251
|
-
async download(remotePath, localPath) {
|
|
1651
|
+
async download(remotePath, localPath, options = {}) {
|
|
1652
|
+
const scope = requestScope(options);
|
|
1653
|
+
scope.check();
|
|
1252
1654
|
const fs = await import("node:fs/promises");
|
|
1253
1655
|
const paths = await import("node:path");
|
|
1254
|
-
const entry = await this.stat(remotePath);
|
|
1656
|
+
const entry = await this.stat(remotePath, scope.request);
|
|
1255
1657
|
if (!entry.exists)
|
|
1256
1658
|
throw new RuntimeError({
|
|
1257
1659
|
message: `${remotePath} does not exist.`,
|
|
@@ -1260,7 +1662,7 @@ export class Files {
|
|
|
1260
1662
|
});
|
|
1261
1663
|
if (entry.type !== "directory") {
|
|
1262
1664
|
await fs.mkdir(paths.dirname(localPath), { recursive: true });
|
|
1263
|
-
await this.#streamTo(remotePath, localPath);
|
|
1665
|
+
await this.#streamTo(remotePath, localPath, scope.request);
|
|
1264
1666
|
return;
|
|
1265
1667
|
}
|
|
1266
1668
|
// The sandbox's own `tar` packs it as it streams, and it is unpacked as
|
|
@@ -1270,15 +1672,24 @@ export class Files {
|
|
|
1270
1672
|
path: this.#path("/files/archive"),
|
|
1271
1673
|
query: { path: remotePath, gzip: true },
|
|
1272
1674
|
accept: "application/gzip",
|
|
1675
|
+
...scope.request,
|
|
1273
1676
|
});
|
|
1274
1677
|
const { unpackStream } = await import("./tar.js");
|
|
1275
|
-
|
|
1678
|
+
try {
|
|
1679
|
+
await unpackStream(archive, localPath, {
|
|
1680
|
+
signal: scope.request.signal,
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
finally {
|
|
1684
|
+
await archive.cancel().catch(() => undefined);
|
|
1685
|
+
}
|
|
1276
1686
|
}
|
|
1277
1687
|
}
|
|
1278
|
-
export function sandboxPage(t, body, query) {
|
|
1688
|
+
export function sandboxPage(t, body, query, options = {}) {
|
|
1279
1689
|
return new Page(body.data.map((info) => new Sandbox(t, info)), body.nextCursor, async (cursor) => sandboxPage(t, await t.json({
|
|
1280
1690
|
method: "GET",
|
|
1281
1691
|
path: "/v1/sandboxes",
|
|
1282
1692
|
query: Object.assign({}, query, { cursor }),
|
|
1283
|
-
|
|
1693
|
+
...options,
|
|
1694
|
+
}), query, options));
|
|
1284
1695
|
}
|