withruntime 0.7.1 → 0.8.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 (42) hide show
  1. package/BLAXEL.md +210 -0
  2. package/README.md +18 -0
  3. package/dist/ai-harness/index.js +5 -0
  4. package/dist/blaxel/client.d.ts +48 -0
  5. package/dist/blaxel/client.js +58 -0
  6. package/dist/blaxel/context.d.ts +59 -0
  7. package/dist/blaxel/context.js +162 -0
  8. package/dist/blaxel/errors.d.ts +51 -0
  9. package/dist/blaxel/errors.js +148 -0
  10. package/dist/blaxel/filesystem.d.ts +147 -0
  11. package/dist/blaxel/filesystem.js +585 -0
  12. package/dist/blaxel/index.d.ts +280 -0
  13. package/dist/blaxel/index.js +332 -0
  14. package/dist/blaxel/interpreter.d.ts +74 -0
  15. package/dist/blaxel/interpreter.js +256 -0
  16. package/dist/blaxel/lookup.d.ts +37 -0
  17. package/dist/blaxel/lookup.js +82 -0
  18. package/dist/blaxel/pagination.d.ts +39 -0
  19. package/dist/blaxel/pagination.js +60 -0
  20. package/dist/blaxel/preview.d.ts +110 -0
  21. package/dist/blaxel/preview.js +228 -0
  22. package/dist/blaxel/process.d.ts +126 -0
  23. package/dist/blaxel/process.js +526 -0
  24. package/dist/blaxel/sandbox.d.ts +173 -0
  25. package/dist/blaxel/sandbox.js +768 -0
  26. package/dist/blaxel/snapshot.d.ts +119 -0
  27. package/dist/blaxel/snapshot.js +197 -0
  28. package/dist/blaxel/types.d.ts +145 -0
  29. package/dist/blaxel/types.js +60 -0
  30. package/dist/blaxel/unsupported.d.ts +22 -0
  31. package/dist/blaxel/unsupported.js +34 -0
  32. package/dist/cli.js +49 -15
  33. package/dist/login.d.ts +9 -36
  34. package/dist/login.js +0 -22
  35. package/dist/products/switching.d.ts +2 -2
  36. package/dist/sandbox.d.ts +3 -2
  37. package/dist/sandbox.js +3 -2
  38. package/dist/tar.js +72 -14
  39. package/dist/transport.d.ts +1 -1
  40. package/dist/transport.js +1 -1
  41. package/dist/types.d.ts +14 -1
  42. package/package.json +10 -2
@@ -0,0 +1,228 @@
1
+ import { guard, isNotFound, NotSupportedError, responseError } from "./errors.js";
2
+ import { PREVIEW } from "./lookup.js";
3
+ /** Runtime's shortest and longest token, in seconds. */
4
+ const TOKEN_MIN = 60;
5
+ const TOKEN_MAX = 7 * 86_400;
6
+ function checkName(name) {
7
+ if (!/^[A-Za-z0-9._-]{1,48}$/.test(name))
8
+ throw new NotSupportedError(`The preview name "${name}"`, "Use 1 to 48 letters, digits, dots, dashes or underscores.");
9
+ }
10
+ function modelOf(preview, name, sandboxName, status = "DEPLOYED") {
11
+ return {
12
+ metadata: {
13
+ name,
14
+ resourceName: sandboxName,
15
+ resourceType: "sandbox",
16
+ createdAt: preview.createdAt,
17
+ },
18
+ spec: {
19
+ port: preview.port,
20
+ public: preview.visibility === "public",
21
+ url: preview.url.replace(/\/$/, ""),
22
+ },
23
+ status,
24
+ };
25
+ }
26
+ /** A preview token, as Blaxel's. Runtime reads it from the
27
+ * `x-runtime-preview-token` header or `?runtime_preview_token=`. */
28
+ export class SandboxPreviewToken {
29
+ #token;
30
+ constructor(previewToken) {
31
+ this.#token = previewToken;
32
+ }
33
+ get value() {
34
+ return this.#token.spec.token ?? "";
35
+ }
36
+ get expiresAt() {
37
+ return this.#token.spec.expiresAt ?? new Date();
38
+ }
39
+ get expired() {
40
+ const at = this.#token.spec.expiresAt;
41
+ return at !== undefined && Date.parse(at) <= Date.now();
42
+ }
43
+ }
44
+ /** A preview's tokens. Runtime signs a token for a duration; it keeps no
45
+ * list of the tokens issued. */
46
+ export class SandboxPreviewTokens {
47
+ #preview;
48
+ #ctx;
49
+ constructor(preview, ctx) {
50
+ this.#preview = preview;
51
+ this.#ctx = ctx;
52
+ }
53
+ get previewName() {
54
+ return this.#preview.name;
55
+ }
56
+ get resourceName() {
57
+ return this.#ctx.sandboxName;
58
+ }
59
+ /** A token that lasts until `expiresAt` (at least a minute, at most a week). */
60
+ async create(expiresAt) {
61
+ const seconds = Math.ceil((expiresAt.getTime() - Date.now()) / 1000);
62
+ if (seconds > TOKEN_MAX)
63
+ throw new NotSupportedError("A preview token lasting more than a week", "Create one for at most seven days, and a new one when it runs out.");
64
+ const port = this.#preview.spec.port;
65
+ const got = await this.#ctx.run((runtime) => runtime.previews.get(port, Math.max(TOKEN_MIN, seconds)));
66
+ return new SandboxPreviewToken({
67
+ metadata: { name: `token-${Date.now()}`, previewName: this.previewName },
68
+ spec: {
69
+ ...(got.token ? { token: got.token } : {}),
70
+ ...(got.tokenExpiresAt ? { expiresAt: got.tokenExpiresAt } : {}),
71
+ },
72
+ });
73
+ }
74
+ list() {
75
+ return Promise.reject(new NotSupportedError("Listing a preview's tokens", "Runtime keeps no list of issued tokens: keep the ones you create, or create a new one."));
76
+ }
77
+ /** Revokes the preview's tokens. Runtime cannot revoke one token alone, so
78
+ * every token issued for this preview stops working. */
79
+ async delete(_tokenName) {
80
+ const port = this.#preview.spec.port;
81
+ await this.#ctx.run((runtime) => runtime.previews.rotate(port));
82
+ return { message: "Every token of this preview was revoked" };
83
+ }
84
+ }
85
+ /** A shared port, as Blaxel's SandboxPreview. */
86
+ export class SandboxPreview {
87
+ #preview;
88
+ tokens;
89
+ constructor(preview, ctx) {
90
+ this.#preview = preview;
91
+ this.tokens = new SandboxPreviewTokens(this, ctx);
92
+ }
93
+ get name() {
94
+ return this.#preview.metadata.name;
95
+ }
96
+ get metadata() {
97
+ return this.#preview.metadata;
98
+ }
99
+ get spec() {
100
+ return this.#preview.spec;
101
+ }
102
+ }
103
+ /** `sandbox.previews`. */
104
+ export class SandboxPreviews {
105
+ #ctx;
106
+ constructor(ctx) {
107
+ this.#ctx = ctx;
108
+ }
109
+ get sandboxName() {
110
+ return this.#ctx.sandboxName;
111
+ }
112
+ /** Names by port, from the sandbox's labels. */
113
+ #names() {
114
+ const names = new Map();
115
+ for (const [key, value] of Object.entries(this.#ctx.labels()))
116
+ if (key.startsWith(PREVIEW))
117
+ names.set(Number(value), key.slice(PREVIEW.length));
118
+ return names;
119
+ }
120
+ #portOf(name) {
121
+ const label = this.#ctx.labels()[`${PREVIEW}${name}`];
122
+ if (label !== undefined)
123
+ return Number(label);
124
+ const plain = /^preview-(\d{1,5})$/.exec(name);
125
+ return plain ? Number(plain[1]) : undefined;
126
+ }
127
+ /** Every shared port. One Runtime shared without a Blaxel name is named
128
+ * `preview-<port>`. */
129
+ async list() {
130
+ const previews = await this.#ctx.run((runtime) => runtime.previews.list());
131
+ const names = this.#names();
132
+ return previews.map((one) => new SandboxPreview(modelOf(one, names.get(one.port) ?? `preview-${one.port}`, this.sandboxName), this.#ctx));
133
+ }
134
+ /** Shares `spec.port`: public with `spec.public`, else behind a token. */
135
+ async create(preview, _force) {
136
+ const spec = preview?.spec ?? {};
137
+ const name = preview?.metadata?.name;
138
+ const refusals = [
139
+ [
140
+ "prefixUrl",
141
+ "Runtime's preview address is fixed: https://<port>-<sandbox>.runtimehost.com.",
142
+ ],
143
+ [
144
+ "customDomain",
145
+ "Point your domain at the preview with runtime.domains, or keep the preview's own address.",
146
+ ],
147
+ [
148
+ "requestHeaders",
149
+ "Set the headers in your server, or in the client that calls the preview.",
150
+ ],
151
+ ["responseHeaders", "Set the headers (CORS and the rest) in your server's responses."],
152
+ [
153
+ "ttl",
154
+ "A Runtime preview lasts until deleted: call sandbox.previews.delete(name) when done.",
155
+ ],
156
+ [
157
+ "expires",
158
+ "A Runtime preview lasts until deleted: call sandbox.previews.delete(name) when done.",
159
+ ],
160
+ ];
161
+ for (const [field, alternative] of refusals)
162
+ if (spec[field] !== undefined &&
163
+ !(typeof spec[field] === "object" && !Object.keys(spec[field]).length))
164
+ throw new NotSupportedError(`A preview's ${field}`, alternative);
165
+ const port = spec.port;
166
+ if (!Number.isInteger(port) || port < 1 || port > 65_535)
167
+ throw responseError(400, "spec.port is required: the port your server listens on");
168
+ if (name !== undefined)
169
+ checkName(name);
170
+ const [made] = await Promise.all([
171
+ this.#ctx.run((runtime) => runtime.previews.create(port, { visibility: spec.public ? "public" : "private" })),
172
+ name !== undefined && this.#portOf(name) !== port
173
+ ? this.#ctx.setLabels({ [`${PREVIEW}${name}`]: String(port) })
174
+ : undefined,
175
+ ]);
176
+ return new SandboxPreview(modelOf(made, name ?? `preview-${port}`, this.sandboxName), this.#ctx);
177
+ }
178
+ /** The named preview, or a new one when there is none. */
179
+ async createIfNotExists(preview, force) {
180
+ try {
181
+ return await this.get(preview.metadata.name);
182
+ }
183
+ catch (error) {
184
+ if (!isNotFound(error))
185
+ throw error;
186
+ return this.create(preview, force);
187
+ }
188
+ }
189
+ async get(previewName) {
190
+ const port = this.#portOf(previewName);
191
+ if (port === undefined)
192
+ throw responseError(404, `preview ${previewName} not found`);
193
+ const found = await this.#ctx.run((runtime) => runtime.previews.get(port));
194
+ return new SandboxPreview(modelOf(found, previewName, this.sandboxName), this.#ctx);
195
+ }
196
+ /** Stops sharing the port; open connections close within seconds. */
197
+ async delete(previewName) {
198
+ const port = this.#portOf(previewName);
199
+ if (port === undefined)
200
+ throw responseError(404, `preview ${previewName} not found`);
201
+ await Promise.all([
202
+ this.#ctx.run((runtime) => runtime.previews.delete(port)),
203
+ `${PREVIEW}${previewName}` in this.#ctx.labels()
204
+ ? this.#ctx.setLabels({ [`${PREVIEW}${previewName}`]: undefined })
205
+ : undefined,
206
+ ]);
207
+ return {
208
+ metadata: { name: previewName, resourceName: this.sandboxName, resourceType: "sandbox" },
209
+ spec: { port },
210
+ status: "DELETING",
211
+ };
212
+ }
213
+ }
214
+ /** A private preview of `port` with a token that lasts a while, for
215
+ * `sandbox.fetch`: made once, then reused until its token nears its end. */
216
+ export async function portAccess(runtime, port) {
217
+ let preview;
218
+ try {
219
+ preview = await guard(() => runtime.previews.get(port, 3600));
220
+ }
221
+ catch (error) {
222
+ if (!isNotFound(error))
223
+ throw error;
224
+ preview = await guard(() => runtime.previews.create(port, { ttlSeconds: 3600 }));
225
+ }
226
+ const until = preview.tokenExpiresAt ? Date.parse(preview.tokenExpiresAt) : Infinity;
227
+ return { url: preview.url.replace(/\/$/, ""), token: preview.token, until };
228
+ }
@@ -0,0 +1,126 @@
1
+ import type { RuntimeSandbox } from "./client.js";
2
+ export type ProcessRequest = {
3
+ command: string;
4
+ env?: {
5
+ [key: string]: string;
6
+ };
7
+ /** Keeps the sandbox awake while the process runs (see BLAXEL.md). */
8
+ keepAlive?: boolean;
9
+ maxRestarts?: number;
10
+ name?: string;
11
+ restartOnFailure?: boolean;
12
+ /** Opens a pipe for writeStdin / closeStdin. */
13
+ stdin?: boolean;
14
+ /** Seconds. Bounds the wait of waitForCompletion and waitForPorts; with
15
+ * keepAlive, the process is killed after it (default 600, 0 never). */
16
+ timeout?: number;
17
+ waitForCompletion?: boolean;
18
+ waitForPorts?: Array<number>;
19
+ workingDir?: string;
20
+ };
21
+ export type ProcessResponse = {
22
+ command: string;
23
+ completedAt: string;
24
+ exitCode: number;
25
+ keepAlive?: boolean;
26
+ logs: string;
27
+ maxRestarts?: number;
28
+ name: string;
29
+ /** Runtime's process id. */
30
+ pid: string;
31
+ restartCount?: number;
32
+ restartOnFailure?: boolean;
33
+ startedAt: string;
34
+ status: "failed" | "killed" | "stopped" | "running" | "completed";
35
+ stderr: string;
36
+ stdin?: boolean;
37
+ stdout: string;
38
+ workingDir: string;
39
+ };
40
+ export type PostProcessResponse = ProcessResponse;
41
+ export type GetProcessResponse = ProcessResponse[];
42
+ export type GetProcessByIdentifierResponse = ProcessResponse;
43
+ export type DeleteProcessByIdentifierResponse = {
44
+ message: string;
45
+ };
46
+ export type DeleteProcessByIdentifierKillResponse = {
47
+ message: string;
48
+ };
49
+ export type ProcessLogs = {
50
+ logs: string;
51
+ stderr: string;
52
+ stdout: string;
53
+ };
54
+ export type ProcessRequestWithLog = ProcessRequest & {
55
+ onLog?: (log: string) => void;
56
+ onStdout?: (stdout: string) => void;
57
+ onStderr?: (stderr: string) => void;
58
+ };
59
+ export type ProcessResponseWithLog = ProcessResponse & {
60
+ close: () => void;
61
+ };
62
+ /** What the process part needs from its sandbox. */
63
+ export interface ProcessContext {
64
+ readonly id: string;
65
+ /** The running sandbox, woken if it was paused; throws once deleted. */
66
+ live(): Promise<RuntimeSandbox>;
67
+ /** Wakes the sandbox after a call found it paused. */
68
+ wake(): Promise<void>;
69
+ /** Keeps the sandbox from pausing for `seconds` (0: until its lease ends). */
70
+ keepAwake(seconds: number): Promise<void>;
71
+ /** A keepAlive process was seen to end: give the sandbox its idle pause
72
+ * back once none runs. Never throws. */
73
+ keepAliveEnded(): Promise<void>;
74
+ }
75
+ /** A command that waits until every port listens on an address other than
76
+ * loopback, up to `seconds`, reading whichever of the kernel's socket tables
77
+ * exist (a guest without IPv6 has no tcp6). Exits 0 when they all listen. */
78
+ export declare function portWaitScript(ports: number[], seconds: number, tables?: string[]): string;
79
+ /** Test hook: forget every process this module has seen, as a fresh client
80
+ * would. */
81
+ export declare function resetProcesses(): void;
82
+ /** `sandbox.process`: run, follow and stop processes, by pid or by name. */
83
+ export declare class SandboxProcess {
84
+ #private;
85
+ constructor(ctx: ProcessContext);
86
+ /** Starts a command. Without waitForCompletion it answers at once, the
87
+ * process running; with it, once the process ends, with its output. */
88
+ exec(request: ProcessRequest | ProcessRequestWithLog): Promise<PostProcessResponse | ProcessResponseWithLog>;
89
+ /** The process, with the output it has written so far. */
90
+ get(identifier: string, _options?: {
91
+ signal?: AbortSignal;
92
+ retry?: boolean;
93
+ }): Promise<GetProcessByIdentifierResponse>;
94
+ /** Waits for the process to end (at most `maxWait` ms, -1 for no limit) and
95
+ * answers it with its output. The process is never stopped by the wait. */
96
+ wait(identifier: string, { maxWait, interval, signal, }?: {
97
+ maxWait?: number;
98
+ interval?: number;
99
+ signal?: AbortSignal;
100
+ }): Promise<GetProcessByIdentifierResponse>;
101
+ /** Every process started through this adapter, running or ended. Their
102
+ * output is left empty: read one with get() or logs(). */
103
+ list(): Promise<GetProcessResponse>;
104
+ /** SIGTERM. */
105
+ stop(identifier: string): Promise<DeleteProcessByIdentifierResponse>;
106
+ /** SIGKILL. */
107
+ kill(identifier: string): Promise<DeleteProcessByIdentifierKillResponse>;
108
+ /** Bytes to the standard input of a process started with `stdin: true`,
109
+ * sent as they are. */
110
+ writeStdin(identifier: string, data: string | Uint8Array): Promise<void>;
111
+ /** Closes the process's standard input (EOF). */
112
+ closeStdin(identifier: string): Promise<void>;
113
+ /** The output so far: both streams in the order written ("all"), or one. */
114
+ logs(identifier: string, type?: "stdout" | "stderr" | "all"): Promise<string>;
115
+ /** Follows the output from the start until the process ends, a line at a
116
+ * time, as Blaxel's streamLogs. `close()` stops following. */
117
+ streamLogs(identifier: string, options?: {
118
+ onLog?: (log: string) => void;
119
+ onStdout?: (stdout: string) => void;
120
+ onStderr?: (stderr: string) => void;
121
+ onError?: (error: Error) => void;
122
+ }): {
123
+ close: () => void;
124
+ wait: () => Promise<void>;
125
+ };
126
+ }