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.
Files changed (49) hide show
  1. package/COMPATIBILITY.md +32 -1
  2. package/README.md +19 -0
  3. package/README.src.md +19 -0
  4. package/dist/agents/index.d.ts +11 -0
  5. package/dist/agents/index.js +12 -0
  6. package/dist/agents-cli.d.ts +10 -0
  7. package/dist/agents-cli.js +362 -0
  8. package/dist/apps-cli.d.ts +2 -0
  9. package/dist/apps-cli.js +173 -0
  10. package/dist/builds/index.d.ts +10 -0
  11. package/dist/builds/index.js +11 -0
  12. package/dist/cli-words.js +1 -0
  13. package/dist/cli.js +176 -21
  14. package/dist/client.d.ts +19 -0
  15. package/dist/client.js +20 -0
  16. package/dist/cloudflare/errors.d.ts +23 -0
  17. package/dist/cloudflare/errors.js +16 -0
  18. package/dist/cloudflare/index.d.ts +82 -9
  19. package/dist/cloudflare/index.js +162 -38
  20. package/dist/cloudflare/interpreter.js +14 -5
  21. package/dist/cloudflare/mime.d.ts +1 -0
  22. package/dist/cloudflare/mime.js +66 -0
  23. package/dist/cloudflare/streams.d.ts +8 -0
  24. package/dist/cloudflare/streams.js +30 -0
  25. package/dist/cloudflare/types.d.ts +338 -0
  26. package/dist/cloudflare/types.js +80 -0
  27. package/dist/cloudflare/worker.d.ts +297 -10
  28. package/dist/cloudflare/worker.js +203 -17
  29. package/dist/containers/index.d.ts +10 -0
  30. package/dist/containers/index.js +11 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.js +1 -0
  33. package/dist/keys.d.ts +6 -0
  34. package/dist/keys.js +10 -1
  35. package/dist/product-names.js +10 -0
  36. package/dist/products/agents.d.ts +272 -0
  37. package/dist/products/agents.js +251 -0
  38. package/dist/products/containers.d.ts +71 -0
  39. package/dist/products/containers.js +6 -0
  40. package/dist/products/index.d.ts +6 -0
  41. package/dist/products/index.js +7 -0
  42. package/dist/products/secrets.d.ts +19 -0
  43. package/dist/products/secrets.js +30 -0
  44. package/dist/sandbox.d.ts +4 -0
  45. package/dist/transport.d.ts +2 -2
  46. package/dist/transport.js +1 -1
  47. package/dist/tunnel.js +1 -1
  48. package/dist/types.d.ts +11 -0
  49. 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
  }
@@ -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: isBinary ? "application/octet-stream" : "text/plain",
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
- bytes =
805
- options.encoding === "base64"
806
- ? Uint8Array.from(atob(content), (c) => c.charCodeAt(0))
807
- : new TextEncoder().encode(content);
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(await this.path(path));
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: "application/octet-stream",
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
- async waitForLog(pattern, timeout = 30000) {
944
- const condition = `log pattern ${String(pattern)}`;
945
- const context = { processId: this.id, command: this.command, condition };
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 this.#waitForLog(pattern, signal);
1031
+ return await wait(signal);
949
1032
  }
950
1033
  catch (error) {
951
1034
  if (!signal.aborted)
952
1035
  throw error;
953
- throw new ProcessReadyTimeoutError(response("PROCESS_READY_TIMEOUT", `Process did not become ready within ${timeout}ms`, { ...context, timeout }, { httpStatus: 408, operation: "process.start" }));
1036
+ throw this.#timedOut(condition, timeout);
954
1037
  }
955
1038
  }
956
- async #waitForLog(pattern, signal) {
957
- let output = "";
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" || e.type === "stderr") {
960
- output += e.data;
961
- const match = typeof pattern === "string" ? output.includes(pattern) : output.match(pattern);
962
- if (match)
963
- return {
964
- line: output
965
- .split("\n")
966
- .find((line) => typeof pattern === "string"
967
- ? line.includes(pattern)
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 new Error("Process output ended before expected output");
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
- return context(await s.interpreter.contexts.create({
110
- language,
111
- cwd: options.cwd ?? "/workspace",
112
- env: Object.fromEntries(Object.entries(options.envVars ?? {}).filter((e) => e[1] !== undefined)),
113
- }, options.timeout !== undefined ? { timeoutMs: options.timeout } : {}));
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) {