withruntime 0.10.0 → 0.11.1
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 +7 -8
- package/DAYTONA.md +40 -35
- package/E2B.md +70 -57
- package/README.md +3 -3
- package/README.src.md +3 -3
- package/VERCEL.md +23 -20
- package/compatibility-lock.json +63 -84
- package/dist/api-defaults.d.ts +1 -1
- package/dist/api-defaults.js +1 -1
- package/dist/blaxel/lookup.d.ts +2 -1
- package/dist/blaxel/lookup.js +2 -1
- package/dist/blaxel/sandbox.d.ts +7 -5
- package/dist/blaxel/sandbox.js +15 -10
- package/dist/cli.js +112 -17
- package/dist/client.d.ts +17 -1
- package/dist/client.js +46 -3
- package/dist/compat/files.js +5 -1
- package/dist/compat/lease.d.ts +37 -0
- package/dist/compat/lease.js +93 -0
- package/dist/credentials.d.ts +9 -0
- package/dist/credentials.js +26 -4
- 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 +150 -26
- package/dist/daytona/filesystem.js +8 -0
- package/dist/daytona/index.d.ts +2 -26
- 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 +25 -6
- package/dist/daytona/sandbox.js +60 -27
- package/dist/e2b/client.d.ts +5 -0
- package/dist/e2b/client.js +5 -1
- package/dist/e2b/commands.d.ts +15 -6
- package/dist/e2b/commands.js +83 -18
- 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 +2 -2
- package/dist/e2b/index.js +1 -1
- package/dist/e2b/pty.js +6 -3
- package/dist/e2b/sandbox.d.ts +20 -6
- package/dist/e2b/sandbox.js +207 -31
- package/dist/e2b/users.d.ts +30 -0
- package/dist/e2b/users.js +185 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +6 -0
- package/dist/identity.js +2 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +6 -1
- package/dist/modal/index.js +6 -2
- package/dist/network-cli.js +33 -5
- package/dist/products/browser.d.ts +61 -0
- package/dist/products/browser.js +27 -0
- package/dist/products/desktop.d.ts +4 -3
- package/dist/products/desktop.js +7 -32
- package/dist/products/images.d.ts +3 -1
- package/dist/products/images.js +14 -2
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/jobs.d.ts +7 -5
- package/dist/products/jobs.js +7 -3
- package/dist/products/mcp.d.ts +5 -1
- package/dist/products/mcp.js +29 -8
- package/dist/products/network-products.d.ts +8 -1
- package/dist/products/network-products.js +4 -1
- package/dist/products/observability.d.ts +1 -1
- package/dist/products/private-network.d.ts +22 -0
- package/dist/products/private-network.js +10 -0
- package/dist/products/secrets.d.ts +2 -2
- package/dist/runloop/index.js +6 -2
- package/dist/sandbox.d.ts +44 -14
- package/dist/sandbox.js +162 -48
- package/dist/tar.d.ts +2 -0
- package/dist/tar.js +74 -16
- package/dist/transport.d.ts +33 -7
- package/dist/transport.js +171 -36
- package/dist/types.d.ts +59 -11
- package/dist/vercel/filesystem.js +3 -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 +57 -15
- package/dist/vercel/sandbox.js +219 -47
- package/dist/wait.d.ts +12 -0
- package/dist/wait.js +47 -0
- package/package.json +1 -1
package/dist/vercel/sandbox.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { extensionSeconds, LeaseKeeper, LEASE_MAX_SECONDS } from "../compat/lease.js";
|
|
1
2
|
import { RuntimeError } from "../errors.js";
|
|
2
3
|
import { clientFor, signalOf, } from "./client.js";
|
|
3
4
|
import { Command, CommandFinished, exitCodeOf } from "./command.js";
|
|
4
5
|
import { APIError, guard, NotSupportedError, translate } from "./errors.js";
|
|
5
6
|
import { FileSystem } from "./filesystem.js";
|
|
7
|
+
import { rootOrThrow, SandboxUser, SandboxUserAlreadyExistsError, validateName, } from "./sandbox-user.js";
|
|
6
8
|
import { Snapshot } from "./snapshot.js";
|
|
7
9
|
/* Vercel Sandbox's `Sandbox` over Runtime's. Every call is one or a few calls
|
|
8
10
|
of the withruntime SDK; nothing here speaks HTTP. What Runtime cannot do the
|
|
@@ -11,10 +13,13 @@ import { Snapshot } from "./snapshot.js";
|
|
|
11
13
|
* 2026). Runtime gives 2 vCPU and 4096 MiB. */
|
|
12
14
|
export const DEFAULT_VCPUS = 2;
|
|
13
15
|
export const MEMORY_MIB_PER_VCPU = 2048;
|
|
14
|
-
/** Vercel's default timeout, 5 minutes.
|
|
16
|
+
/** Vercel's default timeout, 5 minutes. Not sent: a sandbox created with no
|
|
17
|
+
* timeout has no time limit on Runtime, running while it works and pausing
|
|
18
|
+
* when idle (0300). Kept for code that imports it. */
|
|
15
19
|
export const DEFAULT_TIMEOUT_MS = 300_000;
|
|
16
20
|
const MIN_LEASE_SECONDS = 60;
|
|
17
|
-
|
|
21
|
+
/** Extend a long timeout's lease when less than this is left. */
|
|
22
|
+
const LEASE_MARGIN_MS = 600_000;
|
|
18
23
|
/** Commands without a timeout run until the sandbox ends: Runtime's longest. */
|
|
19
24
|
const LONGEST_MS = 86_400_000;
|
|
20
25
|
/** Vercel's working directory; `/workspace` on Runtime. */
|
|
@@ -27,13 +32,14 @@ const LEGACY_RUNTIMES = new Set(["node22", "node24", "node26", "python3.13"]);
|
|
|
27
32
|
/** Vercel regions in the United States, where Runtime runs. */
|
|
28
33
|
const US_REGIONS = new Set(["iad1", "sfo1", "cle1", "pdx1"]);
|
|
29
34
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
30
|
-
function
|
|
35
|
+
function checkTimeout(timeoutMs) {
|
|
31
36
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
32
37
|
throw new RangeError(`timeout must be a positive number of milliseconds, not ${timeoutMs}.`);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
return Math.max(timeoutMs, MIN_LEASE_SECONDS * 1000);
|
|
39
|
+
}
|
|
40
|
+
/** The first lease for a timeout: all of it, or the hour a lease may run. */
|
|
41
|
+
function leaseSeconds(timeoutMs) {
|
|
42
|
+
return Math.min(Math.ceil(checkTimeout(timeoutMs) / 1000), LEASE_MAX_SECONDS);
|
|
37
43
|
}
|
|
38
44
|
function refuseCreate(params) {
|
|
39
45
|
if (params.keepLastSnapshots !== undefined)
|
|
@@ -140,6 +146,12 @@ export class Sandbox {
|
|
|
140
146
|
#persistent;
|
|
141
147
|
#routes = new Map();
|
|
142
148
|
#onResume;
|
|
149
|
+
#keeper;
|
|
150
|
+
/** Epoch ms the current session started, as the timeout counts from it. */
|
|
151
|
+
#sessionStart;
|
|
152
|
+
/** Vercel's timeout, when this object was given one or extended it. */
|
|
153
|
+
#timeoutMs;
|
|
154
|
+
#defaultUser;
|
|
143
155
|
#linked;
|
|
144
156
|
#deleted = false;
|
|
145
157
|
/** Use Sandbox.create, Sandbox.get or Sandbox.getOrCreate. */
|
|
@@ -149,6 +161,17 @@ export class Sandbox {
|
|
|
149
161
|
this.#env = { ...resolved.params.env };
|
|
150
162
|
this.#persistent = resolved.params.persistent ?? resolved.runtime.info.onLeaseEnd !== "stop";
|
|
151
163
|
this.#onResume = resolved.params.onResume;
|
|
164
|
+
const timeout = resolved.params.timeout;
|
|
165
|
+
this.#timeoutMs = timeout === undefined ? undefined : checkTimeout(timeout);
|
|
166
|
+
this.#sessionStart =
|
|
167
|
+
timeout === undefined
|
|
168
|
+
? Date.parse(this.#rt.info.expiresAt) - this.#rt.info.timeoutSeconds * 1000
|
|
169
|
+
: Date.now();
|
|
170
|
+
this.#keeper = new LeaseKeeper({
|
|
171
|
+
sandbox: () => this.#rt,
|
|
172
|
+
until: () => this.#until(),
|
|
173
|
+
marginMs: () => Math.min(LEASE_MARGIN_MS, this.timeout / 2),
|
|
174
|
+
});
|
|
152
175
|
this.fs = new FileSystem({
|
|
153
176
|
name: this.name,
|
|
154
177
|
files: async () => (await this.#live()).files,
|
|
@@ -213,9 +236,15 @@ export class Sandbox {
|
|
|
213
236
|
get status() {
|
|
214
237
|
return statusOf(this.#rt.state);
|
|
215
238
|
}
|
|
216
|
-
/** Milliseconds
|
|
239
|
+
/** Milliseconds from the start of the session: the timeout given at
|
|
240
|
+
* create, with every extension, or the time limit of a sandbox found by
|
|
241
|
+
* name. 0 for one with no time limit, which runs while it works. */
|
|
217
242
|
get timeout() {
|
|
218
|
-
return this.#rt.info.timeoutSeconds * 1000;
|
|
243
|
+
return this.#timeoutMs ?? this.#rt.info.timeoutSeconds * 1000;
|
|
244
|
+
}
|
|
245
|
+
/** Epoch ms the session should end: its start plus the timeout. */
|
|
246
|
+
#until() {
|
|
247
|
+
return this.#sessionStart + this.timeout;
|
|
219
248
|
}
|
|
220
249
|
get expiresAt() {
|
|
221
250
|
return new Date(this.#rt.info.expiresAt);
|
|
@@ -229,7 +258,8 @@ export class Sandbox {
|
|
|
229
258
|
}
|
|
230
259
|
// ---- create, get, list ------------------------------------------------
|
|
231
260
|
/** Creates a sandbox and waits until it runs. Vercel's defaults: 2 vCPUs,
|
|
232
|
-
* 2048 MiB per vCPU,
|
|
261
|
+
* 2048 MiB per vCPU, persistent. With no `timeout` it has no time limit: it
|
|
262
|
+
* runs while it works and pauses when idle. A `timeout` is kept as given. Funding is left to
|
|
233
263
|
* Runtime: the free trial while the account has trial time, then prepaid
|
|
234
264
|
* credit, exactly as withruntime's own create. */
|
|
235
265
|
static async create(params = {}) {
|
|
@@ -242,7 +272,8 @@ export class Sandbox {
|
|
|
242
272
|
const source = await guard(() => resolveImage(client, params));
|
|
243
273
|
const input = {
|
|
244
274
|
...(source.snapshot ? {} : { vcpu: vcpus, memoryMiB: vcpus * MEMORY_MIB_PER_VCPU }),
|
|
245
|
-
|
|
275
|
+
// No timeout, no time limit: it runs while it works (0300).
|
|
276
|
+
...(params.timeout === undefined ? {} : { timeoutSeconds: leaseSeconds(params.timeout) }),
|
|
246
277
|
onLeaseEnd: persistent ? "pause" : "stop",
|
|
247
278
|
...(params.name ? { name: params.name } : {}),
|
|
248
279
|
...(params.tags && Object.keys(params.tags).length ? { labels: params.tags } : {}),
|
|
@@ -251,11 +282,18 @@ export class Sandbox {
|
|
|
251
282
|
...params.withruntime?.create,
|
|
252
283
|
};
|
|
253
284
|
const runtime = await guard(() => client.sandboxes.create(input, signalOf(params)));
|
|
254
|
-
const sandbox = new Sandbox({
|
|
285
|
+
const sandbox = new Sandbox({
|
|
286
|
+
runtime,
|
|
287
|
+
client,
|
|
288
|
+
params: { ...params, persistent },
|
|
289
|
+
});
|
|
255
290
|
try {
|
|
256
291
|
await sandbox.#setUp(params);
|
|
292
|
+
// Starts renewing at once when the timeout runs past the first lease.
|
|
293
|
+
await sandbox.#keeper.check();
|
|
257
294
|
}
|
|
258
295
|
catch (error) {
|
|
296
|
+
sandbox.#keeper.end();
|
|
259
297
|
await runtime.stop({ wait: false }).catch(() => undefined);
|
|
260
298
|
throw translate(error, sandbox.name);
|
|
261
299
|
}
|
|
@@ -367,13 +405,16 @@ export class Sandbox {
|
|
|
367
405
|
});
|
|
368
406
|
}
|
|
369
407
|
/** Running and stopped (paused) sandboxes, oldest first, with Vercel's
|
|
370
|
-
* paginator: `for await (const s of await Sandbox.list())`.
|
|
408
|
+
* paginator: `for await (const s of await Sandbox.list())`. A name prefix,
|
|
409
|
+
* creation-time bounds (`since`, `until`, epoch ms), `sortBy` and
|
|
410
|
+
* `sortOrder`, and a `cursor` from an earlier page are applied here, over
|
|
411
|
+
* every sandbox the key sees. */
|
|
371
412
|
static async list(params = {}) {
|
|
372
413
|
params.signal?.throwIfAborted();
|
|
373
|
-
for (const field of ["since", "until", "cursor", "sortBy", "sortOrder", "namePrefix"])
|
|
374
|
-
if (params[field] !== undefined)
|
|
375
|
-
throw new NotSupportedError(`Listing sandboxes by ${field}`, "List them all (oldest first) and filter the result yourself.");
|
|
376
414
|
const client = clientFor(params);
|
|
415
|
+
const local = ["namePrefix", "sortBy", "sortOrder", "cursor", "since", "until"].some((field) => params[field] !== undefined);
|
|
416
|
+
if (local)
|
|
417
|
+
return listHere(client, params);
|
|
377
418
|
const page = await guard(() => client.sandboxes.list({
|
|
378
419
|
...(params.tags ? { labels: params.tags } : {}),
|
|
379
420
|
...(params.limit ? { limit: Math.min(params.limit, 100) } : {}),
|
|
@@ -393,10 +434,15 @@ export class Sandbox {
|
|
|
393
434
|
const state = this.#rt.state;
|
|
394
435
|
if (state === "paused" || state === "pausing")
|
|
395
436
|
await this.#wake(opts);
|
|
437
|
+
await guard(() => this.#keeper.check(), this.name);
|
|
396
438
|
return this.#rt;
|
|
397
439
|
}
|
|
440
|
+
/** A woken sandbox starts a new session, whose timeout counts from now,
|
|
441
|
+
* as Vercel's resumed sessions do. */
|
|
398
442
|
async #wake(opts = {}) {
|
|
399
443
|
await guard(() => this.#rt.wake(signalOf(opts)), this.name);
|
|
444
|
+
if (this.#timeoutMs !== undefined)
|
|
445
|
+
this.#sessionStart = Date.now();
|
|
400
446
|
await this.#onResume?.(this);
|
|
401
447
|
}
|
|
402
448
|
/** Runs `work` on the live sandbox; if the lease paused it meanwhile, wakes
|
|
@@ -583,6 +629,8 @@ export class Sandbox {
|
|
|
583
629
|
else
|
|
584
630
|
await guard(() => this.#rt.stop(signalOf(opts)), this.name);
|
|
585
631
|
}
|
|
632
|
+
if (!this.#persistent)
|
|
633
|
+
this.#keeper.end();
|
|
586
634
|
return { name: this.name, status: this.status };
|
|
587
635
|
}
|
|
588
636
|
/** Ends the sandbox for good. */
|
|
@@ -593,18 +641,34 @@ export class Sandbox {
|
|
|
593
641
|
if (this.#rt.state !== "stopped")
|
|
594
642
|
await guard(() => this.#rt.stop({ wait: false, ...signalOf(opts) }), this.name);
|
|
595
643
|
this.#deleted = true;
|
|
644
|
+
this.#keeper.end();
|
|
596
645
|
}
|
|
597
646
|
async [Symbol.asyncDispose]() {
|
|
598
647
|
if (this.#deleted)
|
|
599
648
|
return;
|
|
600
649
|
await this.stop().catch(() => undefined);
|
|
601
650
|
}
|
|
602
|
-
/** Moves the end `duration` milliseconds later
|
|
603
|
-
*
|
|
651
|
+
/** Moves the end `duration` milliseconds later. The time limit moves at
|
|
652
|
+
* once as far as it may (an hour ahead of now); past that it is extended
|
|
653
|
+
* while this object lives. A sandbox with no time limit has no end to
|
|
654
|
+
* move, and nothing changes. */
|
|
604
655
|
async extendTimeout(duration, opts = {}) {
|
|
605
656
|
if (!Number.isFinite(duration) || duration <= 0)
|
|
606
657
|
throw new RangeError(`duration must be a positive number of milliseconds, not ${duration}.`);
|
|
607
|
-
await this.#withResume((runtime) =>
|
|
658
|
+
await this.#withResume(async (runtime) => {
|
|
659
|
+
if (runtime.info.endsAt === null)
|
|
660
|
+
return;
|
|
661
|
+
this.#timeoutMs = this.timeout + duration;
|
|
662
|
+
await this.#leaseToTimeout(runtime, opts);
|
|
663
|
+
}, opts);
|
|
664
|
+
}
|
|
665
|
+
/** Extends the lease now toward the timeout, an hour ahead at most. */
|
|
666
|
+
async #leaseToTimeout(runtime, opts) {
|
|
667
|
+
if (runtime.info.endsAt === null)
|
|
668
|
+
return;
|
|
669
|
+
const seconds = extensionSeconds(Date.parse(runtime.info.endsAt ?? runtime.info.expiresAt), this.#until(), Date.now(), Infinity);
|
|
670
|
+
if (seconds >= 1)
|
|
671
|
+
await runtime.extend(seconds, signalOf(opts));
|
|
608
672
|
}
|
|
609
673
|
/** Replaces the network rules; returns the policy given. */
|
|
610
674
|
async updateNetworkPolicy(networkPolicy, opts = {}) {
|
|
@@ -615,7 +679,8 @@ export class Sandbox {
|
|
|
615
679
|
}
|
|
616
680
|
/** Changes what Runtime can change on a sandbox: `timeout`, `ports`,
|
|
617
681
|
* `networkPolicy` and `snapshotExpiration`. Anything else is refused before
|
|
618
|
-
* anything changes.
|
|
682
|
+
* anything changes. A `timeout` on a sandbox with no time limit changes
|
|
683
|
+
* nothing: it runs while it works. */
|
|
619
684
|
async update(params, opts = {}) {
|
|
620
685
|
opts.signal?.throwIfAborted();
|
|
621
686
|
const allowed = new Set(["timeout", "ports", "networkPolicy", "snapshotExpiration"]);
|
|
@@ -629,12 +694,19 @@ export class Sandbox {
|
|
|
629
694
|
if (params.ports !== undefined)
|
|
630
695
|
validatePorts(params.ports);
|
|
631
696
|
const wanted = params.ports === undefined ? undefined : new Set(params.ports);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
697
|
+
// A sandbox with no time limit has none to change (0300).
|
|
698
|
+
if (params.timeout !== undefined && this.#rt.info.endsAt !== null) {
|
|
699
|
+
const timeout = checkTimeout(params.timeout);
|
|
700
|
+
// A shorter timeout is kept when the lease has not reached it yet: the
|
|
701
|
+
// lease then stops being extended there. Runtime cannot end a lease
|
|
702
|
+
// earlier than it was given.
|
|
703
|
+
const end = this.#rt.info.endsAt ?? this.#rt.info.expiresAt;
|
|
704
|
+
if (this.#sessionStart + timeout < Date.parse(end) - 1000)
|
|
705
|
+
throw new NotSupportedError("A timeout that ends earlier than the current lease", `The lease runs to ${end}; call stop() when the work is done, or pass a timeout that ends after it.`);
|
|
706
|
+
const longer = timeout > this.timeout;
|
|
707
|
+
this.#timeoutMs = timeout;
|
|
708
|
+
if (longer)
|
|
709
|
+
await this.#withResume((runtime) => this.#leaseToTimeout(runtime, opts), opts);
|
|
638
710
|
}
|
|
639
711
|
if (rules !== undefined)
|
|
640
712
|
await this.#withResume((runtime) => runtime.network.set(rules, signalOf(opts)), opts);
|
|
@@ -700,26 +772,68 @@ export class Sandbox {
|
|
|
700
772
|
return new Snapshot(taken, this.#client);
|
|
701
773
|
}
|
|
702
774
|
// ---- what Runtime does differently -----------------------------------
|
|
775
|
+
/** Refused: Vercel hands back a token scoped to this one shell, and
|
|
776
|
+
* Runtime's terminal opens with the account key, which must not leave the
|
|
777
|
+
* server. */
|
|
703
778
|
openInteractive() {
|
|
704
779
|
return Promise.reject(new NotSupportedError("Vercel's interactive shell (openInteractive)", "Use `await sandbox.withruntime.terminal({ cols, rows, onData })`, or `npx withruntime sandbox shell <id>`."));
|
|
705
780
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
781
|
+
/** The user commands run as, `runtime`, and its group. */
|
|
782
|
+
getDefaultUser(_opts = {}) {
|
|
783
|
+
this.#defaultUser ??= Promise.resolve({ username: "runtime", group: "runtime" });
|
|
784
|
+
return this.#defaultUser;
|
|
785
|
+
}
|
|
786
|
+
/** Makes a Linux user with its own home, as Vercel does: `useradd -m`,
|
|
787
|
+
* then the home given to the user and the sandbox's group, mode 770. */
|
|
788
|
+
async createUser(username, opts = {}) {
|
|
789
|
+
validateName(username, "username");
|
|
790
|
+
const { group } = await this.getDefaultUser(opts);
|
|
791
|
+
const signal = opts.signal;
|
|
792
|
+
const useradd = await this.runCommand({
|
|
793
|
+
cmd: "useradd",
|
|
794
|
+
args: ["-m", "-s", "/bin/bash", username],
|
|
795
|
+
sudo: true,
|
|
796
|
+
...(signal ? { signal } : {}),
|
|
797
|
+
});
|
|
798
|
+
if (useradd.exitCode !== 0) {
|
|
799
|
+
if (useradd.exitCode === 9)
|
|
800
|
+
throw new SandboxUserAlreadyExistsError(username);
|
|
801
|
+
throw new Error(`Failed to create user "${username}": ${await useradd.stderr()}`);
|
|
802
|
+
}
|
|
803
|
+
const home = `/home/${username}`;
|
|
804
|
+
await rootOrThrow(this, "chown", [`${username}:${group}`, home], signal, () => `Failed to set ownership on ${home}`);
|
|
805
|
+
await rootOrThrow(this, "chmod", ["770", home], signal, () => `Failed to set permissions on ${home}`);
|
|
806
|
+
return new SandboxUser({ sandbox: this, username });
|
|
807
|
+
}
|
|
808
|
+
/** A handle for an existing user, such as `root`. */
|
|
809
|
+
asUser(username) {
|
|
810
|
+
validateName(username, "username");
|
|
811
|
+
return new SandboxUser({ sandbox: this, username });
|
|
812
|
+
}
|
|
813
|
+
/** Makes a group and its shared directory, `/shared/<group>`, setgid 2770. */
|
|
814
|
+
async createGroup(groupname, opts = {}) {
|
|
815
|
+
validateName(groupname, "group name");
|
|
816
|
+
const { username } = await this.getDefaultUser(opts);
|
|
817
|
+
const sharedDir = `/shared/${groupname}`;
|
|
818
|
+
const steps = [
|
|
819
|
+
["groupadd", [groupname], `Failed to create group "${groupname}"`],
|
|
820
|
+
["mkdir", ["-p", sharedDir], `Failed to create shared directory ${sharedDir}`],
|
|
821
|
+
["chown", [`${username}:${groupname}`, sharedDir], `Failed to set ownership on ${sharedDir}`],
|
|
822
|
+
["chmod", ["2770", sharedDir], `Failed to set permissions on ${sharedDir}`],
|
|
823
|
+
];
|
|
824
|
+
for (const [cmd, args, what] of steps)
|
|
825
|
+
await rootOrThrow(this, cmd, args, opts.signal, () => what);
|
|
826
|
+
return { groupname, sharedDir };
|
|
717
827
|
}
|
|
718
|
-
addUserToGroup() {
|
|
719
|
-
|
|
828
|
+
async addUserToGroup(username, groupname, opts = {}) {
|
|
829
|
+
validateName(username, "username");
|
|
830
|
+
validateName(groupname, "group name");
|
|
831
|
+
await rootOrThrow(this, "usermod", ["-aG", groupname, username], opts.signal, () => `Failed to add "${username}" to group "${groupname}"`);
|
|
720
832
|
}
|
|
721
|
-
removeUserFromGroup() {
|
|
722
|
-
|
|
833
|
+
async removeUserFromGroup(username, groupname, opts = {}) {
|
|
834
|
+
validateName(username, "username");
|
|
835
|
+
validateName(groupname, "group name");
|
|
836
|
+
await rootOrThrow(this, "gpasswd", ["-d", username, groupname], opts.signal, () => `Failed to remove "${username}" from group "${groupname}"`);
|
|
723
837
|
}
|
|
724
838
|
currentSession() {
|
|
725
839
|
throw new NotSupportedError("Vercel sessions (currentSession)", "A Runtime sandbox is its own session: call the methods on the sandbox.");
|
|
@@ -731,12 +845,6 @@ export class Sandbox {
|
|
|
731
845
|
return Promise.reject(new NotSupportedError("Listing a sandbox's snapshots from the sandbox", "Use Snapshot.list({ name }) or runtime.snapshots.list({ sandboxId })."));
|
|
732
846
|
}
|
|
733
847
|
}
|
|
734
|
-
function usersError() {
|
|
735
|
-
return new NotSupportedError("Extra Linux users and groups (createUser, asUser, groups)", "Commands run as the sandbox owner with passwordless sudo; run `sudo useradd ...` and `sudo -u <user> ...` with runCommand.");
|
|
736
|
-
}
|
|
737
|
-
function usersRefused() {
|
|
738
|
-
return Promise.reject(usersError());
|
|
739
|
-
}
|
|
740
848
|
/** Milliseconds as whole days for Runtime's retention: 1 to 365, and 0 (no
|
|
741
849
|
* expiration) as 365. */
|
|
742
850
|
export function retentionDays(ms) {
|
|
@@ -791,6 +899,70 @@ function itemOf(runtime) {
|
|
|
791
899
|
tags: { ...info.labels },
|
|
792
900
|
};
|
|
793
901
|
}
|
|
902
|
+
const CURSOR = /^rt\.([0-9]+)$/;
|
|
903
|
+
/** Sandbox.list with a name prefix, time bounds, sorting or a cursor:
|
|
904
|
+
* applied here over every sandbox the key sees, since Runtime's list filters
|
|
905
|
+
* only by labels and name. The cursor is this adapter's own, an offset into
|
|
906
|
+
* the same query. */
|
|
907
|
+
async function listHere(client, params) {
|
|
908
|
+
let offset = 0;
|
|
909
|
+
if (params.cursor !== undefined) {
|
|
910
|
+
const match = CURSOR.exec(params.cursor);
|
|
911
|
+
if (!match)
|
|
912
|
+
throw new RangeError(`The cursor "${params.cursor}" was not made by Sandbox.list here; pass pagination.next from an earlier page of the same query.`);
|
|
913
|
+
offset = Number(match[1]);
|
|
914
|
+
}
|
|
915
|
+
if (params.sortBy !== undefined &&
|
|
916
|
+
!["createdAt", "name", "statusUpdatedAt"].includes(params.sortBy))
|
|
917
|
+
throw new RangeError("sortBy must be createdAt, name or statusUpdatedAt.");
|
|
918
|
+
if (params.sortOrder !== undefined && !["asc", "desc"].includes(params.sortOrder))
|
|
919
|
+
throw new RangeError("sortOrder must be asc or desc.");
|
|
920
|
+
const page = await guard(() => client.sandboxes.list(params.tags ? { labels: params.tags } : {}, signalOf(params)));
|
|
921
|
+
const all = (await guard(() => page.toArray(10_000))).map(itemOf);
|
|
922
|
+
params.signal?.throwIfAborted();
|
|
923
|
+
const time = (value) => value === undefined ? undefined : value instanceof Date ? value.getTime() : value;
|
|
924
|
+
const since = time(params.since);
|
|
925
|
+
const until = time(params.until);
|
|
926
|
+
const items = all.filter((one) => (params.namePrefix === undefined || one.name.startsWith(params.namePrefix)) &&
|
|
927
|
+
(since === undefined || one.createdAt >= since) &&
|
|
928
|
+
(until === undefined || one.createdAt <= until));
|
|
929
|
+
const by = params.sortBy ?? "createdAt";
|
|
930
|
+
const key = (one) => by === "name" ? one.name : by === "statusUpdatedAt" ? one.updatedAt : one.createdAt;
|
|
931
|
+
const direction = params.sortOrder === "desc" ? -1 : 1;
|
|
932
|
+
items.sort((a, b) => {
|
|
933
|
+
const [x, y] = [key(a), key(b)];
|
|
934
|
+
return (x < y ? -1 : x > y ? 1 : 0) * direction;
|
|
935
|
+
});
|
|
936
|
+
const size = Math.max(1, Math.min(params.limit ?? 50, 100));
|
|
937
|
+
const pageAt = (from) => {
|
|
938
|
+
const sandboxes = items.slice(from, from + size);
|
|
939
|
+
return {
|
|
940
|
+
sandboxes,
|
|
941
|
+
pagination: {
|
|
942
|
+
count: sandboxes.length,
|
|
943
|
+
next: from + size < items.length ? `rt.${from + size}` : null,
|
|
944
|
+
},
|
|
945
|
+
};
|
|
946
|
+
};
|
|
947
|
+
return Object.assign(pageAt(offset), {
|
|
948
|
+
async *pages() {
|
|
949
|
+
for (let from = offset; from === offset || from < items.length; from += size) {
|
|
950
|
+
params.signal?.throwIfAborted();
|
|
951
|
+
yield pageAt(from);
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
async *[Symbol.asyncIterator]() {
|
|
955
|
+
for (const item of items.slice(offset)) {
|
|
956
|
+
params.signal?.throwIfAborted();
|
|
957
|
+
yield item;
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
async toArray() {
|
|
961
|
+
params.signal?.throwIfAborted();
|
|
962
|
+
return items.slice(offset);
|
|
963
|
+
},
|
|
964
|
+
});
|
|
965
|
+
}
|
|
794
966
|
/** Vercel's paginator over a Runtime page: the first page's fields, async
|
|
795
967
|
* iteration over every item, `pages()` and `toArray()`. */
|
|
796
968
|
function paginate(first, signal) {
|
package/dist/wait.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RequestOptions } from "./transport.js";
|
|
2
|
+
/** A wait that an abort ends at once, rejecting with the signal's reason. */
|
|
3
|
+
export declare function pause(ms: number, signal?: AbortSignal): Promise<void>;
|
|
4
|
+
export declare function abortError(signal: AbortSignal): Error;
|
|
5
|
+
/**
|
|
6
|
+
* Calls `attempt` until it stops answering one of `codes` (something is
|
|
7
|
+
* still installing), within one budget: the caller's `timeoutMs`, else ten
|
|
8
|
+
* minutes. Each try gets what is left of it; a wait between tries ends at once
|
|
9
|
+
* on abort, and no try starts once the budget cannot cover the wait the API
|
|
10
|
+
* asked for: the installing error is thrown instead.
|
|
11
|
+
*/
|
|
12
|
+
export declare function whileInstalling<T>(codes: readonly string[], options: RequestOptions | undefined, attempt: (options: RequestOptions) => Promise<T>): Promise<T>;
|
package/dist/wait.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** A wait that an abort ends at once, rejecting with the signal's reason. */
|
|
2
|
+
export function pause(ms, signal) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
if (signal?.aborted)
|
|
5
|
+
return reject(abortError(signal));
|
|
6
|
+
const timer = setTimeout(() => {
|
|
7
|
+
signal?.removeEventListener("abort", stop);
|
|
8
|
+
resolve();
|
|
9
|
+
}, ms);
|
|
10
|
+
function stop() {
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
reject(abortError(signal));
|
|
13
|
+
}
|
|
14
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function abortError(signal) {
|
|
18
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Aborted");
|
|
19
|
+
}
|
|
20
|
+
/** How long a start waits for a first install (Chromium, the desktop) when
|
|
21
|
+
* the caller sets no deadline. */
|
|
22
|
+
const INSTALL_WAIT_MS = 600_000;
|
|
23
|
+
/**
|
|
24
|
+
* Calls `attempt` until it stops answering one of `codes` (something is
|
|
25
|
+
* still installing), within one budget: the caller's `timeoutMs`, else ten
|
|
26
|
+
* minutes. Each try gets what is left of it; a wait between tries ends at once
|
|
27
|
+
* on abort, and no try starts once the budget cannot cover the wait the API
|
|
28
|
+
* asked for: the installing error is thrown instead.
|
|
29
|
+
*/
|
|
30
|
+
export async function whileInstalling(codes, options, attempt) {
|
|
31
|
+
const deadline = performance.now() + (options?.timeoutMs || INSTALL_WAIT_MS);
|
|
32
|
+
for (;;) {
|
|
33
|
+
if (options?.signal?.aborted)
|
|
34
|
+
throw abortError(options.signal);
|
|
35
|
+
const left = Math.max(1, Math.ceil(deadline - performance.now()));
|
|
36
|
+
try {
|
|
37
|
+
return await attempt(options?.timeoutMs ? { ...options, timeoutMs: left } : { ...options });
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const code = error.code;
|
|
41
|
+
const wait = error.retryAfterMs ?? 10_000;
|
|
42
|
+
if (!code || !codes.includes(code) || performance.now() + wait > deadline)
|
|
43
|
+
throw error;
|
|
44
|
+
await pause(wait, options?.signal);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|