@cloudflare/sandbox 0.8.4 → 0.8.6

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
@@ -773,6 +773,44 @@ var BaseTransport = class {
773
773
  }
774
774
  }
775
775
  /**
776
+ * Build a URL targeting the container's HTTP server.
777
+ */
778
+ buildContainerUrl(path$1) {
779
+ if (this.config.stub) return `http://localhost:${this.config.port || 3e3}${path$1}`;
780
+ return `${this.config.baseUrl ?? `http://localhost:${this.config.port || 3e3}`}${path$1}`;
781
+ }
782
+ /**
783
+ * Single HTTP request to the container — no WebSocket, no 503 retry.
784
+ */
785
+ httpFetch(path$1, options) {
786
+ const url = this.buildContainerUrl(path$1);
787
+ if (this.config.stub) return this.config.stub.containerFetch(url, options || {}, this.config.port);
788
+ return globalThis.fetch(url, options);
789
+ }
790
+ /**
791
+ * Streaming HTTP request to the container — no WebSocket, no 503 retry.
792
+ */
793
+ async httpFetchStream(path$1, body, method = "POST", headers) {
794
+ const url = this.buildContainerUrl(path$1);
795
+ const init = {
796
+ method,
797
+ headers: body && method === "POST" ? {
798
+ ...headers,
799
+ "Content-Type": "application/json"
800
+ } : headers,
801
+ body: body && method === "POST" ? JSON.stringify(body) : void 0
802
+ };
803
+ let response;
804
+ if (this.config.stub) response = await this.config.stub.containerFetch(url, init, this.config.port);
805
+ else response = await globalThis.fetch(url, init);
806
+ if (!response.ok) {
807
+ const errorBody = await response.text();
808
+ throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`);
809
+ }
810
+ if (!response.body) throw new Error("No response body for streaming");
811
+ return response.body;
812
+ }
813
+ /**
776
814
  * Sleep utility for retry delays
777
815
  */
778
816
  sleep(ms) {
@@ -787,13 +825,12 @@ var BaseTransport = class {
787
825
  *
788
826
  * Uses standard fetch API for communication with the container.
789
827
  * HTTP is stateless, so connect/disconnect are no-ops.
828
+ *
829
+ * All HTTP request logic lives in {@link BaseTransport.httpFetch} and
830
+ * {@link BaseTransport.httpFetchStream}; this subclass simply wires
831
+ * the abstract `doFetch` / `fetchStream` hooks to those shared helpers.
790
832
  */
791
833
  var HttpTransport = class extends BaseTransport {
792
- baseUrl;
793
- constructor(config) {
794
- super(config);
795
- this.baseUrl = config.baseUrl ?? "http://localhost:3000";
796
- }
797
834
  getMode() {
798
835
  return "http";
799
836
  }
@@ -803,36 +840,10 @@ var HttpTransport = class extends BaseTransport {
803
840
  return true;
804
841
  }
805
842
  async doFetch(path$1, options) {
806
- const url = this.buildUrl(path$1);
807
- if (this.config.stub) return this.config.stub.containerFetch(url, options || {}, this.config.port);
808
- return globalThis.fetch(url, options);
843
+ return this.httpFetch(path$1, options);
809
844
  }
810
845
  async fetchStream(path$1, body, method = "POST", headers) {
811
- const url = this.buildUrl(path$1);
812
- const options = this.buildStreamOptions(body, method, headers);
813
- let response;
814
- if (this.config.stub) response = await this.config.stub.containerFetch(url, options, this.config.port);
815
- else response = await globalThis.fetch(url, options);
816
- if (!response.ok) {
817
- const errorBody = await response.text();
818
- throw new Error(`HTTP error! status: ${response.status} - ${errorBody}`);
819
- }
820
- if (!response.body) throw new Error("No response body for streaming");
821
- return response.body;
822
- }
823
- buildUrl(path$1) {
824
- if (this.config.stub) return `http://localhost:${this.config.port}${path$1}`;
825
- return `${this.baseUrl}${path$1}`;
826
- }
827
- buildStreamOptions(body, method, headers) {
828
- return {
829
- method,
830
- headers: body && method === "POST" ? {
831
- ...headers,
832
- "Content-Type": "application/json"
833
- } : headers,
834
- body: body && method === "POST" ? JSON.stringify(body) : void 0
835
- };
846
+ return this.httpFetchStream(path$1, body, method, headers);
836
847
  }
837
848
  };
838
849
 
@@ -899,10 +910,30 @@ var WebSocketTransport = class extends BaseTransport {
899
910
  this.cleanup();
900
911
  }
901
912
  /**
902
- * Transport-specific fetch implementation
913
+ * Whether a WebSocket connection is currently being established.
914
+ *
915
+ * When true, awaiting `connectPromise` from a nested call would deadlock:
916
+ * the outer `connectViaFetch → stub.fetch → containerFetch →
917
+ * startAndWaitForPorts → blockConcurrencyWhile(onStart)` chain may call
918
+ * back into the SDK (e.g. `exec()`), which would await the same
919
+ * `connectPromise` that cannot resolve until `onStart` returns.
920
+ *
921
+ * Callers use this to fall back to a direct HTTP request, which is safe
922
+ * because `startAndWaitForPorts()` calls `setHealthy()` before invoking
923
+ * `onStart()`, so `containerFetch()` routes directly to the container.
924
+ */
925
+ isWebSocketConnecting() {
926
+ return this.state === "connecting";
927
+ }
928
+ /**
929
+ * Transport-specific fetch implementation.
903
930
  * Converts WebSocket response to standard Response object.
931
+ *
932
+ * Falls back to HTTP while a WebSocket connection is being established
933
+ * to avoid the re-entrant deadlock described in `isWebSocketConnecting()`.
904
934
  */
905
935
  async doFetch(path$1, options) {
936
+ if (this.isWebSocketConnecting()) return this.httpFetch(path$1, options);
906
937
  await this.connect();
907
938
  const method = options?.method || "GET";
908
939
  const body = this.parseBody(options?.body);
@@ -914,7 +945,9 @@ var WebSocketTransport = class extends BaseTransport {
914
945
  });
915
946
  }
916
947
  /**
917
- * Streaming fetch implementation
948
+ * Streaming fetch implementation.
949
+ *
950
+ * Delegates to `requestStream()`, which applies the re-entrancy guard.
918
951
  */
919
952
  async fetchStream(path$1, body, method = "POST", headers) {
920
953
  return this.requestStream(method, path$1, body, headers);
@@ -1054,7 +1087,12 @@ var WebSocketTransport = class extends BaseTransport {
1054
1087
  });
1055
1088
  }
1056
1089
  /**
1057
- * Send a request and wait for response
1090
+ * Send a request and wait for response.
1091
+ *
1092
+ * Only reachable from `doFetch()`, which already applies the re-entrancy
1093
+ * guard via `isWebSocketConnecting()`. The `connect()` call here handles
1094
+ * the case where the WebSocket was closed between `doFetch` and `request`
1095
+ * (idle disconnect).
1058
1096
  */
1059
1097
  async request(method, path$1, body, headers) {
1060
1098
  await this.connect();
@@ -1105,7 +1143,7 @@ var WebSocketTransport = class extends BaseTransport {
1105
1143
  });
1106
1144
  }
1107
1145
  /**
1108
- * Send a streaming request and return a ReadableStream
1146
+ * Send a streaming request and return a ReadableStream.
1109
1147
  *
1110
1148
  * The stream will receive data chunks as they arrive over the WebSocket.
1111
1149
  * Format matches SSE for compatibility with existing streaming code.
@@ -1117,8 +1155,12 @@ var WebSocketTransport = class extends BaseTransport {
1117
1155
  * Uses an inactivity timeout instead of a total-duration timeout so that
1118
1156
  * long-running streams (e.g. execStream from an agent) stay alive as long
1119
1157
  * as data is flowing. The timer resets on every chunk or response message.
1158
+ *
1159
+ * Falls back to HTTP while a WebSocket connection is being established
1160
+ * to avoid the re-entrant deadlock described in `isWebSocketConnecting()`.
1120
1161
  */
1121
1162
  async requestStream(method, path$1, body, headers) {
1163
+ if (this.isWebSocketConnecting()) return this.httpFetchStream(path$1, body, method, headers);
1122
1164
  await this.connect();
1123
1165
  this.clearIdleDisconnectTimer();
1124
1166
  const id = generateRequestId();
@@ -2813,6 +2855,19 @@ var SandboxClient = class {
2813
2855
  }
2814
2856
  };
2815
2857
 
2858
+ //#endregion
2859
+ //#region ../shared/src/backup.ts
2860
+ /**
2861
+ * Absolute directory prefixes supported by backup and restore operations.
2862
+ */
2863
+ const BACKUP_ALLOWED_PREFIXES = [
2864
+ "/workspace",
2865
+ "/home",
2866
+ "/tmp",
2867
+ "/var/tmp",
2868
+ "/app"
2869
+ ];
2870
+
2816
2871
  //#endregion
2817
2872
  //#region src/security.ts
2818
2873
  /**
@@ -3632,7 +3687,7 @@ function buildS3fsSource(bucket, prefix) {
3632
3687
  * This file is auto-updated by .github/changeset-version.ts during releases
3633
3688
  * DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
3634
3689
  */
3635
- const SDK_VERSION = "0.8.4";
3690
+ const SDK_VERSION = "0.8.6";
3636
3691
 
3637
3692
  //#endregion
3638
3693
  //#region src/sandbox.ts
@@ -5554,7 +5609,7 @@ var Sandbox = class Sandbox extends Container {
5554
5609
  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
5610
  /**
5556
5611
  * Validate that a directory path is safe for backup operations.
5557
- * Rejects empty, relative, traversal, and null-byte paths.
5612
+ * Rejects empty, relative, traversal, null-byte, and unsupported-root paths.
5558
5613
  */
5559
5614
  static validateBackupDir(dir, label) {
5560
5615
  if (!dir || !dir.startsWith("/")) throw new InvalidBackupConfigError({
@@ -5578,6 +5633,13 @@ var Sandbox = class Sandbox extends Container {
5578
5633
  context: { reason: `${label} must not contain ".." path segments` },
5579
5634
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5580
5635
  });
5636
+ if (!BACKUP_ALLOWED_PREFIXES.some((prefix) => dir === prefix || dir.startsWith(`${prefix}/`))) throw new InvalidBackupConfigError({
5637
+ message: `${label} must be inside one of the supported backup roots (${BACKUP_ALLOWED_PREFIXES.join(", ")})`,
5638
+ code: ErrorCode.INVALID_BACKUP_CONFIG,
5639
+ httpStatus: 400,
5640
+ context: { reason: `${label} must be inside one of the supported backup roots` },
5641
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5642
+ });
5581
5643
  }
5582
5644
  /**
5583
5645
  * Returns the R2 bucket or throws if backup is not configured.
@@ -5948,19 +6010,19 @@ var Sandbox = class Sandbox extends Container {
5948
6010
  const restoreStartTime = Date.now();
5949
6011
  const bucket = this.requireBackupBucket();
5950
6012
  this.requirePresignedUrlSupport();
5951
- const { id: backupId, dir } = backup;
6013
+ const { id, dir } = backup;
5952
6014
  let outcome = "error";
5953
6015
  let caughtError;
5954
6016
  let backupSession;
5955
6017
  try {
5956
- if (!backupId || typeof backupId !== "string") throw new InvalidBackupConfigError({
6018
+ if (!id || typeof id !== "string") throw new InvalidBackupConfigError({
5957
6019
  message: "Invalid backup: missing or invalid id",
5958
6020
  code: ErrorCode.INVALID_BACKUP_CONFIG,
5959
6021
  httpStatus: 400,
5960
6022
  context: { reason: "missing or invalid id" },
5961
6023
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5962
6024
  });
5963
- if (!Sandbox.UUID_REGEX.test(backupId)) throw new InvalidBackupConfigError({
6025
+ if (!Sandbox.UUID_REGEX.test(id)) throw new InvalidBackupConfigError({
5964
6026
  message: "Invalid backup: id must be a valid UUID (e.g. from createBackup)",
5965
6027
  code: ErrorCode.INVALID_BACKUP_CONFIG,
5966
6028
  httpStatus: 400,
@@ -5968,13 +6030,13 @@ var Sandbox = class Sandbox extends Container {
5968
6030
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5969
6031
  });
5970
6032
  Sandbox.validateBackupDir(dir, "Invalid backup: dir");
5971
- const metaKey = `backups/${backupId}/meta.json`;
6033
+ const metaKey = `backups/${id}/meta.json`;
5972
6034
  const metaObject = await bucket.get(metaKey);
5973
6035
  if (!metaObject) throw new BackupNotFoundError({
5974
- message: `Backup not found: ${backupId}. Verify the backup ID is correct and the backup has not been deleted.`,
6036
+ message: `Backup not found: ${id}. Verify the backup ID is correct and the backup has not been deleted.`,
5975
6037
  code: ErrorCode.BACKUP_NOT_FOUND,
5976
6038
  httpStatus: 404,
5977
- context: { backupId },
6039
+ context: { backupId: id },
5978
6040
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5979
6041
  });
5980
6042
  const metadata = await metaObject.json();
@@ -5986,44 +6048,44 @@ var Sandbox = class Sandbox extends Container {
5986
6048
  httpStatus: 500,
5987
6049
  context: {
5988
6050
  dir,
5989
- backupId
6051
+ backupId: id
5990
6052
  },
5991
6053
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5992
6054
  });
5993
6055
  const expiresAt = createdAt + metadata.ttl * 1e3;
5994
6056
  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.`,
6057
+ message: `Backup ${id} has expired (created: ${metadata.createdAt}, TTL: ${metadata.ttl}s). Create a new backup.`,
5996
6058
  code: ErrorCode.BACKUP_EXPIRED,
5997
6059
  httpStatus: 400,
5998
6060
  context: {
5999
- backupId,
6061
+ backupId: id,
6000
6062
  expiredAt: new Date(expiresAt).toISOString()
6001
6063
  },
6002
6064
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6003
6065
  });
6004
- const r2Key = `backups/${backupId}/data.sqsh`;
6066
+ const r2Key = `backups/${id}/data.sqsh`;
6005
6067
  const archiveHead = await bucket.head(r2Key);
6006
6068
  if (!archiveHead) throw new BackupNotFoundError({
6007
- message: `Backup archive not found in R2: ${backupId}. The archive may have been deleted by R2 lifecycle rules.`,
6069
+ message: `Backup archive not found in R2: ${id}. The archive may have been deleted by R2 lifecycle rules.`,
6008
6070
  code: ErrorCode.BACKUP_NOT_FOUND,
6009
6071
  httpStatus: 404,
6010
- context: { backupId },
6072
+ context: { backupId: id },
6011
6073
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6012
6074
  });
6013
6075
  backupSession = await this.ensureBackupSession();
6014
- const archivePath = `/var/backups/${backupId}.sqsh`;
6015
- const mountGlob = `/var/backups/mounts/${backupId}`;
6076
+ const archivePath = `/var/backups/${id}.sqsh`;
6077
+ const mountGlob = `/var/backups/mounts/${id}`;
6016
6078
  await this.execWithSession(`/usr/bin/fusermount3 -uz ${shellEscape(dir)} 2>/dev/null || true`, backupSession, { origin: "internal" }).catch(() => {});
6017
6079
  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
6080
  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);
6081
+ if (Number.parseInt((sizeCheck.stdout ?? "0").trim(), 10) !== archiveHead.size) await this.downloadBackupPresigned(archivePath, r2Key, archiveHead.size, id, dir, backupSession);
6020
6082
  if (!(await this.client.backup.restoreArchive(dir, archivePath, backupSession)).success) throw new BackupRestoreError({
6021
6083
  message: "Container failed to restore backup archive",
6022
6084
  code: ErrorCode.BACKUP_RESTORE_FAILED,
6023
6085
  httpStatus: 500,
6024
6086
  context: {
6025
6087
  dir,
6026
- backupId
6088
+ backupId: id
6027
6089
  },
6028
6090
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6029
6091
  });
@@ -6031,12 +6093,12 @@ var Sandbox = class Sandbox extends Container {
6031
6093
  return {
6032
6094
  success: true,
6033
6095
  dir,
6034
- id: backupId
6096
+ id
6035
6097
  };
6036
6098
  } catch (error) {
6037
6099
  caughtError = error instanceof Error ? error : new Error(String(error));
6038
- if (backupId && backupSession) {
6039
- const archivePath = `/var/backups/${backupId}.sqsh`;
6100
+ if (id && backupSession) {
6101
+ const archivePath = `/var/backups/${id}.sqsh`;
6040
6102
  await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession, { origin: "internal" }).catch(() => {});
6041
6103
  }
6042
6104
  throw error;
@@ -6046,7 +6108,7 @@ var Sandbox = class Sandbox extends Container {
6046
6108
  event: "backup.restore",
6047
6109
  outcome,
6048
6110
  durationMs: Date.now() - restoreStartTime,
6049
- backupId,
6111
+ backupId: id,
6050
6112
  dir,
6051
6113
  error: caughtError
6052
6114
  });