withruntime 0.12.0 → 0.16.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 +97 -32
- package/COMPATIBILITY.md +120 -5
- package/DAYTONA.md +34 -3
- package/E2B.md +34 -5
- package/README.md +36 -3
- package/README.src.md +35 -2
- package/VERCEL.md +52 -5
- package/compatibility-lock.json +71 -111
- package/dist/billing-cli.js +8 -4
- package/dist/blaxel/client.d.ts +11 -0
- package/dist/blaxel/client.js +28 -0
- package/dist/blaxel/errors.d.ts +65 -0
- package/dist/blaxel/errors.js +114 -0
- package/dist/blaxel/index.d.ts +10 -3
- package/dist/blaxel/index.js +23 -4
- package/dist/blaxel/preview.d.ts +29 -4
- package/dist/blaxel/preview.js +79 -5
- package/dist/blaxel/sandbox.d.ts +1 -1
- package/dist/blaxel/sandbox.js +2 -1
- package/dist/blaxel/session.js +4 -2
- package/dist/cli-extras.js +2 -1
- package/dist/cli-words.js +1 -0
- package/dist/cli.d.ts +17 -2
- package/dist/cli.js +291 -85
- package/dist/client.d.ts +1 -1
- package/dist/credentials.d.ts +3 -0
- package/dist/credentials.js +15 -0
- package/dist/daytona/client.d.ts +4 -0
- package/dist/daytona/client.js +8 -3
- package/dist/daytona/daytona.d.ts +4 -1
- package/dist/daytona/daytona.js +3 -2
- package/dist/e2b/sandbox.d.ts +7 -6
- package/dist/e2b/sandbox.js +9 -12
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/freestyle/index.d.ts +77 -5
- package/dist/freestyle/index.js +380 -48
- package/dist/identity.js +36 -18
- package/dist/index.d.ts +4 -3
- package/dist/machine.d.ts +39 -13
- package/dist/machine.js +72 -25
- package/dist/mcp.js +157 -20
- package/dist/modal/index.d.ts +15 -0
- package/dist/modal/index.js +35 -7
- package/dist/morph/index.d.ts +255 -0
- package/dist/morph/index.js +641 -0
- package/dist/network-cli.js +2 -2
- package/dist/product-names.d.ts +2 -2
- package/dist/product-names.js +2 -2
- package/dist/products/api-keys.d.ts +73 -0
- package/dist/products/api-keys.js +23 -0
- package/dist/products/apps.d.ts +12 -31
- package/dist/products/apps.js +8 -113
- package/dist/products/balancers.d.ts +87 -0
- package/dist/products/balancers.js +8 -0
- package/dist/products/builds.d.ts +30 -0
- package/dist/products/builds.js +6 -0
- package/dist/products/index.d.ts +2 -0
- package/dist/products/index.js +2 -0
- package/dist/products/jobs.d.ts +2 -2
- package/dist/products/jobs.js +1 -1
- package/dist/products/limits.d.ts +16 -2
- package/dist/products/limits.js +2 -2
- package/dist/products/mcp.d.ts +5 -0
- package/dist/products/network-products.d.ts +21 -3
- package/dist/products/network-products.js +3 -1
- package/dist/products/observability.d.ts +7 -2
- package/dist/products/previews.d.ts +6 -3
- package/dist/products/previews.js +1 -1
- package/dist/products/resources.d.ts +54 -0
- package/dist/products/resources.js +122 -0
- package/dist/products/secrets.d.ts +22 -0
- package/dist/products/secrets.js +8 -0
- package/dist/products/volumes.d.ts +3 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/sandbox.js +27 -2
- package/dist/snapshots.d.ts +4 -0
- package/dist/ssh.d.ts +35 -0
- package/dist/ssh.js +93 -1
- package/dist/transport.d.ts +14 -8
- package/dist/transport.js +70 -15
- package/dist/types.d.ts +33 -5
- package/dist/vercel/command.d.ts +48 -1
- package/dist/vercel/command.js +237 -6
- package/dist/vercel/sandbox-user.js +2 -1
- package/dist/vercel/sandbox.d.ts +17 -3
- package/dist/vercel/sandbox.js +46 -3
- package/dist/wallet.d.ts +4 -1
- package/dist/wallet.js +2 -1
- package/package.json +7 -14
- package/dist/cli/logs.test.d.ts +0 -1
- package/dist/cli/logs.test.js +0 -272
- package/dist/products/logs.test.d.ts +0 -1
- package/dist/products/logs.test.js +0 -343
|
@@ -8,7 +8,10 @@ export type DnsRecord = {
|
|
|
8
8
|
export type Domain = {
|
|
9
9
|
id: string;
|
|
10
10
|
hostname: string;
|
|
11
|
-
|
|
11
|
+
/** The sandbox it serves; null for an App's. */
|
|
12
|
+
sandboxId: string | null;
|
|
13
|
+
/** The App it serves, through every deploy and rollback; null for a sandbox's. */
|
|
14
|
+
appId?: string | null;
|
|
12
15
|
port: number;
|
|
13
16
|
/** pending until the TXT record proves the name is yours, then active. */
|
|
14
17
|
state: "pending" | "active" | "released";
|
|
@@ -20,6 +23,13 @@ export type Domain = {
|
|
|
20
23
|
verifiedAt: string | null;
|
|
21
24
|
checkedAt: string | null;
|
|
22
25
|
checkError: string | null;
|
|
26
|
+
/** An App's domain's certificate, issued before visitors arrive: point the
|
|
27
|
+
* name at the App once `ready` is true. Null for a sandbox's. */
|
|
28
|
+
certificate?: {
|
|
29
|
+
ready: boolean;
|
|
30
|
+
expiresAt: string | null;
|
|
31
|
+
issuers: string[];
|
|
32
|
+
} | null;
|
|
23
33
|
createdAt: string;
|
|
24
34
|
};
|
|
25
35
|
export type CheckedDomain = Domain & {
|
|
@@ -97,12 +107,20 @@ export type TunnelPeerCreated = {
|
|
|
97
107
|
/** `runtime.domains`: serve a sandbox's port at your own hostname, with HTTPS. */
|
|
98
108
|
export declare function domains(t: Transport): {
|
|
99
109
|
/** Claims the hostname and answers the DNS records to set. Calling it again
|
|
100
|
-
* with another sandbox or port points the name there.
|
|
110
|
+
* with another sandbox or port points the name there. With `appId`, an App
|
|
111
|
+
* serves it: add the `_acme-challenge` CNAME too, and point the name at
|
|
112
|
+
* the App once `certificate.ready` is true. */
|
|
101
113
|
add: (input: {
|
|
102
114
|
hostname: string;
|
|
115
|
+
} & ({
|
|
103
116
|
sandboxId: string;
|
|
104
117
|
port: number;
|
|
105
|
-
|
|
118
|
+
appId?: never;
|
|
119
|
+
} | {
|
|
120
|
+
appId: string;
|
|
121
|
+
sandboxId?: never;
|
|
122
|
+
port?: never;
|
|
123
|
+
}), options?: RequestOptions) => Promise<CheckedDomain>;
|
|
106
124
|
/** Checks the TXT record now; the name goes live when it matches. */
|
|
107
125
|
verify: (hostname: string, options?: RequestOptions) => Promise<CheckedDomain>;
|
|
108
126
|
get: (hostname: string, options?: RequestOptions) => Promise<Domain>;
|
|
@@ -5,7 +5,9 @@ const enc = encodeURIComponent;
|
|
|
5
5
|
export function domains(t) {
|
|
6
6
|
return {
|
|
7
7
|
/** Claims the hostname and answers the DNS records to set. Calling it again
|
|
8
|
-
* with another sandbox or port points the name there.
|
|
8
|
+
* with another sandbox or port points the name there. With `appId`, an App
|
|
9
|
+
* serves it: add the `_acme-challenge` CNAME too, and point the name at
|
|
10
|
+
* the App once `certificate.ready` is true. */
|
|
9
11
|
add: (input, options) => t.json({ method: "POST", path: "/v1/domains", body: input, ...options }),
|
|
10
12
|
/** Checks the TXT record now; the name goes live when it matches. */
|
|
11
13
|
verify: (hostname, options) => t.json({
|
|
@@ -35,7 +35,7 @@ export type SandboxMetrics = {
|
|
|
35
35
|
latest: MetricPoint | null;
|
|
36
36
|
points: MetricPoint[];
|
|
37
37
|
};
|
|
38
|
-
export type WebhookEventType = "sandbox.created" | "sandbox.running" | "sandbox.paused" | "sandbox.woken" | "sandbox.stopped" | "sandbox.start_failed" | "sandbox.wake_failed" | "sandbox.host_unreachable" | "sandbox.host_reachable" | "sandbox.recovered" | "sandbox.recovery_failed" | "sandbox.files.changed" | "sandbox.out_of_memory" | "sandbox.secret_not_applied" | "snapshot.ready" | "snapshot.failed" | "snapshot.deleted" | "volume.ready" | "volume.failed" | "volume.deleted" | "egress_secret.created" | "egress_secret.updated" | "egress_secret.deleted" | "upstream_proxy.set" | "upstream_proxy.removed";
|
|
38
|
+
export type WebhookEventType = "sandbox.created" | "sandbox.running" | "sandbox.paused" | "sandbox.woken" | "sandbox.stopped" | "sandbox.start_failed" | "sandbox.wake_failed" | "sandbox.host_unreachable" | "sandbox.host_reachable" | "sandbox.recovered" | "sandbox.recovery_failed" | "sandbox.files.changed" | "sandbox.out_of_memory" | "sandbox.secret_not_applied" | "snapshot.ready" | "snapshot.failed" | "snapshot.deleted" | "volume.ready" | "volume.failed" | "volume.deleted" | "volume.recovered" | "egress_secret.created" | "egress_secret.updated" | "egress_secret.deleted" | "upstream_proxy.set" | "upstream_proxy.removed";
|
|
39
39
|
/** An event as /v1/events lists it and a webhook delivers it. */
|
|
40
40
|
export type RuntimeEvent = {
|
|
41
41
|
id: string;
|
|
@@ -113,7 +113,7 @@ export type UsageExportRow = {
|
|
|
113
113
|
name: string | null;
|
|
114
114
|
kind: string;
|
|
115
115
|
meter: string;
|
|
116
|
-
/** credit, the trial's hours, or the month's
|
|
116
|
+
/** credit, the trial's hours, or the month's free usage. */
|
|
117
117
|
fundedBy: "credit" | "trial" | "allowance";
|
|
118
118
|
startsAt: string;
|
|
119
119
|
endsAt: string;
|
|
@@ -128,12 +128,17 @@ export type UsageExportRow = {
|
|
|
128
128
|
/** Exact JSON text, preserving integers beyond JavaScript's number range. */
|
|
129
129
|
measurementsJson: string | null;
|
|
130
130
|
evidence: "measured" | "partial" | "missing" | null;
|
|
131
|
+
/** The resource is deleted and every charge it will ever have settled
|
|
132
|
+
* before finalThrough. A volume's backups bill on until retention ends. */
|
|
133
|
+
final: boolean;
|
|
131
134
|
};
|
|
132
135
|
export type UsageExportPage = {
|
|
133
136
|
since: string;
|
|
134
137
|
until: string;
|
|
135
138
|
data: UsageExportRow[];
|
|
136
139
|
nextCursor: string | null;
|
|
140
|
+
/** No row with settledAt before this can still appear or change. */
|
|
141
|
+
finalThrough: string | null;
|
|
137
142
|
/** Concatenate pages in order; only the first page includes the header. */
|
|
138
143
|
csv: string;
|
|
139
144
|
};
|
|
@@ -9,9 +9,12 @@ export type Preview = {
|
|
|
9
9
|
/** `https://<port>-<sandbox>.<preview domain>/`. A private preview also
|
|
10
10
|
* needs the token. */
|
|
11
11
|
url: string;
|
|
12
|
-
/** Private previews: a
|
|
13
|
-
* `x-runtime-preview-token` header.
|
|
12
|
+
/** Private previews: a token signed for this answer. Send it as the
|
|
13
|
+
* `x-runtime-preview-token` header. Every create and read signs its own;
|
|
14
|
+
* one signed before keeps working until its own expiry or a rotate. */
|
|
14
15
|
token: string | null;
|
|
16
|
+
/** When this answer's token stops working. Not stored on the preview: a
|
|
17
|
+
* later read signs another token and leaves this one as it is. */
|
|
15
18
|
tokenExpiresAt: string | null;
|
|
16
19
|
/** Private previews: a link that carries the token once, for a browser. */
|
|
17
20
|
urlWithToken: string | null;
|
|
@@ -48,7 +51,7 @@ export declare function sandboxPreviews(t: Transport, sandbox: Sandbox): {
|
|
|
48
51
|
/** Shares `port`, or changes its visibility and embedOrigins if it is
|
|
49
52
|
* shared already. */
|
|
50
53
|
create: (port: number, input?: CreatePreview, options?: RequestOptions) => Promise<Preview>;
|
|
51
|
-
/** Every shared port, each private one with a fresh token. */
|
|
54
|
+
/** Every shared port, each private one with a fresh token that lasts a day; tokens signed before keep their own expiry. */
|
|
52
55
|
list: (options?: RequestOptions) => Promise<Preview[]>;
|
|
53
56
|
/** One preview, with a fresh private token. `expiresAt` bounds its expiry
|
|
54
57
|
* to an absolute ISO deadline; with no TTL, that deadline is used exactly. */
|
|
@@ -10,7 +10,7 @@ export function sandboxPreviews(t, sandbox) {
|
|
|
10
10
|
/** Shares `port`, or changes its visibility and embedOrigins if it is
|
|
11
11
|
* shared already. */
|
|
12
12
|
create: (port, input = {}, options) => t.json({ method: "POST", path: base(), body: { port, ...input }, ...options }),
|
|
13
|
-
/** Every shared port, each private one with a fresh token. */
|
|
13
|
+
/** Every shared port, each private one with a fresh token that lasts a day; tokens signed before keep their own expiry. */
|
|
14
14
|
list: async (options) => (await t.json({ method: "GET", path: base(), ...options })).data,
|
|
15
15
|
/** One preview, with a fresh private token. `expiresAt` bounds its expiry
|
|
16
16
|
* to an absolute ISO deadline; with no TTL, that deadline is used exactly. */
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Page } from "../page.js";
|
|
2
|
+
import type { RequestOptions, Transport } from "../transport.js";
|
|
3
|
+
/** The five verbs every resource product shares (CLOUD.md section 4, "One
|
|
4
|
+
* shape for every interface"; packages/cloud/src/api/resource-product.ts),
|
|
5
|
+
* and following one to a state. A product builds its client on these and
|
|
6
|
+
* adds only its own verbs. */
|
|
7
|
+
export type ResourceRecord = {
|
|
8
|
+
id: string;
|
|
9
|
+
state: string;
|
|
10
|
+
error: {
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
} | null;
|
|
14
|
+
};
|
|
15
|
+
export type ResourceListQuery<S extends string = string> = {
|
|
16
|
+
name?: string;
|
|
17
|
+
/** Repeated key:value filters; all must match. */
|
|
18
|
+
label?: readonly string[];
|
|
19
|
+
/** Defaults to the product's live states. */
|
|
20
|
+
state?: readonly S[];
|
|
21
|
+
/** 1 to 100; defaults to 50. */
|
|
22
|
+
limit?: number;
|
|
23
|
+
cursor?: string;
|
|
24
|
+
};
|
|
25
|
+
export type ResourceGetOptions<S extends string = string> = RequestOptions & {
|
|
26
|
+
waitFor?: S;
|
|
27
|
+
/** Server wait, 1 to 60 seconds; defaults to 30. Check the returned state. */
|
|
28
|
+
timeoutSeconds?: number;
|
|
29
|
+
};
|
|
30
|
+
export type ResourceWaitOptions = RequestOptions & {
|
|
31
|
+
/** Whole wait, including retries; defaults to 300000 ms. 0 disables it. */
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
/** Delay between reads; defaults to 250 ms. */
|
|
34
|
+
pollMs?: number;
|
|
35
|
+
};
|
|
36
|
+
export type ResourceDeleteOptions = ResourceWaitOptions & {
|
|
37
|
+
wait?: boolean;
|
|
38
|
+
};
|
|
39
|
+
export declare const enc: typeof encodeURIComponent;
|
|
40
|
+
/** One deadline covers the write, retries and every read. A timed-out delete
|
|
41
|
+
* can be retried after a process restart with this same key. */
|
|
42
|
+
export declare function following<T>(options: ResourceWaitOptions, work: (request: RequestOptions, pause: () => Promise<void>) => Promise<T>): Promise<T>;
|
|
43
|
+
/** A resource product's client at `/v1/<path>`. A wait ends at its target
|
|
44
|
+
* or at any of `settled`, which answers with the resource's error. */
|
|
45
|
+
export declare function resource<R extends ResourceRecord, C, U, S extends string = R["state"]>(t: Transport, path: string, settled: readonly S[]): {
|
|
46
|
+
base: string;
|
|
47
|
+
/** Defaults belong to the API, so the SDK does not override them. */
|
|
48
|
+
create: (input: C, options?: RequestOptions) => Promise<R>;
|
|
49
|
+
get: (id: string, options?: ResourceGetOptions<S>) => Promise<R>;
|
|
50
|
+
list: (query?: ResourceListQuery<S>, options?: RequestOptions) => Promise<Page<R>>;
|
|
51
|
+
update: (id: string, input: U, options?: RequestOptions) => Promise<R>;
|
|
52
|
+
wait: (id: string, target: S, options?: ResourceWaitOptions) => Promise<R>;
|
|
53
|
+
delete: (id: string, options?: ResourceDeleteOptions) => Promise<R>;
|
|
54
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { RuntimeError } from "../errors.js";
|
|
2
|
+
import { Page } from "../page.js";
|
|
3
|
+
import { pause } from "../wait.js";
|
|
4
|
+
export const enc = encodeURIComponent;
|
|
5
|
+
function invalid(field) {
|
|
6
|
+
return new RuntimeError({
|
|
7
|
+
code: "invalid_request",
|
|
8
|
+
status: 0,
|
|
9
|
+
message: `${field} must be a finite, nonnegative number.`,
|
|
10
|
+
hint: "Fix the option and call again; nothing was sent.",
|
|
11
|
+
details: { field },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/** One deadline covers the write, retries and every read. A timed-out delete
|
|
15
|
+
* can be retried after a process restart with this same key. */
|
|
16
|
+
export async function following(options, work) {
|
|
17
|
+
const { pollMs = 250, timeoutMs = 300_000, signal, ...rest } = options;
|
|
18
|
+
if (!Number.isFinite(pollMs) || pollMs < 0)
|
|
19
|
+
throw invalid("pollMs");
|
|
20
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0)
|
|
21
|
+
throw invalid("timeoutMs");
|
|
22
|
+
const deadline = new AbortController();
|
|
23
|
+
const combined = signal ? AbortSignal.any([signal, deadline.signal]) : deadline.signal;
|
|
24
|
+
const timer = timeoutMs > 0 ? setTimeout(() => deadline.abort(), timeoutMs) : undefined;
|
|
25
|
+
const request = { ...rest, timeoutMs: 0, signal: combined };
|
|
26
|
+
const poll = () => pause(pollMs, combined);
|
|
27
|
+
try {
|
|
28
|
+
combined.throwIfAborted();
|
|
29
|
+
const result = await work(request, poll);
|
|
30
|
+
combined.throwIfAborted();
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
catch (cause) {
|
|
34
|
+
if (!combined.aborted) {
|
|
35
|
+
// A read failure still belongs to the write that started this wait.
|
|
36
|
+
// Keep its key so a caller can recover after losing the reply.
|
|
37
|
+
if (cause instanceof RuntimeError && rest.idempotencyKey && !cause.idempotencyKey)
|
|
38
|
+
throw new RuntimeError({
|
|
39
|
+
message: cause.message,
|
|
40
|
+
code: cause.code,
|
|
41
|
+
status: cause.status,
|
|
42
|
+
requestId: cause.requestId,
|
|
43
|
+
hint: cause.hint,
|
|
44
|
+
details: cause.details,
|
|
45
|
+
retryAfterMs: cause.retryAfterMs,
|
|
46
|
+
idempotencyKey: rest.idempotencyKey,
|
|
47
|
+
cause,
|
|
48
|
+
});
|
|
49
|
+
throw cause;
|
|
50
|
+
}
|
|
51
|
+
throw new RuntimeError({
|
|
52
|
+
code: "timeout",
|
|
53
|
+
status: 0,
|
|
54
|
+
message: "Waiting was cancelled or reached its deadline; the resource may still change.",
|
|
55
|
+
hint: "Read the resource to resume waiting. Retry a write with the same idempotencyKey.",
|
|
56
|
+
...(rest.idempotencyKey === undefined ? {} : { idempotencyKey: rest.idempotencyKey }),
|
|
57
|
+
cause,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** A resource product's client at `/v1/<path>`. A wait ends at its target
|
|
65
|
+
* or at any of `settled`, which answers with the resource's error. */
|
|
66
|
+
export function resource(t, path, settled) {
|
|
67
|
+
const base = `/v1/${path}`;
|
|
68
|
+
const get = (id, options = {}) => {
|
|
69
|
+
const { waitFor, timeoutSeconds, ...request } = options;
|
|
70
|
+
return t.json({
|
|
71
|
+
method: "GET",
|
|
72
|
+
path: `${base}/${enc(id)}`,
|
|
73
|
+
query: { waitFor, timeoutSeconds },
|
|
74
|
+
...request,
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const list = async (query = {}, options) => {
|
|
78
|
+
const page = await t.json({
|
|
79
|
+
method: "GET",
|
|
80
|
+
path: base,
|
|
81
|
+
query,
|
|
82
|
+
...options,
|
|
83
|
+
});
|
|
84
|
+
return new Page(page.data, page.nextCursor, (cursor) => list({ ...query, cursor }, options));
|
|
85
|
+
};
|
|
86
|
+
const follow = async (id, target, request, pause, initial) => {
|
|
87
|
+
let record = initial ?? (await get(id, request));
|
|
88
|
+
for (;;) {
|
|
89
|
+
if (record.state === target)
|
|
90
|
+
return record;
|
|
91
|
+
if (settled.includes(record.state))
|
|
92
|
+
throw new RuntimeError({
|
|
93
|
+
code: record.error?.code ?? "unexpected_state",
|
|
94
|
+
status: 0,
|
|
95
|
+
message: record.error?.message ?? `Expected ${target}, but ${id} reached ${record.state}.`,
|
|
96
|
+
hint: "Read the resource and inspect its error before starting another change.",
|
|
97
|
+
details: { id, state: record.state, target, resource: record },
|
|
98
|
+
...(request.idempotencyKey === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: { idempotencyKey: request.idempotencyKey }),
|
|
101
|
+
});
|
|
102
|
+
await pause();
|
|
103
|
+
record = await get(id, request);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
base,
|
|
108
|
+
/** Defaults belong to the API, so the SDK does not override them. */
|
|
109
|
+
create: (input, options) => t.json({ method: "POST", path: base, body: input, ...options }),
|
|
110
|
+
get,
|
|
111
|
+
list,
|
|
112
|
+
update: (id, input, options) => t.json({ method: "POST", path: `${base}/${enc(id)}:update`, body: input, ...options }),
|
|
113
|
+
wait: (id, target, options = {}) => following(options, (request, pause) => follow(id, target, request, pause)),
|
|
114
|
+
delete: (id, options = {}) => {
|
|
115
|
+
const { wait = false, pollMs, ...request } = options;
|
|
116
|
+
const remove = (rest) => t.json({ method: "DELETE", path: `${base}/${enc(id)}`, ...rest });
|
|
117
|
+
if (!wait)
|
|
118
|
+
return remove(request);
|
|
119
|
+
return following({ ...request, pollMs, idempotencyKey: request.idempotencyKey ?? crypto.randomUUID() }, async (rest, pause) => follow(id, "deleted", rest, pause, await remove(rest)));
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -9,6 +9,17 @@ export type SecretRule = {
|
|
|
9
9
|
methods?: string[];
|
|
10
10
|
paths: string[];
|
|
11
11
|
};
|
|
12
|
+
/** Which sandboxes get a secret: those whose labels hold every pair of any one
|
|
13
|
+
* selector in `labels` (including sandboxes created or labelled later), and
|
|
14
|
+
* those named in `sandboxes`. Any other sandbox of the organization sees no
|
|
15
|
+
* placeholder, and its requests never carry the value. Absent on a secret:
|
|
16
|
+
* every sandbox of the organization. */
|
|
17
|
+
export type SecretScope = {
|
|
18
|
+
/** 1 to 8 selectors of 1 to 8 key/value pairs: `[{ role: "hub" }]`. */
|
|
19
|
+
labels?: Array<Record<string, string>>;
|
|
20
|
+
/** 1 to 100 sandbox ids of the organization. */
|
|
21
|
+
sandboxes?: string[];
|
|
22
|
+
};
|
|
12
23
|
/** A secret your sandboxes use without seeing it. Every sandbox of your
|
|
13
24
|
* organization has an environment variable of the secret's name holding
|
|
14
25
|
* `placeholder`; the egress proxy sets `header` on every HTTPS request to
|
|
@@ -22,6 +33,8 @@ export type Secret = {
|
|
|
22
33
|
header?: string;
|
|
23
34
|
format?: string;
|
|
24
35
|
rules?: SecretRule[];
|
|
36
|
+
/** Absent: every sandbox of the organization. */
|
|
37
|
+
only?: SecretScope;
|
|
25
38
|
placeholder: string;
|
|
26
39
|
valueBytes: number;
|
|
27
40
|
createdAt: string;
|
|
@@ -45,6 +58,10 @@ export type SetSecret = {
|
|
|
45
58
|
* Absent: every request to the hosts; replacing a secret without rules
|
|
46
59
|
* clears them. */
|
|
47
60
|
rules?: SecretRule[];
|
|
61
|
+
/** Only these sandboxes get it. Absent when replacing a secret: the scope it
|
|
62
|
+
* has stays, so a new value never widens who gets it. `{}`: every sandbox of
|
|
63
|
+
* the organization again. */
|
|
64
|
+
only?: SecretScope;
|
|
48
65
|
/** Also keep a copy for scheduled jobs, which a job puts into its run's
|
|
49
66
|
* environment (`jobs.create({ secrets: [...] })`). Unlike the sandboxes'
|
|
50
67
|
* copy, the jobs copy is yours rather than the account's, versioned, and can
|
|
@@ -98,6 +115,11 @@ export declare function secrets(t: Transport): {
|
|
|
98
115
|
enforced: boolean;
|
|
99
116
|
}>>;
|
|
100
117
|
};
|
|
118
|
+
/** Changes which sandboxes get a secret, without its value: `{}` is every
|
|
119
|
+
* sandbox of the organization again. */
|
|
120
|
+
scope: (name: string, only: SecretScope, options?: RequestOptions) => Promise<Secret & {
|
|
121
|
+
enforced: boolean;
|
|
122
|
+
}>;
|
|
101
123
|
/** The sandboxes' secrets: names, hosts and placeholders. Never values.
|
|
102
124
|
* `all()` includes the jobs copies. */
|
|
103
125
|
list: (options?: RequestOptions) => Promise<Secret[]>;
|
package/dist/products/secrets.js
CHANGED
|
@@ -153,6 +153,14 @@ export function secrets(t) {
|
|
|
153
153
|
* at once store both; if only the first lands, `SecretPartlyStoredError`
|
|
154
154
|
* says so and the same call again finishes it. */
|
|
155
155
|
set,
|
|
156
|
+
/** Changes which sandboxes get a secret, without its value: `{}` is every
|
|
157
|
+
* sandbox of the organization again. */
|
|
158
|
+
scope: (name, only, options) => t.json({
|
|
159
|
+
method: "PUT",
|
|
160
|
+
path: `${path(name)}/sandboxes`,
|
|
161
|
+
body: only,
|
|
162
|
+
...options,
|
|
163
|
+
}),
|
|
156
164
|
/** The sandboxes' secrets: names, hosts and placeholders. Never values.
|
|
157
165
|
* `all()` includes the jobs copies. */
|
|
158
166
|
list,
|
|
@@ -90,6 +90,9 @@ export type VolumeBackup = {
|
|
|
90
90
|
* the host never began; back it up again. */
|
|
91
91
|
errorCode?: string | null;
|
|
92
92
|
createdAt: string;
|
|
93
|
+
/** When its copy off the volume's server began; null while it waits its
|
|
94
|
+
* turn behind other copies there. */
|
|
95
|
+
copyStartedAt?: string | null;
|
|
93
96
|
readyAt: string | null;
|
|
94
97
|
expiresAt: string;
|
|
95
98
|
};
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ import type { StreamUploadSource } from "./file-source.js";
|
|
|
8
8
|
import { Machine, type MachineProduct } from "./machine.js";
|
|
9
9
|
/** Sandboxes, as a product of the machine engine (machine.ts). */
|
|
10
10
|
export declare const SANDBOXES: MachineProduct;
|
|
11
|
+
/** How long past its timeoutMs a streamed exec waits for an exit that never came. */
|
|
12
|
+
export declare const EXEC_EXIT_GRACE: {
|
|
13
|
+
ms: number;
|
|
14
|
+
};
|
|
11
15
|
/** A running (or stopped) sandbox. Every method is one API call or a short
|
|
12
16
|
* sequence of them; `await using sbx = await Sandbox.create()` deletes it at
|
|
13
17
|
* the end of the block, or stops it, keeping its disk, if created with
|
package/dist/sandbox.js
CHANGED
|
@@ -87,6 +87,8 @@ function commandBody(command, options) {
|
|
|
87
87
|
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
+
/** How long past its timeoutMs a streamed exec waits for an exit that never came. */
|
|
91
|
+
export const EXEC_EXIT_GRACE = { ms: 120_000 };
|
|
90
92
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
91
93
|
export class Sandbox extends Machine {
|
|
92
94
|
#t;
|
|
@@ -147,8 +149,22 @@ export class Sandbox extends Machine {
|
|
|
147
149
|
let exit;
|
|
148
150
|
let processId;
|
|
149
151
|
let dropped = false;
|
|
152
|
+
// A command with a time limit ends by it, the server killing it if need
|
|
153
|
+
// be; an exit that never comes after (a follow that resumes forever)
|
|
154
|
+
// ends the wait past the limit and EXEC_EXIT_GRACE.ms, as a timeout,
|
|
155
|
+
// its process named and sent SIGTERM. The hub's release train waited
|
|
156
|
+
// 29 min on a 10 min command (15 October 2026).
|
|
157
|
+
// A timer cleared when the call ends, so none outlives the exec.
|
|
158
|
+
const deadline = options.timeoutMs === undefined ? undefined : new AbortController();
|
|
159
|
+
const timer = deadline
|
|
160
|
+
? setTimeout(() => deadline.abort(), options.timeoutMs + EXEC_EXIT_GRACE.ms)
|
|
161
|
+
: undefined;
|
|
162
|
+
timer?.unref?.();
|
|
163
|
+
const signal = deadline && options.signal
|
|
164
|
+
? AbortSignal.any([options.signal, deadline.signal])
|
|
165
|
+
: (deadline?.signal ?? options.signal);
|
|
150
166
|
try {
|
|
151
|
-
for await (const event of this.execStream(command, options)) {
|
|
167
|
+
for await (const event of this.execStream(command, { ...options, signal })) {
|
|
152
168
|
if (event.type === "start")
|
|
153
169
|
processId = event.processId;
|
|
154
170
|
else if (event.type === "stdout" || event.type === "stderr") {
|
|
@@ -162,7 +178,16 @@ export class Sandbox extends Machine {
|
|
|
162
178
|
}
|
|
163
179
|
}
|
|
164
180
|
catch (error) {
|
|
165
|
-
|
|
181
|
+
if (!deadline?.signal.aborted || options.signal?.aborted)
|
|
182
|
+
throw await this.#unfinished(error, processId, signal);
|
|
183
|
+
if (processId)
|
|
184
|
+
await new Process(this.#t, this.id, { id: processId })
|
|
185
|
+
.kill("SIGTERM")
|
|
186
|
+
.catch(() => { });
|
|
187
|
+
exit = { type: "exit", exitCode: null, timedOut: true };
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
clearTimeout(timer);
|
|
166
191
|
}
|
|
167
192
|
result = {
|
|
168
193
|
exitCode: exit?.exitCode ?? null,
|
package/dist/snapshots.d.ts
CHANGED
|
@@ -37,6 +37,10 @@ export type Snapshot = {
|
|
|
37
37
|
durableAt: string | null;
|
|
38
38
|
storedBytes: number | null;
|
|
39
39
|
};
|
|
40
|
+
/** An opaque tag for the server it is kept on, like `pl_3f9a0c1b2d4e5f60`.
|
|
41
|
+
* The same tag as a sandbox of yours means the same server. Null before it
|
|
42
|
+
* is placed. Absent from older servers. */
|
|
43
|
+
placement?: string | null;
|
|
40
44
|
error: string | null;
|
|
41
45
|
createdAt: string;
|
|
42
46
|
readyAt: string | null;
|
package/dist/ssh.d.ts
CHANGED
|
@@ -16,6 +16,41 @@ export declare function sshKey(env: NodeJS.ProcessEnv): Promise<{
|
|
|
16
16
|
export declare function resolveSandbox(runtime: Runtime, target: string): Promise<Sandbox>;
|
|
17
17
|
export declare function sshConfig(env: NodeJS.ProcessEnv): Promise<string>;
|
|
18
18
|
export declare function sshCommand(argv: string[], env: NodeJS.ProcessEnv, out: Out, connect: () => Promise<Runtime>, me: string): Promise<number>;
|
|
19
|
+
/** The editors `runtime sandboxes code` opens: each one's command line program
|
|
20
|
+
* and the URL scheme its app registers. All three are VS Code underneath and
|
|
21
|
+
* name an SSH remote the same way, `ssh-remote+<host>`. */
|
|
22
|
+
export declare const EDITORS: {
|
|
23
|
+
readonly vscode: {
|
|
24
|
+
readonly name: "VS Code";
|
|
25
|
+
readonly bin: "code";
|
|
26
|
+
readonly scheme: "vscode";
|
|
27
|
+
};
|
|
28
|
+
readonly cursor: {
|
|
29
|
+
readonly name: "Cursor";
|
|
30
|
+
readonly bin: "cursor";
|
|
31
|
+
readonly scheme: "cursor";
|
|
32
|
+
};
|
|
33
|
+
readonly windsurf: {
|
|
34
|
+
readonly name: "Windsurf";
|
|
35
|
+
readonly bin: "windsurf";
|
|
36
|
+
readonly scheme: "windsurf";
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
export type EditorName = keyof typeof EDITORS;
|
|
40
|
+
/** What opens a sandbox's folder in an editor over the `*.runtime` ssh host:
|
|
41
|
+
* the editor's own program and its arguments, or, without the program on the
|
|
42
|
+
* PATH, the link its app answers. */
|
|
43
|
+
export declare function editorLaunch(editor: EditorName, sandboxId: string, path: string): {
|
|
44
|
+
host: string;
|
|
45
|
+
bin: "code" | "cursor" | "windsurf";
|
|
46
|
+
args: string[];
|
|
47
|
+
link: string;
|
|
48
|
+
};
|
|
49
|
+
/** `runtime sandboxes code <id|name> [--editor cursor] [--path /workspace] [--print]`:
|
|
50
|
+
* a sandbox opened in VS Code, Cursor or Windsurf in one step. It makes sure
|
|
51
|
+
* the sandbox has an SSH server and this machine has the `*.runtime` ssh
|
|
52
|
+
* entry, then hands the editor the SSH remote. */
|
|
53
|
+
export declare function editorCommand(argv: string[], env: NodeJS.ProcessEnv, out: Out, connect: () => Promise<Runtime>, me: string): Promise<number>;
|
|
19
54
|
/** `runtime sandboxes port-forward <id> 5432 8080:3000 ...` */
|
|
20
55
|
export declare function portForwardCommand(argv: string[], out: Out, connect: () => Promise<Runtime>, me: string): Promise<number>;
|
|
21
56
|
export {};
|
package/dist/ssh.js
CHANGED
|
@@ -11,6 +11,9 @@ ${me} sandboxes ssh config [--install] The ssh config for \`ssh <id|nam
|
|
|
11
11
|
VS Code Remote-SSH and JetBrains Gateway; --install adds
|
|
12
12
|
it to ~/.ssh/config
|
|
13
13
|
${me} sandboxes ssh proxy <id|name>[.runtime] ProxyCommand mode: SSH on stdin and stdout
|
|
14
|
+
${me} sandboxes code <id|name> [--editor vscode|cursor|windsurf] [--path /workspace] [--print]
|
|
15
|
+
Open it in your editor over SSH; installs the ssh config
|
|
16
|
+
above once. --print gives the link instead
|
|
14
17
|
${me} sandboxes port-forward <id|name> <port|local:remote>... [--address 127.0.0.1]
|
|
15
18
|
Forward local ports to ports in the sandbox, until Ctrl-C
|
|
16
19
|
|
|
@@ -98,7 +101,7 @@ Host *.runtime
|
|
|
98
101
|
async function installConfig(env) {
|
|
99
102
|
const file = join(configDirectory(env), "ssh", "config");
|
|
100
103
|
await writeFile(file, await sshConfig(env), { mode: 0o600 });
|
|
101
|
-
const sshDir = join(homedir(), ".ssh");
|
|
104
|
+
const sshDir = join(env.HOME || homedir(), ".ssh");
|
|
102
105
|
await mkdir(sshDir, { recursive: true, mode: 0o700 });
|
|
103
106
|
const main = join(sshDir, "config");
|
|
104
107
|
const current = await readFile(main, "utf8").catch(() => "");
|
|
@@ -236,6 +239,95 @@ function usageError(message, me) {
|
|
|
236
239
|
hint: `Run \`${me} sandboxes ssh help\`.`,
|
|
237
240
|
});
|
|
238
241
|
}
|
|
242
|
+
/** The editors `runtime sandboxes code` opens: each one's command line program
|
|
243
|
+
* and the URL scheme its app registers. All three are VS Code underneath and
|
|
244
|
+
* name an SSH remote the same way, `ssh-remote+<host>`. */
|
|
245
|
+
export const EDITORS = {
|
|
246
|
+
vscode: { name: "VS Code", bin: "code", scheme: "vscode" },
|
|
247
|
+
cursor: { name: "Cursor", bin: "cursor", scheme: "cursor" },
|
|
248
|
+
windsurf: { name: "Windsurf", bin: "windsurf", scheme: "windsurf" },
|
|
249
|
+
};
|
|
250
|
+
/** What opens a sandbox's folder in an editor over the `*.runtime` ssh host:
|
|
251
|
+
* the editor's own program and its arguments, or, without the program on the
|
|
252
|
+
* PATH, the link its app answers. */
|
|
253
|
+
export function editorLaunch(editor, sandboxId, path) {
|
|
254
|
+
const { bin, scheme } = EDITORS[editor];
|
|
255
|
+
const remote = `ssh-remote+${sandboxId}.runtime`;
|
|
256
|
+
return {
|
|
257
|
+
host: `${sandboxId}.runtime`,
|
|
258
|
+
bin,
|
|
259
|
+
args: ["--remote", remote, path],
|
|
260
|
+
link: `${scheme}://vscode-remote/${remote}${path}`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/** Runs a program, answering whether it started and exited 0; a program
|
|
264
|
+
* missing from the PATH is false, not an error. */
|
|
265
|
+
function launched(bin, args, env) {
|
|
266
|
+
return new Promise((resolve) => {
|
|
267
|
+
const child = spawn(bin, args, {
|
|
268
|
+
env,
|
|
269
|
+
stdio: "ignore",
|
|
270
|
+
// `code` and `cursor` are .cmd scripts on Windows.
|
|
271
|
+
shell: process.platform === "win32",
|
|
272
|
+
});
|
|
273
|
+
child.on("error", () => resolve(false));
|
|
274
|
+
child.on("exit", (code) => resolve(code === 0));
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/** `runtime sandboxes code <id|name> [--editor cursor] [--path /workspace] [--print]`:
|
|
278
|
+
* a sandbox opened in VS Code, Cursor or Windsurf in one step. It makes sure
|
|
279
|
+
* the sandbox has an SSH server and this machine has the `*.runtime` ssh
|
|
280
|
+
* entry, then hands the editor the SSH remote. */
|
|
281
|
+
export async function editorCommand(argv, env, out, connect, me) {
|
|
282
|
+
const valueOf = (flag) => {
|
|
283
|
+
const at = argv.indexOf(flag);
|
|
284
|
+
return at === -1 ? undefined : argv[at + 1];
|
|
285
|
+
};
|
|
286
|
+
const editor = (valueOf("--editor") ?? "vscode").toLowerCase().replace(/^code$/, "vscode");
|
|
287
|
+
const path = valueOf("--path") ?? "/workspace";
|
|
288
|
+
const valued = new Set(["--editor", "--path"]);
|
|
289
|
+
const [target] = argv.filter((word, i) => !word.startsWith("--") && !valued.has(argv[i - 1] ?? ""));
|
|
290
|
+
if (!target || target === "help") {
|
|
291
|
+
out.write(SSH_HELP(me));
|
|
292
|
+
return target ? 0 : 64;
|
|
293
|
+
}
|
|
294
|
+
if (!(editor in EDITORS))
|
|
295
|
+
throw new RuntimeError({
|
|
296
|
+
message: `${editor} is not an editor this opens.`,
|
|
297
|
+
code: "usage",
|
|
298
|
+
status: 0,
|
|
299
|
+
hint: `Use --editor ${Object.keys(EDITORS).join(", --editor ")}; JetBrains Gateway connects to <id>.runtime after \`${me} sandboxes ssh config --install\`.`,
|
|
300
|
+
});
|
|
301
|
+
if (!path.startsWith("/"))
|
|
302
|
+
throw usageError(`--path is a folder in the sandbox, from /: ${path}`, me);
|
|
303
|
+
const sbx = await resolveSandbox(await connect(), target);
|
|
304
|
+
await ensureSshd(sbx, out);
|
|
305
|
+
await installConfig(env);
|
|
306
|
+
const how = editorLaunch(editor, sbx.id, path);
|
|
307
|
+
const { name } = EDITORS[editor];
|
|
308
|
+
const print = argv.includes("--print");
|
|
309
|
+
// Without the editor's program on the PATH (VS Code's `code` is opt-in on a
|
|
310
|
+
// Mac), the desktop opens the editor's link.
|
|
311
|
+
const [opener, ...openerArgs] = process.platform === "darwin"
|
|
312
|
+
? ["open", how.link]
|
|
313
|
+
: process.platform === "win32"
|
|
314
|
+
? ["cmd", "/c", "start", '""', how.link]
|
|
315
|
+
: ["xdg-open", how.link];
|
|
316
|
+
const opened = print
|
|
317
|
+
? false
|
|
318
|
+
: (await launched(how.bin, how.args, env))
|
|
319
|
+
? "program"
|
|
320
|
+
: (await launched(opener, openerArgs, env))
|
|
321
|
+
? "link"
|
|
322
|
+
: false;
|
|
323
|
+
if (out.json)
|
|
324
|
+
out.write(JSON.stringify({ sandboxId: sbx.id, editor, host: how.host, path, link: how.link, opened }));
|
|
325
|
+
else if (opened)
|
|
326
|
+
out.write(`Opening ${path} of ${sbx.id} in ${name} over SSH (host ${how.host}). It needs ${name === "VS Code" ? "the Remote - SSH extension" : "its Remote - SSH extension"}; the first connection installs the editor's server in the sandbox.`);
|
|
327
|
+
else
|
|
328
|
+
out.write(`${print ? "" : `Could not start ${how.bin} or open a link here. `}Open this link on this machine:\n${how.link}\nor run: ${how.bin} ${how.args.join(" ")}`);
|
|
329
|
+
return opened || print ? 0 : 1;
|
|
330
|
+
}
|
|
239
331
|
/** `runtime sandboxes port-forward <id> 5432 8080:3000 ...` */
|
|
240
332
|
export async function portForwardCommand(argv, out, connect, me) {
|
|
241
333
|
const at = argv.indexOf("--address");
|