@cloudflare/sandbox 0.8.4 → 0.8.5

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as CheckChangesOptions, A as DesktopStopResponse, At as SandboxOptions, B as ExecuteResponse, Bt as ExposePortRequest, C as ClickOptions, Ct as ProcessListResult, D as DesktopStartOptions, Dt as ProcessStatus, E as DesktopClient, Et as ProcessStartResult, F as ScreenshotRegion, Ft as WatchOptions, G as HttpClientOptions, Gt as Execution, H as BaseApiResponse, Ht as PtyOptions, I as ScreenshotResponse, It as isExecResult, J as SessionRequest, K as RequestConfig, Kt as ExecutionResult, L as ScrollDirection, Lt as isProcess, M as ScreenSizeResponse, Mt as StreamOptions, N as ScreenshotBytesResponse, Nt as WaitForLogResult, O as DesktopStartResponse, Ot as RemoteMountBucketOptions, P as ScreenshotOptions, Pt as WaitForPortOptions, Q as BucketProvider, R as TypeOptions, Rt as isProcessStatus, S as WriteFileRequest, St as ProcessKillResult, T as Desktop, Tt as ProcessOptions, U as ContainerStub, Ut as CodeContext, V as BackupClient, Vt as StartProcessRequest, W as ErrorResponse, Wt as CreateContextOptions, X as BaseExecOptions, Y as BackupOptions, Z as BucketCredentials, _ as GitClient, _t as PortExposeResult, a as CreateSessionRequest, at as ExecutionSession, b as MkdirRequest, bt as ProcessCleanupResult, c as DeleteSessionResponse, ct as FileStreamEvent, d as ProcessClient, dt as ISandbox, et as CheckChangesResult, f as PortClient, ft as ListFilesOptions, g as GitCheckoutRequest, gt as PortCloseResult, h as InterpreterClient, ht as MountBucketOptions, i as CommandsResponse, it as ExecResult, j as KeyInput, jt as SessionOptions, k as DesktopStatusResponse, kt as RestoreBackupResult, l as PingResponse, lt as FileWatchSSEEvent, m as ExecutionCallbacks, mt as LogEvent, n as getSandbox, nt as ExecEvent, o as CreateSessionResponse, ot as FileChunk, p as UnexposePortRequest, pt as LocalMountBucketOptions, q as ResponseHandler, qt as RunCodeOptions, r as SandboxClient, rt as ExecOptions, s as DeleteSessionRequest, st as FileMetadata, t as Sandbox, tt as DirectoryBackup, u as UtilityClient, ut as GitCheckoutResult, v as FileClient, vt as PortListResult, w as CursorPositionResponse, wt as ProcessLogsResult, x as ReadFileRequest, xt as ProcessInfoResult, y as FileOperationRequest, yt as Process, z as CommandClient, zt as ExecuteRequest } from "./sandbox-BoLbdjOe.js";
1
+ import { $ as CheckChangesOptions, A as DesktopStopResponse, At as SandboxOptions, B as ExecuteResponse, Bt as ExposePortRequest, C as ClickOptions, Ct as ProcessListResult, D as DesktopStartOptions, Dt as ProcessStatus, E as DesktopClient, Et as ProcessStartResult, F as ScreenshotRegion, Ft as WatchOptions, G as HttpClientOptions, Gt as Execution, H as BaseApiResponse, Ht as PtyOptions, I as ScreenshotResponse, It as isExecResult, J as SessionRequest, K as RequestConfig, Kt as ExecutionResult, L as ScrollDirection, Lt as isProcess, M as ScreenSizeResponse, Mt as StreamOptions, N as ScreenshotBytesResponse, Nt as WaitForLogResult, O as DesktopStartResponse, Ot as RemoteMountBucketOptions, P as ScreenshotOptions, Pt as WaitForPortOptions, Q as BucketProvider, R as TypeOptions, Rt as isProcessStatus, S as WriteFileRequest, St as ProcessKillResult, T as Desktop, Tt as ProcessOptions, U as ContainerStub, Ut as CodeContext, V as BackupClient, Vt as StartProcessRequest, W as ErrorResponse, Wt as CreateContextOptions, X as BaseExecOptions, Y as BackupOptions, Z as BucketCredentials, _ as GitClient, _t as PortExposeResult, a as CreateSessionRequest, at as ExecutionSession, b as MkdirRequest, bt as ProcessCleanupResult, c as DeleteSessionResponse, ct as FileStreamEvent, d as ProcessClient, dt as ISandbox, et as CheckChangesResult, f as PortClient, ft as ListFilesOptions, g as GitCheckoutRequest, gt as PortCloseResult, h as InterpreterClient, ht as MountBucketOptions, i as CommandsResponse, it as ExecResult, j as KeyInput, jt as SessionOptions, k as DesktopStatusResponse, kt as RestoreBackupResult, l as PingResponse, lt as FileWatchSSEEvent, m as ExecutionCallbacks, mt as LogEvent, n as getSandbox, nt as ExecEvent, o as CreateSessionResponse, ot as FileChunk, p as UnexposePortRequest, pt as LocalMountBucketOptions, q as ResponseHandler, qt as RunCodeOptions, r as SandboxClient, rt as ExecOptions, s as DeleteSessionRequest, st as FileMetadata, t as Sandbox, tt as DirectoryBackup, u as UtilityClient, ut as GitCheckoutResult, v as FileClient, vt as PortListResult, w as CursorPositionResponse, wt as ProcessLogsResult, x as ReadFileRequest, xt as ProcessInfoResult, y as FileOperationRequest, yt as Process, z as CommandClient, zt as ExecuteRequest } from "./sandbox-DW5aQ1lD.js";
2
2
  import { a as DesktopCoordinateErrorContext, d as ErrorResponse$1, f as OperationType, i as BackupRestoreContext, l as ProcessExitedBeforeReadyContext, n as BackupExpiredContext, o as DesktopErrorContext, p as ErrorCode, r as BackupNotFoundContext, s as InvalidBackupConfigContext, t as BackupCreateContext, u as ProcessReadyTimeoutContext } from "./contexts-CeQR115r.js";
3
3
  import { ContainerProxy } from "@cloudflare/containers";
4
4
 
package/dist/index.js CHANGED
@@ -2813,6 +2813,19 @@ var SandboxClient = class {
2813
2813
  }
2814
2814
  };
2815
2815
 
2816
+ //#endregion
2817
+ //#region ../shared/src/backup.ts
2818
+ /**
2819
+ * Absolute directory prefixes supported by backup and restore operations.
2820
+ */
2821
+ const BACKUP_ALLOWED_PREFIXES = [
2822
+ "/workspace",
2823
+ "/home",
2824
+ "/tmp",
2825
+ "/var/tmp",
2826
+ "/app"
2827
+ ];
2828
+
2816
2829
  //#endregion
2817
2830
  //#region src/security.ts
2818
2831
  /**
@@ -3632,7 +3645,7 @@ function buildS3fsSource(bucket, prefix) {
3632
3645
  * This file is auto-updated by .github/changeset-version.ts during releases
3633
3646
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
3634
3647
  */
3635
- const SDK_VERSION = "0.8.4";
3648
+ const SDK_VERSION = "0.8.5";
3636
3649
 
3637
3650
  //#endregion
3638
3651
  //#region src/sandbox.ts
@@ -5554,7 +5567,7 @@ var Sandbox = class Sandbox extends Container {
5554
5567
  static UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5555
5568
  /**
5556
5569
  * Validate that a directory path is safe for backup operations.
5557
- * Rejects empty, relative, traversal, and null-byte paths.
5570
+ * Rejects empty, relative, traversal, null-byte, and unsupported-root paths.
5558
5571
  */
5559
5572
  static validateBackupDir(dir, label) {
5560
5573
  if (!dir || !dir.startsWith("/")) throw new InvalidBackupConfigError({
@@ -5578,6 +5591,13 @@ var Sandbox = class Sandbox extends Container {
5578
5591
  context: { reason: `${label} must not contain ".." path segments` },
5579
5592
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5580
5593
  });
5594
+ if (!BACKUP_ALLOWED_PREFIXES.some((prefix) => dir === prefix || dir.startsWith(`${prefix}/`))) throw new InvalidBackupConfigError({
5595
+ message: `${label} must be inside one of the supported backup roots (${BACKUP_ALLOWED_PREFIXES.join(", ")})`,
5596
+ code: ErrorCode.INVALID_BACKUP_CONFIG,
5597
+ httpStatus: 400,
5598
+ context: { reason: `${label} must be inside one of the supported backup roots` },
5599
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5600
+ });
5581
5601
  }
5582
5602
  /**
5583
5603
  * Returns the R2 bucket or throws if backup is not configured.
@@ -5948,19 +5968,19 @@ var Sandbox = class Sandbox extends Container {
5948
5968
  const restoreStartTime = Date.now();
5949
5969
  const bucket = this.requireBackupBucket();
5950
5970
  this.requirePresignedUrlSupport();
5951
- const { id: backupId, dir } = backup;
5971
+ const { id, dir } = backup;
5952
5972
  let outcome = "error";
5953
5973
  let caughtError;
5954
5974
  let backupSession;
5955
5975
  try {
5956
- if (!backupId || typeof backupId !== "string") throw new InvalidBackupConfigError({
5976
+ if (!id || typeof id !== "string") throw new InvalidBackupConfigError({
5957
5977
  message: "Invalid backup: missing or invalid id",
5958
5978
  code: ErrorCode.INVALID_BACKUP_CONFIG,
5959
5979
  httpStatus: 400,
5960
5980
  context: { reason: "missing or invalid id" },
5961
5981
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5962
5982
  });
5963
- if (!Sandbox.UUID_REGEX.test(backupId)) throw new InvalidBackupConfigError({
5983
+ if (!Sandbox.UUID_REGEX.test(id)) throw new InvalidBackupConfigError({
5964
5984
  message: "Invalid backup: id must be a valid UUID (e.g. from createBackup)",
5965
5985
  code: ErrorCode.INVALID_BACKUP_CONFIG,
5966
5986
  httpStatus: 400,
@@ -5968,13 +5988,13 @@ var Sandbox = class Sandbox extends Container {
5968
5988
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5969
5989
  });
5970
5990
  Sandbox.validateBackupDir(dir, "Invalid backup: dir");
5971
- const metaKey = `backups/${backupId}/meta.json`;
5991
+ const metaKey = `backups/${id}/meta.json`;
5972
5992
  const metaObject = await bucket.get(metaKey);
5973
5993
  if (!metaObject) throw new BackupNotFoundError({
5974
- message: `Backup not found: ${backupId}. Verify the backup ID is correct and the backup has not been deleted.`,
5994
+ message: `Backup not found: ${id}. Verify the backup ID is correct and the backup has not been deleted.`,
5975
5995
  code: ErrorCode.BACKUP_NOT_FOUND,
5976
5996
  httpStatus: 404,
5977
- context: { backupId },
5997
+ context: { backupId: id },
5978
5998
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5979
5999
  });
5980
6000
  const metadata = await metaObject.json();
@@ -5986,44 +6006,44 @@ var Sandbox = class Sandbox extends Container {
5986
6006
  httpStatus: 500,
5987
6007
  context: {
5988
6008
  dir,
5989
- backupId
6009
+ backupId: id
5990
6010
  },
5991
6011
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5992
6012
  });
5993
6013
  const expiresAt = createdAt + metadata.ttl * 1e3;
5994
6014
  if (Date.now() + TTL_BUFFER_MS > expiresAt) throw new BackupExpiredError({
5995
- message: `Backup ${backupId} has expired (created: ${metadata.createdAt}, TTL: ${metadata.ttl}s). Create a new backup.`,
6015
+ message: `Backup ${id} has expired (created: ${metadata.createdAt}, TTL: ${metadata.ttl}s). Create a new backup.`,
5996
6016
  code: ErrorCode.BACKUP_EXPIRED,
5997
6017
  httpStatus: 400,
5998
6018
  context: {
5999
- backupId,
6019
+ backupId: id,
6000
6020
  expiredAt: new Date(expiresAt).toISOString()
6001
6021
  },
6002
6022
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6003
6023
  });
6004
- const r2Key = `backups/${backupId}/data.sqsh`;
6024
+ const r2Key = `backups/${id}/data.sqsh`;
6005
6025
  const archiveHead = await bucket.head(r2Key);
6006
6026
  if (!archiveHead) throw new BackupNotFoundError({
6007
- message: `Backup archive not found in R2: ${backupId}. The archive may have been deleted by R2 lifecycle rules.`,
6027
+ message: `Backup archive not found in R2: ${id}. The archive may have been deleted by R2 lifecycle rules.`,
6008
6028
  code: ErrorCode.BACKUP_NOT_FOUND,
6009
6029
  httpStatus: 404,
6010
- context: { backupId },
6030
+ context: { backupId: id },
6011
6031
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6012
6032
  });
6013
6033
  backupSession = await this.ensureBackupSession();
6014
- const archivePath = `/var/backups/${backupId}.sqsh`;
6015
- const mountGlob = `/var/backups/mounts/${backupId}`;
6034
+ const archivePath = `/var/backups/${id}.sqsh`;
6035
+ const mountGlob = `/var/backups/mounts/${id}`;
6016
6036
  await this.execWithSession(`/usr/bin/fusermount3 -uz ${shellEscape(dir)} 2>/dev/null || true`, backupSession, { origin: "internal" }).catch(() => {});
6017
6037
  await this.execWithSession(`for d in ${shellEscape(mountGlob)}_*/lower ${shellEscape(mountGlob)}/lower; do [ -d "$d" ] && /usr/bin/fusermount3 -uz "$d" 2>/dev/null; done; true`, backupSession, { origin: "internal" }).catch(() => {});
6018
6038
  const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(archivePath)} 2>/dev/null || echo 0`, backupSession, { origin: "internal" }).catch(() => ({ stdout: "0" }));
6019
- if (Number.parseInt((sizeCheck.stdout ?? "0").trim(), 10) !== archiveHead.size) await this.downloadBackupPresigned(archivePath, r2Key, archiveHead.size, backupId, dir, backupSession);
6039
+ if (Number.parseInt((sizeCheck.stdout ?? "0").trim(), 10) !== archiveHead.size) await this.downloadBackupPresigned(archivePath, r2Key, archiveHead.size, id, dir, backupSession);
6020
6040
  if (!(await this.client.backup.restoreArchive(dir, archivePath, backupSession)).success) throw new BackupRestoreError({
6021
6041
  message: "Container failed to restore backup archive",
6022
6042
  code: ErrorCode.BACKUP_RESTORE_FAILED,
6023
6043
  httpStatus: 500,
6024
6044
  context: {
6025
6045
  dir,
6026
- backupId
6046
+ backupId: id
6027
6047
  },
6028
6048
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6029
6049
  });
@@ -6031,12 +6051,12 @@ var Sandbox = class Sandbox extends Container {
6031
6051
  return {
6032
6052
  success: true,
6033
6053
  dir,
6034
- id: backupId
6054
+ id
6035
6055
  };
6036
6056
  } catch (error) {
6037
6057
  caughtError = error instanceof Error ? error : new Error(String(error));
6038
- if (backupId && backupSession) {
6039
- const archivePath = `/var/backups/${backupId}.sqsh`;
6058
+ if (id && backupSession) {
6059
+ const archivePath = `/var/backups/${id}.sqsh`;
6040
6060
  await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession, { origin: "internal" }).catch(() => {});
6041
6061
  }
6042
6062
  throw error;
@@ -6046,7 +6066,7 @@ var Sandbox = class Sandbox extends Container {
6046
6066
  event: "backup.restore",
6047
6067
  outcome,
6048
6068
  durationMs: Date.now() - restoreStartTime,
6049
- backupId,
6069
+ backupId: id,
6050
6070
  dir,
6051
6071
  error: caughtError
6052
6072
  });