@cloudflare/sandbox 0.7.19 → 0.7.21
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/{dist-CwUZf_TJ.js → dist-CmfvOT-w.js} +290 -75
- package/dist/dist-CmfvOT-w.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +652 -625
- package/dist/index.js.map +1 -1
- package/dist/openai/index.d.ts +1 -1
- package/dist/openai/index.js +1 -1
- package/dist/opencode/index.d.ts +1 -1
- package/dist/opencode/index.d.ts.map +1 -1
- package/dist/opencode/index.js +1 -1
- package/dist/{sandbox-raa8coh3.d.ts → sandbox-DDDSuWA6.d.ts} +39 -24
- package/dist/sandbox-DDDSuWA6.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/dist-CwUZf_TJ.js.map +0 -1
- package/dist/sandbox-raa8coh3.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as extractRepoName, a as isExecResult, b as partitionEnvVars, c as parseSSEFrames, d as createNoOpLogger, f as TraceContext, g as GitLogger, h as ResultImpl, i as isWSStreamChunk, l as shellEscape, m as Execution, n as isWSError, o as isProcess, p as logCanonicalEvent, r as isWSResponse, s as isProcessStatus, t as generateRequestId, u as createLogger, v as filterEnvVars, y as getEnvString } from "./dist-CmfvOT-w.js";
|
|
2
2
|
import { t as ErrorCode } from "./errors-CaSfB5Bm.js";
|
|
3
3
|
import { Container, getContainer, switchPort } from "@cloudflare/containers";
|
|
4
4
|
import { AwsClient } from "aws4fetch";
|
|
@@ -804,9 +804,9 @@ var HttpTransport = class extends BaseTransport {
|
|
|
804
804
|
if (this.config.stub) return this.config.stub.containerFetch(url, options || {}, this.config.port);
|
|
805
805
|
return globalThis.fetch(url, options);
|
|
806
806
|
}
|
|
807
|
-
async fetchStream(path$1, body, method = "POST") {
|
|
807
|
+
async fetchStream(path$1, body, method = "POST", headers) {
|
|
808
808
|
const url = this.buildUrl(path$1);
|
|
809
|
-
const options = this.buildStreamOptions(body, method);
|
|
809
|
+
const options = this.buildStreamOptions(body, method, headers);
|
|
810
810
|
let response;
|
|
811
811
|
if (this.config.stub) response = await this.config.stub.containerFetch(url, options, this.config.port);
|
|
812
812
|
else response = await globalThis.fetch(url, options);
|
|
@@ -821,10 +821,13 @@ var HttpTransport = class extends BaseTransport {
|
|
|
821
821
|
if (this.config.stub) return `http://localhost:${this.config.port}${path$1}`;
|
|
822
822
|
return `${this.baseUrl}${path$1}`;
|
|
823
823
|
}
|
|
824
|
-
buildStreamOptions(body, method) {
|
|
824
|
+
buildStreamOptions(body, method, headers) {
|
|
825
825
|
return {
|
|
826
826
|
method,
|
|
827
|
-
headers: body && method === "POST" ? {
|
|
827
|
+
headers: body && method === "POST" ? {
|
|
828
|
+
...headers,
|
|
829
|
+
"Content-Type": "application/json"
|
|
830
|
+
} : headers,
|
|
828
831
|
body: body && method === "POST" ? JSON.stringify(body) : void 0
|
|
829
832
|
};
|
|
830
833
|
}
|
|
@@ -896,7 +899,8 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
896
899
|
await this.connect();
|
|
897
900
|
const method = options?.method || "GET";
|
|
898
901
|
const body = this.parseBody(options?.body);
|
|
899
|
-
const
|
|
902
|
+
const headers = this.normalizeHeaders(options?.headers);
|
|
903
|
+
const result = await this.request(method, path$1, body, headers);
|
|
900
904
|
return new Response(JSON.stringify(result.body), {
|
|
901
905
|
status: result.status,
|
|
902
906
|
headers: { "Content-Type": "application/json" }
|
|
@@ -905,8 +909,8 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
905
909
|
/**
|
|
906
910
|
* Streaming fetch implementation
|
|
907
911
|
*/
|
|
908
|
-
async fetchStream(path$1, body, method = "POST") {
|
|
909
|
-
return this.requestStream(method, path$1, body);
|
|
912
|
+
async fetchStream(path$1, body, method = "POST", headers) {
|
|
913
|
+
return this.requestStream(method, path$1, body, headers);
|
|
910
914
|
}
|
|
911
915
|
/**
|
|
912
916
|
* Parse request body from RequestInit
|
|
@@ -921,6 +925,17 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
921
925
|
throw new Error(`WebSocket transport only supports string bodies. Got: ${typeof body}`);
|
|
922
926
|
}
|
|
923
927
|
/**
|
|
928
|
+
* Normalize RequestInit headers into a plain object for WSRequest.
|
|
929
|
+
*/
|
|
930
|
+
normalizeHeaders(headers) {
|
|
931
|
+
if (!headers) return;
|
|
932
|
+
const normalized = {};
|
|
933
|
+
new Headers(headers).forEach((value, key) => {
|
|
934
|
+
normalized[key] = value;
|
|
935
|
+
});
|
|
936
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
924
939
|
* Internal connection logic
|
|
925
940
|
*/
|
|
926
941
|
async doConnect() {
|
|
@@ -1009,7 +1024,7 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
1009
1024
|
/**
|
|
1010
1025
|
* Send a request and wait for response
|
|
1011
1026
|
*/
|
|
1012
|
-
async request(method, path$1, body) {
|
|
1027
|
+
async request(method, path$1, body, headers) {
|
|
1013
1028
|
await this.connect();
|
|
1014
1029
|
const id = generateRequestId();
|
|
1015
1030
|
const request = {
|
|
@@ -1017,7 +1032,8 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
1017
1032
|
id,
|
|
1018
1033
|
method,
|
|
1019
1034
|
path: path$1,
|
|
1020
|
-
body
|
|
1035
|
+
body,
|
|
1036
|
+
headers
|
|
1021
1037
|
};
|
|
1022
1038
|
return new Promise((resolve, reject) => {
|
|
1023
1039
|
const timeoutMs = this.config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
@@ -1065,7 +1081,7 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
1065
1081
|
* long-running streams (e.g. execStream from an agent) stay alive as long
|
|
1066
1082
|
* as data is flowing. The timer resets on every chunk or response message.
|
|
1067
1083
|
*/
|
|
1068
|
-
async requestStream(method, path$1, body) {
|
|
1084
|
+
async requestStream(method, path$1, body, headers) {
|
|
1069
1085
|
await this.connect();
|
|
1070
1086
|
const id = generateRequestId();
|
|
1071
1087
|
const request = {
|
|
@@ -1073,7 +1089,8 @@ var WebSocketTransport = class extends BaseTransport {
|
|
|
1073
1089
|
id,
|
|
1074
1090
|
method,
|
|
1075
1091
|
path: path$1,
|
|
1076
|
-
body
|
|
1092
|
+
body,
|
|
1093
|
+
headers
|
|
1077
1094
|
};
|
|
1078
1095
|
const idleTimeoutMs = this.config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
|
|
1079
1096
|
return new Promise((resolveStream, rejectStream) => {
|
|
@@ -1398,6 +1415,14 @@ var BaseHttpClient = class {
|
|
|
1398
1415
|
* Core fetch method - delegates to Transport which handles retry logic
|
|
1399
1416
|
*/
|
|
1400
1417
|
async doFetch(path$1, options) {
|
|
1418
|
+
const { defaultHeaders } = this.options;
|
|
1419
|
+
if (defaultHeaders) options = {
|
|
1420
|
+
...options,
|
|
1421
|
+
headers: {
|
|
1422
|
+
...defaultHeaders,
|
|
1423
|
+
...options?.headers
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1401
1426
|
return this.transport.fetch(path$1, options);
|
|
1402
1427
|
}
|
|
1403
1428
|
/**
|
|
@@ -1482,12 +1507,11 @@ var BaseHttpClient = class {
|
|
|
1482
1507
|
* @param method - HTTP method (default: POST, use GET for process logs)
|
|
1483
1508
|
*/
|
|
1484
1509
|
async doStreamFetch(path$1, body, method = "POST") {
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}
|
|
1510
|
+
const streamHeaders = method === "POST" ? {
|
|
1511
|
+
...this.options.defaultHeaders,
|
|
1512
|
+
"Content-Type": "application/json"
|
|
1513
|
+
} : this.options.defaultHeaders;
|
|
1514
|
+
if (this.transport.getMode() === "websocket") return this.transport.fetchStream(path$1, body, method, streamHeaders);
|
|
1491
1515
|
const response = await this.doFetch(path$1, {
|
|
1492
1516
|
method,
|
|
1493
1517
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1495,25 +1519,6 @@ var BaseHttpClient = class {
|
|
|
1495
1519
|
});
|
|
1496
1520
|
return this.handleStreamResponse(response);
|
|
1497
1521
|
}
|
|
1498
|
-
/**
|
|
1499
|
-
* Utility method to log successful operations
|
|
1500
|
-
*/
|
|
1501
|
-
logSuccess(operation, details) {
|
|
1502
|
-
this.logger.info(operation, details ? { details } : void 0);
|
|
1503
|
-
}
|
|
1504
|
-
/**
|
|
1505
|
-
* Utility method to log errors intelligently
|
|
1506
|
-
* Only logs unexpected errors (5xx), not expected errors (4xx)
|
|
1507
|
-
*
|
|
1508
|
-
* - 4xx errors (validation, not found, conflicts): Don't log (expected client errors)
|
|
1509
|
-
* - 5xx errors (server failures, internal errors): DO log (unexpected server errors)
|
|
1510
|
-
*/
|
|
1511
|
-
logError(operation, error) {
|
|
1512
|
-
if (error && typeof error === "object" && "httpStatus" in error) {
|
|
1513
|
-
const httpStatus = error.httpStatus;
|
|
1514
|
-
if (httpStatus >= 500) this.logger.error(`Unexpected error in ${operation}`, error instanceof Error ? error : new Error(String(error)), { httpStatus });
|
|
1515
|
-
} else this.logger.error(`Error in ${operation}`, error instanceof Error ? error : new Error(String(error)));
|
|
1516
|
-
}
|
|
1517
1522
|
};
|
|
1518
1523
|
|
|
1519
1524
|
//#endregion
|
|
@@ -1541,11 +1546,8 @@ var BackupClient = class extends BaseHttpClient {
|
|
|
1541
1546
|
excludes,
|
|
1542
1547
|
sessionId
|
|
1543
1548
|
};
|
|
1544
|
-
|
|
1545
|
-
this.logSuccess("Backup archive created", `${dir} -> ${archivePath}`);
|
|
1546
|
-
return response;
|
|
1549
|
+
return await this.post("/api/backup/create", data);
|
|
1547
1550
|
} catch (error) {
|
|
1548
|
-
this.logError("createArchive", error);
|
|
1549
1551
|
throw error;
|
|
1550
1552
|
}
|
|
1551
1553
|
}
|
|
@@ -1562,11 +1564,8 @@ var BackupClient = class extends BaseHttpClient {
|
|
|
1562
1564
|
archivePath,
|
|
1563
1565
|
sessionId
|
|
1564
1566
|
};
|
|
1565
|
-
|
|
1566
|
-
this.logSuccess("Backup archive restored", `${archivePath} -> ${dir}`);
|
|
1567
|
-
return response;
|
|
1567
|
+
return await this.post("/api/backup/restore", data);
|
|
1568
1568
|
} catch (error) {
|
|
1569
|
-
this.logError("restoreArchive", error);
|
|
1570
1569
|
throw error;
|
|
1571
1570
|
}
|
|
1572
1571
|
}
|
|
@@ -1593,14 +1592,13 @@ var CommandClient = class extends BaseHttpClient {
|
|
|
1593
1592
|
sessionId,
|
|
1594
1593
|
...options?.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
|
|
1595
1594
|
...options?.env !== void 0 && { env: options.env },
|
|
1596
|
-
...options?.cwd !== void 0 && { cwd: options.cwd }
|
|
1595
|
+
...options?.cwd !== void 0 && { cwd: options.cwd },
|
|
1596
|
+
...options?.origin !== void 0 && { origin: options.origin }
|
|
1597
1597
|
};
|
|
1598
1598
|
const response = await this.post("/api/execute", data);
|
|
1599
|
-
this.logSuccess("Command executed", `${command}, Success: ${response.success}`);
|
|
1600
1599
|
this.options.onCommandComplete?.(response.success, response.exitCode, response.stdout, response.stderr, response.command);
|
|
1601
1600
|
return response;
|
|
1602
1601
|
} catch (error) {
|
|
1603
|
-
this.logError("execute", error);
|
|
1604
1602
|
this.options.onError?.(error instanceof Error ? error.message : String(error), command);
|
|
1605
1603
|
throw error;
|
|
1606
1604
|
}
|
|
@@ -1618,13 +1616,11 @@ var CommandClient = class extends BaseHttpClient {
|
|
|
1618
1616
|
sessionId,
|
|
1619
1617
|
...options?.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
|
|
1620
1618
|
...options?.env !== void 0 && { env: options.env },
|
|
1621
|
-
...options?.cwd !== void 0 && { cwd: options.cwd }
|
|
1619
|
+
...options?.cwd !== void 0 && { cwd: options.cwd },
|
|
1620
|
+
...options?.origin !== void 0 && { origin: options.origin }
|
|
1622
1621
|
};
|
|
1623
|
-
|
|
1624
|
-
this.logSuccess("Command stream started", command);
|
|
1625
|
-
return stream;
|
|
1622
|
+
return await this.doStreamFetch("/api/execute/stream", data);
|
|
1626
1623
|
} catch (error) {
|
|
1627
|
-
this.logError("executeStream", error);
|
|
1628
1624
|
this.options.onError?.(error instanceof Error ? error.message : String(error), command);
|
|
1629
1625
|
throw error;
|
|
1630
1626
|
}
|
|
@@ -1646,11 +1642,8 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1646
1642
|
...options?.resolution !== void 0 && { resolution: options.resolution },
|
|
1647
1643
|
...options?.dpi !== void 0 && { dpi: options.dpi }
|
|
1648
1644
|
};
|
|
1649
|
-
|
|
1650
|
-
this.logSuccess("Desktop started", `${response.resolution[0]}x${response.resolution[1]}`);
|
|
1651
|
-
return response;
|
|
1645
|
+
return await this.post("/api/desktop/start", data);
|
|
1652
1646
|
} catch (error) {
|
|
1653
|
-
this.logError("desktop.start", error);
|
|
1654
1647
|
this.options.onError?.(error instanceof Error ? error.message : String(error));
|
|
1655
1648
|
throw error;
|
|
1656
1649
|
}
|
|
@@ -1660,11 +1653,8 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1660
1653
|
*/
|
|
1661
1654
|
async stop() {
|
|
1662
1655
|
try {
|
|
1663
|
-
|
|
1664
|
-
this.logSuccess("Desktop stopped");
|
|
1665
|
-
return response;
|
|
1656
|
+
return await this.post("/api/desktop/stop", {});
|
|
1666
1657
|
} catch (error) {
|
|
1667
|
-
this.logError("desktop.stop", error);
|
|
1668
1658
|
this.options.onError?.(error instanceof Error ? error.message : String(error));
|
|
1669
1659
|
throw error;
|
|
1670
1660
|
}
|
|
@@ -1674,11 +1664,8 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1674
1664
|
*/
|
|
1675
1665
|
async status() {
|
|
1676
1666
|
try {
|
|
1677
|
-
|
|
1678
|
-
this.logSuccess("Desktop status retrieved", response.status);
|
|
1679
|
-
return response;
|
|
1667
|
+
return await this.get("/api/desktop/status");
|
|
1680
1668
|
} catch (error) {
|
|
1681
|
-
this.logError("desktop.status", error);
|
|
1682
1669
|
throw error;
|
|
1683
1670
|
}
|
|
1684
1671
|
}
|
|
@@ -1692,7 +1679,6 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1692
1679
|
...options?.showCursor !== void 0 && { showCursor: options.showCursor }
|
|
1693
1680
|
};
|
|
1694
1681
|
const response = await this.post("/api/desktop/screenshot", data);
|
|
1695
|
-
this.logSuccess("Screenshot captured", `${response.width}x${response.height}`);
|
|
1696
1682
|
if (wantsBytes) {
|
|
1697
1683
|
const binaryString = atob(response.data);
|
|
1698
1684
|
const bytes = new Uint8Array(binaryString.length);
|
|
@@ -1704,7 +1690,6 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1704
1690
|
}
|
|
1705
1691
|
return response;
|
|
1706
1692
|
} catch (error) {
|
|
1707
|
-
this.logError("desktop.screenshot", error);
|
|
1708
1693
|
throw error;
|
|
1709
1694
|
}
|
|
1710
1695
|
}
|
|
@@ -1719,7 +1704,6 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1719
1704
|
...options?.showCursor !== void 0 && { showCursor: options.showCursor }
|
|
1720
1705
|
};
|
|
1721
1706
|
const response = await this.post("/api/desktop/screenshot/region", data);
|
|
1722
|
-
this.logSuccess("Region screenshot captured", `${region.width}x${region.height}`);
|
|
1723
1707
|
if (wantsBytes) {
|
|
1724
1708
|
const binaryString = atob(response.data);
|
|
1725
1709
|
const bytes = new Uint8Array(binaryString.length);
|
|
@@ -1731,7 +1715,6 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1731
1715
|
}
|
|
1732
1716
|
return response;
|
|
1733
1717
|
} catch (error) {
|
|
1734
|
-
this.logError("desktop.screenshotRegion", error);
|
|
1735
1718
|
throw error;
|
|
1736
1719
|
}
|
|
1737
1720
|
}
|
|
@@ -1746,9 +1729,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1746
1729
|
button: options?.button ?? "left",
|
|
1747
1730
|
clickCount: 1
|
|
1748
1731
|
});
|
|
1749
|
-
this.logSuccess("Mouse click", `(${x}, ${y})`);
|
|
1750
1732
|
} catch (error) {
|
|
1751
|
-
this.logError("desktop.click", error);
|
|
1752
1733
|
throw error;
|
|
1753
1734
|
}
|
|
1754
1735
|
}
|
|
@@ -1763,9 +1744,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1763
1744
|
button: options?.button ?? "left",
|
|
1764
1745
|
clickCount: 2
|
|
1765
1746
|
});
|
|
1766
|
-
this.logSuccess("Mouse double click", `(${x}, ${y})`);
|
|
1767
1747
|
} catch (error) {
|
|
1768
|
-
this.logError("desktop.doubleClick", error);
|
|
1769
1748
|
throw error;
|
|
1770
1749
|
}
|
|
1771
1750
|
}
|
|
@@ -1780,9 +1759,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1780
1759
|
button: options?.button ?? "left",
|
|
1781
1760
|
clickCount: 3
|
|
1782
1761
|
});
|
|
1783
|
-
this.logSuccess("Mouse triple click", `(${x}, ${y})`);
|
|
1784
1762
|
} catch (error) {
|
|
1785
|
-
this.logError("desktop.tripleClick", error);
|
|
1786
1763
|
throw error;
|
|
1787
1764
|
}
|
|
1788
1765
|
}
|
|
@@ -1797,9 +1774,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1797
1774
|
button: "right",
|
|
1798
1775
|
clickCount: 1
|
|
1799
1776
|
});
|
|
1800
|
-
this.logSuccess("Mouse right click", `(${x}, ${y})`);
|
|
1801
1777
|
} catch (error) {
|
|
1802
|
-
this.logError("desktop.rightClick", error);
|
|
1803
1778
|
throw error;
|
|
1804
1779
|
}
|
|
1805
1780
|
}
|
|
@@ -1814,9 +1789,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1814
1789
|
button: "middle",
|
|
1815
1790
|
clickCount: 1
|
|
1816
1791
|
});
|
|
1817
|
-
this.logSuccess("Mouse middle click", `(${x}, ${y})`);
|
|
1818
1792
|
} catch (error) {
|
|
1819
|
-
this.logError("desktop.middleClick", error);
|
|
1820
1793
|
throw error;
|
|
1821
1794
|
}
|
|
1822
1795
|
}
|
|
@@ -1830,9 +1803,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1830
1803
|
...y !== void 0 && { y },
|
|
1831
1804
|
button: options?.button ?? "left"
|
|
1832
1805
|
});
|
|
1833
|
-
this.logSuccess("Mouse down", x !== void 0 ? `(${x}, ${y})` : "current position");
|
|
1834
1806
|
} catch (error) {
|
|
1835
|
-
this.logError("desktop.mouseDown", error);
|
|
1836
1807
|
throw error;
|
|
1837
1808
|
}
|
|
1838
1809
|
}
|
|
@@ -1846,9 +1817,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1846
1817
|
...y !== void 0 && { y },
|
|
1847
1818
|
button: options?.button ?? "left"
|
|
1848
1819
|
});
|
|
1849
|
-
this.logSuccess("Mouse up", x !== void 0 ? `(${x}, ${y})` : "current position");
|
|
1850
1820
|
} catch (error) {
|
|
1851
|
-
this.logError("desktop.mouseUp", error);
|
|
1852
1821
|
throw error;
|
|
1853
1822
|
}
|
|
1854
1823
|
}
|
|
@@ -1861,9 +1830,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1861
1830
|
x,
|
|
1862
1831
|
y
|
|
1863
1832
|
});
|
|
1864
|
-
this.logSuccess("Mouse move", `(${x}, ${y})`);
|
|
1865
1833
|
} catch (error) {
|
|
1866
|
-
this.logError("desktop.moveMouse", error);
|
|
1867
1834
|
throw error;
|
|
1868
1835
|
}
|
|
1869
1836
|
}
|
|
@@ -1879,9 +1846,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1879
1846
|
endY,
|
|
1880
1847
|
button: options?.button ?? "left"
|
|
1881
1848
|
});
|
|
1882
|
-
this.logSuccess("Mouse drag", `(${startX},${startY}) -> (${endX},${endY})`);
|
|
1883
1849
|
} catch (error) {
|
|
1884
|
-
this.logError("desktop.drag", error);
|
|
1885
1850
|
throw error;
|
|
1886
1851
|
}
|
|
1887
1852
|
}
|
|
@@ -1896,9 +1861,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1896
1861
|
direction,
|
|
1897
1862
|
amount
|
|
1898
1863
|
});
|
|
1899
|
-
this.logSuccess("Mouse scroll", `${direction} ${amount} at (${x}, ${y})`);
|
|
1900
1864
|
} catch (error) {
|
|
1901
|
-
this.logError("desktop.scroll", error);
|
|
1902
1865
|
throw error;
|
|
1903
1866
|
}
|
|
1904
1867
|
}
|
|
@@ -1907,11 +1870,8 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1907
1870
|
*/
|
|
1908
1871
|
async getCursorPosition() {
|
|
1909
1872
|
try {
|
|
1910
|
-
|
|
1911
|
-
this.logSuccess("Cursor position retrieved", `(${response.x}, ${response.y})`);
|
|
1912
|
-
return response;
|
|
1873
|
+
return await this.get("/api/desktop/mouse/position");
|
|
1913
1874
|
} catch (error) {
|
|
1914
|
-
this.logError("desktop.getCursorPosition", error);
|
|
1915
1875
|
throw error;
|
|
1916
1876
|
}
|
|
1917
1877
|
}
|
|
@@ -1924,9 +1884,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1924
1884
|
text,
|
|
1925
1885
|
...options?.delayMs !== void 0 && { delayMs: options.delayMs }
|
|
1926
1886
|
});
|
|
1927
|
-
this.logSuccess("Keyboard type", `${text.length} chars`);
|
|
1928
1887
|
} catch (error) {
|
|
1929
|
-
this.logError("desktop.type", error);
|
|
1930
1888
|
throw error;
|
|
1931
1889
|
}
|
|
1932
1890
|
}
|
|
@@ -1936,9 +1894,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1936
1894
|
async press(key) {
|
|
1937
1895
|
try {
|
|
1938
1896
|
await this.post("/api/desktop/keyboard/press", { key });
|
|
1939
|
-
this.logSuccess("Key press", key);
|
|
1940
1897
|
} catch (error) {
|
|
1941
|
-
this.logError("desktop.press", error);
|
|
1942
1898
|
throw error;
|
|
1943
1899
|
}
|
|
1944
1900
|
}
|
|
@@ -1948,9 +1904,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1948
1904
|
async keyDown(key) {
|
|
1949
1905
|
try {
|
|
1950
1906
|
await this.post("/api/desktop/keyboard/down", { key });
|
|
1951
|
-
this.logSuccess("Key down", key);
|
|
1952
1907
|
} catch (error) {
|
|
1953
|
-
this.logError("desktop.keyDown", error);
|
|
1954
1908
|
throw error;
|
|
1955
1909
|
}
|
|
1956
1910
|
}
|
|
@@ -1960,9 +1914,7 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1960
1914
|
async keyUp(key) {
|
|
1961
1915
|
try {
|
|
1962
1916
|
await this.post("/api/desktop/keyboard/up", { key });
|
|
1963
|
-
this.logSuccess("Key up", key);
|
|
1964
1917
|
} catch (error) {
|
|
1965
|
-
this.logError("desktop.keyUp", error);
|
|
1966
1918
|
throw error;
|
|
1967
1919
|
}
|
|
1968
1920
|
}
|
|
@@ -1971,11 +1923,8 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1971
1923
|
*/
|
|
1972
1924
|
async getScreenSize() {
|
|
1973
1925
|
try {
|
|
1974
|
-
|
|
1975
|
-
this.logSuccess("Screen size retrieved", `${response.width}x${response.height}`);
|
|
1976
|
-
return response;
|
|
1926
|
+
return await this.get("/api/desktop/screen/size");
|
|
1977
1927
|
} catch (error) {
|
|
1978
|
-
this.logError("desktop.getScreenSize", error);
|
|
1979
1928
|
throw error;
|
|
1980
1929
|
}
|
|
1981
1930
|
}
|
|
@@ -1984,11 +1933,8 @@ var DesktopClient = class extends BaseHttpClient {
|
|
|
1984
1933
|
*/
|
|
1985
1934
|
async getProcessStatus(name) {
|
|
1986
1935
|
try {
|
|
1987
|
-
|
|
1988
|
-
this.logSuccess("Desktop process status retrieved", name);
|
|
1989
|
-
return response;
|
|
1936
|
+
return await this.get(`/api/desktop/process/${encodeURIComponent(name)}/status`);
|
|
1990
1937
|
} catch (error) {
|
|
1991
|
-
this.logError("desktop.getProcessStatus", error);
|
|
1992
1938
|
throw error;
|
|
1993
1939
|
}
|
|
1994
1940
|
}
|
|
@@ -2013,11 +1959,8 @@ var FileClient = class extends BaseHttpClient {
|
|
|
2013
1959
|
sessionId,
|
|
2014
1960
|
recursive: options?.recursive ?? false
|
|
2015
1961
|
};
|
|
2016
|
-
|
|
2017
|
-
this.logSuccess("Directory created", `${path$1} (recursive: ${data.recursive})`);
|
|
2018
|
-
return response;
|
|
1962
|
+
return await this.post("/api/mkdir", data);
|
|
2019
1963
|
} catch (error) {
|
|
2020
|
-
this.logError("mkdir", error);
|
|
2021
1964
|
throw error;
|
|
2022
1965
|
}
|
|
2023
1966
|
}
|
|
@@ -2036,11 +1979,8 @@ var FileClient = class extends BaseHttpClient {
|
|
|
2036
1979
|
sessionId,
|
|
2037
1980
|
encoding: options?.encoding
|
|
2038
1981
|
};
|
|
2039
|
-
|
|
2040
|
-
this.logSuccess("File written", `${path$1} (${content.length} chars)`);
|
|
2041
|
-
return response;
|
|
1982
|
+
return await this.post("/api/write", data);
|
|
2042
1983
|
} catch (error) {
|
|
2043
|
-
this.logError("writeFile", error);
|
|
2044
1984
|
throw error;
|
|
2045
1985
|
}
|
|
2046
1986
|
}
|
|
@@ -2057,11 +1997,8 @@ var FileClient = class extends BaseHttpClient {
|
|
|
2057
1997
|
sessionId,
|
|
2058
1998
|
encoding: options?.encoding
|
|
2059
1999
|
};
|
|
2060
|
-
|
|
2061
|
-
this.logSuccess("File read", `${path$1} (${response.content.length} chars)`);
|
|
2062
|
-
return response;
|
|
2000
|
+
return await this.post("/api/read", data);
|
|
2063
2001
|
} catch (error) {
|
|
2064
|
-
this.logError("readFile", error);
|
|
2065
2002
|
throw error;
|
|
2066
2003
|
}
|
|
2067
2004
|
}
|
|
@@ -2077,11 +2014,8 @@ var FileClient = class extends BaseHttpClient {
|
|
|
2077
2014
|
path: path$1,
|
|
2078
2015
|
sessionId
|
|
2079
2016
|
};
|
|
2080
|
-
|
|
2081
|
-
this.logSuccess("File stream started", path$1);
|
|
2082
|
-
return stream;
|
|
2017
|
+
return await this.doStreamFetch("/api/read/stream", data);
|
|
2083
2018
|
} catch (error) {
|
|
2084
|
-
this.logError("readFileStream", error);
|
|
2085
2019
|
throw error;
|
|
2086
2020
|
}
|
|
2087
2021
|
}
|
|
@@ -2096,11 +2030,8 @@ var FileClient = class extends BaseHttpClient {
|
|
|
2096
2030
|
path: path$1,
|
|
2097
2031
|
sessionId
|
|
2098
2032
|
};
|
|
2099
|
-
|
|
2100
|
-
this.logSuccess("File deleted", path$1);
|
|
2101
|
-
return response;
|
|
2033
|
+
return await this.post("/api/delete", data);
|
|
2102
2034
|
} catch (error) {
|
|
2103
|
-
this.logError("deleteFile", error);
|
|
2104
2035
|
throw error;
|
|
2105
2036
|
}
|
|
2106
2037
|
}
|
|
@@ -2117,11 +2048,8 @@ var FileClient = class extends BaseHttpClient {
|
|
|
2117
2048
|
newPath,
|
|
2118
2049
|
sessionId
|
|
2119
2050
|
};
|
|
2120
|
-
|
|
2121
|
-
this.logSuccess("File renamed", `${path$1} -> ${newPath}`);
|
|
2122
|
-
return response;
|
|
2051
|
+
return await this.post("/api/rename", data);
|
|
2123
2052
|
} catch (error) {
|
|
2124
|
-
this.logError("renameFile", error);
|
|
2125
2053
|
throw error;
|
|
2126
2054
|
}
|
|
2127
2055
|
}
|
|
@@ -2138,11 +2066,8 @@ var FileClient = class extends BaseHttpClient {
|
|
|
2138
2066
|
destinationPath: newPath,
|
|
2139
2067
|
sessionId
|
|
2140
2068
|
};
|
|
2141
|
-
|
|
2142
|
-
this.logSuccess("File moved", `${path$1} -> ${newPath}`);
|
|
2143
|
-
return response;
|
|
2069
|
+
return await this.post("/api/move", data);
|
|
2144
2070
|
} catch (error) {
|
|
2145
|
-
this.logError("moveFile", error);
|
|
2146
2071
|
throw error;
|
|
2147
2072
|
}
|
|
2148
2073
|
}
|
|
@@ -2159,11 +2084,8 @@ var FileClient = class extends BaseHttpClient {
|
|
|
2159
2084
|
sessionId,
|
|
2160
2085
|
options: options || {}
|
|
2161
2086
|
};
|
|
2162
|
-
|
|
2163
|
-
this.logSuccess("Files listed", `${path$1} (${response.count} files)`);
|
|
2164
|
-
return response;
|
|
2087
|
+
return await this.post("/api/list-files", data);
|
|
2165
2088
|
} catch (error) {
|
|
2166
|
-
this.logError("listFiles", error);
|
|
2167
2089
|
throw error;
|
|
2168
2090
|
}
|
|
2169
2091
|
}
|
|
@@ -2178,11 +2100,8 @@ var FileClient = class extends BaseHttpClient {
|
|
|
2178
2100
|
path: path$1,
|
|
2179
2101
|
sessionId
|
|
2180
2102
|
};
|
|
2181
|
-
|
|
2182
|
-
this.logSuccess("Path existence checked", `${path$1} (exists: ${response.exists})`);
|
|
2183
|
-
return response;
|
|
2103
|
+
return await this.post("/api/exists", data);
|
|
2184
2104
|
} catch (error) {
|
|
2185
|
-
this.logError("exists", error);
|
|
2186
2105
|
throw error;
|
|
2187
2106
|
}
|
|
2188
2107
|
}
|
|
@@ -2218,11 +2137,8 @@ var GitClient = class extends BaseHttpClient {
|
|
|
2218
2137
|
if (!Number.isInteger(options.depth) || options.depth <= 0) throw new Error(`Invalid depth value: ${options.depth}. Must be a positive integer (e.g., 1, 5, 10).`);
|
|
2219
2138
|
data.depth = options.depth;
|
|
2220
2139
|
}
|
|
2221
|
-
|
|
2222
|
-
this.logSuccess("Repository cloned", `${repoUrl} (branch: ${response.branch}) -> ${response.targetDir}`);
|
|
2223
|
-
return response;
|
|
2140
|
+
return await this.post("/api/git/checkout", data);
|
|
2224
2141
|
} catch (error) {
|
|
2225
|
-
this.logError("checkout", error);
|
|
2226
2142
|
throw error;
|
|
2227
2143
|
}
|
|
2228
2144
|
}
|
|
@@ -2313,7 +2229,6 @@ var InterpreterClient = class extends BaseHttpClient {
|
|
|
2313
2229
|
for (let attempt = 0; attempt < this.maxRetries; attempt++) try {
|
|
2314
2230
|
return await operation();
|
|
2315
2231
|
} catch (error) {
|
|
2316
|
-
this.logError("executeWithRetry", error);
|
|
2317
2232
|
lastError = error;
|
|
2318
2233
|
if (this.isRetryableError(error)) {
|
|
2319
2234
|
if (attempt < this.maxRetries - 1) {
|
|
@@ -2401,9 +2316,7 @@ var InterpreterClient = class extends BaseHttpClient {
|
|
|
2401
2316
|
break;
|
|
2402
2317
|
case "execution_complete": break;
|
|
2403
2318
|
}
|
|
2404
|
-
} catch
|
|
2405
|
-
this.logError("parseExecutionResult", error);
|
|
2406
|
-
}
|
|
2319
|
+
} catch {}
|
|
2407
2320
|
}
|
|
2408
2321
|
};
|
|
2409
2322
|
|
|
@@ -2426,11 +2339,8 @@ var PortClient = class extends BaseHttpClient {
|
|
|
2426
2339
|
sessionId,
|
|
2427
2340
|
name
|
|
2428
2341
|
};
|
|
2429
|
-
|
|
2430
|
-
this.logSuccess("Port exposed", `${port} exposed at ${response.url}${name ? ` (${name})` : ""}`);
|
|
2431
|
-
return response;
|
|
2342
|
+
return await this.post("/api/expose-port", data);
|
|
2432
2343
|
} catch (error) {
|
|
2433
|
-
this.logError("exposePort", error);
|
|
2434
2344
|
throw error;
|
|
2435
2345
|
}
|
|
2436
2346
|
}
|
|
@@ -2442,11 +2352,8 @@ var PortClient = class extends BaseHttpClient {
|
|
|
2442
2352
|
async unexposePort(port, sessionId) {
|
|
2443
2353
|
try {
|
|
2444
2354
|
const url = `/api/exposed-ports/${port}?session=${encodeURIComponent(sessionId)}`;
|
|
2445
|
-
|
|
2446
|
-
this.logSuccess("Port unexposed", `${port}`);
|
|
2447
|
-
return response;
|
|
2355
|
+
return await this.delete(url);
|
|
2448
2356
|
} catch (error) {
|
|
2449
|
-
this.logError("unexposePort", error);
|
|
2450
2357
|
throw error;
|
|
2451
2358
|
}
|
|
2452
2359
|
}
|
|
@@ -2457,11 +2364,8 @@ var PortClient = class extends BaseHttpClient {
|
|
|
2457
2364
|
async getExposedPorts(sessionId) {
|
|
2458
2365
|
try {
|
|
2459
2366
|
const url = `/api/exposed-ports?session=${encodeURIComponent(sessionId)}`;
|
|
2460
|
-
|
|
2461
|
-
this.logSuccess("Exposed ports retrieved", `${response.ports.length} ports exposed`);
|
|
2462
|
-
return response;
|
|
2367
|
+
return await this.get(url);
|
|
2463
2368
|
} catch (error) {
|
|
2464
|
-
this.logError("getExposedPorts", error);
|
|
2465
2369
|
throw error;
|
|
2466
2370
|
}
|
|
2467
2371
|
}
|
|
@@ -2472,11 +2376,8 @@ var PortClient = class extends BaseHttpClient {
|
|
|
2472
2376
|
*/
|
|
2473
2377
|
async watchPort(request) {
|
|
2474
2378
|
try {
|
|
2475
|
-
|
|
2476
|
-
this.logSuccess("Port watch started", `port ${request.port}`);
|
|
2477
|
-
return stream;
|
|
2379
|
+
return await this.doStreamFetch("/api/port-watch", request);
|
|
2478
2380
|
} catch (error) {
|
|
2479
|
-
this.logError("watchPort", error);
|
|
2480
2381
|
throw error;
|
|
2481
2382
|
}
|
|
2482
2383
|
}
|
|
@@ -2499,6 +2400,7 @@ var ProcessClient = class extends BaseHttpClient {
|
|
|
2499
2400
|
const data = {
|
|
2500
2401
|
command,
|
|
2501
2402
|
sessionId,
|
|
2403
|
+
...options?.origin !== void 0 && { origin: options.origin },
|
|
2502
2404
|
...options?.processId !== void 0 && { processId: options.processId },
|
|
2503
2405
|
...options?.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
|
|
2504
2406
|
...options?.env !== void 0 && { env: options.env },
|
|
@@ -2506,11 +2408,8 @@ var ProcessClient = class extends BaseHttpClient {
|
|
|
2506
2408
|
...options?.encoding !== void 0 && { encoding: options.encoding },
|
|
2507
2409
|
...options?.autoCleanup !== void 0 && { autoCleanup: options.autoCleanup }
|
|
2508
2410
|
};
|
|
2509
|
-
|
|
2510
|
-
this.logSuccess("Process started", `${command} (ID: ${response.processId})`);
|
|
2511
|
-
return response;
|
|
2411
|
+
return await this.post("/api/process/start", data);
|
|
2512
2412
|
} catch (error) {
|
|
2513
|
-
this.logError("startProcess", error);
|
|
2514
2413
|
throw error;
|
|
2515
2414
|
}
|
|
2516
2415
|
}
|
|
@@ -2519,11 +2418,8 @@ var ProcessClient = class extends BaseHttpClient {
|
|
|
2519
2418
|
*/
|
|
2520
2419
|
async listProcesses() {
|
|
2521
2420
|
try {
|
|
2522
|
-
|
|
2523
|
-
this.logSuccess("Processes listed", `${response.processes.length} processes`);
|
|
2524
|
-
return response;
|
|
2421
|
+
return await this.get(`/api/process/list`);
|
|
2525
2422
|
} catch (error) {
|
|
2526
|
-
this.logError("listProcesses", error);
|
|
2527
2423
|
throw error;
|
|
2528
2424
|
}
|
|
2529
2425
|
}
|
|
@@ -2534,11 +2430,8 @@ var ProcessClient = class extends BaseHttpClient {
|
|
|
2534
2430
|
async getProcess(processId) {
|
|
2535
2431
|
try {
|
|
2536
2432
|
const url = `/api/process/${processId}`;
|
|
2537
|
-
|
|
2538
|
-
this.logSuccess("Process retrieved", `ID: ${processId}`);
|
|
2539
|
-
return response;
|
|
2433
|
+
return await this.get(url);
|
|
2540
2434
|
} catch (error) {
|
|
2541
|
-
this.logError("getProcess", error);
|
|
2542
2435
|
throw error;
|
|
2543
2436
|
}
|
|
2544
2437
|
}
|
|
@@ -2549,11 +2442,8 @@ var ProcessClient = class extends BaseHttpClient {
|
|
|
2549
2442
|
async killProcess(processId) {
|
|
2550
2443
|
try {
|
|
2551
2444
|
const url = `/api/process/${processId}`;
|
|
2552
|
-
|
|
2553
|
-
this.logSuccess("Process killed", `ID: ${processId}`);
|
|
2554
|
-
return response;
|
|
2445
|
+
return await this.delete(url);
|
|
2555
2446
|
} catch (error) {
|
|
2556
|
-
this.logError("killProcess", error);
|
|
2557
2447
|
throw error;
|
|
2558
2448
|
}
|
|
2559
2449
|
}
|
|
@@ -2562,11 +2452,8 @@ var ProcessClient = class extends BaseHttpClient {
|
|
|
2562
2452
|
*/
|
|
2563
2453
|
async killAllProcesses() {
|
|
2564
2454
|
try {
|
|
2565
|
-
|
|
2566
|
-
this.logSuccess("All processes killed", `${response.cleanedCount} processes terminated`);
|
|
2567
|
-
return response;
|
|
2455
|
+
return await this.delete(`/api/process/kill-all`);
|
|
2568
2456
|
} catch (error) {
|
|
2569
|
-
this.logError("killAllProcesses", error);
|
|
2570
2457
|
throw error;
|
|
2571
2458
|
}
|
|
2572
2459
|
}
|
|
@@ -2577,11 +2464,8 @@ var ProcessClient = class extends BaseHttpClient {
|
|
|
2577
2464
|
async getProcessLogs(processId) {
|
|
2578
2465
|
try {
|
|
2579
2466
|
const url = `/api/process/${processId}/logs`;
|
|
2580
|
-
|
|
2581
|
-
this.logSuccess("Process logs retrieved", `ID: ${processId}, stdout: ${response.stdout.length} chars, stderr: ${response.stderr.length} chars`);
|
|
2582
|
-
return response;
|
|
2467
|
+
return await this.get(url);
|
|
2583
2468
|
} catch (error) {
|
|
2584
|
-
this.logError("getProcessLogs", error);
|
|
2585
2469
|
throw error;
|
|
2586
2470
|
}
|
|
2587
2471
|
}
|
|
@@ -2592,11 +2476,8 @@ var ProcessClient = class extends BaseHttpClient {
|
|
|
2592
2476
|
async streamProcessLogs(processId) {
|
|
2593
2477
|
try {
|
|
2594
2478
|
const url = `/api/process/${processId}/stream`;
|
|
2595
|
-
|
|
2596
|
-
this.logSuccess("Process log stream started", `ID: ${processId}`);
|
|
2597
|
-
return stream;
|
|
2479
|
+
return await this.doStreamFetch(url, void 0, "GET");
|
|
2598
2480
|
} catch (error) {
|
|
2599
|
-
this.logError("streamProcessLogs", error);
|
|
2600
2481
|
throw error;
|
|
2601
2482
|
}
|
|
2602
2483
|
}
|
|
@@ -2613,11 +2494,8 @@ var UtilityClient = class extends BaseHttpClient {
|
|
|
2613
2494
|
*/
|
|
2614
2495
|
async ping() {
|
|
2615
2496
|
try {
|
|
2616
|
-
|
|
2617
|
-
this.logSuccess("Ping successful", response.message);
|
|
2618
|
-
return response.message;
|
|
2497
|
+
return (await this.get("/api/ping")).message;
|
|
2619
2498
|
} catch (error) {
|
|
2620
|
-
this.logError("ping", error);
|
|
2621
2499
|
throw error;
|
|
2622
2500
|
}
|
|
2623
2501
|
}
|
|
@@ -2626,11 +2504,8 @@ var UtilityClient = class extends BaseHttpClient {
|
|
|
2626
2504
|
*/
|
|
2627
2505
|
async getCommands() {
|
|
2628
2506
|
try {
|
|
2629
|
-
|
|
2630
|
-
this.logSuccess("Commands retrieved", `${response.count} commands available`);
|
|
2631
|
-
return response.availableCommands;
|
|
2507
|
+
return (await this.get("/api/commands")).availableCommands;
|
|
2632
2508
|
} catch (error) {
|
|
2633
|
-
this.logError("getCommands", error);
|
|
2634
2509
|
throw error;
|
|
2635
2510
|
}
|
|
2636
2511
|
}
|
|
@@ -2640,11 +2515,8 @@ var UtilityClient = class extends BaseHttpClient {
|
|
|
2640
2515
|
*/
|
|
2641
2516
|
async createSession(options) {
|
|
2642
2517
|
try {
|
|
2643
|
-
|
|
2644
|
-
this.logSuccess("Session created", `ID: ${options.id}`);
|
|
2645
|
-
return response;
|
|
2518
|
+
return await this.post("/api/session/create", options);
|
|
2646
2519
|
} catch (error) {
|
|
2647
|
-
this.logError("createSession", error);
|
|
2648
2520
|
throw error;
|
|
2649
2521
|
}
|
|
2650
2522
|
}
|
|
@@ -2654,11 +2526,8 @@ var UtilityClient = class extends BaseHttpClient {
|
|
|
2654
2526
|
*/
|
|
2655
2527
|
async deleteSession(sessionId) {
|
|
2656
2528
|
try {
|
|
2657
|
-
|
|
2658
|
-
this.logSuccess("Session deleted", `ID: ${sessionId}`);
|
|
2659
|
-
return response;
|
|
2529
|
+
return await this.post("/api/session/delete", { sessionId });
|
|
2660
2530
|
} catch (error) {
|
|
2661
|
-
this.logError("deleteSession", error);
|
|
2662
2531
|
throw error;
|
|
2663
2532
|
}
|
|
2664
2533
|
}
|
|
@@ -2668,9 +2537,7 @@ var UtilityClient = class extends BaseHttpClient {
|
|
|
2668
2537
|
*/
|
|
2669
2538
|
async getVersion() {
|
|
2670
2539
|
try {
|
|
2671
|
-
|
|
2672
|
-
this.logSuccess("Version retrieved", response.version);
|
|
2673
|
-
return response.version;
|
|
2540
|
+
return (await this.get("/api/version")).version;
|
|
2674
2541
|
} catch (error) {
|
|
2675
2542
|
this.logger.debug("Failed to get container version (may be old container)", { error });
|
|
2676
2543
|
return "unknown";
|
|
@@ -2700,11 +2567,8 @@ var WatchClient = class extends BaseHttpClient {
|
|
|
2700
2567
|
async watch(request) {
|
|
2701
2568
|
try {
|
|
2702
2569
|
const stream = await this.doStreamFetch("/api/watch", request);
|
|
2703
|
-
|
|
2704
|
-
this.logSuccess("File watch started", request.path);
|
|
2705
|
-
return readyStream;
|
|
2570
|
+
return await this.waitForReadiness(stream);
|
|
2706
2571
|
} catch (error) {
|
|
2707
|
-
this.logError("watch", error);
|
|
2708
2572
|
throw error;
|
|
2709
2573
|
}
|
|
2710
2574
|
}
|
|
@@ -3698,7 +3562,7 @@ function buildS3fsSource(bucket, prefix) {
|
|
|
3698
3562
|
* This file is auto-updated by .github/changeset-version.ts during releases
|
|
3699
3563
|
* DO NOT EDIT MANUALLY - Changes will be overwritten on the next version bump
|
|
3700
3564
|
*/
|
|
3701
|
-
const SDK_VERSION = "0.7.
|
|
3565
|
+
const SDK_VERSION = "0.7.21";
|
|
3702
3566
|
|
|
3703
3567
|
//#endregion
|
|
3704
3568
|
//#region src/sandbox.ts
|
|
@@ -3929,6 +3793,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
3929
3793
|
port: 3e3,
|
|
3930
3794
|
stub: this,
|
|
3931
3795
|
retryTimeoutMs: this.computeRetryTimeoutMs(),
|
|
3796
|
+
defaultHeaders: { "X-Sandbox-Id": this.ctx.id.toString() },
|
|
3932
3797
|
...this.transport === "websocket" && {
|
|
3933
3798
|
transportMode: "websocket",
|
|
3934
3799
|
wsUrl: "ws://localhost:3000/ws"
|
|
@@ -4022,12 +3887,12 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4022
3887
|
if (this.defaultSession) {
|
|
4023
3888
|
for (const key of toUnset) {
|
|
4024
3889
|
const unsetCommand = `unset ${key}`;
|
|
4025
|
-
const result = await this.client.commands.execute(unsetCommand, this.defaultSession);
|
|
3890
|
+
const result = await this.client.commands.execute(unsetCommand, this.defaultSession, { origin: "internal" });
|
|
4026
3891
|
if (result.exitCode !== 0) throw new Error(`Failed to unset ${key}: ${result.stderr || "Unknown error"}`);
|
|
4027
3892
|
}
|
|
4028
3893
|
for (const [key, value] of Object.entries(toSet)) {
|
|
4029
3894
|
const exportCommand = `export ${key}=${shellEscape(value)}`;
|
|
4030
|
-
const result = await this.client.commands.execute(exportCommand, this.defaultSession);
|
|
3895
|
+
const result = await this.client.commands.execute(exportCommand, this.defaultSession, { origin: "internal" });
|
|
4031
3896
|
if (result.exitCode !== 0) throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
|
|
4032
3897
|
}
|
|
4033
3898
|
}
|
|
@@ -4093,7 +3958,6 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4093
3958
|
* @throws InvalidMountConfigError if bucket name, mount path, or endpoint is invalid
|
|
4094
3959
|
*/
|
|
4095
3960
|
async mountBucket(bucket, mountPath, options) {
|
|
4096
|
-
this.logger.info(`Mounting bucket ${bucket} to ${mountPath}`);
|
|
4097
3961
|
if ("localBucket" in options && options.localBucket) {
|
|
4098
3962
|
await this.mountBucketLocal(bucket, mountPath, options);
|
|
4099
3963
|
return;
|
|
@@ -4104,82 +3968,118 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4104
3968
|
* Local dev mount: bidirectional sync via R2 binding + file/watch APIs
|
|
4105
3969
|
*/
|
|
4106
3970
|
async mountBucketLocal(bucket, mountPath, options) {
|
|
4107
|
-
const
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
if (this.activeMounts.has(mountPath)) throw new InvalidMountConfigError(`Mount path already in use: ${mountPath}`);
|
|
4111
|
-
const sessionId = await this.ensureDefaultSession();
|
|
4112
|
-
const syncManager = new LocalMountSyncManager({
|
|
4113
|
-
bucket: r2Binding,
|
|
4114
|
-
mountPath,
|
|
4115
|
-
prefix: options.prefix,
|
|
4116
|
-
readOnly: options.readOnly ?? false,
|
|
4117
|
-
client: this.client,
|
|
4118
|
-
sessionId,
|
|
4119
|
-
logger: this.logger
|
|
4120
|
-
});
|
|
4121
|
-
const mountInfo = {
|
|
4122
|
-
mountType: "local-sync",
|
|
4123
|
-
bucket,
|
|
4124
|
-
mountPath,
|
|
4125
|
-
syncManager,
|
|
4126
|
-
mounted: false
|
|
4127
|
-
};
|
|
4128
|
-
this.activeMounts.set(mountPath, mountInfo);
|
|
3971
|
+
const mountStartTime = Date.now();
|
|
3972
|
+
let mountOutcome = "error";
|
|
3973
|
+
let mountError;
|
|
4129
3974
|
try {
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
3975
|
+
const r2Binding = this.env[bucket];
|
|
3976
|
+
if (!r2Binding || !isR2Bucket(r2Binding)) throw new InvalidMountConfigError(`R2 binding "${bucket}" not found in env or is not an R2Bucket. Make sure the binding name matches your wrangler.jsonc R2 binding.`);
|
|
3977
|
+
if (!mountPath || !mountPath.startsWith("/")) throw new InvalidMountConfigError(`Invalid mount path: "${mountPath}". Must be an absolute path starting with /`);
|
|
3978
|
+
if (this.activeMounts.has(mountPath)) throw new InvalidMountConfigError(`Mount path already in use: ${mountPath}`);
|
|
3979
|
+
const sessionId = await this.ensureDefaultSession();
|
|
3980
|
+
const syncManager = new LocalMountSyncManager({
|
|
3981
|
+
bucket: r2Binding,
|
|
3982
|
+
mountPath,
|
|
3983
|
+
prefix: options.prefix,
|
|
3984
|
+
readOnly: options.readOnly ?? false,
|
|
3985
|
+
client: this.client,
|
|
3986
|
+
sessionId,
|
|
3987
|
+
logger: this.logger
|
|
3988
|
+
});
|
|
3989
|
+
const mountInfo = {
|
|
3990
|
+
mountType: "local-sync",
|
|
3991
|
+
bucket,
|
|
3992
|
+
mountPath,
|
|
3993
|
+
syncManager,
|
|
3994
|
+
mounted: false
|
|
3995
|
+
};
|
|
3996
|
+
this.activeMounts.set(mountPath, mountInfo);
|
|
3997
|
+
try {
|
|
3998
|
+
await syncManager.start();
|
|
3999
|
+
mountInfo.mounted = true;
|
|
4000
|
+
} catch (error) {
|
|
4001
|
+
await syncManager.stop();
|
|
4002
|
+
this.activeMounts.delete(mountPath);
|
|
4003
|
+
throw error;
|
|
4004
|
+
}
|
|
4005
|
+
mountOutcome = "success";
|
|
4133
4006
|
} catch (error) {
|
|
4134
|
-
|
|
4135
|
-
this.activeMounts.delete(mountPath);
|
|
4007
|
+
mountError = error instanceof Error ? error : new Error(String(error));
|
|
4136
4008
|
throw error;
|
|
4009
|
+
} finally {
|
|
4010
|
+
logCanonicalEvent(this.logger, {
|
|
4011
|
+
event: "bucket.mount",
|
|
4012
|
+
outcome: mountOutcome,
|
|
4013
|
+
durationMs: Date.now() - mountStartTime,
|
|
4014
|
+
bucket,
|
|
4015
|
+
mountPath,
|
|
4016
|
+
provider: "local-sync",
|
|
4017
|
+
prefix: options.prefix,
|
|
4018
|
+
error: mountError
|
|
4019
|
+
});
|
|
4137
4020
|
}
|
|
4138
4021
|
}
|
|
4139
4022
|
/**
|
|
4140
4023
|
* Production mount: S3FS-FUSE inside the container
|
|
4141
4024
|
*/
|
|
4142
4025
|
async mountBucketFuse(bucket, mountPath, options) {
|
|
4026
|
+
const mountStartTime = Date.now();
|
|
4143
4027
|
const prefix = options.prefix || void 0;
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
const s3fsSource = buildS3fsSource(bucket, prefix);
|
|
4149
|
-
const provider = options.provider || detectProviderFromUrl(options.endpoint);
|
|
4150
|
-
this.logger.debug(`Detected provider: ${provider || "unknown"}`, {
|
|
4151
|
-
explicitProvider: options.provider,
|
|
4152
|
-
prefix
|
|
4153
|
-
});
|
|
4154
|
-
const envObj = this.env;
|
|
4155
|
-
const credentials = detectCredentials(options, {
|
|
4156
|
-
AWS_ACCESS_KEY_ID: getEnvString(envObj, "AWS_ACCESS_KEY_ID"),
|
|
4157
|
-
AWS_SECRET_ACCESS_KEY: getEnvString(envObj, "AWS_SECRET_ACCESS_KEY"),
|
|
4158
|
-
R2_ACCESS_KEY_ID: this.r2AccessKeyId || void 0,
|
|
4159
|
-
R2_SECRET_ACCESS_KEY: this.r2SecretAccessKey || void 0,
|
|
4160
|
-
...this.envVars
|
|
4161
|
-
});
|
|
4162
|
-
const passwordFilePath = this.generatePasswordFilePath();
|
|
4163
|
-
const mountInfo = {
|
|
4164
|
-
mountType: "fuse",
|
|
4165
|
-
bucket: s3fsSource,
|
|
4166
|
-
mountPath,
|
|
4167
|
-
endpoint: options.endpoint,
|
|
4168
|
-
provider,
|
|
4169
|
-
passwordFilePath,
|
|
4170
|
-
mounted: false
|
|
4171
|
-
};
|
|
4172
|
-
this.activeMounts.set(mountPath, mountInfo);
|
|
4028
|
+
let mountOutcome = "error";
|
|
4029
|
+
let mountError;
|
|
4030
|
+
let passwordFilePath;
|
|
4031
|
+
let provider = null;
|
|
4173
4032
|
try {
|
|
4033
|
+
this.validateMountOptions(bucket, mountPath, {
|
|
4034
|
+
...options,
|
|
4035
|
+
prefix
|
|
4036
|
+
});
|
|
4037
|
+
const s3fsSource = buildS3fsSource(bucket, prefix);
|
|
4038
|
+
provider = options.provider || detectProviderFromUrl(options.endpoint);
|
|
4039
|
+
this.logger.debug(`Detected provider: ${provider || "unknown"}`, {
|
|
4040
|
+
explicitProvider: options.provider,
|
|
4041
|
+
prefix
|
|
4042
|
+
});
|
|
4043
|
+
const envObj = this.env;
|
|
4044
|
+
const credentials = detectCredentials(options, {
|
|
4045
|
+
AWS_ACCESS_KEY_ID: getEnvString(envObj, "AWS_ACCESS_KEY_ID"),
|
|
4046
|
+
AWS_SECRET_ACCESS_KEY: getEnvString(envObj, "AWS_SECRET_ACCESS_KEY"),
|
|
4047
|
+
R2_ACCESS_KEY_ID: this.r2AccessKeyId || void 0,
|
|
4048
|
+
R2_SECRET_ACCESS_KEY: this.r2SecretAccessKey || void 0,
|
|
4049
|
+
...this.envVars
|
|
4050
|
+
});
|
|
4051
|
+
passwordFilePath = this.generatePasswordFilePath();
|
|
4052
|
+
const mountInfo = {
|
|
4053
|
+
mountType: "fuse",
|
|
4054
|
+
bucket: s3fsSource,
|
|
4055
|
+
mountPath,
|
|
4056
|
+
endpoint: options.endpoint,
|
|
4057
|
+
provider,
|
|
4058
|
+
passwordFilePath,
|
|
4059
|
+
mounted: false
|
|
4060
|
+
};
|
|
4061
|
+
this.activeMounts.set(mountPath, mountInfo);
|
|
4174
4062
|
await this.createPasswordFile(passwordFilePath, bucket, credentials);
|
|
4175
|
-
await this.
|
|
4063
|
+
await this.execInternal(`mkdir -p ${shellEscape(mountPath)}`);
|
|
4176
4064
|
await this.executeS3FSMount(s3fsSource, mountPath, options, provider, passwordFilePath);
|
|
4177
4065
|
mountInfo.mounted = true;
|
|
4178
|
-
|
|
4066
|
+
mountOutcome = "success";
|
|
4179
4067
|
} catch (error) {
|
|
4180
|
-
|
|
4068
|
+
mountError = error instanceof Error ? error : new Error(String(error));
|
|
4069
|
+
if (passwordFilePath) await this.deletePasswordFile(passwordFilePath);
|
|
4181
4070
|
this.activeMounts.delete(mountPath);
|
|
4182
4071
|
throw error;
|
|
4072
|
+
} finally {
|
|
4073
|
+
logCanonicalEvent(this.logger, {
|
|
4074
|
+
event: "bucket.mount",
|
|
4075
|
+
outcome: mountOutcome,
|
|
4076
|
+
durationMs: Date.now() - mountStartTime,
|
|
4077
|
+
bucket,
|
|
4078
|
+
mountPath,
|
|
4079
|
+
provider: provider || "unknown",
|
|
4080
|
+
prefix,
|
|
4081
|
+
error: mountError
|
|
4082
|
+
});
|
|
4183
4083
|
}
|
|
4184
4084
|
}
|
|
4185
4085
|
/**
|
|
@@ -4189,21 +4089,37 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4189
4089
|
* @throws InvalidMountConfigError if mount path doesn't exist or isn't mounted
|
|
4190
4090
|
*/
|
|
4191
4091
|
async unmountBucket(mountPath) {
|
|
4192
|
-
|
|
4092
|
+
const unmountStartTime = Date.now();
|
|
4093
|
+
let unmountOutcome = "error";
|
|
4094
|
+
let unmountError;
|
|
4193
4095
|
const mountInfo = this.activeMounts.get(mountPath);
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4096
|
+
try {
|
|
4097
|
+
if (!mountInfo) throw new InvalidMountConfigError(`No active mount found at path: ${mountPath}`);
|
|
4098
|
+
if (mountInfo.mountType === "local-sync") {
|
|
4099
|
+
await mountInfo.syncManager.stop();
|
|
4100
|
+
mountInfo.mounted = false;
|
|
4101
|
+
this.activeMounts.delete(mountPath);
|
|
4102
|
+
} else try {
|
|
4103
|
+
await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
|
|
4104
|
+
mountInfo.mounted = false;
|
|
4105
|
+
this.activeMounts.delete(mountPath);
|
|
4106
|
+
} finally {
|
|
4107
|
+
await this.deletePasswordFile(mountInfo.passwordFilePath);
|
|
4108
|
+
}
|
|
4109
|
+
unmountOutcome = "success";
|
|
4110
|
+
} catch (error) {
|
|
4111
|
+
unmountError = error instanceof Error ? error : new Error(String(error));
|
|
4112
|
+
throw error;
|
|
4203
4113
|
} finally {
|
|
4204
|
-
|
|
4114
|
+
logCanonicalEvent(this.logger, {
|
|
4115
|
+
event: "bucket.unmount",
|
|
4116
|
+
outcome: unmountOutcome,
|
|
4117
|
+
durationMs: Date.now() - unmountStartTime,
|
|
4118
|
+
mountPath,
|
|
4119
|
+
bucket: mountInfo?.bucket,
|
|
4120
|
+
error: unmountError
|
|
4121
|
+
});
|
|
4205
4122
|
}
|
|
4206
|
-
this.logger.info(`Successfully unmounted bucket from ${mountPath}`);
|
|
4207
4123
|
}
|
|
4208
4124
|
/**
|
|
4209
4125
|
* Validate mount options
|
|
@@ -4232,18 +4148,19 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4232
4148
|
async createPasswordFile(passwordFilePath, bucket, credentials) {
|
|
4233
4149
|
const content = `${bucket}:${credentials.accessKeyId}:${credentials.secretAccessKey}`;
|
|
4234
4150
|
await this.writeFile(passwordFilePath, content);
|
|
4235
|
-
await this.
|
|
4236
|
-
this.logger.debug(`Created password file: ${passwordFilePath}`);
|
|
4151
|
+
await this.execInternal(`chmod 0600 ${shellEscape(passwordFilePath)}`);
|
|
4237
4152
|
}
|
|
4238
4153
|
/**
|
|
4239
4154
|
* Delete password file
|
|
4240
4155
|
*/
|
|
4241
4156
|
async deletePasswordFile(passwordFilePath) {
|
|
4242
4157
|
try {
|
|
4243
|
-
await this.
|
|
4244
|
-
this.logger.debug(`Deleted password file: ${passwordFilePath}`);
|
|
4158
|
+
await this.execInternal(`rm -f ${shellEscape(passwordFilePath)}`);
|
|
4245
4159
|
} catch (error) {
|
|
4246
|
-
this.logger.warn(
|
|
4160
|
+
this.logger.warn("password file cleanup failed", {
|
|
4161
|
+
passwordFilePath,
|
|
4162
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4163
|
+
});
|
|
4247
4164
|
}
|
|
4248
4165
|
}
|
|
4249
4166
|
/**
|
|
@@ -4258,44 +4175,61 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4258
4175
|
s3fsArgs.push(`url=${options.endpoint}`);
|
|
4259
4176
|
const optionsStr = shellEscape(s3fsArgs.join(","));
|
|
4260
4177
|
const mountCmd = `s3fs ${shellEscape(bucket)} ${shellEscape(mountPath)} -o ${optionsStr}`;
|
|
4261
|
-
this.
|
|
4262
|
-
bucket,
|
|
4263
|
-
mountPath,
|
|
4264
|
-
provider,
|
|
4265
|
-
resolvedOptions
|
|
4266
|
-
});
|
|
4267
|
-
const result = await this.exec(mountCmd);
|
|
4178
|
+
const result = await this.execInternal(mountCmd);
|
|
4268
4179
|
if (result.exitCode !== 0) throw new S3FSMountError(`S3FS mount failed: ${result.stderr || result.stdout || "Unknown error"}`);
|
|
4269
|
-
this.logger.debug("Mount command executed successfully");
|
|
4270
4180
|
}
|
|
4271
4181
|
/**
|
|
4272
4182
|
* Cleanup and destroy the sandbox container
|
|
4273
4183
|
*/
|
|
4274
4184
|
async destroy() {
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4185
|
+
const startTime = Date.now();
|
|
4186
|
+
let mountsProcessed = 0;
|
|
4187
|
+
let mountFailures = 0;
|
|
4188
|
+
let outcome = "error";
|
|
4189
|
+
let caughtError;
|
|
4190
|
+
try {
|
|
4191
|
+
if (this.ctx.container?.running) try {
|
|
4192
|
+
await this.client.desktop.stop();
|
|
4193
|
+
} catch {}
|
|
4194
|
+
this.client.disconnect();
|
|
4195
|
+
for (const [mountPath, mountInfo] of this.activeMounts.entries()) {
|
|
4196
|
+
mountsProcessed++;
|
|
4197
|
+
if (mountInfo.mountType === "local-sync") try {
|
|
4198
|
+
await mountInfo.syncManager.stop();
|
|
4199
|
+
mountInfo.mounted = false;
|
|
4200
|
+
} catch (error) {
|
|
4201
|
+
mountFailures++;
|
|
4202
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
4203
|
+
this.logger.warn(`Failed to stop local sync for ${mountPath}: ${errorMsg}`);
|
|
4204
|
+
}
|
|
4205
|
+
else {
|
|
4206
|
+
if (mountInfo.mounted) try {
|
|
4207
|
+
this.logger.debug(`Unmounting bucket ${mountInfo.bucket} from ${mountPath}`);
|
|
4208
|
+
await this.execInternal(`fusermount -u ${shellEscape(mountPath)}`);
|
|
4209
|
+
mountInfo.mounted = false;
|
|
4210
|
+
} catch (error) {
|
|
4211
|
+
mountFailures++;
|
|
4212
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
4213
|
+
this.logger.warn(`Failed to unmount bucket ${mountInfo.bucket} from ${mountPath}: ${errorMsg}`);
|
|
4214
|
+
}
|
|
4215
|
+
await this.deletePasswordFile(mountInfo.passwordFilePath);
|
|
4216
|
+
}
|
|
4295
4217
|
}
|
|
4296
|
-
|
|
4218
|
+
outcome = "success";
|
|
4219
|
+
await super.destroy();
|
|
4220
|
+
} catch (error) {
|
|
4221
|
+
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
4222
|
+
throw error;
|
|
4223
|
+
} finally {
|
|
4224
|
+
logCanonicalEvent(this.logger, {
|
|
4225
|
+
event: "sandbox.destroy",
|
|
4226
|
+
outcome,
|
|
4227
|
+
durationMs: Date.now() - startTime,
|
|
4228
|
+
mountsProcessed,
|
|
4229
|
+
mountFailures,
|
|
4230
|
+
error: caughtError
|
|
4231
|
+
});
|
|
4297
4232
|
}
|
|
4298
|
-
await super.destroy();
|
|
4299
4233
|
}
|
|
4300
4234
|
onStart() {
|
|
4301
4235
|
this.logger.debug("Sandbox started");
|
|
@@ -4308,23 +4242,27 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4308
4242
|
* Logs a warning if there's a mismatch
|
|
4309
4243
|
*/
|
|
4310
4244
|
async checkVersionCompatibility() {
|
|
4245
|
+
const sdkVersion = SDK_VERSION;
|
|
4246
|
+
let containerVersion;
|
|
4247
|
+
let outcome;
|
|
4311
4248
|
try {
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
if (containerVersion
|
|
4315
|
-
|
|
4316
|
-
return;
|
|
4317
|
-
}
|
|
4318
|
-
if (containerVersion !== sdkVersion) {
|
|
4319
|
-
const message = `Version mismatch detected! SDK version (${sdkVersion}) does not match container version (${containerVersion}). This may cause compatibility issues. Please update your container image to version ${sdkVersion}`;
|
|
4320
|
-
this.logger.warn(message);
|
|
4321
|
-
} else this.logger.debug("Version check passed", {
|
|
4322
|
-
sdkVersion,
|
|
4323
|
-
containerVersion
|
|
4324
|
-
});
|
|
4249
|
+
containerVersion = await this.client.utils.getVersion();
|
|
4250
|
+
if (containerVersion === "unknown") outcome = "container_version_unknown";
|
|
4251
|
+
else if (containerVersion !== sdkVersion) outcome = "version_mismatch";
|
|
4252
|
+
else outcome = "compatible";
|
|
4325
4253
|
} catch (error) {
|
|
4326
|
-
|
|
4327
|
-
|
|
4254
|
+
outcome = "check_failed";
|
|
4255
|
+
containerVersion = void 0;
|
|
4256
|
+
}
|
|
4257
|
+
const successLevel = outcome === "compatible" ? "debug" : outcome === "container_version_unknown" ? "info" : "warn";
|
|
4258
|
+
logCanonicalEvent(this.logger, {
|
|
4259
|
+
event: "version.check",
|
|
4260
|
+
outcome: "success",
|
|
4261
|
+
durationMs: 0,
|
|
4262
|
+
sdkVersion,
|
|
4263
|
+
containerVersion: containerVersion ?? "unknown",
|
|
4264
|
+
versionOutcome: outcome
|
|
4265
|
+
}, { successLevel });
|
|
4328
4266
|
}
|
|
4329
4267
|
async onStop() {
|
|
4330
4268
|
this.logger.debug("Sandbox stopped");
|
|
@@ -4345,84 +4283,66 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4345
4283
|
const state = await this.getState();
|
|
4346
4284
|
const containerRunning = this.ctx.container?.running;
|
|
4347
4285
|
const staleStateDetected = state.status === "healthy" && containerRunning === false;
|
|
4348
|
-
if (state.status !== "healthy" || containerRunning === false) {
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4286
|
+
if (state.status !== "healthy" || containerRunning === false) try {
|
|
4287
|
+
await this.startAndWaitForPorts({
|
|
4288
|
+
ports: port,
|
|
4289
|
+
cancellationOptions: {
|
|
4290
|
+
instanceGetTimeoutMS: this.containerTimeouts.instanceGetTimeoutMS,
|
|
4291
|
+
portReadyTimeoutMS: this.containerTimeouts.portReadyTimeoutMS,
|
|
4292
|
+
waitInterval: this.containerTimeouts.waitIntervalMS,
|
|
4293
|
+
abort: request.signal
|
|
4294
|
+
}
|
|
4295
|
+
});
|
|
4296
|
+
} catch (e) {
|
|
4297
|
+
if (this.isNoInstanceError(e)) {
|
|
4298
|
+
const errorBody$1 = {
|
|
4299
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
4300
|
+
message: "Container is currently provisioning. This can take several minutes on first deployment.",
|
|
4301
|
+
context: { phase: "provisioning" },
|
|
4302
|
+
httpStatus: 503,
|
|
4303
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4304
|
+
suggestion: "This is expected during first deployment. The SDK will retry automatically."
|
|
4305
|
+
};
|
|
4306
|
+
return new Response(JSON.stringify(errorBody$1), {
|
|
4307
|
+
status: 503,
|
|
4308
|
+
headers: {
|
|
4309
|
+
"Content-Type": "application/json",
|
|
4310
|
+
"Retry-After": "10"
|
|
4362
4311
|
}
|
|
4363
4312
|
});
|
|
4364
|
-
}
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
}
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
}
|
|
4381
|
-
}
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
error: e instanceof Error ? e.message : String(e)
|
|
4390
|
-
},
|
|
4391
|
-
httpStatus: 500,
|
|
4392
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4393
|
-
suggestion: "This error will not resolve with retries. Check container logs, image name, and resource limits."
|
|
4394
|
-
};
|
|
4395
|
-
return new Response(JSON.stringify(errorBody$1), {
|
|
4396
|
-
status: 500,
|
|
4397
|
-
headers: { "Content-Type": "application/json" }
|
|
4398
|
-
});
|
|
4399
|
-
}
|
|
4400
|
-
if (this.isTransientStartupError(e)) {
|
|
4401
|
-
if (staleStateDetected) {
|
|
4402
|
-
this.logger.warn("Container startup failed after stale state detection, aborting DO for recovery", { error: e instanceof Error ? e.message : String(e) });
|
|
4403
|
-
this.ctx.abort();
|
|
4404
|
-
} else this.logger.debug("Transient container startup error, returning 503", { error: e instanceof Error ? e.message : String(e) });
|
|
4405
|
-
const errorBody$1 = {
|
|
4406
|
-
code: ErrorCode.INTERNAL_ERROR,
|
|
4407
|
-
message: "Container is starting. Please retry in a moment.",
|
|
4408
|
-
context: {
|
|
4409
|
-
phase: "startup",
|
|
4410
|
-
error: e instanceof Error ? e.message : String(e)
|
|
4411
|
-
},
|
|
4412
|
-
httpStatus: 503,
|
|
4413
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4414
|
-
suggestion: "The container is booting. The SDK will retry automatically."
|
|
4415
|
-
};
|
|
4416
|
-
return new Response(JSON.stringify(errorBody$1), {
|
|
4417
|
-
status: 503,
|
|
4418
|
-
headers: {
|
|
4419
|
-
"Content-Type": "application/json",
|
|
4420
|
-
"Retry-After": "3"
|
|
4421
|
-
}
|
|
4313
|
+
}
|
|
4314
|
+
if (this.isPermanentStartupError(e)) {
|
|
4315
|
+
this.logger.error("Permanent container startup error, returning 500", e instanceof Error ? e : new Error(String(e)));
|
|
4316
|
+
const errorBody$1 = {
|
|
4317
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
4318
|
+
message: "Container failed to start due to a permanent error. Check your container configuration.",
|
|
4319
|
+
context: {
|
|
4320
|
+
phase: "startup",
|
|
4321
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4322
|
+
},
|
|
4323
|
+
httpStatus: 500,
|
|
4324
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4325
|
+
suggestion: "This error will not resolve with retries. Check container logs, image name, and resource limits."
|
|
4326
|
+
};
|
|
4327
|
+
return new Response(JSON.stringify(errorBody$1), {
|
|
4328
|
+
status: 500,
|
|
4329
|
+
headers: { "Content-Type": "application/json" }
|
|
4330
|
+
});
|
|
4331
|
+
}
|
|
4332
|
+
if (this.isTransientStartupError(e)) {
|
|
4333
|
+
if (staleStateDetected) {
|
|
4334
|
+
this.logger.warn("container.startup", {
|
|
4335
|
+
outcome: "stale_state_abort",
|
|
4336
|
+
staleStateDetected: true,
|
|
4337
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4422
4338
|
});
|
|
4423
|
-
|
|
4424
|
-
this.logger.
|
|
4425
|
-
|
|
4339
|
+
this.ctx.abort();
|
|
4340
|
+
} else this.logger.debug("container.startup", {
|
|
4341
|
+
outcome: "transient_error",
|
|
4342
|
+
staleStateDetected,
|
|
4343
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4344
|
+
});
|
|
4345
|
+
const errorBody$1 = {
|
|
4426
4346
|
code: ErrorCode.INTERNAL_ERROR,
|
|
4427
4347
|
message: "Container is starting. Please retry in a moment.",
|
|
4428
4348
|
context: {
|
|
@@ -4431,16 +4351,39 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4431
4351
|
},
|
|
4432
4352
|
httpStatus: 503,
|
|
4433
4353
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4434
|
-
suggestion: "The
|
|
4354
|
+
suggestion: "The container is booting. The SDK will retry automatically."
|
|
4435
4355
|
};
|
|
4436
|
-
return new Response(JSON.stringify(errorBody), {
|
|
4356
|
+
return new Response(JSON.stringify(errorBody$1), {
|
|
4437
4357
|
status: 503,
|
|
4438
4358
|
headers: {
|
|
4439
4359
|
"Content-Type": "application/json",
|
|
4440
|
-
"Retry-After": "
|
|
4360
|
+
"Retry-After": "3"
|
|
4441
4361
|
}
|
|
4442
4362
|
});
|
|
4443
4363
|
}
|
|
4364
|
+
this.logger.warn("container.startup", {
|
|
4365
|
+
outcome: "unrecognized_error",
|
|
4366
|
+
staleStateDetected,
|
|
4367
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4368
|
+
});
|
|
4369
|
+
const errorBody = {
|
|
4370
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
4371
|
+
message: "Container is starting. Please retry in a moment.",
|
|
4372
|
+
context: {
|
|
4373
|
+
phase: "startup",
|
|
4374
|
+
error: e instanceof Error ? e.message : String(e)
|
|
4375
|
+
},
|
|
4376
|
+
httpStatus: 503,
|
|
4377
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4378
|
+
suggestion: "The SDK will retry automatically. If this persists, the container may need redeployment."
|
|
4379
|
+
};
|
|
4380
|
+
return new Response(JSON.stringify(errorBody), {
|
|
4381
|
+
status: 503,
|
|
4382
|
+
headers: {
|
|
4383
|
+
"Content-Type": "application/json",
|
|
4384
|
+
"Retry-After": "5"
|
|
4385
|
+
}
|
|
4386
|
+
});
|
|
4444
4387
|
}
|
|
4445
4388
|
return await super.containerFetch(requestOrUrl, portOrInit, portParam);
|
|
4446
4389
|
}
|
|
@@ -4610,31 +4553,59 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4610
4553
|
return this.execWithSession(command, session, options);
|
|
4611
4554
|
}
|
|
4612
4555
|
/**
|
|
4556
|
+
* Execute an infrastructure command (backup, mount, env setup, etc.)
|
|
4557
|
+
* tagged with origin: 'internal' so logging demotes it to debug level.
|
|
4558
|
+
*/
|
|
4559
|
+
async execInternal(command) {
|
|
4560
|
+
const session = await this.ensureDefaultSession();
|
|
4561
|
+
return this.execWithSession(command, session, { origin: "internal" });
|
|
4562
|
+
}
|
|
4563
|
+
/**
|
|
4613
4564
|
* Internal session-aware exec implementation
|
|
4614
4565
|
* Used by both public exec() and session wrappers
|
|
4615
4566
|
*/
|
|
4616
4567
|
async execWithSession(command, sessionId, options) {
|
|
4617
4568
|
const startTime = Date.now();
|
|
4618
4569
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4570
|
+
let execOutcome;
|
|
4571
|
+
let execError;
|
|
4619
4572
|
try {
|
|
4620
4573
|
if (options?.signal?.aborted) throw new Error("Operation was aborted");
|
|
4621
4574
|
let result;
|
|
4622
4575
|
if (options?.stream && options?.onOutput) result = await this.executeWithStreaming(command, sessionId, options, startTime, timestamp);
|
|
4623
4576
|
else {
|
|
4624
|
-
const commandOptions = options && (options.timeout !== void 0 || options.env !== void 0 || options.cwd !== void 0) ? {
|
|
4577
|
+
const commandOptions = options && (options.timeout !== void 0 || options.env !== void 0 || options.cwd !== void 0 || options.origin !== void 0) ? {
|
|
4625
4578
|
timeoutMs: options.timeout,
|
|
4626
4579
|
env: options.env,
|
|
4627
|
-
cwd: options.cwd
|
|
4580
|
+
cwd: options.cwd,
|
|
4581
|
+
origin: options.origin
|
|
4628
4582
|
} : void 0;
|
|
4629
4583
|
const response = await this.client.commands.execute(command, sessionId, commandOptions);
|
|
4630
4584
|
const duration = Date.now() - startTime;
|
|
4631
4585
|
result = this.mapExecuteResponseToExecResult(response, duration, sessionId);
|
|
4632
4586
|
}
|
|
4587
|
+
execOutcome = {
|
|
4588
|
+
exitCode: result.exitCode,
|
|
4589
|
+
success: result.success
|
|
4590
|
+
};
|
|
4633
4591
|
if (options?.onComplete) options.onComplete(result);
|
|
4634
4592
|
return result;
|
|
4635
4593
|
} catch (error) {
|
|
4594
|
+
execError = error instanceof Error ? error : new Error(String(error));
|
|
4636
4595
|
if (options?.onError && error instanceof Error) options.onError(error);
|
|
4637
4596
|
throw error;
|
|
4597
|
+
} finally {
|
|
4598
|
+
logCanonicalEvent(this.logger, {
|
|
4599
|
+
event: "sandbox.exec",
|
|
4600
|
+
outcome: execError ? "error" : "success",
|
|
4601
|
+
command,
|
|
4602
|
+
exitCode: execOutcome?.exitCode,
|
|
4603
|
+
durationMs: Date.now() - startTime,
|
|
4604
|
+
sessionId,
|
|
4605
|
+
origin: options?.origin ?? "user",
|
|
4606
|
+
error: execError ?? void 0,
|
|
4607
|
+
errorMessage: execError?.message
|
|
4608
|
+
});
|
|
4638
4609
|
}
|
|
4639
4610
|
}
|
|
4640
4611
|
async executeWithStreaming(command, sessionId, options, startTime, timestamp) {
|
|
@@ -4644,7 +4615,8 @@ var Sandbox = class Sandbox extends Container {
|
|
|
4644
4615
|
const stream = await this.client.commands.executeStream(command, sessionId, {
|
|
4645
4616
|
timeoutMs: options.timeout,
|
|
4646
4617
|
env: options.env,
|
|
4647
|
-
cwd: options.cwd
|
|
4618
|
+
cwd: options.cwd,
|
|
4619
|
+
origin: options.origin
|
|
4648
4620
|
});
|
|
4649
4621
|
for await (const event of parseSSEStream(stream)) {
|
|
4650
4622
|
if (options.signal?.aborted) throw new Error("Operation was aborted");
|
|
@@ -5197,41 +5169,78 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5197
5169
|
* // url: https://8080-sandbox-id-my_token_v1.example.com
|
|
5198
5170
|
*/
|
|
5199
5171
|
async exposePort(port, options) {
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
port
|
|
5224
|
-
|
|
5225
|
-
|
|
5172
|
+
const exposeStartTime = Date.now();
|
|
5173
|
+
let outcome = "error";
|
|
5174
|
+
let caughtError;
|
|
5175
|
+
try {
|
|
5176
|
+
if (!validatePort(port)) throw new SecurityError(`Invalid port number: ${port}. Must be 1024-65535, excluding 3000 (sandbox control plane).`);
|
|
5177
|
+
if (options.hostname.endsWith(".workers.dev")) throw new CustomDomainRequiredError({
|
|
5178
|
+
code: ErrorCode.CUSTOM_DOMAIN_REQUIRED,
|
|
5179
|
+
message: `Port exposure requires a custom domain. .workers.dev domains do not support wildcard subdomains required for port proxying.`,
|
|
5180
|
+
context: { originalError: options.hostname },
|
|
5181
|
+
httpStatus: 400,
|
|
5182
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5183
|
+
});
|
|
5184
|
+
if (!this.sandboxName) throw new Error("Sandbox name not available. Ensure sandbox is accessed through getSandbox()");
|
|
5185
|
+
let token;
|
|
5186
|
+
if (options.token !== void 0) {
|
|
5187
|
+
this.validateCustomToken(options.token);
|
|
5188
|
+
token = options.token;
|
|
5189
|
+
} else token = this.generatePortToken();
|
|
5190
|
+
const tokens = await this.ctx.storage.get("portTokens") || {};
|
|
5191
|
+
const existingPort = Object.entries(tokens).find(([p, t]) => t === token && p !== port.toString());
|
|
5192
|
+
if (existingPort) throw new SecurityError(`Token '${token}' is already in use by port ${existingPort[0]}. Please use a different token.`);
|
|
5193
|
+
const sessionId = await this.ensureDefaultSession();
|
|
5194
|
+
await this.client.ports.exposePort(port, sessionId, options?.name);
|
|
5195
|
+
tokens[port.toString()] = token;
|
|
5196
|
+
await this.ctx.storage.put("portTokens", tokens);
|
|
5197
|
+
const url = this.constructPreviewUrl(port, this.sandboxName, options.hostname, token);
|
|
5198
|
+
outcome = "success";
|
|
5199
|
+
return {
|
|
5200
|
+
url,
|
|
5201
|
+
port,
|
|
5202
|
+
name: options?.name
|
|
5203
|
+
};
|
|
5204
|
+
} catch (error) {
|
|
5205
|
+
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
5206
|
+
throw error;
|
|
5207
|
+
} finally {
|
|
5208
|
+
logCanonicalEvent(this.logger, {
|
|
5209
|
+
event: "port.expose",
|
|
5210
|
+
outcome,
|
|
5211
|
+
port,
|
|
5212
|
+
durationMs: Date.now() - exposeStartTime,
|
|
5213
|
+
name: options?.name,
|
|
5214
|
+
hostname: options.hostname,
|
|
5215
|
+
error: caughtError
|
|
5216
|
+
});
|
|
5217
|
+
}
|
|
5226
5218
|
}
|
|
5227
5219
|
async unexposePort(port) {
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
await this.
|
|
5220
|
+
const unexposeStartTime = Date.now();
|
|
5221
|
+
let outcome = "error";
|
|
5222
|
+
let caughtError;
|
|
5223
|
+
try {
|
|
5224
|
+
if (!validatePort(port)) throw new SecurityError(`Invalid port number: ${port}. Must be 1024-65535, excluding 3000 (sandbox control plane).`);
|
|
5225
|
+
const sessionId = await this.ensureDefaultSession();
|
|
5226
|
+
await this.client.ports.unexposePort(port, sessionId);
|
|
5227
|
+
const tokens = await this.ctx.storage.get("portTokens") || {};
|
|
5228
|
+
if (tokens[port.toString()]) {
|
|
5229
|
+
delete tokens[port.toString()];
|
|
5230
|
+
await this.ctx.storage.put("portTokens", tokens);
|
|
5231
|
+
}
|
|
5232
|
+
outcome = "success";
|
|
5233
|
+
} catch (error) {
|
|
5234
|
+
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
5235
|
+
throw error;
|
|
5236
|
+
} finally {
|
|
5237
|
+
logCanonicalEvent(this.logger, {
|
|
5238
|
+
event: "port.unexpose",
|
|
5239
|
+
outcome,
|
|
5240
|
+
port,
|
|
5241
|
+
durationMs: Date.now() - unexposeStartTime,
|
|
5242
|
+
error: caughtError
|
|
5243
|
+
});
|
|
5235
5244
|
}
|
|
5236
5245
|
}
|
|
5237
5246
|
async getExposedPorts(hostname) {
|
|
@@ -5405,12 +5414,12 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5405
5414
|
try {
|
|
5406
5415
|
for (const key of toUnset) {
|
|
5407
5416
|
const unsetCommand = `unset ${key}`;
|
|
5408
|
-
const result = await this.client.commands.execute(unsetCommand, sessionId);
|
|
5417
|
+
const result = await this.client.commands.execute(unsetCommand, sessionId, { origin: "internal" });
|
|
5409
5418
|
if (result.exitCode !== 0) throw new Error(`Failed to unset ${key}: ${result.stderr || "Unknown error"}`);
|
|
5410
5419
|
}
|
|
5411
5420
|
for (const [key, value] of Object.entries(toSet)) {
|
|
5412
5421
|
const exportCommand = `export ${key}=${shellEscape(value)}`;
|
|
5413
|
-
const result = await this.client.commands.execute(exportCommand, sessionId);
|
|
5422
|
+
const result = await this.client.commands.execute(exportCommand, sessionId, { origin: "internal" });
|
|
5414
5423
|
if (result.exitCode !== 0) throw new Error(`Failed to set ${key}: ${result.stderr || "Unknown error"}`);
|
|
5415
5424
|
}
|
|
5416
5425
|
} catch (error) {
|
|
@@ -5490,20 +5499,17 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5490
5499
|
}
|
|
5491
5500
|
static PRESIGNED_URL_EXPIRY_SECONDS = 3600;
|
|
5492
5501
|
/**
|
|
5493
|
-
*
|
|
5494
|
-
*
|
|
5495
|
-
*
|
|
5502
|
+
* Create a unique, dedicated session for a single backup operation.
|
|
5503
|
+
* Each call produces a fresh session ID so concurrent or sequential
|
|
5504
|
+
* operations never share shell state. Callers must destroy the session
|
|
5505
|
+
* in a finally block via `client.utils.deleteSession()`.
|
|
5496
5506
|
*/
|
|
5497
5507
|
async ensureBackupSession() {
|
|
5498
|
-
const sessionId =
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
});
|
|
5504
|
-
} catch (error) {
|
|
5505
|
-
if (!(error instanceof SessionAlreadyExistsError)) throw error;
|
|
5506
|
-
}
|
|
5508
|
+
const sessionId = `__sandbox_backup_${crypto.randomUUID()}`;
|
|
5509
|
+
await this.client.utils.createSession({
|
|
5510
|
+
id: sessionId,
|
|
5511
|
+
cwd: "/"
|
|
5512
|
+
});
|
|
5507
5513
|
return sessionId;
|
|
5508
5514
|
}
|
|
5509
5515
|
/**
|
|
@@ -5562,11 +5568,6 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5562
5568
|
*/
|
|
5563
5569
|
async uploadBackupPresigned(archivePath, r2Key, archiveSize, backupId, dir, backupSession) {
|
|
5564
5570
|
const presignedUrl = await this.generatePresignedPutUrl(r2Key);
|
|
5565
|
-
this.logger.info("Uploading backup via presigned PUT", {
|
|
5566
|
-
r2Key,
|
|
5567
|
-
archiveSize,
|
|
5568
|
-
backupId
|
|
5569
|
-
});
|
|
5570
5571
|
const curlCmd = [
|
|
5571
5572
|
"curl -sSf",
|
|
5572
5573
|
"-X PUT",
|
|
@@ -5578,7 +5579,10 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5578
5579
|
`-T ${shellEscape(archivePath)}`,
|
|
5579
5580
|
shellEscape(presignedUrl)
|
|
5580
5581
|
].join(" ");
|
|
5581
|
-
const result = await this.execWithSession(curlCmd, backupSession, {
|
|
5582
|
+
const result = await this.execWithSession(curlCmd, backupSession, {
|
|
5583
|
+
timeout: 181e4,
|
|
5584
|
+
origin: "internal"
|
|
5585
|
+
});
|
|
5582
5586
|
if (result.exitCode !== 0) throw new BackupCreateError({
|
|
5583
5587
|
message: `Presigned URL upload failed (exit code ${result.exitCode}): ${result.stderr}`,
|
|
5584
5588
|
code: ErrorCode.BACKUP_CREATE_FAILED,
|
|
@@ -5611,12 +5615,7 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5611
5615
|
*/
|
|
5612
5616
|
async downloadBackupPresigned(archivePath, r2Key, expectedSize, backupId, dir, backupSession) {
|
|
5613
5617
|
const presignedUrl = await this.generatePresignedGetUrl(r2Key);
|
|
5614
|
-
this.
|
|
5615
|
-
r2Key,
|
|
5616
|
-
expectedSize,
|
|
5617
|
-
backupId
|
|
5618
|
-
});
|
|
5619
|
-
await this.execWithSession("mkdir -p /var/backups", backupSession);
|
|
5618
|
+
await this.execWithSession("mkdir -p /var/backups", backupSession, { origin: "internal" });
|
|
5620
5619
|
const tmpPath = `${archivePath}.tmp`;
|
|
5621
5620
|
const curlCmd = [
|
|
5622
5621
|
"curl -sSf",
|
|
@@ -5627,9 +5626,12 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5627
5626
|
`-o ${shellEscape(tmpPath)}`,
|
|
5628
5627
|
shellEscape(presignedUrl)
|
|
5629
5628
|
].join(" ");
|
|
5630
|
-
const result = await this.execWithSession(curlCmd, backupSession, {
|
|
5629
|
+
const result = await this.execWithSession(curlCmd, backupSession, {
|
|
5630
|
+
timeout: 181e4,
|
|
5631
|
+
origin: "internal"
|
|
5632
|
+
});
|
|
5631
5633
|
if (result.exitCode !== 0) {
|
|
5632
|
-
await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession).catch(() => {});
|
|
5634
|
+
await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession, { origin: "internal" }).catch(() => {});
|
|
5633
5635
|
throw new BackupRestoreError({
|
|
5634
5636
|
message: `Presigned URL download failed (exit code ${result.exitCode}): ${result.stderr}`,
|
|
5635
5637
|
code: ErrorCode.BACKUP_RESTORE_FAILED,
|
|
@@ -5641,10 +5643,10 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5641
5643
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5642
5644
|
});
|
|
5643
5645
|
}
|
|
5644
|
-
const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(tmpPath)}`, backupSession);
|
|
5646
|
+
const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(tmpPath)}`, backupSession, { origin: "internal" });
|
|
5645
5647
|
const actualSize = parseInt(sizeCheck.stdout.trim(), 10);
|
|
5646
5648
|
if (actualSize !== expectedSize) {
|
|
5647
|
-
await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession).catch(() => {});
|
|
5649
|
+
await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession, { origin: "internal" }).catch(() => {});
|
|
5648
5650
|
throw new BackupRestoreError({
|
|
5649
5651
|
message: `Downloaded archive size mismatch: expected ${expectedSize}, got ${actualSize}`,
|
|
5650
5652
|
code: ErrorCode.BACKUP_RESTORE_FAILED,
|
|
@@ -5656,9 +5658,9 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5656
5658
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5657
5659
|
});
|
|
5658
5660
|
}
|
|
5659
|
-
const mvResult = await this.execWithSession(`mv ${shellEscape(tmpPath)} ${shellEscape(archivePath)}`, backupSession);
|
|
5661
|
+
const mvResult = await this.execWithSession(`mv ${shellEscape(tmpPath)} ${shellEscape(archivePath)}`, backupSession, { origin: "internal" });
|
|
5660
5662
|
if (mvResult.exitCode !== 0) {
|
|
5661
|
-
await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession).catch(() => {});
|
|
5663
|
+
await this.execWithSession(`rm -f ${shellEscape(tmpPath)}`, backupSession, { origin: "internal" }).catch(() => {});
|
|
5662
5664
|
throw new BackupRestoreError({
|
|
5663
5665
|
message: `Failed to finalize downloaded archive: ${mvResult.stderr}`,
|
|
5664
5666
|
code: ErrorCode.BACKUP_RESTORE_FAILED,
|
|
@@ -5713,68 +5715,68 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5713
5715
|
const DEFAULT_TTL_SECONDS = 259200;
|
|
5714
5716
|
const MAX_NAME_LENGTH = 256;
|
|
5715
5717
|
const { dir, name, ttl = DEFAULT_TTL_SECONDS, gitignore = false, excludes = [] } = options;
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5718
|
+
const backupStartTime = Date.now();
|
|
5719
|
+
let backupId;
|
|
5720
|
+
let sizeBytes;
|
|
5721
|
+
let outcome = "error";
|
|
5722
|
+
let caughtError;
|
|
5723
|
+
let backupSession;
|
|
5724
|
+
try {
|
|
5725
|
+
Sandbox.validateBackupDir(dir, "BackupOptions.dir");
|
|
5726
|
+
if (name !== void 0) {
|
|
5727
|
+
if (typeof name !== "string" || name.length > MAX_NAME_LENGTH) throw new InvalidBackupConfigError({
|
|
5728
|
+
message: `BackupOptions.name must be a string of at most ${MAX_NAME_LENGTH} characters`,
|
|
5729
|
+
code: ErrorCode.INVALID_BACKUP_CONFIG,
|
|
5730
|
+
httpStatus: 400,
|
|
5731
|
+
context: { reason: `name must be a string of at most ${MAX_NAME_LENGTH} characters` },
|
|
5732
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5733
|
+
});
|
|
5734
|
+
if (/[\u0000-\u001f\u007f]/.test(name)) throw new InvalidBackupConfigError({
|
|
5735
|
+
message: "BackupOptions.name must not contain control characters",
|
|
5736
|
+
code: ErrorCode.INVALID_BACKUP_CONFIG,
|
|
5737
|
+
httpStatus: 400,
|
|
5738
|
+
context: { reason: "name must not contain control characters" },
|
|
5739
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5740
|
+
});
|
|
5741
|
+
}
|
|
5742
|
+
if (ttl <= 0) throw new InvalidBackupConfigError({
|
|
5743
|
+
message: "BackupOptions.ttl must be a positive number of seconds",
|
|
5720
5744
|
code: ErrorCode.INVALID_BACKUP_CONFIG,
|
|
5721
5745
|
httpStatus: 400,
|
|
5722
|
-
context: { reason:
|
|
5746
|
+
context: { reason: "ttl must be a positive number of seconds" },
|
|
5723
5747
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5724
5748
|
});
|
|
5725
|
-
if (
|
|
5726
|
-
message: "BackupOptions.
|
|
5749
|
+
if (typeof gitignore !== "boolean") throw new InvalidBackupConfigError({
|
|
5750
|
+
message: "BackupOptions.gitignore must be a boolean",
|
|
5727
5751
|
code: ErrorCode.INVALID_BACKUP_CONFIG,
|
|
5728
5752
|
httpStatus: 400,
|
|
5729
|
-
context: { reason: "
|
|
5753
|
+
context: { reason: "gitignore must be a boolean" },
|
|
5730
5754
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5731
5755
|
});
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
const archivePath = `/var/backups/${backupId}.sqsh`;
|
|
5757
|
-
this.logger.info("Creating backup", {
|
|
5758
|
-
backupId,
|
|
5759
|
-
dir,
|
|
5760
|
-
name,
|
|
5761
|
-
gitignore,
|
|
5762
|
-
excludes
|
|
5763
|
-
});
|
|
5764
|
-
const createResult = await this.client.backup.createArchive(dir, archivePath, backupSession, gitignore, excludes);
|
|
5765
|
-
if (!createResult.success) throw new BackupCreateError({
|
|
5766
|
-
message: "Container failed to create backup archive",
|
|
5767
|
-
code: ErrorCode.BACKUP_CREATE_FAILED,
|
|
5768
|
-
httpStatus: 500,
|
|
5769
|
-
context: {
|
|
5770
|
-
dir,
|
|
5771
|
-
backupId
|
|
5772
|
-
},
|
|
5773
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5774
|
-
});
|
|
5775
|
-
const r2Key = `backups/${backupId}/data.sqsh`;
|
|
5776
|
-
const metaKey = `backups/${backupId}/meta.json`;
|
|
5777
|
-
try {
|
|
5756
|
+
if (!Array.isArray(excludes) || !excludes.every((e) => typeof e === "string")) throw new InvalidBackupConfigError({
|
|
5757
|
+
message: "BackupOptions.excludes must be an array of strings",
|
|
5758
|
+
code: ErrorCode.INVALID_BACKUP_CONFIG,
|
|
5759
|
+
httpStatus: 400,
|
|
5760
|
+
context: { reason: "excludes must be an array of strings" },
|
|
5761
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5762
|
+
});
|
|
5763
|
+
backupSession = await this.ensureBackupSession();
|
|
5764
|
+
backupId = crypto.randomUUID();
|
|
5765
|
+
const archivePath = `/var/backups/${backupId}.sqsh`;
|
|
5766
|
+
const createResult = await this.client.backup.createArchive(dir, archivePath, backupSession, gitignore, excludes);
|
|
5767
|
+
if (!createResult.success) throw new BackupCreateError({
|
|
5768
|
+
message: "Container failed to create backup archive",
|
|
5769
|
+
code: ErrorCode.BACKUP_CREATE_FAILED,
|
|
5770
|
+
httpStatus: 500,
|
|
5771
|
+
context: {
|
|
5772
|
+
dir,
|
|
5773
|
+
backupId
|
|
5774
|
+
},
|
|
5775
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5776
|
+
});
|
|
5777
|
+
sizeBytes = createResult.sizeBytes;
|
|
5778
|
+
const r2Key = `backups/${backupId}/data.sqsh`;
|
|
5779
|
+
const metaKey = `backups/${backupId}/meta.json`;
|
|
5778
5780
|
await this.uploadBackupPresigned(archivePath, r2Key, createResult.sizeBytes, backupId, dir, backupSession);
|
|
5779
5781
|
const metadata = {
|
|
5780
5782
|
id: backupId,
|
|
@@ -5785,21 +5787,35 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5785
5787
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5786
5788
|
};
|
|
5787
5789
|
await bucket.put(metaKey, JSON.stringify(metadata));
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
r2Key,
|
|
5791
|
-
sizeBytes: createResult.sizeBytes
|
|
5792
|
-
});
|
|
5793
|
-
await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession).catch(() => {});
|
|
5790
|
+
outcome = "success";
|
|
5791
|
+
await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession, { origin: "internal" }).catch(() => {});
|
|
5794
5792
|
return {
|
|
5795
5793
|
id: backupId,
|
|
5796
5794
|
dir
|
|
5797
5795
|
};
|
|
5798
5796
|
} catch (error) {
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5797
|
+
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
5798
|
+
if (backupId && backupSession) {
|
|
5799
|
+
const archivePath = `/var/backups/${backupId}.sqsh`;
|
|
5800
|
+
const r2Key = `backups/${backupId}/data.sqsh`;
|
|
5801
|
+
const metaKey = `backups/${backupId}/meta.json`;
|
|
5802
|
+
await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession, { origin: "internal" }).catch(() => {});
|
|
5803
|
+
await bucket.delete(r2Key).catch(() => {});
|
|
5804
|
+
await bucket.delete(metaKey).catch(() => {});
|
|
5805
|
+
}
|
|
5802
5806
|
throw error;
|
|
5807
|
+
} finally {
|
|
5808
|
+
if (backupSession) await this.client.utils.deleteSession(backupSession).catch(() => {});
|
|
5809
|
+
logCanonicalEvent(this.logger, {
|
|
5810
|
+
event: "backup.create",
|
|
5811
|
+
outcome,
|
|
5812
|
+
durationMs: Date.now() - backupStartTime,
|
|
5813
|
+
backupId,
|
|
5814
|
+
dir,
|
|
5815
|
+
name,
|
|
5816
|
+
sizeBytes,
|
|
5817
|
+
error: caughtError
|
|
5818
|
+
});
|
|
5803
5819
|
}
|
|
5804
5820
|
}
|
|
5805
5821
|
/**
|
|
@@ -5834,77 +5850,77 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5834
5850
|
return this.enqueueBackupOp(() => this.doRestoreBackup(backup));
|
|
5835
5851
|
}
|
|
5836
5852
|
async doRestoreBackup(backup) {
|
|
5853
|
+
const restoreStartTime = Date.now();
|
|
5837
5854
|
const bucket = this.requireBackupBucket();
|
|
5838
5855
|
this.requirePresignedUrlSupport();
|
|
5839
5856
|
const { id: backupId, dir } = backup;
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
httpStatus: 400,
|
|
5844
|
-
context: { reason: "missing or invalid id" },
|
|
5845
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5846
|
-
});
|
|
5847
|
-
if (!Sandbox.UUID_REGEX.test(backupId)) throw new InvalidBackupConfigError({
|
|
5848
|
-
message: "Invalid backup: id must be a valid UUID (e.g. from createBackup)",
|
|
5849
|
-
code: ErrorCode.INVALID_BACKUP_CONFIG,
|
|
5850
|
-
httpStatus: 400,
|
|
5851
|
-
context: { reason: "id must be a valid UUID" },
|
|
5852
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5853
|
-
});
|
|
5854
|
-
Sandbox.validateBackupDir(dir, "Invalid backup: dir");
|
|
5855
|
-
this.logger.info("Restoring backup", {
|
|
5856
|
-
backupId,
|
|
5857
|
-
dir
|
|
5858
|
-
});
|
|
5859
|
-
const metaKey = `backups/${backupId}/meta.json`;
|
|
5860
|
-
const metaObject = await bucket.get(metaKey);
|
|
5861
|
-
if (!metaObject) throw new BackupNotFoundError({
|
|
5862
|
-
message: `Backup not found: ${backupId}. Verify the backup ID is correct and the backup has not been deleted.`,
|
|
5863
|
-
code: ErrorCode.BACKUP_NOT_FOUND,
|
|
5864
|
-
httpStatus: 404,
|
|
5865
|
-
context: { backupId },
|
|
5866
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5867
|
-
});
|
|
5868
|
-
const metadata = await metaObject.json();
|
|
5869
|
-
const TTL_BUFFER_MS = 60 * 1e3;
|
|
5870
|
-
const createdAt = new Date(metadata.createdAt).getTime();
|
|
5871
|
-
if (Number.isNaN(createdAt)) throw new BackupRestoreError({
|
|
5872
|
-
message: `Backup metadata has invalid createdAt timestamp: ${metadata.createdAt}`,
|
|
5873
|
-
code: ErrorCode.BACKUP_RESTORE_FAILED,
|
|
5874
|
-
httpStatus: 500,
|
|
5875
|
-
context: {
|
|
5876
|
-
dir,
|
|
5877
|
-
backupId
|
|
5878
|
-
},
|
|
5879
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5880
|
-
});
|
|
5881
|
-
const expiresAt = createdAt + metadata.ttl * 1e3;
|
|
5882
|
-
if (Date.now() + TTL_BUFFER_MS > expiresAt) throw new BackupExpiredError({
|
|
5883
|
-
message: `Backup ${backupId} has expired (created: ${metadata.createdAt}, TTL: ${metadata.ttl}s). Create a new backup.`,
|
|
5884
|
-
code: ErrorCode.BACKUP_EXPIRED,
|
|
5885
|
-
httpStatus: 400,
|
|
5886
|
-
context: {
|
|
5887
|
-
backupId,
|
|
5888
|
-
expiredAt: new Date(expiresAt).toISOString()
|
|
5889
|
-
},
|
|
5890
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5891
|
-
});
|
|
5892
|
-
const r2Key = `backups/${backupId}/data.sqsh`;
|
|
5893
|
-
const archiveHead = await bucket.head(r2Key);
|
|
5894
|
-
if (!archiveHead) throw new BackupNotFoundError({
|
|
5895
|
-
message: `Backup archive not found in R2: ${backupId}. The archive may have been deleted by R2 lifecycle rules.`,
|
|
5896
|
-
code: ErrorCode.BACKUP_NOT_FOUND,
|
|
5897
|
-
httpStatus: 404,
|
|
5898
|
-
context: { backupId },
|
|
5899
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5900
|
-
});
|
|
5901
|
-
const backupSession = await this.ensureBackupSession();
|
|
5902
|
-
const archivePath = `/var/backups/${backupId}.sqsh`;
|
|
5857
|
+
let outcome = "error";
|
|
5858
|
+
let caughtError;
|
|
5859
|
+
let backupSession;
|
|
5903
5860
|
try {
|
|
5861
|
+
if (!backupId || typeof backupId !== "string") throw new InvalidBackupConfigError({
|
|
5862
|
+
message: "Invalid backup: missing or invalid id",
|
|
5863
|
+
code: ErrorCode.INVALID_BACKUP_CONFIG,
|
|
5864
|
+
httpStatus: 400,
|
|
5865
|
+
context: { reason: "missing or invalid id" },
|
|
5866
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5867
|
+
});
|
|
5868
|
+
if (!Sandbox.UUID_REGEX.test(backupId)) throw new InvalidBackupConfigError({
|
|
5869
|
+
message: "Invalid backup: id must be a valid UUID (e.g. from createBackup)",
|
|
5870
|
+
code: ErrorCode.INVALID_BACKUP_CONFIG,
|
|
5871
|
+
httpStatus: 400,
|
|
5872
|
+
context: { reason: "id must be a valid UUID" },
|
|
5873
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5874
|
+
});
|
|
5875
|
+
Sandbox.validateBackupDir(dir, "Invalid backup: dir");
|
|
5876
|
+
const metaKey = `backups/${backupId}/meta.json`;
|
|
5877
|
+
const metaObject = await bucket.get(metaKey);
|
|
5878
|
+
if (!metaObject) throw new BackupNotFoundError({
|
|
5879
|
+
message: `Backup not found: ${backupId}. Verify the backup ID is correct and the backup has not been deleted.`,
|
|
5880
|
+
code: ErrorCode.BACKUP_NOT_FOUND,
|
|
5881
|
+
httpStatus: 404,
|
|
5882
|
+
context: { backupId },
|
|
5883
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5884
|
+
});
|
|
5885
|
+
const metadata = await metaObject.json();
|
|
5886
|
+
const TTL_BUFFER_MS = 60 * 1e3;
|
|
5887
|
+
const createdAt = new Date(metadata.createdAt).getTime();
|
|
5888
|
+
if (Number.isNaN(createdAt)) throw new BackupRestoreError({
|
|
5889
|
+
message: `Backup metadata has invalid createdAt timestamp: ${metadata.createdAt}`,
|
|
5890
|
+
code: ErrorCode.BACKUP_RESTORE_FAILED,
|
|
5891
|
+
httpStatus: 500,
|
|
5892
|
+
context: {
|
|
5893
|
+
dir,
|
|
5894
|
+
backupId
|
|
5895
|
+
},
|
|
5896
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5897
|
+
});
|
|
5898
|
+
const expiresAt = createdAt + metadata.ttl * 1e3;
|
|
5899
|
+
if (Date.now() + TTL_BUFFER_MS > expiresAt) throw new BackupExpiredError({
|
|
5900
|
+
message: `Backup ${backupId} has expired (created: ${metadata.createdAt}, TTL: ${metadata.ttl}s). Create a new backup.`,
|
|
5901
|
+
code: ErrorCode.BACKUP_EXPIRED,
|
|
5902
|
+
httpStatus: 400,
|
|
5903
|
+
context: {
|
|
5904
|
+
backupId,
|
|
5905
|
+
expiredAt: new Date(expiresAt).toISOString()
|
|
5906
|
+
},
|
|
5907
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5908
|
+
});
|
|
5909
|
+
const r2Key = `backups/${backupId}/data.sqsh`;
|
|
5910
|
+
const archiveHead = await bucket.head(r2Key);
|
|
5911
|
+
if (!archiveHead) throw new BackupNotFoundError({
|
|
5912
|
+
message: `Backup archive not found in R2: ${backupId}. The archive may have been deleted by R2 lifecycle rules.`,
|
|
5913
|
+
code: ErrorCode.BACKUP_NOT_FOUND,
|
|
5914
|
+
httpStatus: 404,
|
|
5915
|
+
context: { backupId },
|
|
5916
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5917
|
+
});
|
|
5918
|
+
backupSession = await this.ensureBackupSession();
|
|
5919
|
+
const archivePath = `/var/backups/${backupId}.sqsh`;
|
|
5904
5920
|
const mountGlob = `/var/backups/mounts/${backupId}`;
|
|
5905
|
-
await this.execWithSession(`/usr/bin/fusermount3 -uz ${shellEscape(dir)} 2>/dev/null || true`, backupSession).catch(() => {});
|
|
5906
|
-
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).catch(() => {});
|
|
5907
|
-
const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(archivePath)} 2>/dev/null || echo 0`, backupSession).catch(() => ({ stdout: "0" }));
|
|
5921
|
+
await this.execWithSession(`/usr/bin/fusermount3 -uz ${shellEscape(dir)} 2>/dev/null || true`, backupSession, { origin: "internal" }).catch(() => {});
|
|
5922
|
+
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(() => {});
|
|
5923
|
+
const sizeCheck = await this.execWithSession(`stat -c %s ${shellEscape(archivePath)} 2>/dev/null || echo 0`, backupSession, { origin: "internal" }).catch(() => ({ stdout: "0" }));
|
|
5908
5924
|
if (Number.parseInt((sizeCheck.stdout ?? "0").trim(), 10) !== archiveHead.size) await this.downloadBackupPresigned(archivePath, r2Key, archiveHead.size, backupId, dir, backupSession);
|
|
5909
5925
|
if (!(await this.client.backup.restoreArchive(dir, archivePath, backupSession)).success) throw new BackupRestoreError({
|
|
5910
5926
|
message: "Container failed to restore backup archive",
|
|
@@ -5916,18 +5932,29 @@ var Sandbox = class Sandbox extends Container {
|
|
|
5916
5932
|
},
|
|
5917
5933
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5918
5934
|
});
|
|
5919
|
-
|
|
5920
|
-
backupId,
|
|
5921
|
-
dir
|
|
5922
|
-
});
|
|
5935
|
+
outcome = "success";
|
|
5923
5936
|
return {
|
|
5924
5937
|
success: true,
|
|
5925
5938
|
dir,
|
|
5926
5939
|
id: backupId
|
|
5927
5940
|
};
|
|
5928
5941
|
} catch (error) {
|
|
5929
|
-
|
|
5942
|
+
caughtError = error instanceof Error ? error : new Error(String(error));
|
|
5943
|
+
if (backupId && backupSession) {
|
|
5944
|
+
const archivePath = `/var/backups/${backupId}.sqsh`;
|
|
5945
|
+
await this.execWithSession(`rm -f ${shellEscape(archivePath)}`, backupSession, { origin: "internal" }).catch(() => {});
|
|
5946
|
+
}
|
|
5930
5947
|
throw error;
|
|
5948
|
+
} finally {
|
|
5949
|
+
if (backupSession) await this.client.utils.deleteSession(backupSession).catch(() => {});
|
|
5950
|
+
logCanonicalEvent(this.logger, {
|
|
5951
|
+
event: "backup.restore",
|
|
5952
|
+
outcome,
|
|
5953
|
+
durationMs: Date.now() - restoreStartTime,
|
|
5954
|
+
backupId,
|
|
5955
|
+
dir,
|
|
5956
|
+
error: caughtError
|
|
5957
|
+
});
|
|
5931
5958
|
}
|
|
5932
5959
|
}
|
|
5933
5960
|
};
|