0sec-cli 0.22.4 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/0.js +73 -68
- package/README.md +95 -61
- package/chunks/{adapt-loop-3CCOWE43.js → adapt-loop-TEJPXWFG.js} +4 -4
- package/chunks/{artifact-scraper-Z4UUIOH3.js → artifact-scraper-AJZ3LOQY.js} +2 -2
- package/chunks/{assumption-mining-2UAKQB6V.js → assumption-mining-EATDC4BB.js} +18 -18
- package/chunks/backend-connections-QE4NASAK.js +23 -0
- package/chunks/chunk-3XLEDCFX.js +566 -0
- package/chunks/{chunk-BGX53365.js → chunk-3XLFB46W.js} +5 -3
- package/chunks/{chunk-IXAHQICR.js → chunk-4TUR47H5.js} +8 -3
- package/chunks/{chunk-5WAJGLCW.js → chunk-4YRXIVPZ.js} +3 -3
- package/chunks/{chunk-4ZYYX5GY.js → chunk-52AWIBCT.js} +1 -1
- package/chunks/{chunk-TO35Y326.js → chunk-554D4AYM.js} +5 -2
- package/chunks/{chunk-GQ3DGHWE.js → chunk-5FHFG5P2.js} +174 -710
- package/chunks/chunk-5POT5OYF.js +733 -0
- package/chunks/chunk-62PGWN7Y.js +700 -0
- package/chunks/{chunk-H6Q2QAIY.js → chunk-A3EKBUEM.js} +1424 -859
- package/chunks/{chunk-6WQDXWQC.js → chunk-A6AZZ6XX.js} +2 -2
- package/chunks/{chunk-6YZE552K.js → chunk-AV5BF4V7.js} +1281 -2580
- package/chunks/{chunk-2WWAPKAW.js → chunk-BIJPMYFA.js} +6 -6
- package/chunks/{chunk-SCWWLABR.js → chunk-CIMN2AO6.js} +1 -1
- package/chunks/{chunk-H4DH7COV.js → chunk-CSPF47S3.js} +30 -20
- package/chunks/{chunk-RCRTSKQN.js → chunk-DNV3ZMPX.js} +1 -1
- package/chunks/{chunk-GVY3S5E5.js → chunk-E42XEJTK.js} +2 -2
- package/chunks/{chunk-VDOWARMJ.js → chunk-ESM447R6.js} +15 -1
- package/chunks/{chunk-WBRGXCU2.js → chunk-EXTN4SQV.js} +1 -1
- package/chunks/{chunk-QVIRWJ45.js → chunk-EYBMTELO.js} +108 -24
- package/chunks/chunk-FEOPHBMY.js +56 -0
- package/chunks/{chunk-IHPTOTLL.js → chunk-FKGFHLLS.js} +1 -1
- package/chunks/chunk-G2EJ3R5I.js +5198 -0
- package/chunks/chunk-GHEBZKUS.js +5417 -0
- package/chunks/chunk-GNR3APR6.js +295 -0
- package/chunks/{chunk-ELI3BVIK.js → chunk-H76JYUPE.js} +3 -3
- package/chunks/{chunk-E74LXQFY.js → chunk-HRBOZBMC.js} +1 -1
- package/chunks/{chunk-TM74SSHC.js → chunk-IIBNQTOK.js} +3 -2
- package/chunks/{chunk-UELXHK2Y.js → chunk-IK27KMVV.js} +16 -5
- package/chunks/{chunk-WHUAUFSX.js → chunk-J6G73WYF.js} +1 -1
- package/chunks/chunk-JSTLXPJS.js +284 -0
- package/chunks/{chunk-G2BE2FC7.js → chunk-O3ADFN2A.js} +1 -1
- package/chunks/chunk-O7Q2G2KW.js +117 -0
- package/chunks/{chunk-XODDLNBI.js → chunk-QEA2VHBD.js} +284 -33
- package/chunks/{chunk-C6JPA32H.js → chunk-RBOMXUYL.js} +3 -111
- package/chunks/{chunk-L2CCO4KE.js → chunk-RCHAFSWS.js} +838 -39
- package/chunks/{chunk-3BPX75OR.js → chunk-RD4FKFWR.js} +1 -1
- package/chunks/{chunk-DPUXXMJW.js → chunk-RPTO5FY2.js} +1 -1
- package/chunks/{chunk-6KFB6AYN.js → chunk-RZR2XXAK.js} +531 -17
- package/chunks/chunk-SGILXLR3.js +289 -0
- package/chunks/{chunk-E3OOBVOK.js → chunk-TOAAYJVN.js} +2 -2
- package/chunks/chunk-TOFIGQ63.js +83 -0
- package/chunks/{chunk-QWK4R3DB.js → chunk-TTDFBFNZ.js} +6 -6
- package/chunks/chunk-TZRHWP7R.js +736 -0
- package/chunks/chunk-UHUW45S3.js +164 -0
- package/chunks/{chunk-5JNTMWPK.js → chunk-V4BD7GSP.js} +2 -2
- package/chunks/{chunk-OAS5SAVZ.js → chunk-V5I5GAQH.js} +3226 -1683
- package/chunks/{chunk-PS5E7LUQ.js → chunk-W6IYTSZH.js} +79 -9
- package/chunks/{chunk-RTBZ36BD.js → chunk-WUPN3D37.js} +22 -248
- package/chunks/{chunk-CF3MNS3O.js → chunk-Y7KV7WV6.js} +2 -2
- package/chunks/{chunk-2OQOQ2FZ.js → chunk-YERUDEXT.js} +763 -688
- package/chunks/{chunk-CPCHLL6A.js → chunk-YJU7QACE.js} +1 -1
- package/chunks/{chunk-YED25PA4.js → chunk-YORL5S5V.js} +1 -1
- package/chunks/{chunk-YAZARMJY.js → chunk-YYOQADBB.js} +4 -3
- package/chunks/chunk-ZPFBQ3SD.js +127 -0
- package/chunks/{codex-models-Z43V3427.js → codex-models-FQPCWRPC.js} +7 -7
- package/chunks/{commands-EUGF4FUZ.js → commands-I5L3VD2A.js} +6187 -12196
- package/chunks/dashboard-B2AEGCFM.js +76 -0
- package/chunks/{db-NVAW4WWR.js → db-P5EIKMLP.js} +3 -3
- package/chunks/deep-review-I7RD5QSS.js +82 -0
- package/chunks/{disclose-LDNKAVRH.js → disclose-PFBWESVJ.js} +4 -2
- package/chunks/{dist-7RUOKPRJ.js → dist-3SWWY5FZ.js} +18 -2
- package/chunks/{dist-27BNG6WD.js → dist-AJU25UXA.js} +112 -63
- package/chunks/{dist-NG2HT5XU.js → dist-VK2M4HNW.js} +85 -1
- package/chunks/eval-runner-XVRX3R2Y.js +31 -0
- package/chunks/{exploit-agent-FCCR2HYX.js → exploit-agent-A4RTIJ4J.js} +1 -1
- package/chunks/{exploit-autoclimb-X5ZS5FAO.js → exploit-autoclimb-ABMMBC3M.js} +1 -1
- package/chunks/{exploit-climb-3JHGEO36.js → exploit-climb-BQ2SVU63.js} +2 -2
- package/chunks/{fix-NSNPU33B.js → fix-4YCLHFDT.js} +7 -7
- package/chunks/{harness-KXQOVIDQ.js → harness-DHWULEIZ.js} +3 -3
- package/chunks/{hunt-scan-ELTKGKC4.js → hunt-scan-DXV5H6YN.js} +16 -16
- package/chunks/{kernel-primitive-MT7YZSIG.js → kernel-primitive-SF4NALVR.js} +2 -2
- package/chunks/{kernel-vm-runner-YFRHYC6G.js → kernel-vm-runner-WEBEZEIC.js} +2 -2
- package/chunks/local-engine-XUG7XRKM.js +21 -0
- package/chunks/managed-operation-SQFMBWHG.js +60 -0
- package/chunks/{memsafety-scan-FCPAXIZE.js → memsafety-scan-L2Z7E7TE.js} +2 -2
- package/chunks/{native-loop-OFJDRHPI.js → native-loop-ZNAELJGB.js} +14 -13
- package/chunks/orchestrate-YHFJXGID.js +63 -0
- package/chunks/{prepare-72GWTRVI.js → prepare-SODWQN6O.js} +5 -5
- package/chunks/provider-model-discovery-QGUGL3XW.js +277 -0
- package/chunks/remote-workflow-runtime-XNNGI2PG.js +12 -0
- package/chunks/{run-WTJCZZHV.js → run-5T4Z3Q2D.js} +924 -327
- package/chunks/{runtime-LCJ4XSKE.js → runtime-HFFRUOPL.js} +4 -2
- package/chunks/{runtime-FCKOUB7V.js → runtime-MB4GEFBY.js} +7 -7
- package/chunks/{scan-stream-GT6GKQ3U.js → scan-stream-GXBJHCDF.js} +3 -2
- package/chunks/{scope-E5QZUOOH.js → scope-M7L3CD6X.js} +2 -2
- package/chunks/service-plugins-BGS6QKSZ.js +64 -0
- package/chunks/{session-store-H3Q7KCYG.js → session-store-K7I4GLNH.js} +2 -2
- package/chunks/{variant-candidates-SZKDCOLK.js → variant-candidates-2KSP5EHH.js} +7 -7
- package/chunks/{web-recon-prepass-IO3GXQMW.js → web-recon-prepass-RAXXTEWH.js} +5 -5
- package/chunks/workflow-runtime-V26HIJO3.js +178 -0
- package/dashboard/assets/abnfDiagram-VCTEODGH-CGCueOp5.js +1 -0
- package/dashboard/assets/arc-BjZaQcCc.js +1 -0
- package/dashboard/assets/architecture-7GRP2DOG-hcbO-9zX.js +1 -0
- package/dashboard/assets/architectureDiagram-5GKGNRK7-DReOXTSq.js +36 -0
- package/dashboard/assets/array-BifhSqXX.js +1 -0
- package/dashboard/assets/blockDiagram-I7D4REHJ-C-byT12_.js +129 -0
- package/dashboard/assets/c4Diagram-7LVT6UL2-DoYjz-Uv.js +38 -0
- package/dashboard/assets/channel-CUURBnpZ.js +1 -0
- package/dashboard/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dashboard/assets/chunk-4HAMMTFA-NBNp2zdt.js +62 -0
- package/dashboard/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dashboard/assets/chunk-75Z2AOVW-BHmkuP0B.js +2 -0
- package/dashboard/assets/chunk-DU6HZSFF-DgfLNaL2.js +127 -0
- package/dashboard/assets/chunk-F27PBJKO-3wwNFUgZ.js +1 -0
- package/dashboard/assets/chunk-FOHPRMQF-DHwB1DNv.js +161 -0
- package/dashboard/assets/chunk-GMAD6QVW-Btg7huZW.js +72 -0
- package/dashboard/assets/chunk-GVQU2GXP-ihCURK5O.js +1 -0
- package/dashboard/assets/chunk-IMKFNOWR-FsXj7slg.js +231 -0
- package/dashboard/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dashboard/assets/chunk-L3NEJ4N5-DorFIxJY.js +1 -0
- package/dashboard/assets/chunk-OSK3NFVY-DKun_KfC.js +10 -0
- package/dashboard/assets/chunk-P2QGCYS3-XGgiU1Bz.js +1 -0
- package/dashboard/assets/chunk-POPQ4Y6H-BTXEnGhJ.js +1 -0
- package/dashboard/assets/chunk-PWAF6VOD-DYZcGkTL.js +1 -0
- package/dashboard/assets/chunk-SHT3W25Y-C4WQfwf8.js +168 -0
- package/dashboard/assets/chunk-SVP7TREG-C4nCh0-1.js +88 -0
- package/dashboard/assets/chunk-TICWLB2K-DSB-IiGc.js +206 -0
- package/dashboard/assets/chunk-XXDRQBXY-CS6fjUcj.js +1 -0
- package/dashboard/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dashboard/assets/classDiagram-ZZMXUADV-CD1Buc26.js +1 -0
- package/dashboard/assets/classDiagram-v2-VYDZK3BY-CD1Buc26.js +1 -0
- package/dashboard/assets/common-BMdQBwfR.js +5 -0
- package/dashboard/assets/cose-bilkent-JH36ORCC-CUjQHTML.js +1 -0
- package/dashboard/assets/cynefin-OW5HDTMX-BC7zCtFq.js +1 -0
- package/dashboard/assets/cynefinDiagram-5FMLGOSQ-CgDOOY67.js +62 -0
- package/dashboard/assets/cytoscape.esm-CGd-uY3x.js +321 -0
- package/dashboard/assets/dagre-GXQ25YYZ-Dqc8FS9x.js +4 -0
- package/dashboard/assets/dagre-PrKaheQc.js +1 -0
- package/dashboard/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dashboard/assets/diagram-S7CK7UJ4-CrKqRXZb.js +30 -0
- package/dashboard/assets/diagram-UQ7AKVKN-Cu6Es0-v.js +41 -0
- package/dashboard/assets/diagram-VSXAHHWV-eK4d8FwS.js +3 -0
- package/dashboard/assets/diagram-VX7I27RA-bFxeFKOc.js +24 -0
- package/dashboard/assets/diagram-Z3DM3KII-C0ClmnhC.js +24 -0
- package/dashboard/assets/dist-BZh8Kb3O.js +1 -0
- package/dashboard/assets/ebnfDiagram-PWID7BFC-CUqxFPzf.js +1 -0
- package/dashboard/assets/erDiagram-RLTQ6QDP-CcYM7pLi.js +99 -0
- package/dashboard/assets/eventmodeling-NTZA5JFV-D8ZNnPkV.js +1 -0
- package/dashboard/assets/flowDiagram-HODETNUW-pDb1AYm8.js +1 -0
- package/dashboard/assets/ganttDiagram-EL5Y4UJY-UmZfbMyD.js +292 -0
- package/dashboard/assets/gitGraph-4MIJSDKK-mvNUo0ZA.js +1 -0
- package/dashboard/assets/gitGraphDiagram-WWUBYQGX-CvzeCxVy.js +106 -0
- package/dashboard/assets/graphlib-DS17s2tU.js +1 -0
- package/dashboard/assets/info-A6RAGUB7-DChKqZMi.js +1 -0
- package/dashboard/assets/infoDiagram-27XIBGKW-DjkpNtVW.js +2 -0
- package/dashboard/assets/init-C-OQMol4.js +1 -0
- package/dashboard/assets/ishikawaDiagram-5VMMS53U-8FhjJaWT.js +70 -0
- package/dashboard/assets/journeyDiagram-3NMN7TZE-DyNJ755f.js +139 -0
- package/dashboard/assets/kanban-definition-UXKFOSKX-_jCmtaY1.js +89 -0
- package/dashboard/assets/katex-BdBmVSus.js +257 -0
- package/dashboard/assets/line-Bf0nxyWf.js +1 -0
- package/dashboard/assets/linear-Bl6JKbf4.js +1 -0
- package/dashboard/assets/mermaid-parser.core-Cc6sgCzs.js +7 -0
- package/dashboard/assets/mermaid.core-B4yCwiN-.js +44 -0
- package/dashboard/assets/mindmap-definition-YA3MSWOX-D5DjWyXj.js +96 -0
- package/dashboard/assets/operations-B7JAWOi6.js +130 -0
- package/dashboard/assets/operations-QjKT0Hwi.css +1 -0
- package/dashboard/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dashboard/assets/packet-AYTQ26CC-bQXLDhQC.js +1 -0
- package/dashboard/assets/path-fybaL0A-.js +1 -0
- package/dashboard/assets/pegDiagram-XKGWAZYB-Bf6a1vaf.js +1 -0
- package/dashboard/assets/pie-WAS4IAKB-DfzoABIi.js +1 -0
- package/dashboard/assets/pieDiagram-E7YTZNPT-D0L1qbch.js +39 -0
- package/dashboard/assets/quadrantDiagram-AXDQQJYC-CbFyy2Pe.js +7 -0
- package/dashboard/assets/radar-RG4KPBEZ-CZ2pyeUT.js +1 -0
- package/dashboard/assets/railroad-74A4TZTK-CP4Jazfd.js +1 -0
- package/dashboard/assets/railroad-abnf-HS5TGJTU-3MvEiHwD.js +1 -0
- package/dashboard/assets/railroad-ebnf-LZEXJU2U-BWto6cXE.js +1 -0
- package/dashboard/assets/railroad-peg-WCYAUIDC-DasEjbyf.js +1 -0
- package/dashboard/assets/railroadDiagram-O6MQD6OU-t7Gp9g2R.js +1 -0
- package/dashboard/assets/requirementDiagram-BXWQKSXE-LYmqeBXi.js +84 -0
- package/dashboard/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dashboard/assets/sankeyDiagram-P5KCCOFB-DnpfA_94.js +40 -0
- package/dashboard/assets/sequenceDiagram-WJ2MYXX4-C8s3pTUE.js +162 -0
- package/dashboard/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
- package/dashboard/assets/src-CaNUT1AQ.js +1 -0
- package/dashboard/assets/stateDiagram-D77RDMKH-BMzm6fK1.js +1 -0
- package/dashboard/assets/stateDiagram-v2-MP3YSRHH-B3REMjpY.js +1 -0
- package/dashboard/assets/swimlanes-42K2YHIH-uJE05Tt_.js +1 -0
- package/dashboard/assets/swimlanesDiagram-VR7AAH4N-CIaVMZEy.js +8 -0
- package/dashboard/assets/timeline-definition-24CTP7MA-BfLJMVeG.js +120 -0
- package/dashboard/assets/treeView-Q6P3EWNA-ZOE2enu2.js +1 -0
- package/dashboard/assets/treemap-WGGIJYW6-yIZoPBF5.js +1 -0
- package/dashboard/assets/vennDiagram-4TSXK5OY-BYJRHEaL.js +34 -0
- package/dashboard/assets/wardley-WFR3VGLG-D5V-jLbB.js +1 -0
- package/dashboard/assets/wardleyDiagram-VM6X3IG4-DgPN4nSe.js +78 -0
- package/dashboard/assets/xychartDiagram-S5SC5T6Z-BhkAMAOT.js +7 -0
- package/dashboard/index.html +3 -3
- package/package.json +3 -2
- package/worker-assets/demo-target/README.md +15 -0
- package/worker-assets/demo-target/include/decoder.h +27 -0
- package/worker-assets/demo-target/patched-decoder.c +40 -0
- package/worker-assets/demo-target/src/decoder.c +50 -0
- package/worker-assets/demo-target/witness-65537x1.bin +0 -0
- package/chunks/chunk-FFHRANYC.js +0 -547
- package/chunks/eval-runner-6Q3H52SP.js +0 -30
- package/chunks/orchestrate-DRZ2SVCP.js +0 -62
- package/chunks/service-plugins-MSK2EQP6.js +0 -62
- package/dashboard/assets/operations-BvB5Qpmk.css +0 -1
- package/dashboard/assets/operations-D8Ii01SV.js +0 -114
- package/dashboard/assets/zero-peek-Dzm3MT3X.png +0 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0CreateRequire } from "node:module";
|
|
3
|
+
const require = __0CreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
resolveOsecDbPath
|
|
6
|
+
} from "./chunk-RCHAFSWS.js";
|
|
7
|
+
import {
|
|
8
|
+
BACKEND_PROTOCOL_VERSION,
|
|
9
|
+
BackendDescriptorSchema,
|
|
10
|
+
BackendHandshakeSchema,
|
|
11
|
+
BackendRequestIdSchema,
|
|
12
|
+
backendCapabilitiesForApiPath,
|
|
13
|
+
homeStateDir,
|
|
14
|
+
parseBackendHandshake
|
|
15
|
+
} from "./chunk-RZR2XXAK.js";
|
|
16
|
+
import {
|
|
17
|
+
external_exports
|
|
18
|
+
} from "./chunk-VIT5ALXF.js";
|
|
19
|
+
|
|
20
|
+
// packages/cli/src/worker-deploy.ts
|
|
21
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
22
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
23
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
var REMOTE_DIR = "/tmp/zero-engine";
|
|
28
|
+
var REMOTE_DEPS = {
|
|
29
|
+
"drizzle-orm": "^0.45.2",
|
|
30
|
+
"node-sqlite3-wasm": "^0.8.55",
|
|
31
|
+
"pdfkit": "^0.15.2",
|
|
32
|
+
"playwright": "1.60.0",
|
|
33
|
+
"react": "^19.2.4",
|
|
34
|
+
"react-dom": "^19.2.4",
|
|
35
|
+
"tree-sitter": `file:${REMOTE_DIR}/vendor/tree-sitter-0.21.1-node24.tgz`,
|
|
36
|
+
"tree-sitter-c": "0.21.4",
|
|
37
|
+
"@opentui/core": "0.5.4",
|
|
38
|
+
"@opentui/react": "0.5.4"
|
|
39
|
+
};
|
|
40
|
+
var SSH_HOST_PATTERN = /^[A-Za-z0-9._@-]+$/;
|
|
41
|
+
var LIMA_NAME_PATTERN = /^[\w.-]+$/;
|
|
42
|
+
var TARGET_GIT_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s'"]+$/;
|
|
43
|
+
var sq = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
44
|
+
function resolveWorkerRepoRoot(moduleUrl = import.meta.url) {
|
|
45
|
+
let directory = dirname(fileURLToPath(moduleUrl));
|
|
46
|
+
while (true) {
|
|
47
|
+
if ((existsSync(join(directory, "dist/0.js")) || existsSync(join(directory, "0.js"))) && existsSync(join(directory, "vendor/tree-sitter-0.21.1-node24.tgz"))) return directory;
|
|
48
|
+
const parent = dirname(directory);
|
|
49
|
+
if (parent === directory) throw new Error("Worker deployment requires the built CLI dist/ and vendor/ assets. Run pnpm build first.");
|
|
50
|
+
directory = parent;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function validateWorkerOptions(options) {
|
|
54
|
+
if (options.enginePort !== void 0 && (!Number.isInteger(options.enginePort) || options.enginePort < 1 || options.enginePort > 65535)) throw new Error("enginePort must be 1-65535");
|
|
55
|
+
if (options.ssh && (!SSH_HOST_PATTERN.test(options.ssh) || options.ssh.startsWith("-"))) {
|
|
56
|
+
throw new Error("--ssh must be an OpenSSH host name or user@host (no options or shell characters)");
|
|
57
|
+
}
|
|
58
|
+
if (options.lima && !LIMA_NAME_PATTERN.test(options.lima)) throw new Error("--lima must be an instance name (letters, digits, ., _, -)");
|
|
59
|
+
if (options.targetGit && !TARGET_GIT_PATTERN.test(options.targetGit)) throw new Error("--target-git must be a URL without whitespace or quotes");
|
|
60
|
+
}
|
|
61
|
+
function createRunner(options) {
|
|
62
|
+
if (options.ssh) {
|
|
63
|
+
const host = options.ssh;
|
|
64
|
+
const configArgs = options.sshConfig ? ["-F", options.sshConfig] : [];
|
|
65
|
+
return {
|
|
66
|
+
label: host,
|
|
67
|
+
shell: (command, timeout = 3e5) => execFileSync("ssh", ["-o", "BatchMode=yes", "-o", "ConnectTimeout=15", ...configArgs, "--", host, "bash", "-s"], { input: command, encoding: "utf8", timeout }),
|
|
68
|
+
copy: (local, remote) => execFileSync("scp", ["-q", "-o", "BatchMode=yes", ...configArgs, "--", local, `${host}:${remote}`], { stdio: "inherit", timeout: 6e5 }),
|
|
69
|
+
tunnel: (port) => {
|
|
70
|
+
const child = spawn("ssh", ["-o", "BatchMode=yes", ...configArgs, "-N", "-L", `${port}:127.0.0.1:${port}`, "--", host], { stdio: "ignore", detached: true });
|
|
71
|
+
child.unref();
|
|
72
|
+
return child.pid;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const vm = options.lima ?? "zero-worker";
|
|
77
|
+
return {
|
|
78
|
+
label: vm,
|
|
79
|
+
shell: (command, timeout = 3e5) => execFileSync("limactl", ["shell", vm, "--", "bash", "-s"], { input: command, encoding: "utf8", timeout }),
|
|
80
|
+
copy: (local, remote) => execFileSync("limactl", ["copy", local, `${vm}:${remote}`], { stdio: "inherit", timeout: 6e5 }),
|
|
81
|
+
tunnel: (port) => {
|
|
82
|
+
const configPath = "/tmp/zero-worker-ssh.conf";
|
|
83
|
+
writeFileSync(configPath, execFileSync("limactl", ["show-ssh", "--format", "config", vm], { encoding: "utf8" }), { mode: 384 });
|
|
84
|
+
const child = spawn("ssh", ["-F", configPath, "-N", "-L", `${port}:127.0.0.1:${port}`, `lima-${vm}`], { stdio: "ignore", detached: true });
|
|
85
|
+
child.unref();
|
|
86
|
+
return child.pid;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function deployWorker(options, runner = createRunner(options)) {
|
|
91
|
+
validateWorkerOptions(options);
|
|
92
|
+
const repoRoot = options.repoRoot ?? resolveWorkerRepoRoot();
|
|
93
|
+
const port = options.enginePort ?? 2e4 + Number.parseInt(createHash("sha256").update(runner.label).digest("hex").slice(0, 4), 16) % 2e4;
|
|
94
|
+
const token = createHash("sha256").update(randomBytes(32)).digest("hex");
|
|
95
|
+
const identity = createHash("sha256").update(`${options.ssh ? "ssh" : "lima"}:${runner.label}`).digest("hex").slice(0, 12);
|
|
96
|
+
const backendId = `${options.ssh ? "ssh" : "lima"}-worker-${identity}`;
|
|
97
|
+
const tokenEnv = `ZERO_WORKER_${identity.toUpperCase()}_TOKEN`;
|
|
98
|
+
const { shell } = runner;
|
|
99
|
+
if (!options.ssh) {
|
|
100
|
+
const status = execFileSync("limactl", ["list", "--format", "{{.Status}}", runner.label], { encoding: "utf8" }).trim();
|
|
101
|
+
if (status !== "Running") throw new Error(`VM ${runner.label} is ${status}; start it first`);
|
|
102
|
+
}
|
|
103
|
+
shell("uname -srm");
|
|
104
|
+
const nodeVersion = shell("command -v node >/dev/null && node --version || echo none").trim();
|
|
105
|
+
if (!(nodeVersion.startsWith("v2") && Number(nodeVersion.slice(1).split(".")[0]) >= 24)) {
|
|
106
|
+
const arch = shell("uname -m").trim() === "aarch64" ? "arm64" : "x64";
|
|
107
|
+
shell(`cd /tmp && curl -fsSL https://nodejs.org/dist/v24.14.0/node-v24.14.0-linux-${arch}.tar.xz -o node.tar.xz && sudo tar -xJf node.tar.xz -C /usr/local --strip-components=1 && node --version`, 6e5);
|
|
108
|
+
}
|
|
109
|
+
const clangVersion = shell("command -v clang >/dev/null && clang --version | head -1 || echo none").trim();
|
|
110
|
+
if (clangVersion === "none") {
|
|
111
|
+
shell("sudo apt-get update -qq && sudo apt-get install -y -qq clang build-essential python3 >/dev/null && clang --version | head -1", 9e5);
|
|
112
|
+
}
|
|
113
|
+
const staging = mkdtempSync(join(tmpdir(), "zero-worker-"));
|
|
114
|
+
try {
|
|
115
|
+
const bundleRoot = existsSync(join(repoRoot, "dist/0.js")) ? repoRoot : staging;
|
|
116
|
+
if (bundleRoot === staging) {
|
|
117
|
+
mkdirSync(join(staging, "dist"));
|
|
118
|
+
for (const asset of ["0.js", "cli-path-traversal-fixture.js", "chunks", "attacks", "dashboard", "worker-assets"]) {
|
|
119
|
+
if (existsSync(join(repoRoot, asset))) cpSync(join(repoRoot, asset), join(staging, "dist", asset), { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
execFileSync("tar", ["-czf", join(staging, "bundle.tar.gz"), "-C", bundleRoot, "dist"], { stdio: "inherit", env: { ...process.env, COPYFILE_DISABLE: "1" } });
|
|
123
|
+
const demoRoot = existsSync(join(bundleRoot, "dist/worker-assets/demo-target")) ? join(bundleRoot, "dist/worker-assets") : join(repoRoot, "research/counterexample-decoder");
|
|
124
|
+
if (!options.targetGit) execFileSync("tar", ["-czf", join(staging, "demo-target.tar.gz"), "-C", demoRoot, "demo-target"], { stdio: "inherit", env: { ...process.env, COPYFILE_DISABLE: "1" } });
|
|
125
|
+
shell(`for entry in /proc/[0-9]*/cwd; do
|
|
126
|
+
test "$(readlink "$entry" 2>/dev/null)" = "${REMOTE_DIR}" || continue
|
|
127
|
+
pid=$(echo "$entry" | cut -d/ -f3)
|
|
128
|
+
command=$(tr '\\000' ' ' < /proc/$pid/cmdline 2>/dev/null) || continue
|
|
129
|
+
case "$command" in 'node dist/0.js dashboard '*|'node /tmp/zero-engine/dist/0.js dashboard '*) kill "$pid" || true;; esac
|
|
130
|
+
done`);
|
|
131
|
+
shell(`rm -rf ${REMOTE_DIR} && mkdir -p ${REMOTE_DIR}/vendor`);
|
|
132
|
+
runner.copy(join(staging, "bundle.tar.gz"), `${REMOTE_DIR}/bundle.tar.gz`);
|
|
133
|
+
if (!options.targetGit) runner.copy(join(staging, "demo-target.tar.gz"), `${REMOTE_DIR}/demo-target.tar.gz`);
|
|
134
|
+
runner.copy(join(repoRoot, "vendor/tree-sitter-0.21.1-node24.tgz"), `${REMOTE_DIR}/vendor/tree-sitter-0.21.1-node24.tgz`);
|
|
135
|
+
shell(`cd ${REMOTE_DIR} && tar -xzf bundle.tar.gz && ${options.targetGit ? "ls dist/0.js" : "tar -xzf demo-target.tar.gz && ls dist/0.js demo-target/src/decoder.c"}`);
|
|
136
|
+
writeFileSync(join(staging, "package.json"), JSON.stringify({ name: "zero-engine-remote", private: true, dependencies: REMOTE_DEPS }, null, 2));
|
|
137
|
+
runner.copy(join(staging, "package.json"), `${REMOTE_DIR}/package.json`);
|
|
138
|
+
shell(`cd ${REMOTE_DIR} && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install --omit=dev --no-audit --no-fund > install.log 2>&1 || { tail -20 install.log; exit 1; }`, 9e5);
|
|
139
|
+
let targetPath;
|
|
140
|
+
if (options.targetGit) {
|
|
141
|
+
shell(`rm -rf ${REMOTE_DIR}/work && mkdir -p ${REMOTE_DIR}/work && git clone --depth 1 ${sq(options.targetGit)} ${REMOTE_DIR}/work/target`, 9e5);
|
|
142
|
+
targetPath = `${REMOTE_DIR}/work/target`;
|
|
143
|
+
} else {
|
|
144
|
+
shell(`rm -rf ${REMOTE_DIR}/work && mkdir -p ${REMOTE_DIR}/work && cp -R ${REMOTE_DIR}/demo-target ${REMOTE_DIR}/work/demo-target`);
|
|
145
|
+
targetPath = `${REMOTE_DIR}/work/demo-target`;
|
|
146
|
+
}
|
|
147
|
+
const providerEnv = options.forwardProviderKey && process.env.OPENAI_API_KEY ? `OPENAI_API_KEY=${sq(process.env.OPENAI_API_KEY)} ` : "";
|
|
148
|
+
try {
|
|
149
|
+
shell(`cd ${REMOTE_DIR} || exit 1; ${providerEnv}ZERO_WORKER_TOKEN=${token} setsid nohup node dist/0.js dashboard --host 127.0.0.1 --port ${port} --no-open --ready-json --engine-token-env ZERO_WORKER_TOKEN --engine-workspace ${REMOTE_DIR}/work > engine.log 2>&1 < /dev/null & echo $! > ${REMOTE_DIR}/engine.pid; echo started`, 3e4);
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
const ready = shell(`for i in $(seq 1 30); do grep -q ZERO_DASHBOARD_READY ${REMOTE_DIR}/engine.log 2>/dev/null && echo ready && break; sleep 1; done; tail -2 ${REMOTE_DIR}/engine.log`, 6e4);
|
|
153
|
+
if (!ready.includes("ready")) throw new Error(`engine did not become ready: ${ready.trim()}`);
|
|
154
|
+
const tunnelPid = runner.tunnel(port);
|
|
155
|
+
const tunnelReady = Promise.withResolvers();
|
|
156
|
+
setTimeout(tunnelReady.resolve, 2e3);
|
|
157
|
+
await tunnelReady.promise;
|
|
158
|
+
execFileSync("curl", ["-sf", `http://127.0.0.1:${port}/`], { stdio: "ignore" });
|
|
159
|
+
const stateDir = join(homeStateDir(), "workers");
|
|
160
|
+
mkdirSync(stateDir, { recursive: true, mode: 448 });
|
|
161
|
+
const backendsConfig = options.backendsConfig ?? join(homeStateDir(), "backends.json");
|
|
162
|
+
let registry = { schemaVersion: 1, backends: [] };
|
|
163
|
+
try {
|
|
164
|
+
registry = JSON.parse(await (await import("node:fs/promises")).readFile(backendsConfig, "utf8"));
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
registry.backends = [...registry.backends.filter((row) => row.id !== backendId), { id: backendId, name: `${runner.label} worker`, url: `http://127.0.0.1:${port}/`, bearerTokenEnv: tokenEnv }];
|
|
168
|
+
writeFileSync(backendsConfig, JSON.stringify(registry, null, 2) + "\n", { mode: 384 });
|
|
169
|
+
writeFileSync(join(stateDir, `${backendId}.env`), `${tokenEnv}=${token}
|
|
170
|
+
`, { mode: 384 });
|
|
171
|
+
writeFileSync(join(stateDir, `${backendId}.json`), JSON.stringify({ schemaVersion: 1, backendId, ssh: options.ssh, lima: options.ssh ? void 0 : options.lima ?? "zero-worker", sshConfig: options.sshConfig, port, tunnelPid, remoteDir: REMOTE_DIR }, null, 2) + "\n", { mode: 384 });
|
|
172
|
+
process.env[tokenEnv] = token;
|
|
173
|
+
return { backendId, engine: `http://127.0.0.1:${port}/`, target: targetPath, backendsConfig, tokenEnv };
|
|
174
|
+
} finally {
|
|
175
|
+
rmSync(staging, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function loadManagedWorker(backendId) {
|
|
179
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(backendId)) throw new Error("Invalid worker ID.");
|
|
180
|
+
let worker;
|
|
181
|
+
try {
|
|
182
|
+
worker = JSON.parse(readFileSync(join(homeStateDir(), "workers", `${backendId}.json`), "utf8"));
|
|
183
|
+
} catch {
|
|
184
|
+
throw new Error("This connection has no managed deployment metadata. Stop its engine directly on the host.");
|
|
185
|
+
}
|
|
186
|
+
if (worker.schemaVersion !== 1 || worker.backendId !== backendId || worker.remoteDir !== REMOTE_DIR || !!worker.ssh === !!worker.lima) throw new Error("Invalid managed deployment metadata.");
|
|
187
|
+
validateWorkerOptions({ ...worker, enginePort: worker.port });
|
|
188
|
+
return worker;
|
|
189
|
+
}
|
|
190
|
+
function stopManagedTunnel(worker) {
|
|
191
|
+
if (!Number.isSafeInteger(worker.tunnelPid) || worker.tunnelPid <= 1) return false;
|
|
192
|
+
const result = spawnSync("ps", ["-p", String(worker.tunnelPid), "-o", "comm=", "-o", "args="], { encoding: "utf8" });
|
|
193
|
+
const command = result.stdout?.trim() ?? "";
|
|
194
|
+
const destination = worker.ssh ?? `lima-${worker.lima}`;
|
|
195
|
+
if (!/^\S*ssh\s/.test(command) || !command.includes(` ${worker.port}:127.0.0.1:${worker.port} `) || !command.endsWith(` ${destination}`)) return false;
|
|
196
|
+
try {
|
|
197
|
+
process.kill(worker.tunnelPid, "SIGTERM");
|
|
198
|
+
return true;
|
|
199
|
+
} catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function stopWorker(backendId, suppliedRunner) {
|
|
204
|
+
const worker = loadManagedWorker(backendId);
|
|
205
|
+
const runner = suppliedRunner ?? createRunner(worker);
|
|
206
|
+
runner.shell(`set -eu
|
|
207
|
+
if test ! -f ${REMOTE_DIR}/engine.pid; then echo 'Managed engine PID is unavailable; engine was not stopped.' >&2; exit 1; fi
|
|
208
|
+
pid=$(cat ${REMOTE_DIR}/engine.pid)
|
|
209
|
+
case "$pid" in ''|*[!0-9]*) echo 'Invalid engine PID' >&2; exit 1;; esac
|
|
210
|
+
if test "$pid" -le 1; then echo 'Invalid engine PID' >&2; exit 1; fi
|
|
211
|
+
if test -e /proc/$pid/cmdline; then
|
|
212
|
+
if test "$(readlink /proc/$pid/cwd)" != "${REMOTE_DIR}"; then echo 'Engine working directory does not match managed Zero engine' >&2; exit 1; fi
|
|
213
|
+
command=$(tr '\\000' ' ' < /proc/$pid/cmdline)
|
|
214
|
+
case "$command" in 'node dist/0.js dashboard '*|'node /tmp/zero-engine/dist/0.js dashboard '*) kill "$pid";; *) echo 'Engine PID does not match managed Zero engine' >&2; exit 1;; esac
|
|
215
|
+
fi
|
|
216
|
+
rm -f ${REMOTE_DIR}/engine.pid`, 3e4);
|
|
217
|
+
return { stopped: true, tunnelStopped: stopManagedTunnel(worker), remoteFilesRemoved: false };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// packages/cli/src/web/backend-connections.ts
|
|
221
|
+
import { randomUUID } from "node:crypto";
|
|
222
|
+
import { constants, closeSync, fstatSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, readSync, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
223
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
224
|
+
var BackendConnectionError = class extends Error {
|
|
225
|
+
constructor(message, statusCode = 502) {
|
|
226
|
+
super(message);
|
|
227
|
+
this.statusCode = statusCode;
|
|
228
|
+
this.name = "BackendConnectionError";
|
|
229
|
+
}
|
|
230
|
+
statusCode;
|
|
231
|
+
};
|
|
232
|
+
var idSchema = BackendDescriptorSchema.shape.id.refine((value) => value !== "local", "The local backend ID is reserved.");
|
|
233
|
+
var connectionSchema = external_exports.object({
|
|
234
|
+
id: idSchema,
|
|
235
|
+
name: external_exports.string().trim().min(1).max(160),
|
|
236
|
+
url: external_exports.string().max(4096),
|
|
237
|
+
bearerTokenEnv: external_exports.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(128),
|
|
238
|
+
expectedEngineId: BackendHandshakeSchema.shape.engineId.optional()
|
|
239
|
+
}).strict();
|
|
240
|
+
var configSchema = external_exports.object({ schemaVersion: external_exports.literal(1), backends: external_exports.array(connectionSchema).max(32) }).strict();
|
|
241
|
+
var MAX_BODY = 1e6;
|
|
242
|
+
var MAX_RESPONSE = 16e6;
|
|
243
|
+
var MAX_CONFIG = 65536;
|
|
244
|
+
function loopback(host) {
|
|
245
|
+
return host === "localhost" || host === "[::1]" || /^127\.(?:\d{1,3}\.){2}\d{1,3}$/.test(host);
|
|
246
|
+
}
|
|
247
|
+
function endpoint(value) {
|
|
248
|
+
const url = new URL(value);
|
|
249
|
+
if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && loopback(url.hostname))) throw new BackendConnectionError("Backend URL must be credential-free HTTPS or loopback HTTP, without query or fragment.", 400);
|
|
250
|
+
if (url.pathname.split("/").some((part) => {
|
|
251
|
+
const decoded = decodeURIComponent(part);
|
|
252
|
+
return decoded === ".." || decoded === "." || /[\\\x00]/.test(decoded);
|
|
253
|
+
})) throw new BackendConnectionError("Invalid backend URL path.", 400);
|
|
254
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/`;
|
|
255
|
+
return url;
|
|
256
|
+
}
|
|
257
|
+
function loadBackendConnectionConfig(configPath) {
|
|
258
|
+
const path = configPath ?? join2(homeStateDir(), "backends.json");
|
|
259
|
+
let fd;
|
|
260
|
+
try {
|
|
261
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (!configPath && error.code === "ENOENT") return [];
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const stat = fstatSync(fd);
|
|
268
|
+
if (!stat.isFile() || stat.size > MAX_CONFIG) throw new BackendConnectionError("Backend configuration must be a regular JSON file of at most 64 KiB.", 400);
|
|
269
|
+
const buffer = Buffer.alloc(MAX_CONFIG + 1);
|
|
270
|
+
let length = 0;
|
|
271
|
+
while (length < buffer.length) {
|
|
272
|
+
const count = readSync(fd, buffer, length, buffer.length - length, length);
|
|
273
|
+
if (!count) break;
|
|
274
|
+
length += count;
|
|
275
|
+
}
|
|
276
|
+
if (length > MAX_CONFIG) throw new BackendConnectionError("Backend configuration exceeds 64 KiB.", 400);
|
|
277
|
+
const config = configSchema.parse(JSON.parse(buffer.subarray(0, length).toString("utf8")));
|
|
278
|
+
if (new Set(config.backends.map((row) => row.id)).size !== config.backends.length) throw new BackendConnectionError("Backend connection IDs must be unique.", 400);
|
|
279
|
+
for (const row of config.backends) endpoint(row.url);
|
|
280
|
+
return config.backends;
|
|
281
|
+
} finally {
|
|
282
|
+
closeSync(fd);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function createBackendHandshake(dbPath, capabilities2 = []) {
|
|
286
|
+
const db = resolveOsecDbPath(dbPath);
|
|
287
|
+
let engineId = `engine-${randomUUID()}`;
|
|
288
|
+
if (db !== ":memory:") {
|
|
289
|
+
const path = `${resolve(db)}.engine-id`;
|
|
290
|
+
mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
|
|
291
|
+
try {
|
|
292
|
+
writeFileSync2(path, `${engineId}
|
|
293
|
+
`, { flag: "wx", mode: 384 });
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (error.code !== "EEXIST") throw error;
|
|
296
|
+
}
|
|
297
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
298
|
+
try {
|
|
299
|
+
if (!fstatSync(fd).isFile() || fstatSync(fd).size > 256) throw new BackendConnectionError("Invalid persisted backend engine identity.", 500);
|
|
300
|
+
const buffer = Buffer.alloc(257);
|
|
301
|
+
const count = readSync(fd, buffer, 0, buffer.length, 0);
|
|
302
|
+
if (count > 256) throw new BackendConnectionError("Invalid persisted backend engine identity.", 500);
|
|
303
|
+
engineId = BackendHandshakeSchema.shape.engineId.parse(buffer.subarray(0, count).toString("utf8").trim());
|
|
304
|
+
} finally {
|
|
305
|
+
closeSync(fd);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { protocolVersion: BACKEND_PROTOCOL_VERSION, engineId, capabilities: capabilities2, platform: { os: process.platform, pathStyle: process.platform === "win32" ? "windows" : "posix" }, serverInstanceId: randomUUID() };
|
|
309
|
+
}
|
|
310
|
+
function backendBearerFromEnv(name, env = process.env) {
|
|
311
|
+
if (!name) return void 0;
|
|
312
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new BackendConnectionError("Invalid engine token environment variable name.", 400);
|
|
313
|
+
const token = env[name];
|
|
314
|
+
if (!token || token.length < 32 || token.length > 4096 || /[\s\x00-\x1f\x7f]/.test(token)) throw new BackendConnectionError(`Configure a valid engine credential of 32\u20134096 characters.`, 400);
|
|
315
|
+
return token;
|
|
316
|
+
}
|
|
317
|
+
function apiPath(path) {
|
|
318
|
+
if (!path.startsWith("/api/") || path.length > 8192 || /[\x00-\x20\\]/.test(path)) throw new BackendConnectionError("Backend requests require an API path.", 400);
|
|
319
|
+
const url = new URL(path, "http://backend.invalid");
|
|
320
|
+
for (const segment of path.split("?")[0].split("/")) {
|
|
321
|
+
const decoded = decodeURIComponent(segment);
|
|
322
|
+
if (decoded === "." || decoded === ".." || /[\/\\\x00]/.test(decoded)) throw new BackendConnectionError("Invalid backend request path.", 400);
|
|
323
|
+
}
|
|
324
|
+
if (url.pathname.startsWith("/api/backends")) throw new BackendConnectionError("Backend connection registration is not a proxy operation.", 403);
|
|
325
|
+
return `${url.pathname}${url.search}`;
|
|
326
|
+
}
|
|
327
|
+
function capabilities(path) {
|
|
328
|
+
const pathname = path.split("?")[0];
|
|
329
|
+
const required = backendCapabilitiesForApiPath(pathname);
|
|
330
|
+
if (!required.length && pathname !== "/api/backend/handshake") throw new BackendConnectionError("API route is not admitted by the backend transport.", 403);
|
|
331
|
+
return required;
|
|
332
|
+
}
|
|
333
|
+
async function boundedResponse(response, maximum, signal) {
|
|
334
|
+
if (!response.body) return new Uint8Array();
|
|
335
|
+
const reader = response.body.getReader();
|
|
336
|
+
const chunks = [];
|
|
337
|
+
let length = 0;
|
|
338
|
+
const onAbort = () => {
|
|
339
|
+
void reader.cancel().catch(() => {
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
343
|
+
try {
|
|
344
|
+
while (true) {
|
|
345
|
+
signal.throwIfAborted();
|
|
346
|
+
const next = await reader.read();
|
|
347
|
+
signal.throwIfAborted();
|
|
348
|
+
if (next.done) break;
|
|
349
|
+
length += next.value.length;
|
|
350
|
+
if (length > maximum) throw new BackendConnectionError("Backend response exceeds the transport limit.");
|
|
351
|
+
chunks.push(next.value);
|
|
352
|
+
}
|
|
353
|
+
} catch (error) {
|
|
354
|
+
await reader.cancel().catch(() => {
|
|
355
|
+
});
|
|
356
|
+
throw error;
|
|
357
|
+
} finally {
|
|
358
|
+
signal.removeEventListener("abort", onAbort);
|
|
359
|
+
reader.releaseLock();
|
|
360
|
+
}
|
|
361
|
+
return Buffer.concat(chunks, length);
|
|
362
|
+
}
|
|
363
|
+
var BackendConnectionRegistry = class {
|
|
364
|
+
#connections;
|
|
365
|
+
#descriptors = /* @__PURE__ */ new Map();
|
|
366
|
+
#pins = /* @__PURE__ */ new Map();
|
|
367
|
+
#handshakes = /* @__PURE__ */ new Map();
|
|
368
|
+
#pending = /* @__PURE__ */ new Map();
|
|
369
|
+
#env;
|
|
370
|
+
#fetch;
|
|
371
|
+
#local;
|
|
372
|
+
#timeoutMs;
|
|
373
|
+
#configPath;
|
|
374
|
+
#reloadFromDisk;
|
|
375
|
+
#abort = new AbortController();
|
|
376
|
+
#active = 0;
|
|
377
|
+
constructor(options = {}) {
|
|
378
|
+
this.#configPath = options.configPath;
|
|
379
|
+
this.#reloadFromDisk = options.connections === void 0;
|
|
380
|
+
const rows = options.connections ?? loadBackendConnectionConfig(options.configPath);
|
|
381
|
+
const parsed = configSchema.parse({ schemaVersion: 1, backends: rows }).backends;
|
|
382
|
+
if (new Set(parsed.map((row) => row.id)).size !== parsed.length) throw new BackendConnectionError("Backend IDs must be unique.", 400);
|
|
383
|
+
this.#connections = new Map(parsed.map((row) => [row.id, { ...row, url: endpoint(row.url).href }]));
|
|
384
|
+
this.#env = { ...options.env ?? process.env };
|
|
385
|
+
this.#fetch = options.fetch ?? fetch;
|
|
386
|
+
this.#timeoutMs = options.timeoutMs ?? 3e4;
|
|
387
|
+
this.#local = parseBackendHandshake(options.localHandshake ?? createBackendHandshake());
|
|
388
|
+
this.#descriptors.set("local", { id: "local", name: "Local engine", transport: "local", status: "connected", protocolVersion: this.#local.protocolVersion, capabilities: this.#local.capabilities, platform: this.#local.platform });
|
|
389
|
+
this.reload();
|
|
390
|
+
for (const row of parsed) if (!this.#descriptors.has(row.id)) this.#descriptors.set(row.id, { id: row.id, name: row.name, transport: "http", status: "disconnected", capabilities: [] });
|
|
391
|
+
}
|
|
392
|
+
reload() {
|
|
393
|
+
if (!this.#reloadFromDisk) return;
|
|
394
|
+
const rows = loadBackendConnectionConfig(this.#configPath);
|
|
395
|
+
for (const id of this.#connections.keys()) if (!rows.some((row) => row.id === id)) {
|
|
396
|
+
this.#connections.delete(id);
|
|
397
|
+
this.#descriptors.delete(id);
|
|
398
|
+
this.#handshakes.delete(id);
|
|
399
|
+
this.#pins.delete(id);
|
|
400
|
+
}
|
|
401
|
+
for (const row of rows) {
|
|
402
|
+
const normalized = { ...row, url: endpoint(row.url).href };
|
|
403
|
+
const previous = this.#connections.get(row.id);
|
|
404
|
+
if (JSON.stringify(previous) !== JSON.stringify(normalized)) {
|
|
405
|
+
this.#connections.set(row.id, normalized);
|
|
406
|
+
this.#handshakes.delete(row.id);
|
|
407
|
+
this.#pins.delete(row.id);
|
|
408
|
+
this.#descriptors.set(row.id, { id: row.id, name: row.name, transport: "http", status: "disconnected", capabilities: [] });
|
|
409
|
+
}
|
|
410
|
+
{
|
|
411
|
+
try {
|
|
412
|
+
const value = readFileSync2(join2(homeStateDir(), "workers", `${row.id}.env`), "utf8");
|
|
413
|
+
const token = value.split("\n").find((line) => line.startsWith(`${row.bearerTokenEnv}=`))?.slice(row.bearerTokenEnv.length + 1);
|
|
414
|
+
if (token) this.#env[row.bearerTokenEnv] = backendBearerFromEnv(row.bearerTokenEnv, { [row.bearerTokenEnv]: token });
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/** Remove this dashboard's trusted connection; remote engine files are untouched. */
|
|
421
|
+
remove(id) {
|
|
422
|
+
id = BackendDescriptorSchema.shape.id.parse(id);
|
|
423
|
+
if (id === "local") throw new BackendConnectionError("The local engine cannot be removed.", 400);
|
|
424
|
+
if (!this.#reloadFromDisk) throw new BackendConnectionError("This backend registry is not persisted.", 409);
|
|
425
|
+
const rows = loadBackendConnectionConfig(this.#configPath);
|
|
426
|
+
const row = rows.find((value) => value.id === id);
|
|
427
|
+
if (!row) throw new BackendConnectionError("Backend is not registered.", 404);
|
|
428
|
+
const path = this.#configPath ?? join2(homeStateDir(), "backends.json");
|
|
429
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
430
|
+
try {
|
|
431
|
+
writeFileSync2(temporary, `${JSON.stringify({ schemaVersion: 1, backends: rows.filter((value) => value.id !== id) }, null, 2)}
|
|
432
|
+
`, { mode: 384, flag: "wx" });
|
|
433
|
+
renameSync(temporary, path);
|
|
434
|
+
} finally {
|
|
435
|
+
rmSync2(temporary, { force: true });
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
stopManagedTunnel(loadManagedWorker(id));
|
|
439
|
+
} catch {
|
|
440
|
+
}
|
|
441
|
+
rmSync2(join2(homeStateDir(), "workers", `${id}.json`), { force: true });
|
|
442
|
+
rmSync2(join2(homeStateDir(), "workers", `${id}.env`), { force: true });
|
|
443
|
+
if (!rows.some((value) => value.id !== id && value.bearerTokenEnv === row.bearerTokenEnv)) delete this.#env[row.bearerTokenEnv];
|
|
444
|
+
this.#connections.delete(id);
|
|
445
|
+
this.#descriptors.delete(id);
|
|
446
|
+
this.#handshakes.delete(id);
|
|
447
|
+
this.#pins.delete(id);
|
|
448
|
+
}
|
|
449
|
+
list() {
|
|
450
|
+
return structuredClone([...this.#descriptors.values()]);
|
|
451
|
+
}
|
|
452
|
+
async handshake(id) {
|
|
453
|
+
if (id === "local") return { backend: this.list()[0], handshake: structuredClone(this.#local) };
|
|
454
|
+
if (!this.#connections.has(id)) throw new BackendConnectionError("Backend is not registered.", 404);
|
|
455
|
+
const pending = this.#pending.get(id);
|
|
456
|
+
if (pending) return pending;
|
|
457
|
+
const run = this.#probe(id);
|
|
458
|
+
this.#pending.set(id, run);
|
|
459
|
+
try {
|
|
460
|
+
return await run;
|
|
461
|
+
} finally {
|
|
462
|
+
this.#pending.delete(id);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async #probe(id) {
|
|
466
|
+
const row = this.#connections.get(id);
|
|
467
|
+
const descriptor = this.#descriptors.get(id);
|
|
468
|
+
descriptor.status = "connecting";
|
|
469
|
+
delete descriptor.error;
|
|
470
|
+
try {
|
|
471
|
+
const response = await this.#send(row, "/api/backend/handshake", {});
|
|
472
|
+
if (!response.ok) throw new BackendConnectionError("Backend handshake was rejected.", response.status === 401 || response.status === 403 ? 403 : 502);
|
|
473
|
+
const raw = BackendHandshakeSchema.parse(await response.json());
|
|
474
|
+
descriptor.protocolVersion = raw.protocolVersion;
|
|
475
|
+
if (raw.protocolVersion !== BACKEND_PROTOCOL_VERSION) {
|
|
476
|
+
descriptor.status = "incompatible";
|
|
477
|
+
throw new BackendConnectionError("Backend protocol is incompatible.", 409);
|
|
478
|
+
}
|
|
479
|
+
const pin = row.expectedEngineId ?? this.#pins.get(id);
|
|
480
|
+
if (pin && pin !== raw.engineId) {
|
|
481
|
+
descriptor.status = "incompatible";
|
|
482
|
+
throw new BackendConnectionError("Backend engine identity changed; review the trusted connection configuration.", 409);
|
|
483
|
+
}
|
|
484
|
+
this.#pins.set(id, raw.engineId);
|
|
485
|
+
this.#handshakes.set(id, { value: raw, at: Date.now() });
|
|
486
|
+
Object.assign(descriptor, { status: "connected", capabilities: raw.capabilities, platform: raw.platform });
|
|
487
|
+
return { backend: structuredClone(descriptor), handshake: raw };
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (descriptor.status !== "incompatible") descriptor.status = "disconnected";
|
|
490
|
+
descriptor.error = error instanceof BackendConnectionError ? error.message : "Backend connection failed.";
|
|
491
|
+
this.#handshakes.delete(id);
|
|
492
|
+
return { backend: structuredClone(descriptor), error: descriptor.error };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async request(id, path, options = {}) {
|
|
496
|
+
if (id === "local") throw new BackendConnectionError("Local requests must use the local engine adapter.", 400);
|
|
497
|
+
const row = this.#connections.get(id);
|
|
498
|
+
if (!row) throw new BackendConnectionError("Backend is not registered.", 404);
|
|
499
|
+
const canonical = apiPath(path);
|
|
500
|
+
const cached = this.#handshakes.get(id);
|
|
501
|
+
const view = cached && Date.now() - cached.at < 5e3 ? { backend: this.#descriptors.get(id) } : await this.handshake(id);
|
|
502
|
+
if (view.backend.status !== "connected") throw new BackendConnectionError(view.backend.error ?? "Backend is disconnected.", view.backend.status === "incompatible" ? 409 : 503);
|
|
503
|
+
for (const required of capabilities(canonical)) if (!view.backend.capabilities.includes(required)) throw new BackendConnectionError(`Backend does not support ${required}.`, 409);
|
|
504
|
+
try {
|
|
505
|
+
return await this.#send(row, canonical, options);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
if (!options.signal?.aborted && (!(error instanceof BackendConnectionError) || error.statusCode >= 500 || error.statusCode === 409)) {
|
|
508
|
+
this.#descriptors.get(id).status = error instanceof BackendConnectionError && error.statusCode === 409 ? "incompatible" : "disconnected";
|
|
509
|
+
this.#handshakes.delete(id);
|
|
510
|
+
}
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async #send(row, path, options) {
|
|
515
|
+
if (this.#active >= 32) throw new BackendConnectionError("Backend request capacity exceeded.", 429);
|
|
516
|
+
this.#abort.signal.throwIfAborted();
|
|
517
|
+
const method = (options.method ?? "GET").toUpperCase();
|
|
518
|
+
if (!["GET", "HEAD", "POST", "PATCH", "DELETE", "PUT"].includes(method)) throw new BackendConnectionError("Backend method is not allowed.", 405);
|
|
519
|
+
const body = options.body === void 0 ? void 0 : JSON.stringify(options.body);
|
|
520
|
+
if (body !== void 0 && Buffer.byteLength(body) > MAX_BODY) throw new BackendConnectionError("Backend request body exceeds 1 MB.", 413);
|
|
521
|
+
const token = backendBearerFromEnv(row.bearerTokenEnv, this.#env);
|
|
522
|
+
const requestId = BackendRequestIdSchema.safeParse(options.requestId).success ? options.requestId : randomUUID();
|
|
523
|
+
const expectedEngineId = this.#pins.get(row.id) ?? row.expectedEngineId;
|
|
524
|
+
if (options.lastEventId !== void 0 && (options.lastEventId.length > 4096 || /[\x00-\x20\x7f]/.test(options.lastEventId))) throw new BackendConnectionError("Invalid event replay cursor.", 400);
|
|
525
|
+
const timeout = new AbortController();
|
|
526
|
+
const timer = setTimeout(() => timeout.abort(new BackendConnectionError("Backend request timed out.", 504)), this.#timeoutMs);
|
|
527
|
+
const signal = AbortSignal.any([this.#abort.signal, timeout.signal, ...options.signal ? [options.signal] : []]);
|
|
528
|
+
this.#active++;
|
|
529
|
+
let retained = false;
|
|
530
|
+
try {
|
|
531
|
+
const url = new URL(`.${path}`, row.url);
|
|
532
|
+
const response = await this.#fetch(url, { method, body, redirect: "error", signal, headers: { Authorization: `Bearer ${token}`, Accept: options.stream ? "text/event-stream" : "application/json", ...body !== void 0 ? { "Content-Type": "application/json" } : {}, "X-0-Request-ID": requestId, ...expectedEngineId ? { "X-0-Expected-Engine-ID": expectedEngineId } : {}, ...options.stream && options.lastEventId ? { "Last-Event-ID": options.lastEventId } : {} } });
|
|
533
|
+
if (path !== "/api/backend/handshake" && expectedEngineId && response.headers.get("x-0-engine-id") !== expectedEngineId) {
|
|
534
|
+
await response.body?.cancel();
|
|
535
|
+
throw new BackendConnectionError("Backend response identity does not match the registered engine.", 409);
|
|
536
|
+
}
|
|
537
|
+
const type = response.headers.get("content-type") ?? "application/json";
|
|
538
|
+
if (type.includes("text/event-stream")) {
|
|
539
|
+
if (!options.stream || method !== "GET") {
|
|
540
|
+
await response.body?.cancel();
|
|
541
|
+
throw new BackendConnectionError("Unexpected event stream response.");
|
|
542
|
+
}
|
|
543
|
+
clearTimeout(timer);
|
|
544
|
+
const reader = response.body?.getReader();
|
|
545
|
+
let released = false;
|
|
546
|
+
let onAbort;
|
|
547
|
+
const release = () => {
|
|
548
|
+
if (!released) {
|
|
549
|
+
released = true;
|
|
550
|
+
this.#active--;
|
|
551
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
let eventBytes = 0;
|
|
555
|
+
let lineBytes = 0;
|
|
556
|
+
const body2 = reader ? new ReadableStream({
|
|
557
|
+
start: (controller) => {
|
|
558
|
+
onAbort = () => {
|
|
559
|
+
release();
|
|
560
|
+
void reader.cancel().catch(() => {
|
|
561
|
+
});
|
|
562
|
+
controller.error(new BackendConnectionError("Backend event connection closed.", 499));
|
|
563
|
+
};
|
|
564
|
+
if (signal.aborted) onAbort();
|
|
565
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
566
|
+
},
|
|
567
|
+
pull: async (controller) => {
|
|
568
|
+
try {
|
|
569
|
+
signal.throwIfAborted();
|
|
570
|
+
const next = await reader.read();
|
|
571
|
+
if (next.done) {
|
|
572
|
+
release();
|
|
573
|
+
controller.close();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
for (const byte of next.value) {
|
|
577
|
+
eventBytes++;
|
|
578
|
+
if (byte === 10) {
|
|
579
|
+
if (lineBytes === 0) eventBytes = 0;
|
|
580
|
+
lineBytes = 0;
|
|
581
|
+
} else if (byte !== 13) lineBytes++;
|
|
582
|
+
if (eventBytes > MAX_BODY) throw new BackendConnectionError("Backend event exceeds 1 MB.");
|
|
583
|
+
}
|
|
584
|
+
controller.enqueue(next.value);
|
|
585
|
+
} catch (error) {
|
|
586
|
+
release();
|
|
587
|
+
await reader.cancel().catch(() => {
|
|
588
|
+
});
|
|
589
|
+
controller.error(error);
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
cancel: async () => {
|
|
593
|
+
release();
|
|
594
|
+
await reader.cancel().catch(() => {
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}) : null;
|
|
598
|
+
if (body2) retained = true;
|
|
599
|
+
return new Response(body2, { status: response.status, headers: { "Content-Type": type, "X-0-Request-ID": requestId, ...response.headers.get("x-0-engine-id") ? { "X-0-Engine-ID": response.headers.get("x-0-engine-id") } : {} } });
|
|
600
|
+
}
|
|
601
|
+
const data = await boundedResponse(response, MAX_RESPONSE, signal);
|
|
602
|
+
return new Response([204, 205, 304].includes(response.status) || method === "HEAD" ? null : data, { status: response.status, headers: { "Content-Type": type, ...attachmentHeaders(response.headers), "X-0-Request-ID": requestId, ...response.headers.get("x-0-engine-id") ? { "X-0-Engine-ID": response.headers.get("x-0-engine-id") } : {} } });
|
|
603
|
+
} catch (error) {
|
|
604
|
+
if (error instanceof BackendConnectionError) throw error;
|
|
605
|
+
if (timeout.signal.aborted) throw new BackendConnectionError("Backend request timed out.", 504);
|
|
606
|
+
if (options.signal?.aborted) throw new BackendConnectionError("Backend request disconnected.", 499);
|
|
607
|
+
throw new BackendConnectionError("Backend transport failed.");
|
|
608
|
+
} finally {
|
|
609
|
+
clearTimeout(timer);
|
|
610
|
+
if (!retained) this.#active--;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
dispose() {
|
|
614
|
+
this.#abort.abort();
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
async function requestJson(req) {
|
|
618
|
+
let length = 0;
|
|
619
|
+
const chunks = [];
|
|
620
|
+
for await (const chunk of req) {
|
|
621
|
+
const buffer = Buffer.from(chunk);
|
|
622
|
+
length += buffer.length;
|
|
623
|
+
if (length > MAX_BODY) {
|
|
624
|
+
req.resume();
|
|
625
|
+
throw new BackendConnectionError("Backend request exceeds 1 MB.", 413);
|
|
626
|
+
}
|
|
627
|
+
chunks.push(buffer);
|
|
628
|
+
}
|
|
629
|
+
if (!length) return void 0;
|
|
630
|
+
try {
|
|
631
|
+
return JSON.parse(Buffer.concat(chunks, length).toString("utf8"));
|
|
632
|
+
} catch {
|
|
633
|
+
throw new BackendConnectionError("Backend request must contain valid JSON.", 400);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function attachmentHeaders(headers) {
|
|
637
|
+
const value = headers.get("content-disposition");
|
|
638
|
+
const match = value?.match(/^attachment;\s*filename="([A-Za-z0-9][A-Za-z0-9_.-]{0,159})"$/i);
|
|
639
|
+
return match ? { "Content-Disposition": `attachment; filename="${match[1]}"` } : {};
|
|
640
|
+
}
|
|
641
|
+
function json(res, status, value) {
|
|
642
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
643
|
+
res.end(JSON.stringify(value));
|
|
644
|
+
}
|
|
645
|
+
async function handleBackendConnectionRequest(req, res, url, registry) {
|
|
646
|
+
if (url.pathname === "/api/backends") {
|
|
647
|
+
if (req.method !== "GET") throw new BackendConnectionError("Backend registry is read-only.", 405);
|
|
648
|
+
registry.reload();
|
|
649
|
+
json(res, 200, { backends: registry.list() });
|
|
650
|
+
return { handled: true };
|
|
651
|
+
}
|
|
652
|
+
if (url.pathname === "/api/backend/handshake") {
|
|
653
|
+
if (req.method !== "GET") throw new BackendConnectionError("Method not allowed.", 405);
|
|
654
|
+
json(res, 200, (await registry.handshake("local")).handshake);
|
|
655
|
+
return { handled: true };
|
|
656
|
+
}
|
|
657
|
+
const removal = url.pathname.match(/^\/api\/backends\/([^/]+)$/);
|
|
658
|
+
if (removal) {
|
|
659
|
+
if (req.method !== "DELETE") throw new BackendConnectionError("Use DELETE to remove a backend registration.", 405);
|
|
660
|
+
registry.remove(decodeURIComponent(removal[1]));
|
|
661
|
+
json(res, 200, { removed: true, remoteEngineRemoved: false });
|
|
662
|
+
return { handled: true };
|
|
663
|
+
}
|
|
664
|
+
const match = url.pathname.match(/^\/api\/backends\/([^/]+)\/(handshake|proxy(?:\/.*)?)$/);
|
|
665
|
+
if (!match) return { handled: false };
|
|
666
|
+
const id = decodeURIComponent(match[1]);
|
|
667
|
+
if (match[2] === "handshake") {
|
|
668
|
+
if (req.method !== "GET") throw new BackendConnectionError("Method not allowed.", 405);
|
|
669
|
+
json(res, 200, await registry.handshake(id));
|
|
670
|
+
return { handled: true };
|
|
671
|
+
}
|
|
672
|
+
const path = apiPath(`${url.pathname.slice(url.pathname.indexOf("/proxy/") + "/proxy".length)}${url.search}`);
|
|
673
|
+
if (id === "local") return { handled: false, localPath: path };
|
|
674
|
+
const abort = new AbortController();
|
|
675
|
+
const disconnect = () => abort.abort();
|
|
676
|
+
req.once("aborted", disconnect);
|
|
677
|
+
res.once("close", disconnect);
|
|
678
|
+
try {
|
|
679
|
+
const body = ["GET", "HEAD"].includes(req.method ?? "GET") ? void 0 : await requestJson(req);
|
|
680
|
+
const stream = req.headers.accept?.includes("text/event-stream") === true;
|
|
681
|
+
const response = await registry.request(id, path, { method: req.method, body, stream, signal: abort.signal, lastEventId: typeof req.headers["last-event-id"] === "string" ? req.headers["last-event-id"] : void 0, requestId: typeof req.headers["x-0-request-id"] === "string" ? req.headers["x-0-request-id"] : void 0 });
|
|
682
|
+
if (res.destroyed) return { handled: true };
|
|
683
|
+
res.writeHead(response.status, { "Content-Type": response.headers.get("content-type") ?? "application/json", ...attachmentHeaders(response.headers), "Cache-Control": "no-store", "X-0-Request-ID": response.headers.get("x-0-request-id") ?? randomUUID(), ...response.headers.get("x-0-engine-id") ? { "X-0-Engine-ID": response.headers.get("x-0-engine-id") } : {}, "X-Content-Type-Options": "nosniff", ...stream ? { "X-Accel-Buffering": "no" } : {} });
|
|
684
|
+
if (!response.body) {
|
|
685
|
+
res.end();
|
|
686
|
+
return { handled: true };
|
|
687
|
+
}
|
|
688
|
+
const reader = response.body.getReader();
|
|
689
|
+
const onAbort = () => {
|
|
690
|
+
void reader.cancel().catch(() => {
|
|
691
|
+
});
|
|
692
|
+
};
|
|
693
|
+
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
694
|
+
try {
|
|
695
|
+
while (!abort.signal.aborted) {
|
|
696
|
+
const next = await reader.read();
|
|
697
|
+
if (next.done) break;
|
|
698
|
+
if (next.value.length > MAX_RESPONSE) throw new BackendConnectionError("Backend stream chunk exceeds the transport limit.");
|
|
699
|
+
if (!res.write(next.value)) await new Promise((resolve2) => {
|
|
700
|
+
const done = () => {
|
|
701
|
+
res.removeListener("drain", done);
|
|
702
|
+
res.removeListener("close", done);
|
|
703
|
+
resolve2();
|
|
704
|
+
};
|
|
705
|
+
res.once("drain", done);
|
|
706
|
+
res.once("close", done);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
res.end();
|
|
710
|
+
} catch {
|
|
711
|
+
res.destroy();
|
|
712
|
+
} finally {
|
|
713
|
+
abort.signal.removeEventListener("abort", onAbort);
|
|
714
|
+
await reader.cancel().catch(() => {
|
|
715
|
+
});
|
|
716
|
+
reader.releaseLock();
|
|
717
|
+
}
|
|
718
|
+
return { handled: true };
|
|
719
|
+
} finally {
|
|
720
|
+
req.removeListener("aborted", disconnect);
|
|
721
|
+
res.removeListener("close", disconnect);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
export {
|
|
726
|
+
validateWorkerOptions,
|
|
727
|
+
deployWorker,
|
|
728
|
+
loadManagedWorker,
|
|
729
|
+
stopWorker,
|
|
730
|
+
BackendConnectionError,
|
|
731
|
+
loadBackendConnectionConfig,
|
|
732
|
+
createBackendHandshake,
|
|
733
|
+
backendBearerFromEnv,
|
|
734
|
+
BackendConnectionRegistry,
|
|
735
|
+
handleBackendConnectionRequest
|
|
736
|
+
};
|