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.
Files changed (45) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +3 -2
  3. package/dist/cli-extras.d.ts +24 -0
  4. package/dist/cli-extras.js +205 -0
  5. package/dist/cli.d.ts +20 -0
  6. package/dist/cli.js +401 -28
  7. package/dist/e2b/filesystem.d.ts +26 -1
  8. package/dist/e2b/filesystem.js +30 -2
  9. package/dist/e2b/index.d.ts +1 -1
  10. package/dist/e2b/index.js +1 -1
  11. package/dist/errors.js +2 -0
  12. package/dist/identity.d.ts +27 -0
  13. package/dist/identity.js +56 -0
  14. package/dist/index.d.ts +14 -0
  15. package/dist/index.js +7 -0
  16. package/dist/mcp.js +16 -3
  17. package/dist/network-cli.d.ts +15 -0
  18. package/dist/network-cli.js +323 -0
  19. package/dist/products/desktop.d.ts +41 -4
  20. package/dist/products/desktop.js +73 -3
  21. package/dist/products/index.d.ts +12 -0
  22. package/dist/products/index.js +12 -0
  23. package/dist/products/interpreter.d.ts +5 -3
  24. package/dist/products/interpreter.js +2 -1
  25. package/dist/products/mcp.d.ts +79 -0
  26. package/dist/products/mcp.js +37 -0
  27. package/dist/products/mounts.d.ts +49 -0
  28. package/dist/products/mounts.js +21 -0
  29. package/dist/products/network-products.d.ts +167 -0
  30. package/dist/products/network-products.js +82 -0
  31. package/dist/products/network.d.ts +1 -1
  32. package/dist/products/sso.d.ts +44 -0
  33. package/dist/products/sso.js +8 -0
  34. package/dist/products/volumes.d.ts +68 -5
  35. package/dist/products/volumes.js +52 -4
  36. package/dist/products/watch.d.ts +91 -0
  37. package/dist/products/watch.js +135 -0
  38. package/dist/sandbox.d.ts +32 -2
  39. package/dist/sandbox.js +93 -16
  40. package/dist/snapshots.d.ts +11 -1
  41. package/dist/transport.d.ts +1 -1
  42. package/dist/transport.js +4 -1
  43. package/dist/wireguard.d.ts +7 -0
  44. package/dist/wireguard.js +13 -0
  45. package/package.json +7 -3
@@ -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(): Promise<never>;
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
  }
@@ -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
- return Promise.reject(new NotSupportedError("Watching a directory (files.watchDir)", "Poll with files.list(path), or run your own watcher with commands.run(..., { background: true })."));
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
  }
@@ -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
@@ -15,6 +15,8 @@ export const WAITS_FOR_ROOM = new Set([
15
15
  "trial_capacity",
16
16
  "quota_exceeded",
17
17
  "no_capacity",
18
+ // A volume whose last sandbox is stopping: free within seconds.
19
+ "volume_releasing",
18
20
  ]);
19
21
  export class RuntimeError extends Error {
20
22
  code;
@@ -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>;
@@ -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 runtime_sandboxes_create.`);
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", finish);
213
- process.once("SIGTERM", finish);
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
+ }