withruntime 0.5.1 → 0.6.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/LICENSE +55 -0
- package/README.md +3 -2
- package/dist/cli-extras.d.ts +24 -0
- package/dist/cli-extras.js +205 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +401 -28
- package/dist/e2b/filesystem.d.ts +26 -1
- package/dist/e2b/filesystem.js +30 -2
- package/dist/e2b/index.d.ts +1 -1
- package/dist/e2b/index.js +1 -1
- package/dist/errors.js +2 -0
- package/dist/identity.d.ts +27 -0
- package/dist/identity.js +56 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +7 -0
- package/dist/mcp.js +16 -3
- package/dist/network-cli.d.ts +15 -0
- package/dist/network-cli.js +323 -0
- package/dist/products/desktop.d.ts +41 -4
- package/dist/products/desktop.js +73 -3
- package/dist/products/index.d.ts +12 -0
- package/dist/products/index.js +12 -0
- package/dist/products/interpreter.d.ts +5 -3
- package/dist/products/interpreter.js +2 -1
- package/dist/products/mcp.d.ts +79 -0
- package/dist/products/mcp.js +37 -0
- package/dist/products/mounts.d.ts +49 -0
- package/dist/products/mounts.js +21 -0
- package/dist/products/network-products.d.ts +167 -0
- package/dist/products/network-products.js +82 -0
- package/dist/products/network.d.ts +1 -1
- package/dist/products/sso.d.ts +44 -0
- package/dist/products/sso.js +8 -0
- package/dist/products/volumes.d.ts +68 -5
- package/dist/products/volumes.js +52 -4
- package/dist/products/watch.d.ts +91 -0
- package/dist/products/watch.js +135 -0
- package/dist/sandbox.d.ts +32 -2
- package/dist/sandbox.js +93 -16
- package/dist/snapshots.d.ts +11 -1
- package/dist/transport.d.ts +1 -1
- package/dist/transport.js +4 -1
- package/dist/wireguard.d.ts +7 -0
- package/dist/wireguard.js +13 -0
- package/package.json +7 -3
package/dist/e2b/filesystem.d.ts
CHANGED
|
@@ -21,6 +21,26 @@ export interface EntryInfo extends WriteInfo {
|
|
|
21
21
|
modifiedTime?: Date;
|
|
22
22
|
symlinkTarget?: string;
|
|
23
23
|
}
|
|
24
|
+
export declare enum FilesystemEventType {
|
|
25
|
+
CHMOD = "chmod",
|
|
26
|
+
CREATE = "create",
|
|
27
|
+
REMOVE = "remove",
|
|
28
|
+
RENAME = "rename",
|
|
29
|
+
WRITE = "write"
|
|
30
|
+
}
|
|
31
|
+
export interface FilesystemEvent {
|
|
32
|
+
/** Relative to the watched directory. */
|
|
33
|
+
name: string;
|
|
34
|
+
type: FilesystemEventType;
|
|
35
|
+
}
|
|
36
|
+
export interface WatchOpts extends FilesystemRequestOpts {
|
|
37
|
+
/** How long the watch runs; 0 is Runtime's maximum, 24 hours. Default 60000. */
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
recursive?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface E2BWatchHandle {
|
|
42
|
+
stop(): Promise<void>;
|
|
43
|
+
}
|
|
24
44
|
export type WriteEntry = {
|
|
25
45
|
path: string;
|
|
26
46
|
data: string | ArrayBuffer | Blob | ReadableStream;
|
|
@@ -78,5 +98,10 @@ export declare class Filesystem {
|
|
|
78
98
|
remove(path: string, opts?: FilesystemRequestOpts): Promise<void>;
|
|
79
99
|
exists(path: string, opts?: FilesystemRequestOpts): Promise<boolean>;
|
|
80
100
|
getInfo(path: string, opts?: FilesystemRequestOpts): Promise<EntryInfo>;
|
|
81
|
-
watchDir
|
|
101
|
+
/** E2B's watchDir on Runtime's file watch: the same events (CREATE, WRITE,
|
|
102
|
+
* REMOVE, RENAME, CHMOD) with names relative to the directory, stopped
|
|
103
|
+
* with `handle.stop()`. `timeoutMs` 0 is Runtime's 24-hour maximum. */
|
|
104
|
+
watchDir(path: string, onEvent: (event: FilesystemEvent) => void | Promise<void>, opts?: WatchOpts & {
|
|
105
|
+
onExit?: (err?: Error) => void | Promise<void>;
|
|
106
|
+
}): Promise<E2BWatchHandle>;
|
|
82
107
|
}
|
package/dist/e2b/filesystem.js
CHANGED
|
@@ -6,6 +6,14 @@ export var FileType;
|
|
|
6
6
|
FileType["DIR"] = "dir";
|
|
7
7
|
FileType["SYMLINK"] = "symlink";
|
|
8
8
|
})(FileType || (FileType = {}));
|
|
9
|
+
export var FilesystemEventType;
|
|
10
|
+
(function (FilesystemEventType) {
|
|
11
|
+
FilesystemEventType["CHMOD"] = "chmod";
|
|
12
|
+
FilesystemEventType["CREATE"] = "create";
|
|
13
|
+
FilesystemEventType["REMOVE"] = "remove";
|
|
14
|
+
FilesystemEventType["RENAME"] = "rename";
|
|
15
|
+
FilesystemEventType["WRITE"] = "write";
|
|
16
|
+
})(FilesystemEventType || (FilesystemEventType = {}));
|
|
9
17
|
/** Runtime's home, where E2B's relative paths land. */
|
|
10
18
|
const HOME = "/workspace";
|
|
11
19
|
function absolute(path) {
|
|
@@ -137,7 +145,27 @@ export class Filesystem {
|
|
|
137
145
|
throw new FileNotFoundError(`${target} does not exist.`);
|
|
138
146
|
return entryInfo(stat);
|
|
139
147
|
}
|
|
140
|
-
watchDir(
|
|
141
|
-
|
|
148
|
+
/** E2B's watchDir on Runtime's file watch: the same events (CREATE, WRITE,
|
|
149
|
+
* REMOVE, RENAME, CHMOD) with names relative to the directory, stopped
|
|
150
|
+
* with `handle.stop()`. `timeoutMs` 0 is Runtime's 24-hour maximum. */
|
|
151
|
+
async watchDir(path, onEvent, opts = {}) {
|
|
152
|
+
const target = await this.#path(path, opts);
|
|
153
|
+
const prefix = target.endsWith("/") ? target : `${target}/`;
|
|
154
|
+
const lifetime = opts.timeoutMs === 0 ? 86_400_000 : (opts.timeoutMs ?? 60_000);
|
|
155
|
+
const watch = await guard("file", () => this.#files.watch(target, (event) => onEvent({
|
|
156
|
+
name: event.path.startsWith(prefix) ? event.path.slice(prefix.length) : event.path,
|
|
157
|
+
type: event.type,
|
|
158
|
+
}), {
|
|
159
|
+
recursive: opts.recursive === true,
|
|
160
|
+
timeoutMs: Math.max(1_000, Math.min(86_400_000, lifetime)),
|
|
161
|
+
onExit: (reason) => {
|
|
162
|
+
if (reason === "stopped")
|
|
163
|
+
return;
|
|
164
|
+
void opts.onExit?.(reason === "timeout" || reason === "paused"
|
|
165
|
+
? undefined
|
|
166
|
+
: new Error(`The watch ended: ${reason}`));
|
|
167
|
+
},
|
|
168
|
+
}));
|
|
169
|
+
return { stop: () => watch.stop() };
|
|
142
170
|
}
|
|
143
171
|
}
|
package/dist/e2b/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type ConnectionOpts } from "./client.js";
|
|
|
9
9
|
import { Sandbox } from "./sandbox.js";
|
|
10
10
|
export { Sandbox, SandboxPaginator, DEFAULT_SHAPE, DEFAULT_TIMEOUT_MS, type SandboxOpts, type SandboxConnectOpts, type SandboxApiOpts, type SandboxInfo, type SandboxListOpts, type SandboxState, type SandboxLifecycle, type SandboxOnTimeout, type SnapshotInfo, } from "./sandbox.js";
|
|
11
11
|
export { Commands, CommandHandle, pidOf, type CommandStartOpts, type CommandConnectOpts, type CommandRequestOpts, type ProcessInfo, type Username, } from "./commands.js";
|
|
12
|
-
export { Filesystem, FileType, type EntryInfo, type WriteInfo, type WriteEntry, type FilesystemRequestOpts, type FilesystemReadOpts, type FilesystemWriteOpts, type FilesystemListOpts, } from "./filesystem.js";
|
|
12
|
+
export { Filesystem, FileType, type EntryInfo, type WriteInfo, type WriteEntry, type FilesystemRequestOpts, type FilesystemReadOpts, type FilesystemWriteOpts, type FilesystemListOpts, FilesystemEventType, type FilesystemEvent, type WatchOpts, type E2BWatchHandle as WatchHandle, } from "./filesystem.js";
|
|
13
13
|
export { SandboxError, TimeoutError, InvalidArgumentError, NotEnoughSpaceError, NotFoundError, FileNotFoundError, SandboxNotFoundError, TemplateError, RateLimitError, AuthenticationError, ServiceBusyError, CommandExitError, NotSupportedError, type CommandResult, } from "./errors.js";
|
|
14
14
|
export type { ConnectionOpts, RuntimeOpts } from "./client.js";
|
|
15
15
|
export { Template, TemplateBase, Volume, Secret, waitForPort, waitForURL, waitForProcess, waitForFile, waitForTimeout, } from "./unsupported.js";
|
package/dist/e2b/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { clientFor } from "./client.js";
|
|
|
2
2
|
import { Sandbox } from "./sandbox.js";
|
|
3
3
|
export { Sandbox, SandboxPaginator, DEFAULT_SHAPE, DEFAULT_TIMEOUT_MS, } from "./sandbox.js";
|
|
4
4
|
export { Commands, CommandHandle, pidOf, } from "./commands.js";
|
|
5
|
-
export { Filesystem, FileType, } from "./filesystem.js";
|
|
5
|
+
export { Filesystem, FileType, FilesystemEventType, } from "./filesystem.js";
|
|
6
6
|
export { SandboxError, TimeoutError, InvalidArgumentError, NotEnoughSpaceError, NotFoundError, FileNotFoundError, SandboxNotFoundError, TemplateError, RateLimitError, AuthenticationError, ServiceBusyError, CommandExitError, NotSupportedError, } from "./errors.js";
|
|
7
7
|
export { Template, TemplateBase, Volume, Secret, waitForPort, waitForURL, waitForProcess, waitForFile, waitForTimeout, } from "./unsupported.js";
|
|
8
8
|
export default Sandbox;
|
package/dist/errors.js
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** An identity token, as the API returns it. */
|
|
2
|
+
export type IdentityToken = {
|
|
3
|
+
/** The OIDC ID token (a JWT), signed by Runtime. */
|
|
4
|
+
token: string;
|
|
5
|
+
expiresAt: string;
|
|
6
|
+
/** https://withruntime.com/oidc */
|
|
7
|
+
issuer: string;
|
|
8
|
+
/** org:<org>:image:<image>:sandbox:<sandbox> */
|
|
9
|
+
subject: string;
|
|
10
|
+
audience: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Inside a Runtime sandbox: a short-lived OIDC token, signed by Runtime,
|
|
14
|
+
* naming this sandbox, its organization and its image, for `audience`. Trade
|
|
15
|
+
* it for cloud credentials without storing a key: AWS
|
|
16
|
+
* AssumeRoleWithWebIdentity (audience `sts.amazonaws.com`), Google Cloud
|
|
17
|
+
* workload identity federation, Azure federated credentials, or your own API.
|
|
18
|
+
* Needs no API key. Lives ten minutes unless asked (60 to 3600 seconds).
|
|
19
|
+
*
|
|
20
|
+
* import { Sandbox } from "withruntime";
|
|
21
|
+
* const { token } = await Sandbox.identityToken({ audience: "sts.amazonaws.com" });
|
|
22
|
+
*/
|
|
23
|
+
export declare function identityToken(input: {
|
|
24
|
+
audience: string;
|
|
25
|
+
lifetimeSeconds?: number;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
}): Promise<IdentityToken>;
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { RuntimeError } from "./errors.js";
|
|
2
|
+
async function requestCredentials() {
|
|
3
|
+
let url = process.env.RUNTIME_ID_TOKEN_REQUEST_URL;
|
|
4
|
+
let token = process.env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
|
|
5
|
+
if (!url || !token) {
|
|
6
|
+
// A process started before the guest wrote its environment still finds it.
|
|
7
|
+
try {
|
|
8
|
+
// Loaded here, so the SDK still loads where there is no file system.
|
|
9
|
+
const { readFile } = await import("node:fs/promises");
|
|
10
|
+
const env = JSON.parse(await readFile("/run/runtime/environment.json", "utf8"));
|
|
11
|
+
url = env.RUNTIME_ID_TOKEN_REQUEST_URL;
|
|
12
|
+
token = env.RUNTIME_ID_TOKEN_REQUEST_TOKEN;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return url && token ? { url, token } : null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Inside a Runtime sandbox: a short-lived OIDC token, signed by Runtime,
|
|
22
|
+
* naming this sandbox, its organization and its image, for `audience`. Trade
|
|
23
|
+
* it for cloud credentials without storing a key: AWS
|
|
24
|
+
* AssumeRoleWithWebIdentity (audience `sts.amazonaws.com`), Google Cloud
|
|
25
|
+
* workload identity federation, Azure federated credentials, or your own API.
|
|
26
|
+
* Needs no API key. Lives ten minutes unless asked (60 to 3600 seconds).
|
|
27
|
+
*
|
|
28
|
+
* import { Sandbox } from "withruntime";
|
|
29
|
+
* const { token } = await Sandbox.identityToken({ audience: "sts.amazonaws.com" });
|
|
30
|
+
*/
|
|
31
|
+
export async function identityToken(input) {
|
|
32
|
+
const found = await requestCredentials();
|
|
33
|
+
if (!found)
|
|
34
|
+
throw new RuntimeError({
|
|
35
|
+
code: "identity_unavailable",
|
|
36
|
+
status: 0,
|
|
37
|
+
message: "Identity tokens are issued only inside a Runtime sandbox (RUNTIME_ID_TOKEN_REQUEST_TOKEN is not set).",
|
|
38
|
+
});
|
|
39
|
+
const url = new URL(found.url);
|
|
40
|
+
url.searchParams.set("audience", input.audience);
|
|
41
|
+
if (input.lifetimeSeconds !== undefined)
|
|
42
|
+
url.searchParams.set("lifetimeSeconds", String(input.lifetimeSeconds));
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
headers: { authorization: `Bearer ${found.token}` },
|
|
45
|
+
signal: input.signal ?? AbortSignal.timeout(10_000),
|
|
46
|
+
});
|
|
47
|
+
const body = (await response.json().catch(() => ({})));
|
|
48
|
+
if (!response.ok || !body.token)
|
|
49
|
+
throw new RuntimeError({
|
|
50
|
+
code: body.error?.code ?? "identity_refused",
|
|
51
|
+
status: response.status,
|
|
52
|
+
message: body.error?.message ?? `Could not get an identity token (HTTP ${response.status}).`,
|
|
53
|
+
...(body.error?.requestId ? { requestId: body.error.requestId } : {}),
|
|
54
|
+
});
|
|
55
|
+
return body;
|
|
56
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export { Page } from "./page.js";
|
|
|
16
16
|
export type { KeyLimits } from "./products/limits.js";
|
|
17
17
|
export type { AuditEvent, AuditPage } from "./products/audit.js";
|
|
18
18
|
export type { Secret, SetSecret } from "./products/secrets.js";
|
|
19
|
+
export type { McpCatalogEntry, McpGateway, McpServerRequest } from "./products/mcp.js";
|
|
20
|
+
export type { FileEvent, FileEventType, WatchHandle, WatchNotice, WatchOptions, } from "./products/watch.js";
|
|
21
|
+
export type { Mount, MountBucket } from "./products/mounts.js";
|
|
22
|
+
export type { Volume, VolumeBackup, CreateVolume } from "./products/volumes.js";
|
|
23
|
+
export type { CheckedDomain, DnsRecord, Domain, EgressAddress, TcpPort, Tunnel as PrivateTunnel, TunnelPeer, TunnelPeerCreated, } from "./products/network-products.js";
|
|
19
24
|
export { verifyWebhook, WebhookVerificationError, type MetricPoint, type MetricRange, type OtelExport, type RuntimeEvent, type SandboxMetrics, type Webhook, type WebhookDelivery, type WebhookEventType, } from "./products/observability.js";
|
|
20
25
|
export { Snapshots, type Snapshot, type SnapshotOptions } from "./snapshots.js";
|
|
21
26
|
export { Transport, VERSION, apiOrigin, type RequestOptions, type Call } from "./transport.js";
|
|
@@ -50,5 +55,14 @@ export declare class Sandbox extends SandboxClass {
|
|
|
50
55
|
static list(filter?: Parameters<Runtime["sandboxes"]["list"]>[0], options?: {
|
|
51
56
|
client?: Runtime;
|
|
52
57
|
}): Promise<import("./page.js").Page<SandboxClass>>;
|
|
58
|
+
/** Inside a sandbox: an OIDC token naming this sandbox, for `audience`
|
|
59
|
+
* (AWS: "sts.amazonaws.com"). No API key needed. See `identityToken`. */
|
|
60
|
+
static identityToken(input: {
|
|
61
|
+
audience: string;
|
|
62
|
+
lifetimeSeconds?: number;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
}): Promise<import("./identity.js").IdentityToken>;
|
|
53
65
|
}
|
|
54
66
|
export type { RuntimeOptions as ClientOptions };
|
|
67
|
+
export { identityToken, type IdentityToken } from "./identity.js";
|
|
68
|
+
export type { SsoConnection, SsoStatus } from "./products/sso.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defaultClient } from "./client.js";
|
|
2
2
|
import { Sandbox as SandboxClass } from "./sandbox.js";
|
|
3
|
+
import { identityToken } from "./identity.js";
|
|
3
4
|
export { Runtime, Sandboxes, FeedbackApi, SupportApi, } from "./client.js";
|
|
4
5
|
export { Files, Process, Processes, Terminal } from "./sandbox.js";
|
|
5
6
|
export { Tunnel, TunnelStream } from "./tunnel.js";
|
|
@@ -37,4 +38,10 @@ export class Sandbox extends SandboxClass {
|
|
|
37
38
|
static list(filter = {}, options = {}) {
|
|
38
39
|
return (options.client ?? defaultClient()).sandboxes.list(filter);
|
|
39
40
|
}
|
|
41
|
+
/** Inside a sandbox: an OIDC token naming this sandbox, for `audience`
|
|
42
|
+
* (AWS: "sts.amazonaws.com"). No API key needed. See `identityToken`. */
|
|
43
|
+
static identityToken(input) {
|
|
44
|
+
return identityToken(input);
|
|
45
|
+
}
|
|
40
46
|
}
|
|
47
|
+
export { identityToken } from "./identity.js";
|
package/dist/mcp.js
CHANGED
|
@@ -158,7 +158,7 @@ export async function serveMcp(env = process.env, options = {}) {
|
|
|
158
158
|
}));
|
|
159
159
|
await post(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }));
|
|
160
160
|
write({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
|
|
161
|
-
return text(`Connected as ${outcome.agentName}. Runtime's tools are available now: list the tools again, then start with
|
|
161
|
+
return text(`Connected as ${outcome.agentName}. Runtime's tools are available now: list the tools again, then start with runtime_sandbox_create.`);
|
|
162
162
|
}
|
|
163
163
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
164
164
|
}
|
|
@@ -208,9 +208,22 @@ export async function serveMcp(env = process.env, options = {}) {
|
|
|
208
208
|
}
|
|
209
209
|
});
|
|
210
210
|
const finish = () => void Promise.allSettled([...inFlight]).then(() => resolve());
|
|
211
|
+
/* A signal ends the bridge at once: stop reading, give a reply already on
|
|
212
|
+
its way half a second to be written, and return. The client that sent
|
|
213
|
+
it has stopped listening; waiting on a long tool call, or on stdin it
|
|
214
|
+
left open, only makes it wait for us or kill us. */
|
|
215
|
+
const stop = () => {
|
|
216
|
+
process.stdin.removeAllListeners("data");
|
|
217
|
+
process.stdin.pause();
|
|
218
|
+
process.stdin.destroy();
|
|
219
|
+
void Promise.race([
|
|
220
|
+
Promise.allSettled([...inFlight]),
|
|
221
|
+
new Promise((settle) => setTimeout(settle, 500)),
|
|
222
|
+
]).then(() => resolve());
|
|
223
|
+
};
|
|
211
224
|
process.stdin.once("end", finish);
|
|
212
|
-
process.once("SIGINT",
|
|
213
|
-
process.once("SIGTERM",
|
|
225
|
+
process.once("SIGINT", stop);
|
|
226
|
+
process.once("SIGTERM", stop);
|
|
214
227
|
});
|
|
215
228
|
}
|
|
216
229
|
async function openBrowser(url) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Runtime } from "./client.js";
|
|
2
|
+
type Out = {
|
|
3
|
+
json: boolean;
|
|
4
|
+
write: (text: string) => void;
|
|
5
|
+
error: (text: string) => void;
|
|
6
|
+
};
|
|
7
|
+
type Args = {
|
|
8
|
+
positional: string[];
|
|
9
|
+
flags: Map<string, string[]>;
|
|
10
|
+
};
|
|
11
|
+
export declare const NETWORK_PRODUCTS: readonly ["domain", "port", "address", "tunnel"];
|
|
12
|
+
export type NetworkProduct = (typeof NETWORK_PRODUCTS)[number];
|
|
13
|
+
export declare const NETWORK_HELP: Record<NetworkProduct, string>;
|
|
14
|
+
export declare function networkProductCommand(product: NetworkProduct, argv: string[], args: Args, out: Out, client: () => Promise<Runtime>): Promise<number>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { chmod, writeFile } from "node:fs/promises";
|
|
2
|
+
import { RuntimeError } from "./errors.js";
|
|
3
|
+
import { named } from "./login.js";
|
|
4
|
+
import { generateWireGuardKeyPair } from "./wireguard.js";
|
|
5
|
+
function fundingState(value) {
|
|
6
|
+
if (value.funded === false)
|
|
7
|
+
return "Traffic disabled. Check balance, spending limits and account permissions. Reservation kept until you release it.";
|
|
8
|
+
if (value.funded !== true)
|
|
9
|
+
return "Funding status unavailable.";
|
|
10
|
+
return value.fundedUntil
|
|
11
|
+
? `Funded until ${value.fundedUntil}.`
|
|
12
|
+
: "Funding active; no deadline reported.";
|
|
13
|
+
}
|
|
14
|
+
export const NETWORK_PRODUCTS = ["domain", "port", "address", "tunnel"];
|
|
15
|
+
export const NETWORK_HELP = {
|
|
16
|
+
domain: `runtime domain <command>
|
|
17
|
+
|
|
18
|
+
add <hostname> <sandbox> <port> Serve a sandbox's port at your own hostname, with HTTPS
|
|
19
|
+
Prints the DNS records to set: a TXT that proves the
|
|
20
|
+
name is yours, and a CNAME (an A for an apex name)
|
|
21
|
+
verify <hostname> Check the TXT record now; the name goes live when it matches
|
|
22
|
+
ls Your domains
|
|
23
|
+
get <hostname> One domain and its DNS records
|
|
24
|
+
rm <hostname> Stop serving it
|
|
25
|
+
Paid accounts only. The first visit gets the certificate, in a few seconds.
|
|
26
|
+
`,
|
|
27
|
+
port: `runtime port <command>
|
|
28
|
+
|
|
29
|
+
open <sandbox> <port> A public TCP port to a port in the sandbox: Postgres,
|
|
30
|
+
Redis, a game server, anything that is not HTTP
|
|
31
|
+
Prints address:port for your client
|
|
32
|
+
ls [--sandbox <id>] Your open ports
|
|
33
|
+
close <portId> Close one; open connections end within seconds
|
|
34
|
+
Paid accounts only. TCP, carried as it is: use your service's own password and TLS.
|
|
35
|
+
`,
|
|
36
|
+
address: `runtime address <command>
|
|
37
|
+
|
|
38
|
+
reserve [--ipv6] A dedicated outbound address: every sandbox of your
|
|
39
|
+
account sends from it, for allow-lists
|
|
40
|
+
ls Your addresses
|
|
41
|
+
release <addressId> Give it back; sandboxes use the shared addresses again
|
|
42
|
+
Paid accounts only.
|
|
43
|
+
`,
|
|
44
|
+
tunnel: `runtime tunnel <command>
|
|
45
|
+
|
|
46
|
+
create [--subnet 10.250.0.0/16] A WireGuard tunnel from your network into your sandboxes
|
|
47
|
+
get The tunnel, its peers, and each sandbox's address in it
|
|
48
|
+
rm Delete the tunnel and every peer
|
|
49
|
+
peer add <name> [--route 10.0.0.0/16]... [--out runtime.conf] [--public-key <key>]
|
|
50
|
+
Add a machine or router of yours; writes its wg-quick
|
|
51
|
+
file. The key pair is made here and the private key
|
|
52
|
+
never leaves this machine. Then: sudo wg-quick up ./runtime.conf
|
|
53
|
+
peer rotate <peerId> [--out runtime.conf]
|
|
54
|
+
A new key; the old one stops working within seconds
|
|
55
|
+
peer rm <peerId> Remove it; its tunnel ends within seconds
|
|
56
|
+
--route: your own ranges behind the peer that sandboxes may reach. Paid accounts only.
|
|
57
|
+
`,
|
|
58
|
+
};
|
|
59
|
+
function usage(product, message) {
|
|
60
|
+
return new RuntimeError({
|
|
61
|
+
message,
|
|
62
|
+
code: "usage",
|
|
63
|
+
status: 0,
|
|
64
|
+
hint: `Run \`${named("runtime")} ${product} help\`.`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function table(rows) {
|
|
68
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => (row[column] ?? "").length)));
|
|
69
|
+
return rows
|
|
70
|
+
.map((row) => row
|
|
71
|
+
.map((cell, column) => cell.padEnd(widths[column]))
|
|
72
|
+
.join(" ")
|
|
73
|
+
.trimEnd())
|
|
74
|
+
.join("\n");
|
|
75
|
+
}
|
|
76
|
+
function portNumber(product, text) {
|
|
77
|
+
const value = Number(text);
|
|
78
|
+
if (!text || !/^\d+$/.test(text) || value < 1 || value > 65_535)
|
|
79
|
+
throw usage(product, "Give a port from 1 to 65535.");
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
/** Writes a wg-quick file readable by its owner alone, or prints it. */
|
|
83
|
+
async function deliver(out, config, path, hint, ready = true) {
|
|
84
|
+
if (!config) {
|
|
85
|
+
out.error(hint);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (path) {
|
|
89
|
+
await writeFile(path, config, { mode: 0o600 });
|
|
90
|
+
await chmod(path, 0o600);
|
|
91
|
+
out.error(ready
|
|
92
|
+
? `Wrote ${path}. Bring it up with: sudo wg-quick up ${path.startsWith("/") ? path : `./${path}`}`
|
|
93
|
+
: `Wrote ${path}. ${hint}`);
|
|
94
|
+
}
|
|
95
|
+
else
|
|
96
|
+
out.write(config);
|
|
97
|
+
}
|
|
98
|
+
export async function networkProductCommand(product, argv, args, out, client) {
|
|
99
|
+
const [verb] = argv;
|
|
100
|
+
const flag = (name) => args.flags.get(name)?.at(-1);
|
|
101
|
+
const has = (name) => flag(name) === "true";
|
|
102
|
+
const print = (human, value) => out.write(out.json ? JSON.stringify(value) : human);
|
|
103
|
+
const rest = args.positional.slice(1);
|
|
104
|
+
const need = (value, what) => {
|
|
105
|
+
if (!value)
|
|
106
|
+
throw usage(product, `Give ${what}.`);
|
|
107
|
+
return value;
|
|
108
|
+
};
|
|
109
|
+
if (!verb || verb === "help" || verb === "--help") {
|
|
110
|
+
print(named(NETWORK_HELP[product]), { usage: named(NETWORK_HELP[product]) });
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
const runtime = await client();
|
|
114
|
+
if (product === "domain") {
|
|
115
|
+
const records = (domain) => table([
|
|
116
|
+
["TYPE", "NAME", "VALUE"],
|
|
117
|
+
...domain.records.map((record) => [record.type, record.name, record.value]),
|
|
118
|
+
]);
|
|
119
|
+
switch (verb) {
|
|
120
|
+
case "add": {
|
|
121
|
+
const found = await runtime.domains.add({
|
|
122
|
+
hostname: need(rest[0], "a hostname, such as app.example.com"),
|
|
123
|
+
sandboxId: need(rest[1], "a sandbox id"),
|
|
124
|
+
port: portNumber(product, rest[2]),
|
|
125
|
+
});
|
|
126
|
+
print(`${found.hostname}: ${found.state}\n\n${records(found)}\n\n${found.hint}`, found);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
case "verify": {
|
|
130
|
+
const found = await runtime.domains.verify(need(rest[0], "a hostname"));
|
|
131
|
+
print(`${found.hostname}: ${found.state}. ${found.hint}${found.checkError ? `\n${found.checkError}` : ""}`, found);
|
|
132
|
+
return found.state === "active" ? 0 : 1;
|
|
133
|
+
}
|
|
134
|
+
case "ls":
|
|
135
|
+
case "list": {
|
|
136
|
+
const all = await runtime.domains.list();
|
|
137
|
+
print(all.length
|
|
138
|
+
? table([
|
|
139
|
+
["HOSTNAME", "STATE", "SANDBOX", "PORT"],
|
|
140
|
+
...all.map((d) => [
|
|
141
|
+
d.hostname,
|
|
142
|
+
d.disabled ? "taken down" : d.state,
|
|
143
|
+
d.sandboxId,
|
|
144
|
+
String(d.port),
|
|
145
|
+
]),
|
|
146
|
+
])
|
|
147
|
+
: named("No domains. Add one: runtime domain add app.example.com <sandbox> 3000"), all);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
case "get": {
|
|
151
|
+
const found = await runtime.domains.get(need(rest[0], "a hostname"));
|
|
152
|
+
print(`${found.hostname}: ${found.state}\n\n${records(found)}`, found);
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
case "rm":
|
|
156
|
+
case "remove": {
|
|
157
|
+
const gone = await runtime.domains.remove(need(rest[0], "a hostname"));
|
|
158
|
+
print(`${gone.hostname} is no longer served. Remove its DNS records too.`, gone);
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (product === "port") {
|
|
164
|
+
switch (verb) {
|
|
165
|
+
case "open": {
|
|
166
|
+
const opened = await runtime.ports.open({
|
|
167
|
+
sandboxId: need(rest[0], "a sandbox id"),
|
|
168
|
+
port: portNumber(product, rest[1]),
|
|
169
|
+
});
|
|
170
|
+
if (!out.json)
|
|
171
|
+
out.error(opened.hint);
|
|
172
|
+
print(opened.connect, opened);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
case "ls":
|
|
176
|
+
case "list": {
|
|
177
|
+
const sandboxId = flag("sandbox");
|
|
178
|
+
const all = await runtime.ports.list(sandboxId ? { sandboxId } : {});
|
|
179
|
+
print(all.length
|
|
180
|
+
? table([
|
|
181
|
+
["ID", "CONNECT", "SANDBOX", "PORT"],
|
|
182
|
+
...all.map((p) => [p.id, p.connect, p.sandboxId, String(p.port)]),
|
|
183
|
+
])
|
|
184
|
+
: named("No open ports. Open one: runtime port open <sandbox> 5432"), all);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
case "close":
|
|
188
|
+
case "rm": {
|
|
189
|
+
const gone = await runtime.ports.close(need(rest[0], "a port id"));
|
|
190
|
+
print(`Closed ${gone.id}.`, gone);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (product === "address") {
|
|
196
|
+
switch (verb) {
|
|
197
|
+
case "reserve": {
|
|
198
|
+
const reserved = await runtime.addresses.reserve({ family: has("ipv6") ? 6 : 4 });
|
|
199
|
+
if (!out.json)
|
|
200
|
+
out.error(reserved.funded === true
|
|
201
|
+
? `Every sandbox of this account sends from it. ${fundingState(reserved)}`
|
|
202
|
+
: fundingState(reserved));
|
|
203
|
+
print(reserved.address, reserved);
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
case "ls":
|
|
207
|
+
case "list": {
|
|
208
|
+
const all = await runtime.addresses.list();
|
|
209
|
+
print(all.length
|
|
210
|
+
? table([
|
|
211
|
+
["ID", "ADDRESS", "FAMILY", "FUNDING"],
|
|
212
|
+
...all.map((a) => [a.id, a.address, `IPv${a.family}`, fundingState(a)]),
|
|
213
|
+
])
|
|
214
|
+
: named("No dedicated address. Reserve one: runtime address reserve"), all);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
case "release":
|
|
218
|
+
case "rm": {
|
|
219
|
+
const gone = await runtime.addresses.release(need(rest[0], "an address id"));
|
|
220
|
+
print(`Released ${gone.address}.`, gone);
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (product === "tunnel") {
|
|
226
|
+
const view = (tunnel) => [
|
|
227
|
+
fundingState(tunnel),
|
|
228
|
+
`Endpoint ${tunnel.endpoint}, subnet ${tunnel.subnet}, gateway key ${tunnel.gatewayPublicKey ?? "(starting)"}`,
|
|
229
|
+
"",
|
|
230
|
+
tunnel.peers.length
|
|
231
|
+
? table([
|
|
232
|
+
["PEER", "NAME", "ADDRESS", "ROUTES"],
|
|
233
|
+
...tunnel.peers.map((p) => [p.id, p.name, p.address, p.routes.join(",") || "-"]),
|
|
234
|
+
])
|
|
235
|
+
: named("No peers. Add one: runtime tunnel peer add office --route 10.0.0.0/16"),
|
|
236
|
+
"",
|
|
237
|
+
tunnel.sandboxes.length
|
|
238
|
+
? table([
|
|
239
|
+
["SANDBOX", "NAME", "ADDRESS", "STATE"],
|
|
240
|
+
...tunnel.sandboxes.map((s) => [s.sandboxId, s.name ?? "-", s.address, s.state]),
|
|
241
|
+
])
|
|
242
|
+
: "No sandboxes yet.",
|
|
243
|
+
].join("\n");
|
|
244
|
+
switch (verb) {
|
|
245
|
+
case "create": {
|
|
246
|
+
const made = await runtime.tunnel.create(flag("subnet") ? { subnet: flag("subnet") } : {});
|
|
247
|
+
print(view(made), made);
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
case "get":
|
|
251
|
+
case "ls": {
|
|
252
|
+
const found = await runtime.tunnel.get();
|
|
253
|
+
print(view(found), found);
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
case "rm":
|
|
257
|
+
case "delete": {
|
|
258
|
+
const gone = await runtime.tunnel.delete();
|
|
259
|
+
print(`Deleted the tunnel ${gone.id}.`, gone);
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
case "peer":
|
|
263
|
+
case "peers": {
|
|
264
|
+
const action = rest[0];
|
|
265
|
+
if (action === "add" || action === "rotate") {
|
|
266
|
+
// The pair is made here unless a key is given: the private key never
|
|
267
|
+
// leaves this machine, and goes only into the file written below.
|
|
268
|
+
const given = flag("public-key");
|
|
269
|
+
const pair = given ? undefined : generateWireGuardKeyPair();
|
|
270
|
+
const publicKey = given ?? pair.publicKey;
|
|
271
|
+
const answer = action === "add"
|
|
272
|
+
? await runtime.tunnel.addPeer({
|
|
273
|
+
name: need(rest[1], "the peer's name, such as office"),
|
|
274
|
+
publicKey,
|
|
275
|
+
routes: args.flags.get("route") ?? [],
|
|
276
|
+
})
|
|
277
|
+
: await runtime.tunnel.rotatePeer(need(rest[1], "a peer id"), { publicKey });
|
|
278
|
+
const configReady = answer.configReady ?? !!answer.config;
|
|
279
|
+
const draftHint = "Keep this file: it preserves your private key. Read runtime tunnel get when the gateway is ready, replace PublicKey in [Peer] with gatewayPublicKey, then run wg-quick up.";
|
|
280
|
+
// Older servers returned no config while the gateway was starting.
|
|
281
|
+
// Preserve the locally generated key in a draft rather than discard it.
|
|
282
|
+
const supplied = answer.config ??
|
|
283
|
+
[
|
|
284
|
+
"# DRAFT: gateway key missing. Keep this file; it preserves your private key.",
|
|
285
|
+
"[Interface]",
|
|
286
|
+
`PrivateKey = ${pair?.privateKey ?? "<your private key>"}`,
|
|
287
|
+
`Address = ${answer.peer.address}/32`,
|
|
288
|
+
"",
|
|
289
|
+
"[Peer]",
|
|
290
|
+
`PublicKey = ${answer.tunnel.gatewayPublicKey ?? "<gatewayPublicKey from runtime tunnel get>"}`,
|
|
291
|
+
`Endpoint = ${answer.tunnel.endpoint}`,
|
|
292
|
+
`AllowedIPs = ${answer.tunnel.subnet}`,
|
|
293
|
+
"PersistentKeepalive = 25",
|
|
294
|
+
"",
|
|
295
|
+
].join("\n");
|
|
296
|
+
const config = pair
|
|
297
|
+
? supplied.replace(/^PrivateKey = .*$/m, `PrivateKey = ${pair.privateKey}`)
|
|
298
|
+
: supplied;
|
|
299
|
+
if (out.json) {
|
|
300
|
+
print("", {
|
|
301
|
+
...answer,
|
|
302
|
+
config,
|
|
303
|
+
configReady,
|
|
304
|
+
...(!configReady ? { hint: draftHint } : {}),
|
|
305
|
+
});
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
if (answer.tunnel.funded === false)
|
|
309
|
+
out.error(fundingState(answer.tunnel));
|
|
310
|
+
await deliver(out, config, flag("out") ?? (pair ? "runtime.conf" : undefined), configReady ? answer.hint : draftHint, configReady);
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
if (action === "rm" || action === "remove") {
|
|
314
|
+
const after = await runtime.tunnel.removePeer(need(rest[1], "a peer id"));
|
|
315
|
+
print(`Removed. ${after.peers.length} peer(s) left.`, after);
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
throw usage(product, "Use peer add, peer rotate or peer rm.");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
throw usage(product, `Unknown ${product} command ${verb}.`);
|
|
323
|
+
}
|