withruntime 0.18.0 → 0.23.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/COMPATIBILITY.md +32 -1
- package/README.md +19 -0
- package/README.src.md +19 -0
- package/dist/agents/index.d.ts +11 -0
- package/dist/agents/index.js +12 -0
- package/dist/agents-cli.d.ts +10 -0
- package/dist/agents-cli.js +362 -0
- package/dist/apps-cli.d.ts +2 -0
- package/dist/apps-cli.js +173 -0
- package/dist/builds/index.d.ts +10 -0
- package/dist/builds/index.js +11 -0
- package/dist/cli-words.js +1 -0
- package/dist/cli.js +176 -21
- package/dist/client.d.ts +19 -0
- package/dist/client.js +20 -0
- package/dist/cloudflare/errors.d.ts +23 -0
- package/dist/cloudflare/errors.js +16 -0
- package/dist/cloudflare/index.d.ts +82 -9
- package/dist/cloudflare/index.js +162 -38
- package/dist/cloudflare/interpreter.js +14 -5
- package/dist/cloudflare/mime.d.ts +1 -0
- package/dist/cloudflare/mime.js +66 -0
- package/dist/cloudflare/streams.d.ts +8 -0
- package/dist/cloudflare/streams.js +30 -0
- package/dist/cloudflare/types.d.ts +338 -0
- package/dist/cloudflare/types.js +80 -0
- package/dist/cloudflare/worker.d.ts +297 -10
- package/dist/cloudflare/worker.js +203 -17
- package/dist/containers/index.d.ts +10 -0
- package/dist/containers/index.js +11 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/keys.d.ts +6 -0
- package/dist/keys.js +10 -1
- package/dist/product-names.js +10 -0
- package/dist/products/agents.d.ts +272 -0
- package/dist/products/agents.js +251 -0
- package/dist/products/containers.d.ts +71 -0
- package/dist/products/containers.js +6 -0
- package/dist/products/index.d.ts +6 -0
- package/dist/products/index.js +7 -0
- package/dist/products/secrets.d.ts +19 -0
- package/dist/products/secrets.js +30 -0
- package/dist/sandbox.d.ts +4 -0
- package/dist/transport.d.ts +2 -2
- package/dist/transport.js +1 -1
- package/dist/tunnel.js +1 -1
- package/dist/types.d.ts +11 -0
- package/package.json +16 -1
package/dist/client.js
CHANGED
|
@@ -167,6 +167,26 @@ export class AccountApi {
|
|
|
167
167
|
* name exactly (`me().orgName`). Needs a key for every product made by an
|
|
168
168
|
* owner. Moves no money: what is unspent of a purchase made in the last 15
|
|
169
169
|
* days is refunded on request; other credit is forfeited. */
|
|
170
|
+
/** Which of Runtime's optional emails this key's person gets for this
|
|
171
|
+
* account. Without `set`, reads them; with `{ category, enabled }`, turns
|
|
172
|
+
* one on or off for that person only, and answers all of them. Sign-in,
|
|
173
|
+
* support replies, credit expiry and deletions Runtime decides are always
|
|
174
|
+
* sent and are not listed. */
|
|
175
|
+
async emails(set, options = {}) {
|
|
176
|
+
const answer = set
|
|
177
|
+
? await this.t.json({
|
|
178
|
+
method: "POST",
|
|
179
|
+
path: "/v1/account/emails:update",
|
|
180
|
+
body: { category: set.category, enabled: set.enabled },
|
|
181
|
+
...options,
|
|
182
|
+
})
|
|
183
|
+
: await this.t.json({
|
|
184
|
+
method: "GET",
|
|
185
|
+
path: "/v1/account/emails",
|
|
186
|
+
...options,
|
|
187
|
+
});
|
|
188
|
+
return answer.settings;
|
|
189
|
+
}
|
|
170
190
|
close(input, options = {}) {
|
|
171
191
|
return this.t.json({
|
|
172
192
|
method: "POST",
|
|
@@ -119,6 +119,29 @@ export declare class OperationInterruptedError extends SandboxError<{
|
|
|
119
119
|
}> {
|
|
120
120
|
get reason(): string;
|
|
121
121
|
}
|
|
122
|
+
/** Cloudflare's transport error for its WebSocket sessions; Runtime's API
|
|
123
|
+
* has no such session, so the adapter never throws it. */
|
|
124
|
+
export declare class RPCTransportError extends SandboxError<{
|
|
125
|
+
kind: string;
|
|
126
|
+
originalMessage: string;
|
|
127
|
+
}> {
|
|
128
|
+
get kind(): string;
|
|
129
|
+
get originalMessage(): string;
|
|
130
|
+
}
|
|
131
|
+
/** Input the container refuses, such as content that is not base64. */
|
|
132
|
+
export declare class ValidationFailedError extends SandboxError<{
|
|
133
|
+
validationErrors: {
|
|
134
|
+
field: string;
|
|
135
|
+
message: string;
|
|
136
|
+
code: string;
|
|
137
|
+
}[];
|
|
138
|
+
}> {
|
|
139
|
+
get validationErrors(): {
|
|
140
|
+
field: string;
|
|
141
|
+
message: string;
|
|
142
|
+
code: string;
|
|
143
|
+
}[];
|
|
144
|
+
}
|
|
122
145
|
export declare class BucketMountError extends Error {
|
|
123
146
|
readonly code: string;
|
|
124
147
|
constructor(message: string, code?: string);
|
|
@@ -125,6 +125,22 @@ export class OperationInterruptedError extends SandboxError {
|
|
|
125
125
|
return this.context.reason;
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
+
/** Cloudflare's transport error for its WebSocket sessions; Runtime's API
|
|
129
|
+
* has no such session, so the adapter never throws it. */
|
|
130
|
+
export class RPCTransportError extends SandboxError {
|
|
131
|
+
get kind() {
|
|
132
|
+
return this.context.kind;
|
|
133
|
+
}
|
|
134
|
+
get originalMessage() {
|
|
135
|
+
return this.context.originalMessage;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Input the container refuses, such as content that is not base64. */
|
|
139
|
+
export class ValidationFailedError extends SandboxError {
|
|
140
|
+
get validationErrors() {
|
|
141
|
+
return this.context.validationErrors;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
128
144
|
export class BucketMountError extends Error {
|
|
129
145
|
code;
|
|
130
146
|
constructor(message, code = "BUCKET_MOUNT_ERROR") {
|
|
@@ -6,8 +6,11 @@ export { CompatibilityError };
|
|
|
6
6
|
export * from "./errors.js";
|
|
7
7
|
export { CodeInterpreter } from "./interpreter.js";
|
|
8
8
|
export type { CodeContext, CreateContextOptions, ExecutionError, OutputMessage, RunCodeOptions, } from "./interpreter.js";
|
|
9
|
-
export { collectFile, isExecResult, isProcess, isProcessStatus, parseSSEStream, streamFile, } from "./streams.js";
|
|
9
|
+
export { collectFile, isExecResult, isProcess, isProcessStatus, parseSSEStream, streamFile, asyncIterableToSSEStream, responseToAsyncIterable, } from "./streams.js";
|
|
10
10
|
export type { FileChunk, FileMetadata, FileStreamEvent } from "./streams.js";
|
|
11
|
+
export * from "./types.js";
|
|
12
|
+
/** Cloudflare's interface for its Sandbox class. */
|
|
13
|
+
export type ISandbox = Sandbox;
|
|
11
14
|
export declare class SandboxSecurityError extends Error {
|
|
12
15
|
readonly code: string;
|
|
13
16
|
constructor(message: string, code: string);
|
|
@@ -58,6 +61,13 @@ export type CheckChangesResult = {
|
|
|
58
61
|
version: string;
|
|
59
62
|
timestamp: string;
|
|
60
63
|
};
|
|
64
|
+
export interface GitCheckoutOptions {
|
|
65
|
+
branch?: string;
|
|
66
|
+
targetDir?: string;
|
|
67
|
+
sessionId?: string;
|
|
68
|
+
depth?: number;
|
|
69
|
+
cloneTimeoutMs?: number;
|
|
70
|
+
}
|
|
61
71
|
export interface GitCheckoutResult {
|
|
62
72
|
success: boolean;
|
|
63
73
|
repoUrl: string;
|
|
@@ -96,6 +106,7 @@ export interface ReadFileResult {
|
|
|
96
106
|
content: string;
|
|
97
107
|
encoding: "utf-8" | "base64";
|
|
98
108
|
isBinary: boolean;
|
|
109
|
+
mimeType: string;
|
|
99
110
|
size: number;
|
|
100
111
|
timestamp: string;
|
|
101
112
|
}
|
|
@@ -115,6 +126,16 @@ export interface SessionOptions {
|
|
|
115
126
|
isolation?: boolean;
|
|
116
127
|
commandTimeoutMs?: number;
|
|
117
128
|
}
|
|
129
|
+
export interface WaitForPortOptions {
|
|
130
|
+
mode?: "http" | "tcp";
|
|
131
|
+
path?: string;
|
|
132
|
+
status?: number | {
|
|
133
|
+
min: number;
|
|
134
|
+
max: number;
|
|
135
|
+
};
|
|
136
|
+
timeout?: number;
|
|
137
|
+
interval?: number;
|
|
138
|
+
}
|
|
118
139
|
export interface ProcessOptions {
|
|
119
140
|
timeout?: number;
|
|
120
141
|
env?: Record<string, string | undefined>;
|
|
@@ -312,13 +333,7 @@ export declare class Sandbox {
|
|
|
312
333
|
signal?: AbortSignal;
|
|
313
334
|
}): Promise<ReadableStream<Uint8Array<ArrayBufferLike>>>;
|
|
314
335
|
/** Clones with git in the sandbox, as Cloudflare's container does. */
|
|
315
|
-
gitCheckout(repoUrl: string, options?:
|
|
316
|
-
branch?: string;
|
|
317
|
-
targetDir?: string;
|
|
318
|
-
sessionId?: string;
|
|
319
|
-
depth?: number;
|
|
320
|
-
cloneTimeoutMs?: number;
|
|
321
|
-
}): Promise<GitCheckoutResult>;
|
|
336
|
+
gitCheckout(repoUrl: string, options?: GitCheckoutOptions): Promise<GitCheckoutResult>;
|
|
322
337
|
/** Changes under a directory as `data:` lines of Cloudflare's watch events,
|
|
323
338
|
* once the watch is established, until the stream is cancelled. */
|
|
324
339
|
watch(path: string, options?: WatchOptions): Promise<ReadableStream<Uint8Array>>;
|
|
@@ -442,6 +457,57 @@ export declare class ExecutionSession {
|
|
|
442
457
|
timestamp: string;
|
|
443
458
|
}>;
|
|
444
459
|
setEnvVars(env: Record<string, string | undefined>): Promise<void>;
|
|
460
|
+
moveFile(path: string, newPath: string): Promise<{
|
|
461
|
+
success: boolean;
|
|
462
|
+
path: string;
|
|
463
|
+
newPath: string;
|
|
464
|
+
timestamp: string;
|
|
465
|
+
}>;
|
|
466
|
+
readFileStream(path: string): Promise<ReadableStream<Uint8Array<ArrayBufferLike>>>;
|
|
467
|
+
gitCheckout(repoUrl: string, options?: Omit<GitCheckoutOptions, "sessionId">): Promise<GitCheckoutResult>;
|
|
468
|
+
watch(path: string, options?: Omit<WatchOptions, "sessionId">): Promise<ReadableStream<Uint8Array<ArrayBufferLike>>>;
|
|
469
|
+
checkChanges(path: string, options?: Omit<CheckChangesOptions, "sessionId">): Promise<CheckChangesResult>;
|
|
470
|
+
listProcesses(): Promise<Process[]>;
|
|
471
|
+
getProcess(id: string): Promise<Process | null>;
|
|
472
|
+
killProcess(id: string, signal?: string): Promise<void>;
|
|
473
|
+
killAllProcesses(): Promise<number>;
|
|
474
|
+
cleanupCompletedProcesses(): Promise<number>;
|
|
475
|
+
getProcessLogs(id: string): Promise<{
|
|
476
|
+
stdout: string;
|
|
477
|
+
stderr: string;
|
|
478
|
+
processId: string;
|
|
479
|
+
}>;
|
|
480
|
+
streamProcessLogs(id: string, options?: {
|
|
481
|
+
signal?: AbortSignal;
|
|
482
|
+
}): Promise<ReadableStream<Uint8Array<ArrayBufferLike>>>;
|
|
483
|
+
createCodeContext(options?: CreateContextOptions): Promise<CodeContext>;
|
|
484
|
+
runCode(code: string, options?: RunCodeOptions): Promise<{
|
|
485
|
+
code: string;
|
|
486
|
+
logs: {
|
|
487
|
+
stdout: string[];
|
|
488
|
+
stderr: string[];
|
|
489
|
+
};
|
|
490
|
+
error: import("./interpreter.js").ExecutionError | undefined;
|
|
491
|
+
executionCount: number | undefined;
|
|
492
|
+
results: {
|
|
493
|
+
text: string | undefined;
|
|
494
|
+
html: string | undefined;
|
|
495
|
+
png: string | undefined;
|
|
496
|
+
jpeg: string | undefined;
|
|
497
|
+
svg: string | undefined;
|
|
498
|
+
latex: string | undefined;
|
|
499
|
+
markdown: string | undefined;
|
|
500
|
+
javascript: string | undefined;
|
|
501
|
+
json: unknown;
|
|
502
|
+
chart: undefined;
|
|
503
|
+
data: Record<string, unknown> | undefined;
|
|
504
|
+
}[];
|
|
505
|
+
}>;
|
|
506
|
+
runCodeStream(code: string, options?: RunCodeOptions): Promise<ReadableStream<any>>;
|
|
507
|
+
listCodeContexts(): Promise<CodeContext[]>;
|
|
508
|
+
deleteCodeContext(contextId: string): Promise<void>;
|
|
509
|
+
mountBucket(bucket: string, mountPath: string, options: MountBucketOptions): Promise<void>;
|
|
510
|
+
unmountBucket(mountPath: string): Promise<void>;
|
|
445
511
|
}
|
|
446
512
|
export declare class Process {
|
|
447
513
|
#private;
|
|
@@ -466,7 +532,14 @@ export declare class Process {
|
|
|
466
532
|
exitCode: number;
|
|
467
533
|
}>;
|
|
468
534
|
waitForLog(pattern: string | RegExp, timeout?: number): Promise<{
|
|
469
|
-
match?: true | RegExpMatchArray | undefined;
|
|
470
535
|
line: string;
|
|
536
|
+
match?: undefined;
|
|
537
|
+
} | {
|
|
538
|
+
line: string;
|
|
539
|
+
match: RegExpMatchArray;
|
|
471
540
|
}>;
|
|
541
|
+
/** Resolves once the port answers: an HTTP status in range (200-399 by
|
|
542
|
+
* default) at `path`, or a TCP connection in `tcp` mode. Throws if the
|
|
543
|
+
* process ends first, or after `timeout` milliseconds when one is given. */
|
|
544
|
+
waitForPort(port: number, options?: WaitForPortOptions): Promise<void>;
|
|
472
545
|
}
|
package/dist/cloudflare/index.js
CHANGED
|
@@ -3,14 +3,16 @@ import { Runtime } from "../client.js";
|
|
|
3
3
|
import { NotFoundError } from "../errors.js";
|
|
4
4
|
import { timeoutSignal } from "../transport.js";
|
|
5
5
|
import { quote, client, closeShell, CompatibilityError, refuse, destroy, environment, execOptions, only, pathAt, saveEnvironment, shellCommand, shellRoot, } from "../compat/core.js";
|
|
6
|
-
import { BucketUnmountError, InvalidMountConfigError, ProcessExitedBeforeReadyError, ProcessReadyTimeoutError, S3FSMountError, response, } from "./errors.js";
|
|
6
|
+
import { BucketUnmountError, InvalidMountConfigError, ProcessExitedBeforeReadyError, ProcessReadyTimeoutError, S3FSMountError, ValidationFailedError, response, } from "./errors.js";
|
|
7
7
|
import { CodeInterpreter, hasEnv, RUN_ENV, } from "./interpreter.js";
|
|
8
8
|
import { sseStream } from "./streams.js";
|
|
9
|
+
import { mimeType } from "./mime.js";
|
|
9
10
|
import { CHANGES, watchFilters } from "./watch.js";
|
|
10
11
|
export { CompatibilityError };
|
|
11
12
|
export * from "./errors.js";
|
|
12
13
|
export { CodeInterpreter } from "./interpreter.js";
|
|
13
|
-
export { collectFile, isExecResult, isProcess, isProcessStatus, parseSSEStream, streamFile, } from "./streams.js";
|
|
14
|
+
export { collectFile, isExecResult, isProcess, isProcessStatus, parseSSEStream, streamFile, asyncIterableToSSEStream, responseToAsyncIterable, } from "./streams.js";
|
|
15
|
+
export * from "./types.js";
|
|
14
16
|
export class SandboxSecurityError extends Error {
|
|
15
17
|
code;
|
|
16
18
|
constructor(message, code) {
|
|
@@ -262,7 +264,7 @@ export class Sandbox {
|
|
|
262
264
|
return sseStream(async (send) => {
|
|
263
265
|
send({
|
|
264
266
|
type: "metadata",
|
|
265
|
-
mimeType:
|
|
267
|
+
mimeType: read.mimeType,
|
|
266
268
|
size: bytes.length,
|
|
267
269
|
isBinary,
|
|
268
270
|
encoding: isBinary ? "base64" : "utf-8",
|
|
@@ -800,11 +802,18 @@ export class ExecutionSession {
|
|
|
800
802
|
if (options.encoding && !["utf8", "utf-8", "base64"].includes(options.encoding))
|
|
801
803
|
throw new CompatibilityError("Cloudflare", `file encoding ${options.encoding}`);
|
|
802
804
|
let bytes;
|
|
803
|
-
if (typeof content === "string")
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
:
|
|
805
|
+
if (typeof content === "string" && options.encoding === "base64") {
|
|
806
|
+
// Cloudflare's container refuses anything but base64's alphabet.
|
|
807
|
+
if (!/^[A-Za-z0-9+/=]*$/.test(content))
|
|
808
|
+
throw new ValidationFailedError(response("VALIDATION_FAILED", `Invalid base64 content for '${path}': must contain only A-Z, a-z, 0-9, +, /, =`, {
|
|
809
|
+
validationErrors: [
|
|
810
|
+
{ field: "content", message: "Invalid base64 characters", code: "INVALID_BASE64" },
|
|
811
|
+
],
|
|
812
|
+
}, { httpStatus: 400 }));
|
|
813
|
+
bytes = Uint8Array.from(atob(content), (c) => c.charCodeAt(0));
|
|
814
|
+
}
|
|
815
|
+
else if (typeof content === "string")
|
|
816
|
+
bytes = new TextEncoder().encode(content);
|
|
808
817
|
else {
|
|
809
818
|
const buffer = await new Response(content).arrayBuffer();
|
|
810
819
|
bytes = new Uint8Array(buffer);
|
|
@@ -813,7 +822,7 @@ export class ExecutionSession {
|
|
|
813
822
|
return { success: true, path, bytesWritten: bytes.length, timestamp: new Date().toISOString() };
|
|
814
823
|
}
|
|
815
824
|
async readFile(path, options = {}) {
|
|
816
|
-
const s = await this.sandbox.native(), bytes = await s.files.read(
|
|
825
|
+
const s = await this.sandbox.native(), resolved = await this.path(path), bytes = await s.files.read(resolved), type = mimeType(resolved, bytes);
|
|
817
826
|
if (options.encoding === "none")
|
|
818
827
|
return {
|
|
819
828
|
success: true,
|
|
@@ -825,7 +834,7 @@ export class ExecutionSession {
|
|
|
825
834
|
},
|
|
826
835
|
}),
|
|
827
836
|
size: bytes.length,
|
|
828
|
-
mimeType:
|
|
837
|
+
mimeType: type,
|
|
829
838
|
timestamp: new Date().toISOString(),
|
|
830
839
|
};
|
|
831
840
|
const text = new TextDecoder().decode(bytes), binary = text.includes("\0") || text.includes("\ufffd"), encoded = options.encoding === "base64" || (options.encoding === undefined && binary);
|
|
@@ -839,6 +848,7 @@ export class ExecutionSession {
|
|
|
839
848
|
content: encoded ? btoa(raw) : text,
|
|
840
849
|
encoding: encoded ? "base64" : "utf-8",
|
|
841
850
|
isBinary: binary,
|
|
851
|
+
mimeType: type,
|
|
842
852
|
size: bytes.length,
|
|
843
853
|
timestamp: new Date().toISOString(),
|
|
844
854
|
};
|
|
@@ -888,7 +898,68 @@ export class ExecutionSession {
|
|
|
888
898
|
this.options = { ...this.options, env: { ...this.options.env, ...cleanEnv(env) } };
|
|
889
899
|
await (await this.sandbox.native()).files.write(`/workspace/.runtime-compat/cloudflare/${encodeURIComponent(this.id)}.json`, JSON.stringify(this.options), { mode: 0o600 });
|
|
890
900
|
}
|
|
901
|
+
// The rest of Cloudflare's ExecutionSession: the sandbox's calls, in this session.
|
|
902
|
+
moveFile(path, newPath) {
|
|
903
|
+
return this.renameFile(path, newPath);
|
|
904
|
+
}
|
|
905
|
+
readFileStream(path) {
|
|
906
|
+
return this.sandbox.readFileStream(path, { sessionId: this.id });
|
|
907
|
+
}
|
|
908
|
+
gitCheckout(repoUrl, options = {}) {
|
|
909
|
+
return this.sandbox.gitCheckout(repoUrl, { ...options, sessionId: this.id });
|
|
910
|
+
}
|
|
911
|
+
watch(path, options = {}) {
|
|
912
|
+
return this.sandbox.watch(path, { ...options, sessionId: this.id });
|
|
913
|
+
}
|
|
914
|
+
checkChanges(path, options = {}) {
|
|
915
|
+
return this.sandbox.checkChanges(path, { ...options, sessionId: this.id });
|
|
916
|
+
}
|
|
917
|
+
listProcesses() {
|
|
918
|
+
return this.sandbox.listProcesses();
|
|
919
|
+
}
|
|
920
|
+
getProcess(id) {
|
|
921
|
+
return this.sandbox.getProcess(id);
|
|
922
|
+
}
|
|
923
|
+
killProcess(id, signal) {
|
|
924
|
+
return this.sandbox.killProcess(id, signal);
|
|
925
|
+
}
|
|
926
|
+
killAllProcesses() {
|
|
927
|
+
return this.sandbox.killAllProcesses();
|
|
928
|
+
}
|
|
929
|
+
cleanupCompletedProcesses() {
|
|
930
|
+
return this.sandbox.cleanupCompletedProcesses();
|
|
931
|
+
}
|
|
932
|
+
getProcessLogs(id) {
|
|
933
|
+
return this.sandbox.getProcessLogs(id);
|
|
934
|
+
}
|
|
935
|
+
streamProcessLogs(id, options) {
|
|
936
|
+
return this.sandbox.streamProcessLogs(id, options);
|
|
937
|
+
}
|
|
938
|
+
createCodeContext(options) {
|
|
939
|
+
return this.sandbox.createCodeContext(options);
|
|
940
|
+
}
|
|
941
|
+
runCode(code, options) {
|
|
942
|
+
return this.sandbox.runCode(code, options);
|
|
943
|
+
}
|
|
944
|
+
runCodeStream(code, options) {
|
|
945
|
+
return this.sandbox.runCodeStream(code, options);
|
|
946
|
+
}
|
|
947
|
+
listCodeContexts() {
|
|
948
|
+
return this.sandbox.listCodeContexts();
|
|
949
|
+
}
|
|
950
|
+
deleteCodeContext(contextId) {
|
|
951
|
+
return this.sandbox.deleteCodeContext(contextId);
|
|
952
|
+
}
|
|
953
|
+
mountBucket(bucket, mountPath, options) {
|
|
954
|
+
return this.sandbox.mountBucket(bucket, mountPath, options);
|
|
955
|
+
}
|
|
956
|
+
unmountBucket(mountPath) {
|
|
957
|
+
return this.sandbox.unmountBucket(mountPath);
|
|
958
|
+
}
|
|
891
959
|
}
|
|
960
|
+
refuse("Cloudflare", "ExecutionSession", ExecutionSession, {
|
|
961
|
+
terminal: "open a terminal with the native Runtime sandbox's terminal()",
|
|
962
|
+
});
|
|
892
963
|
export class Process {
|
|
893
964
|
sandbox;
|
|
894
965
|
native;
|
|
@@ -940,43 +1011,96 @@ export class Process {
|
|
|
940
1011
|
});
|
|
941
1012
|
return { exitCode: r.exitCode ?? -1 };
|
|
942
1013
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1014
|
+
/** Cloudflare's errors for a wait, with its messages: a timeout, or the
|
|
1015
|
+
* process ending first. */
|
|
1016
|
+
#timedOut(condition, timeout) {
|
|
1017
|
+
return new ProcessReadyTimeoutError(response("PROCESS_READY_TIMEOUT", `Process did not become ready within ${timeout}ms. Waiting for: ${condition}`, { processId: this.id, command: this.command, condition, timeout }, {
|
|
1018
|
+
httpStatus: 408,
|
|
1019
|
+
suggestion: `Check if your process outputs ${condition}. You can increase the timeout parameter.`,
|
|
1020
|
+
}));
|
|
1021
|
+
}
|
|
1022
|
+
#exited(condition, exitCode) {
|
|
1023
|
+
return new ProcessExitedBeforeReadyError(response("PROCESS_EXITED_BEFORE_READY", `Process exited with code ${exitCode} before becoming ready. Waiting for: ${condition}`, { processId: this.id, command: this.command, condition, exitCode }, { httpStatus: 500, suggestion: "Check process logs with getLogs() for error messages" }));
|
|
1024
|
+
}
|
|
1025
|
+
/** Waits as Cloudflare does: with no limit unless `timeout` is given. */
|
|
1026
|
+
async #within(condition, timeout, wait) {
|
|
1027
|
+
if (timeout === undefined)
|
|
1028
|
+
return wait();
|
|
946
1029
|
const signal = timeoutSignal(timeout);
|
|
947
1030
|
try {
|
|
948
|
-
return await
|
|
1031
|
+
return await wait(signal);
|
|
949
1032
|
}
|
|
950
1033
|
catch (error) {
|
|
951
1034
|
if (!signal.aborted)
|
|
952
1035
|
throw error;
|
|
953
|
-
throw
|
|
1036
|
+
throw this.#timedOut(condition, timeout);
|
|
954
1037
|
}
|
|
955
1038
|
}
|
|
956
|
-
async
|
|
957
|
-
|
|
1039
|
+
async waitForLog(pattern, timeout) {
|
|
1040
|
+
const condition = typeof pattern === "string" ? `"${pattern}"` : pattern.toString();
|
|
1041
|
+
return this.#within(condition, timeout, (signal) => this.#waitForLog(pattern, condition, signal));
|
|
1042
|
+
}
|
|
1043
|
+
async #waitForLog(pattern, condition, signal) {
|
|
1044
|
+
let stdout = "", stderr = "";
|
|
958
1045
|
for await (const e of this.native.output({ signal })) {
|
|
959
|
-
if (e.type === "stdout"
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
: new RegExp(pattern.source, pattern.flags.replace("g", "")).test(line)) ?? output,
|
|
969
|
-
...(typeof pattern === "string" ? {} : { match }),
|
|
970
|
-
};
|
|
971
|
-
}
|
|
972
|
-
else if (e.type === "exit")
|
|
973
|
-
throw new ProcessExitedBeforeReadyError(response("PROCESS_EXITED_BEFORE_READY", "Process exited before expected output", {
|
|
974
|
-
processId: this.id,
|
|
975
|
-
command: this.command,
|
|
976
|
-
condition: `log pattern ${String(pattern)}`,
|
|
977
|
-
exitCode: e.exitCode ?? -1,
|
|
978
|
-
}, { httpStatus: 409, operation: "process.start" }));
|
|
1046
|
+
if (e.type === "stdout")
|
|
1047
|
+
stdout += e.data;
|
|
1048
|
+
else if (e.type === "stderr")
|
|
1049
|
+
stderr += e.data;
|
|
1050
|
+
const found = matchLine(stdout, pattern) ?? matchLine(stderr, pattern);
|
|
1051
|
+
if (found)
|
|
1052
|
+
return found;
|
|
1053
|
+
if (e.type === "exit")
|
|
1054
|
+
throw this.#exited(condition, e.exitCode ?? 1);
|
|
979
1055
|
}
|
|
980
|
-
throw
|
|
1056
|
+
throw this.#exited(condition, 0);
|
|
1057
|
+
}
|
|
1058
|
+
/** Resolves once the port answers: an HTTP status in range (200-399 by
|
|
1059
|
+
* default) at `path`, or a TCP connection in `tcp` mode. Throws if the
|
|
1060
|
+
* process ends first, or after `timeout` milliseconds when one is given. */
|
|
1061
|
+
async waitForPort(port, options = {}) {
|
|
1062
|
+
only("Cloudflare waitForPort", options, ["mode", "path", "status", "timeout", "interval"]);
|
|
1063
|
+
const { mode = "http", path = "/", status = { min: 200, max: 399 }, interval = 500 } = options;
|
|
1064
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
1065
|
+
throw new TypeError(`Invalid port number: ${port}`);
|
|
1066
|
+
if (mode !== "http" && mode !== "tcp")
|
|
1067
|
+
throw new TypeError("mode is http or tcp");
|
|
1068
|
+
const condition = mode === "http" ? `port ${port} (HTTP ${path})` : `port ${port} (TCP)`;
|
|
1069
|
+
const min = typeof status === "number" ? status : status.min;
|
|
1070
|
+
const max = typeof status === "number" ? status : status.max;
|
|
1071
|
+
const url = `http://127.0.0.1:${port}${path.startsWith("/") ? path : `/${path}`}`;
|
|
1072
|
+
const probe = mode === "http"
|
|
1073
|
+
? `code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 ${quote(url)}) && [ "$code" -ge ${min} ] && [ "$code" -le ${max} ]`
|
|
1074
|
+
: `exec 3<>/dev/tcp/127.0.0.1/${port}`;
|
|
1075
|
+
const s = await this.sandbox.native();
|
|
1076
|
+
await this.#within(condition, options.timeout, async (signal) => {
|
|
1077
|
+
for (;;) {
|
|
1078
|
+
await this.native.refresh();
|
|
1079
|
+
if (this.native.info.state !== "running")
|
|
1080
|
+
throw this.#exited(condition, this.native.info.exitCode ?? 1);
|
|
1081
|
+
if ((await s.exec(["bash", "-c", probe], { signal })).exitCode === 0)
|
|
1082
|
+
return;
|
|
1083
|
+
await new Promise((done) => setTimeout(done, interval));
|
|
1084
|
+
signal?.throwIfAborted();
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
981
1087
|
}
|
|
982
1088
|
}
|
|
1089
|
+
/** The first line holding a match, as Cloudflare's waitForLog answers. */
|
|
1090
|
+
function matchLine(text, pattern) {
|
|
1091
|
+
if (typeof pattern === "string") {
|
|
1092
|
+
if (!text.includes(pattern))
|
|
1093
|
+
return null;
|
|
1094
|
+
return { line: text.split("\n").find((line) => line.includes(pattern)) ?? pattern };
|
|
1095
|
+
}
|
|
1096
|
+
const safe = new RegExp(pattern.source, pattern.flags.replace("g", ""));
|
|
1097
|
+
const match = text.match(safe);
|
|
1098
|
+
if (!match)
|
|
1099
|
+
return null;
|
|
1100
|
+
for (const line of text.split("\n")) {
|
|
1101
|
+
const lineMatch = line.match(safe);
|
|
1102
|
+
if (lineMatch)
|
|
1103
|
+
return { line, match: lineMatch };
|
|
1104
|
+
}
|
|
1105
|
+
return { line: match[0], match };
|
|
1106
|
+
}
|
|
@@ -106,11 +106,20 @@ export class CodeInterpreter {
|
|
|
106
106
|
only("Cloudflare createCodeContext", options, ["language", "cwd", "envVars", "timeout"]);
|
|
107
107
|
const language = this.#language(options.language);
|
|
108
108
|
const s = await this.#native();
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
try {
|
|
110
|
+
return context(await s.interpreter.contexts.create({
|
|
111
|
+
language,
|
|
112
|
+
cwd: options.cwd ?? "/workspace",
|
|
113
|
+
env: Object.fromEntries(Object.entries(options.envVars ?? {}).filter((e) => e[1] !== undefined)),
|
|
114
|
+
}, options.timeout !== undefined ? { timeoutMs: options.timeout } : {}));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// Cloudflare's container has no fixed count; Runtime's does. Say so,
|
|
118
|
+
// and what frees one, in the message: only it survives Worker RPC.
|
|
119
|
+
if (error.code !== "context_limit")
|
|
120
|
+
throw error;
|
|
121
|
+
throw new Error(`${error.message} Cloudflare's container has no fixed limit; delete a context you no longer need with deleteCodeContext(), and list them with listCodeContexts(). runCode without a context uses one per language.`, { cause: error });
|
|
122
|
+
}
|
|
114
123
|
}
|
|
115
124
|
async runCode(code, options = {}) {
|
|
116
125
|
if (hasEnv(options.envVars))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function mimeType(path: string, bytes: Uint8Array): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** A file's MIME type as Cloudflare's container reports it: from the
|
|
2
|
+
* extension first, then from the content (`file --mime-type` there; the
|
|
3
|
+
* common signatures and text here), else application/octet-stream. */
|
|
4
|
+
const BY_EXTENSION = {
|
|
5
|
+
png: "image/png",
|
|
6
|
+
jpg: "image/jpeg",
|
|
7
|
+
jpeg: "image/jpeg",
|
|
8
|
+
gif: "image/gif",
|
|
9
|
+
webp: "image/webp",
|
|
10
|
+
svg: "image/svg+xml",
|
|
11
|
+
ico: "image/x-icon",
|
|
12
|
+
bmp: "image/bmp",
|
|
13
|
+
avif: "image/avif",
|
|
14
|
+
pdf: "application/pdf",
|
|
15
|
+
zip: "application/zip",
|
|
16
|
+
gz: "application/gzip",
|
|
17
|
+
tgz: "application/gzip",
|
|
18
|
+
tar: "application/x-tar",
|
|
19
|
+
wasm: "application/wasm",
|
|
20
|
+
json: "application/json",
|
|
21
|
+
xml: "application/xml",
|
|
22
|
+
js: "text/javascript",
|
|
23
|
+
mjs: "text/javascript",
|
|
24
|
+
cjs: "text/javascript",
|
|
25
|
+
css: "text/css",
|
|
26
|
+
html: "text/html",
|
|
27
|
+
htm: "text/html",
|
|
28
|
+
md: "text/markdown",
|
|
29
|
+
txt: "text/plain",
|
|
30
|
+
csv: "text/csv",
|
|
31
|
+
yaml: "text/yaml",
|
|
32
|
+
yml: "text/yaml",
|
|
33
|
+
py: "text/x-python",
|
|
34
|
+
sh: "application/x-sh",
|
|
35
|
+
mp3: "audio/mpeg",
|
|
36
|
+
wav: "audio/wav",
|
|
37
|
+
mp4: "video/mp4",
|
|
38
|
+
webm: "video/webm",
|
|
39
|
+
woff: "font/woff",
|
|
40
|
+
woff2: "font/woff2",
|
|
41
|
+
ttf: "font/ttf",
|
|
42
|
+
};
|
|
43
|
+
const SIGNATURES = [
|
|
44
|
+
[[0x89, 0x50, 0x4e, 0x47], "image/png"],
|
|
45
|
+
[[0xff, 0xd8, 0xff], "image/jpeg"],
|
|
46
|
+
[[0x47, 0x49, 0x46, 0x38], "image/gif"],
|
|
47
|
+
[[0x25, 0x50, 0x44, 0x46], "application/pdf"],
|
|
48
|
+
[[0x50, 0x4b, 0x03, 0x04], "application/zip"],
|
|
49
|
+
[[0x1f, 0x8b], "application/gzip"],
|
|
50
|
+
[[0x00, 0x61, 0x73, 0x6d], "application/wasm"],
|
|
51
|
+
[[0x7f, 0x45, 0x4c, 0x46], "application/x-executable"],
|
|
52
|
+
];
|
|
53
|
+
export function mimeType(path, bytes) {
|
|
54
|
+
const name = path.split("/").pop() ?? "";
|
|
55
|
+
const dot = name.lastIndexOf(".");
|
|
56
|
+
const known = dot > 0 ? BY_EXTENSION[name.slice(dot + 1).toLowerCase()] : undefined;
|
|
57
|
+
if (known)
|
|
58
|
+
return known;
|
|
59
|
+
for (const [signature, type] of SIGNATURES)
|
|
60
|
+
if (signature.every((byte, i) => bytes[i] === byte))
|
|
61
|
+
return type;
|
|
62
|
+
if (bytes.length === 0)
|
|
63
|
+
return "inode/x-empty";
|
|
64
|
+
const text = new TextDecoder().decode(bytes.subarray(0, 8192));
|
|
65
|
+
return text.includes("\0") || text.includes("�") ? "application/octet-stream" : "text/plain";
|
|
66
|
+
}
|
|
@@ -25,6 +25,14 @@ export declare function sseStream<T>(events: (send: (event: T) => void, signal:
|
|
|
25
25
|
/** The events of a server-sent-events stream, each `data:` parsed as JSON;
|
|
26
26
|
* `[DONE]`, blank and unparsable data are skipped, as Cloudflare's do. */
|
|
27
27
|
export declare function parseSSEStream<T>(stream: ReadableStream<Uint8Array>, signal?: AbortSignal): AsyncIterable<T>;
|
|
28
|
+
/** The events of a server-sent-events Response, as parseSSEStream reads them. */
|
|
29
|
+
export declare function responseToAsyncIterable<T>(response: Response, signal?: AbortSignal): AsyncIterable<T>;
|
|
30
|
+
/** Events as a server-sent-events stream, one `data:` line each, then
|
|
31
|
+
* `data: [DONE]`, as Cloudflare's helper writes them. */
|
|
32
|
+
export declare function asyncIterableToSSEStream<T>(events: AsyncIterable<T>, options?: {
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
serialize?: (event: T) => string;
|
|
35
|
+
}): ReadableStream<Uint8Array>;
|
|
28
36
|
/** A `readFileStream()` stream's chunks: strings for text, bytes for binary.
|
|
29
37
|
* Returns the file's metadata. */
|
|
30
38
|
export declare function streamFile(stream: ReadableStream<Uint8Array>): AsyncGenerator<FileChunk, FileMetadata>;
|
|
@@ -77,6 +77,36 @@ export async function* parseSSEStream(stream, signal) {
|
|
|
77
77
|
reader.releaseLock();
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
/** The events of a server-sent-events Response, as parseSSEStream reads them. */
|
|
81
|
+
export async function* responseToAsyncIterable(response, signal) {
|
|
82
|
+
if (!response.ok)
|
|
83
|
+
throw new Error(`Response not ok: ${response.status} ${response.statusText}`);
|
|
84
|
+
if (!response.body)
|
|
85
|
+
throw new Error("No response body");
|
|
86
|
+
yield* parseSSEStream(response.body, signal);
|
|
87
|
+
}
|
|
88
|
+
/** Events as a server-sent-events stream, one `data:` line each, then
|
|
89
|
+
* `data: [DONE]`, as Cloudflare's helper writes them. */
|
|
90
|
+
export function asyncIterableToSSEStream(events, options = {}) {
|
|
91
|
+
const encoder = new TextEncoder();
|
|
92
|
+
const serialize = options.serialize ?? ((event) => JSON.stringify(event));
|
|
93
|
+
return new ReadableStream({
|
|
94
|
+
async start(controller) {
|
|
95
|
+
try {
|
|
96
|
+
for await (const event of events) {
|
|
97
|
+
if (options.signal?.aborted)
|
|
98
|
+
throw new Error("Operation was aborted");
|
|
99
|
+
controller.enqueue(encoder.encode(`data: ${serialize(event)}\n\n`));
|
|
100
|
+
}
|
|
101
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
102
|
+
controller.close();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
controller.error(error);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
80
110
|
/** A `readFileStream()` stream's chunks: strings for text, bytes for binary.
|
|
81
111
|
* Returns the file's metadata. */
|
|
82
112
|
export async function* streamFile(stream) {
|