@jait/gateway 0.1.453 → 0.1.455

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/lib/network-scan.js +1 -1
  2. package/dist/lib/network-scan.js.map +1 -1
  3. package/dist/routes/network.d.ts +15 -3
  4. package/dist/routes/network.d.ts.map +1 -1
  5. package/dist/routes/network.js +454 -83
  6. package/dist/routes/network.js.map +1 -1
  7. package/dist/server.js +1 -1
  8. package/dist/server.js.map +1 -1
  9. package/package.json +1 -1
  10. package/web-dist/assets/{_basePickBy-BEMoZb9X.js → _basePickBy-DhrEJmuf.js} +1 -1
  11. package/web-dist/assets/{_baseUniq-BMu-Yyas.js → _baseUniq-B9lsiCrY.js} +1 -1
  12. package/web-dist/assets/{arc-DokX-xUf.js → arc-DQasnSqr.js} +1 -1
  13. package/web-dist/assets/{architectureDiagram-2XIMDMQ5-DTyrnzZO.js → architectureDiagram-2XIMDMQ5-CAhLzuSZ.js} +1 -1
  14. package/web-dist/assets/{blockDiagram-WCTKOSBZ-DIZsmYRn.js → blockDiagram-WCTKOSBZ-CEmZjcFP.js} +1 -1
  15. package/web-dist/assets/{c4Diagram-IC4MRINW-D4Xmw5b_.js → c4Diagram-IC4MRINW-BzrMrO0E.js} +1 -1
  16. package/web-dist/assets/channel-D-4ycc7r.js +1 -0
  17. package/web-dist/assets/{chunk-4BX2VUAB-ClZXuAzG.js → chunk-4BX2VUAB-B8YNTYoT.js} +1 -1
  18. package/web-dist/assets/{chunk-55IACEB6-le9at6Aj.js → chunk-55IACEB6-DpA7Ir5k.js} +1 -1
  19. package/web-dist/assets/{chunk-FMBD7UC4-D_9Z0dXn.js → chunk-FMBD7UC4-uwxI9lSc.js} +1 -1
  20. package/web-dist/assets/{chunk-JSJVCQXG-7Y9jBM6I.js → chunk-JSJVCQXG-CNLH80Cd.js} +1 -1
  21. package/web-dist/assets/{chunk-KX2RTZJC-Dbm3Qdpm.js → chunk-KX2RTZJC-CUOrdjCY.js} +1 -1
  22. package/web-dist/assets/{chunk-NQ4KR5QH-m6N1FujP.js → chunk-NQ4KR5QH-DXXe6pKB.js} +1 -1
  23. package/web-dist/assets/{chunk-QZHKN3VN-fmakBEY8.js → chunk-QZHKN3VN-C6GRquwf.js} +1 -1
  24. package/web-dist/assets/{chunk-WL4C6EOR-CI3Zs1lJ.js → chunk-WL4C6EOR-DXZVBqL-.js} +1 -1
  25. package/web-dist/assets/classDiagram-VBA2DB6C-Cdx9fpNb.js +1 -0
  26. package/web-dist/assets/classDiagram-v2-RAHNMMFH-Cdx9fpNb.js +1 -0
  27. package/web-dist/assets/clone-CIUyWx4d.js +1 -0
  28. package/web-dist/assets/{cose-bilkent-S5V4N54A-COos7fSJ.js → cose-bilkent-S5V4N54A-OUfHU879.js} +1 -1
  29. package/web-dist/assets/{dagre-KLK3FWXG-DbqtsZNn.js → dagre-KLK3FWXG-BDmShaFD.js} +1 -1
  30. package/web-dist/assets/{diagram-E7M64L7V-B_DcHEEb.js → diagram-E7M64L7V-DluFOBAK.js} +1 -1
  31. package/web-dist/assets/{diagram-IFDJBPK2-DlgkROoz.js → diagram-IFDJBPK2-CUW-K_M4.js} +1 -1
  32. package/web-dist/assets/{diagram-P4PSJMXO--eYjiGbF.js → diagram-P4PSJMXO-CRzFfjCy.js} +1 -1
  33. package/web-dist/assets/{erDiagram-INFDFZHY-DNIDHmAe.js → erDiagram-INFDFZHY-Dj_TgyFg.js} +1 -1
  34. package/web-dist/assets/{flowDiagram-PKNHOUZH-zyKrZI1m.js → flowDiagram-PKNHOUZH-CiTURYMj.js} +1 -1
  35. package/web-dist/assets/{ganttDiagram-A5KZAMGK-CkjWXRsE.js → ganttDiagram-A5KZAMGK-CzJL2HvO.js} +1 -1
  36. package/web-dist/assets/{gitGraphDiagram-K3NZZRJ6-DnYfRLO3.js → gitGraphDiagram-K3NZZRJ6-CHHFM6a3.js} +1 -1
  37. package/web-dist/assets/{graph-CdcnOrT4.js → graph-C5ciFCCm.js} +1 -1
  38. package/web-dist/assets/{index-ASt2_6mv.js → index-B5pTzjfu.js} +1 -1
  39. package/web-dist/assets/{index-C5d-5FBS.js → index-BomeC2u7.js} +77 -77
  40. package/web-dist/assets/{index-CRAyd4xj.js → index-ny6LkF9y.js} +1 -1
  41. package/web-dist/assets/{infoDiagram-LFFYTUFH-BMTD68N-.js → infoDiagram-LFFYTUFH-BGl8Z5dE.js} +1 -1
  42. package/web-dist/assets/{ishikawaDiagram-PHBUUO56-DWPjUN3w.js → ishikawaDiagram-PHBUUO56-DIgudH__.js} +1 -1
  43. package/web-dist/assets/{journeyDiagram-4ABVD52K-DO7dBE6b.js → journeyDiagram-4ABVD52K-DyU5seZb.js} +1 -1
  44. package/web-dist/assets/{kanban-definition-K7BYSVSG-Q6sxBc0r.js → kanban-definition-K7BYSVSG-CXX-Vfuy.js} +1 -1
  45. package/web-dist/assets/{layout-DBoV6KqC.js → layout-Fvvu44tp.js} +1 -1
  46. package/web-dist/assets/{linear-ByQ36XLK.js → linear-CKbEx1zZ.js} +1 -1
  47. package/web-dist/assets/{mindmap-definition-YRQLILUH-C94d2j1J.js → mindmap-definition-YRQLILUH-BAhVhvP4.js} +1 -1
  48. package/web-dist/assets/{pieDiagram-SKSYHLDU-iQ_s4rNZ.js → pieDiagram-SKSYHLDU-Codzr7Xq.js} +1 -1
  49. package/web-dist/assets/{quadrantDiagram-337W2JSQ-Bggp3_jC.js → quadrantDiagram-337W2JSQ-rBP93tu-.js} +1 -1
  50. package/web-dist/assets/{requirementDiagram-Z7DCOOCP-DEpYoXiQ.js → requirementDiagram-Z7DCOOCP-Cl-tIjbE.js} +1 -1
  51. package/web-dist/assets/{sankeyDiagram-WA2Y5GQK-CmcKKwux.js → sankeyDiagram-WA2Y5GQK-BjEbuAaJ.js} +1 -1
  52. package/web-dist/assets/{sequenceDiagram-2WXFIKYE-BP4ala3N.js → sequenceDiagram-2WXFIKYE-CjuDrNgv.js} +1 -1
  53. package/web-dist/assets/{stateDiagram-RAJIS63D-B63qNKRq.js → stateDiagram-RAJIS63D-VP-67XJS.js} +1 -1
  54. package/web-dist/assets/stateDiagram-v2-FVOUBMTO-xlGUplYt.js +1 -0
  55. package/web-dist/assets/{timeline-definition-YZTLITO2-yTT3dubh.js → timeline-definition-YZTLITO2-BYBaBs79.js} +1 -1
  56. package/web-dist/assets/{treemap-KZPCXAKY-H02GTJ0N.js → treemap-KZPCXAKY-Cql-h4_N.js} +1 -1
  57. package/web-dist/assets/{vennDiagram-LZ73GAT5-CZnlhpxS.js → vennDiagram-LZ73GAT5-C6IaEUPA.js} +1 -1
  58. package/web-dist/assets/{xychartDiagram-JWTSCODW-1mVqxtm_.js → xychartDiagram-JWTSCODW-D9VOxnGG.js} +1 -1
  59. package/web-dist/index.html +1 -1
  60. package/web-dist/assets/channel-BNGi7anu.js +0 -1
  61. package/web-dist/assets/classDiagram-VBA2DB6C-BwNok8Om.js +0 -1
  62. package/web-dist/assets/classDiagram-v2-RAHNMMFH-BwNok8Om.js +0 -1
  63. package/web-dist/assets/clone-4s7G4Vo3.js +0 -1
  64. package/web-dist/assets/stateDiagram-v2-FVOUBMTO-CsdHGXj3.js +0 -1
@@ -9,7 +9,6 @@ import { existsSync } from "node:fs";
9
9
  import { getLatestNetworkScan, setLatestNetworkScan } from "../tools/network-tools.js";
10
10
  import { createRequire } from "node:module";
11
11
  import { scanNetwork } from "../lib/network-scan.js";
12
- import { TerminalSurface } from "../surfaces/terminal.js";
13
12
  const require = createRequire(import.meta.url);
14
13
  const { version: PKG_VERSION } = require("../../package.json");
15
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -29,80 +28,223 @@ function probePort(ip, port, timeoutMs = 2000) {
29
28
  export function shellQuote(value) {
30
29
  return `'${value.replace(/'/g, `'\"'\"'`)}'`;
31
30
  }
32
- export function buildSshAuthArgs(authMethod, password) {
33
- if (authMethod === "password" && password) {
34
- return [
35
- "-o", "BatchMode=no",
36
- "-o", "PreferredAuthentications=password,keyboard-interactive",
37
- "-o", "PubkeyAuthentication=no",
38
- "-o", "NumberOfPasswordPrompts=1",
39
- ];
31
+ function loadNodePty() {
32
+ return require("node-pty");
33
+ }
34
+ function stripAnsi(value) {
35
+ // eslint-disable-next-line no-control-regex
36
+ return value.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\].*?(?:\x07|\x1B\\))/g, "");
37
+ }
38
+ function truncateDeployOutput(value) {
39
+ const clean = stripAnsi(value).replace(/\r/g, "").trim();
40
+ if (clean.length > 200_000)
41
+ return `...(truncated)\n${clean.slice(-200_000)}`;
42
+ return clean;
43
+ }
44
+ function isPasswordFailure(output) {
45
+ return /permission denied|authentication failed|password was not provided|too many authentication failures/i.test(output);
46
+ }
47
+ function validateSshTargetPart(value, label) {
48
+ if (!value.trim())
49
+ return `${label} is required`;
50
+ if (/[\s@'"]/u.test(value))
51
+ return `${label} cannot contain whitespace, quotes, or @`;
52
+ return null;
53
+ }
54
+ export function buildNonInteractiveSshArgs(input) {
55
+ const args = [
56
+ "-o", "StrictHostKeyChecking=no",
57
+ "-o", "UserKnownHostsFile=/dev/null",
58
+ "-o", "ConnectTimeout=10",
59
+ "-o", "LogLevel=ERROR",
60
+ "-o", "NumberOfPasswordPrompts=1",
61
+ "-o", input.password ? "BatchMode=no" : "BatchMode=yes",
62
+ ];
63
+ if (input.password) {
64
+ args.push("-o", "PreferredAuthentications=password,keyboard-interactive", "-o", "PubkeyAuthentication=no");
40
65
  }
41
- return ["-o", "BatchMode=yes"];
66
+ args.push(`${input.username}@${input.ip}`, input.command);
67
+ return args;
42
68
  }
43
- export function buildInteractiveDeployCommand(ip, username, version = PKG_VERSION) {
44
- const tsEntry = resolve(__dirname, "../index.ts");
45
- const jsEntry = resolve(__dirname, "../index.js");
46
- const entry = existsSync(tsEntry) ? tsEntry : jsEntry;
69
+ export function buildNonInteractiveScpArgs(input) {
70
+ const args = [
71
+ "-o", "StrictHostKeyChecking=no",
72
+ "-o", "UserKnownHostsFile=/dev/null",
73
+ "-o", "ConnectTimeout=10",
74
+ "-o", "LogLevel=ERROR",
75
+ "-o", "NumberOfPasswordPrompts=1",
76
+ "-o", input.password ? "BatchMode=no" : "BatchMode=yes",
77
+ ];
78
+ if (input.password) {
79
+ args.push("-o", "PreferredAuthentications=password,keyboard-interactive", "-o", "PubkeyAuthentication=no");
80
+ }
81
+ args.push(input.source, `${input.username}@${input.ip}:${input.target}`);
82
+ return args;
83
+ }
84
+ function runPtyCommand(input) {
85
+ return new Promise((resolve) => {
86
+ const pty = loadNodePty().spawn(input.command, input.args, {
87
+ name: "xterm-256color",
88
+ cols: 120,
89
+ rows: 30,
90
+ cwd: process.cwd(),
91
+ env: {
92
+ ...process.env,
93
+ SSH_ASKPASS: undefined,
94
+ DISPLAY: undefined,
95
+ },
96
+ });
97
+ let raw = "";
98
+ let exitCode = null;
99
+ let timedOut = false;
100
+ let settled = false;
101
+ let passwordSent = false;
102
+ let stdinSent = false;
103
+ let lastOutputLength = 0;
104
+ const sendStdin = () => {
105
+ if (stdinSent || !input.stdinAfterAuth)
106
+ return;
107
+ stdinSent = true;
108
+ pty.write(input.stdinAfterAuth);
109
+ };
110
+ const timer = setTimeout(() => {
111
+ timedOut = true;
112
+ try {
113
+ pty.kill("SIGTERM");
114
+ }
115
+ catch { }
116
+ setTimeout(() => finish(), 300);
117
+ }, input.timeoutMs);
118
+ const finish = () => {
119
+ if (settled)
120
+ return;
121
+ settled = true;
122
+ clearTimeout(timer);
123
+ resolve({
124
+ output: truncateDeployOutput(raw),
125
+ exitCode,
126
+ timedOut,
127
+ });
128
+ };
129
+ const sendNewOutput = () => {
130
+ if (!input.onOutput)
131
+ return;
132
+ const clean = truncateDeployOutput(raw);
133
+ const next = clean.slice(lastOutputLength);
134
+ lastOutputLength = clean.length;
135
+ for (const line of next.split("\n").map((value) => value.trim()).filter(Boolean)) {
136
+ if (!/password|passphrase/i.test(line))
137
+ input.onOutput(line);
138
+ }
139
+ };
140
+ pty.onData((data) => {
141
+ raw += data;
142
+ const visible = stripAnsi(raw).replace(/\r/g, "");
143
+ if (input.password && !passwordSent && /(?:password|passphrase).*:\s*$/im.test(visible)) {
144
+ passwordSent = true;
145
+ pty.write(`${input.password}\r`);
146
+ setTimeout(sendStdin, 300);
147
+ return;
148
+ }
149
+ sendNewOutput();
150
+ });
151
+ pty.onExit((event) => {
152
+ exitCode = typeof event.exitCode === "number" ? event.exitCode : null;
153
+ finish();
154
+ });
155
+ if (input.stdinAfterAuth) {
156
+ setTimeout(sendStdin, input.password ? 1500 : 300);
157
+ }
158
+ });
159
+ }
160
+ async function requestDeployPassword(input) {
161
+ if (!input.secretInput)
162
+ throw new Error("Secret input service is unavailable");
163
+ return input.secretInput.requestSecret({
164
+ sessionId: input.sessionId,
165
+ title: input.title,
166
+ prompt: input.prompt,
167
+ requestedBy: input.requestedBy,
168
+ rememberable: true,
169
+ rememberLabel: input.prompt,
170
+ secretType: "network-deploy-password",
171
+ secretKey: input.secretKey,
172
+ timeoutMs: 180_000,
173
+ });
174
+ }
175
+ function buildRemoteSetupScript(input) {
47
176
  return [
48
- "bash <<'JAIT_DEPLOY'",
49
- "set -euo pipefail",
50
- `IP=${shellQuote(ip)}`,
51
- `USERNAME=${shellQuote(username)}`,
52
- `VERSION=${shellQuote(version)}`,
53
- `ENTRY=${shellQuote(entry)}`,
54
- "",
55
- "echo \"Connecting to ${USERNAME}@${IP}...\"",
56
- "raw=\"$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 \"${USERNAME}@${IP}\" \"uname -m\" | tr -d '\\r' | tail -n 1)\"",
57
- "case \"$raw\" in",
58
- " x86_64|amd64) arch='x64' ;;",
59
- " aarch64|arm64) arch='arm64' ;;",
60
- " *) echo \"Unsupported architecture: $raw\"; exit 1 ;;",
61
- "esac",
62
- "echo \"Detected linux-${arch}\"",
177
+ "set -e",
178
+ `SUDO_MODE=${shellQuote(input.sudoMode)}`,
179
+ `SUDO_PASSWORD=${shellQuote(input.sudoPassword ?? "")}`,
180
+ "run_priv() {",
181
+ " if [ \"$SUDO_MODE\" = 'root' ]; then",
182
+ " sh -c \"$1\"",
183
+ " elif [ \"$SUDO_MODE\" = 'nopass' ]; then",
184
+ " sudo -n sh -c \"$1\"",
185
+ " else",
186
+ " printf '%s\\n' \"$SUDO_PASSWORD\" | sudo -S -p '' sh -c \"$1\"",
187
+ " fi",
188
+ "}",
189
+ "chmod +x ~/.jait/jait-gateway",
190
+ "[ -f ~/.jait/.env ] || cat > ~/.jait/.env <<'ENVEOF'",
191
+ "PORT=8000",
192
+ "HOST=0.0.0.0",
193
+ "LOG_LEVEL=info",
194
+ "CORS_ORIGIN=*",
195
+ "ENVEOF",
196
+ "service_file=\"${TMPDIR:-/tmp}/jait-gateway.service.$$\"",
197
+ "cat > \"$service_file\" <<'SVCEOF'",
198
+ "[Unit]",
199
+ "Description=Jait Gateway",
200
+ "After=network.target",
63
201
  "",
64
- "echo '[1/3] Compiling gateway binary...'",
65
- "cacheDir=\"${TMPDIR:-/tmp}/jait-deploy\"",
66
- "mkdir -p \"$cacheDir\"",
67
- "outFile=\"$cacheDir/jait-gateway-${VERSION}-linux-${arch}\"",
68
- "if [ -f \"$outFile\" ]; then",
69
- " echo \"Using cached binary (v${VERSION})\"",
70
- "else",
71
- " target=\"bun-linux-${arch}\"",
72
- " echo \"bun build --compile --target=${target} --minify\"",
73
- " bun build --compile \"--target=${target}\" --minify \"$ENTRY\" --outfile \"$outFile\"",
74
- " echo 'Binary compiled'",
75
- "fi",
202
+ "[Service]",
203
+ `User=${input.username}`,
204
+ "WorkingDirectory=%h/.jait",
205
+ "ExecStart=%h/.jait/jait-gateway",
206
+ "Restart=on-failure",
76
207
  "",
77
- "size_mb=\"$(du -m \"$outFile\" | cut -f1)\"",
78
- "echo \"[2/3] Transferring binary (${size_mb} MB)...\"",
79
- "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 \"${USERNAME}@${IP}\" 'mkdir -p ~/.jait'",
80
- "scp -o StrictHostKeyChecking=no -o ConnectTimeout=10 \"$outFile\" \"${USERNAME}@${IP}:~/.jait/jait-gateway\"",
81
- "echo 'Binary transferred'",
208
+ "[Install]",
209
+ "WantedBy=multi-user.target",
210
+ "SVCEOF",
211
+ "run_priv \"install -m 0644 '$service_file' /etc/systemd/system/jait-gateway.service\"",
212
+ "rm -f \"$service_file\"",
213
+ "run_priv \"systemctl daemon-reload\"",
214
+ "run_priv \"systemctl enable --now jait-gateway\"",
215
+ "echo 'Jait Gateway deployed successfully'",
82
216
  "",
83
- "echo '[3/3] Configuring and starting...'",
84
- `ssh -tt -o StrictHostKeyChecking=no -o ConnectTimeout=10 "${username}@${ip}" 'bash -s' <<'REMOTE_SETUP'`,
217
+ ].join("\n");
218
+ }
219
+ function buildInteractiveDeployCommand(input) {
220
+ const cacheDir = `${process.env.TMPDIR ?? "/tmp"}/jait-deploy`;
221
+ const tsEntry = resolve(__dirname, "../index.ts");
222
+ const jsEntry = resolve(__dirname, "../index.js");
223
+ const entry = existsSync(tsEntry) ? tsEntry : jsEntry;
224
+ const remoteScript = [
85
225
  "set -e",
226
+ "run_priv() {",
227
+ " if [ \"$(id -u)\" -eq 0 ]; then",
228
+ " sh -c \"$1\"",
229
+ " else",
230
+ " sudo sh -c \"$1\"",
231
+ " fi",
232
+ "}",
86
233
  "chmod +x ~/.jait/jait-gateway",
87
- "SUDO=''",
88
- "if [ \"$(id -u)\" -ne 0 ] && command -v sudo >/dev/null 2>&1; then",
89
- " SUDO='sudo'",
90
- "fi",
91
- "",
92
234
  "[ -f ~/.jait/.env ] || cat > ~/.jait/.env <<'ENVEOF'",
93
235
  "PORT=8000",
94
236
  "HOST=0.0.0.0",
95
237
  "LOG_LEVEL=info",
96
238
  "CORS_ORIGIN=*",
97
239
  "ENVEOF",
98
- "",
99
- "${SUDO:+$SUDO }tee /etc/systemd/system/jait-gateway.service > /dev/null <<'SVCEOF'",
240
+ "service_file=\"${TMPDIR:-/tmp}/jait-gateway.service.$$\"",
241
+ "cat > \"$service_file\" <<'SVCEOF'",
100
242
  "[Unit]",
101
243
  "Description=Jait Gateway",
102
244
  "After=network.target",
103
245
  "",
104
246
  "[Service]",
105
- `User=${username}`,
247
+ `User=${input.username}`,
106
248
  "WorkingDirectory=%h/.jait",
107
249
  "ExecStart=%h/.jait/jait-gateway",
108
250
  "Restart=on-failure",
@@ -110,15 +252,225 @@ export function buildInteractiveDeployCommand(ip, username, version = PKG_VERSIO
110
252
  "[Install]",
111
253
  "WantedBy=multi-user.target",
112
254
  "SVCEOF",
113
- "",
114
- "${SUDO:+$SUDO }systemctl daemon-reload",
115
- "${SUDO:+$SUDO }systemctl enable --now jait-gateway",
116
- "echo 'Jait Gateway deployed successfully!'",
117
- "REMOTE_SETUP",
118
- "",
119
- "echo \"Gateway v${VERSION} deployed to ${IP}\"",
120
- "JAIT_DEPLOY",
255
+ "run_priv \"install -m 0644 '$service_file' /etc/systemd/system/jait-gateway.service\"",
256
+ "rm -f \"$service_file\"",
257
+ "run_priv \"systemctl daemon-reload\"",
258
+ "run_priv \"systemctl enable --now jait-gateway\"",
259
+ "echo 'Jait Gateway deployed successfully'",
121
260
  ].join("\n");
261
+ return [
262
+ "set -e",
263
+ `TARGET=${shellQuote(`${input.username}@${input.ip}`)}`,
264
+ `CACHE_DIR=${shellQuote(cacheDir)}`,
265
+ `VERSION=${shellQuote(PKG_VERSION)}`,
266
+ `ENTRY=${shellQuote(entry)}`,
267
+ "SSH_OPTS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -o LogLevel=ERROR)",
268
+ "mkdir -p \"$CACHE_DIR\"",
269
+ "echo \"[1/5] Detecting target architecture...\"",
270
+ "raw_arch=$(ssh \"${SSH_OPTS[@]}\" \"$TARGET\" 'uname -m')",
271
+ "case \"$raw_arch\" in",
272
+ " x86_64|amd64) arch=x64 ;;",
273
+ " aarch64|arm64) arch=arm64 ;;",
274
+ " *) echo \"Unsupported architecture: $raw_arch\"; exit 1 ;;",
275
+ "esac",
276
+ "out=\"$CACHE_DIR/jait-gateway-$VERSION-linux-$arch\"",
277
+ "if [ -f \"$out\" ]; then",
278
+ " echo \"[2/5] Using cached binary v$VERSION for linux-$arch\"",
279
+ "else",
280
+ " echo \"[2/5] Compiling gateway binary for linux-$arch...\"",
281
+ " bun build --compile --target=\"bun-linux-$arch\" --minify \"$ENTRY\" --outfile \"$out\"",
282
+ "fi",
283
+ "setup_script=$(mktemp)",
284
+ "cat > \"$setup_script\" <<'JAIT_REMOTE_SCRIPT'",
285
+ remoteScript,
286
+ "JAIT_REMOTE_SCRIPT",
287
+ "echo \"[3/5] Preparing remote directory...\"",
288
+ "ssh \"${SSH_OPTS[@]}\" \"$TARGET\" 'mkdir -p ~/.jait'",
289
+ "echo \"[4/5] Transferring gateway binary...\"",
290
+ "scp \"${SSH_OPTS[@]}\" \"$out\" \"$TARGET:~/.jait/jait-gateway\"",
291
+ "scp \"${SSH_OPTS[@]}\" \"$setup_script\" \"$TARGET:~/.jait/jait-setup.sh\"",
292
+ "rm -f \"$setup_script\"",
293
+ "echo \"[5/5] Configuring service. Enter SSH/sudo passwords here if prompted.\"",
294
+ "ssh -tt \"${SSH_OPTS[@]}\" \"$TARGET\" 'bash ~/.jait/jait-setup.sh'",
295
+ `echo "Dashboard: http://${input.ip}:8000"`,
296
+ ].join("\n");
297
+ }
298
+ async function runGuidedDeploy(input) {
299
+ const logs = [];
300
+ const addLog = (line) => {
301
+ if (!line.trim())
302
+ return;
303
+ logs.push(line);
304
+ };
305
+ let sshPassword = null;
306
+ const passwordKey = `${input.username}@${input.ip}:22`;
307
+ if (input.authMethod === "password") {
308
+ sshPassword = await requestDeployPassword({
309
+ secretInput: input.secretInput,
310
+ sessionId: input.sessionId,
311
+ title: "SSH password",
312
+ prompt: `Password for ${input.username}@${input.ip}`,
313
+ requestedBy: "network.deploy",
314
+ secretKey: passwordKey,
315
+ });
316
+ if (!sshPassword)
317
+ return { ok: false, logs, error: "SSH password was not provided" };
318
+ }
319
+ addLog(`Connecting to ${input.username}@${input.ip}...`);
320
+ let archResult = await runPtyCommand({
321
+ command: "ssh",
322
+ args: buildNonInteractiveSshArgs({
323
+ ip: input.ip,
324
+ username: input.username,
325
+ password: sshPassword,
326
+ command: "uname -m",
327
+ }),
328
+ password: sshPassword,
329
+ timeoutMs: 30_000,
330
+ });
331
+ if (input.authMethod === "auto" && archResult.exitCode !== 0 && isPasswordFailure(archResult.output)) {
332
+ sshPassword = await requestDeployPassword({
333
+ secretInput: input.secretInput,
334
+ sessionId: input.sessionId,
335
+ title: "SSH password",
336
+ prompt: `Password for ${input.username}@${input.ip}`,
337
+ requestedBy: "network.deploy",
338
+ secretKey: passwordKey,
339
+ });
340
+ if (!sshPassword)
341
+ return { ok: false, logs, error: "SSH password was not provided" };
342
+ archResult = await runPtyCommand({
343
+ command: "ssh",
344
+ args: buildNonInteractiveSshArgs({
345
+ ip: input.ip,
346
+ username: input.username,
347
+ password: sshPassword,
348
+ command: "uname -m",
349
+ }),
350
+ password: sshPassword,
351
+ timeoutMs: 30_000,
352
+ });
353
+ }
354
+ if (archResult.timedOut || archResult.exitCode !== 0) {
355
+ return { ok: false, logs, error: archResult.output || "Unable to connect over SSH" };
356
+ }
357
+ const rawArch = archResult.output.split("\n").map((line) => line.trim()).filter(Boolean).pop() ?? "";
358
+ const arch = rawArch === "x86_64" || rawArch === "amd64"
359
+ ? "x64"
360
+ : rawArch === "aarch64" || rawArch === "arm64"
361
+ ? "arm64"
362
+ : null;
363
+ if (!arch)
364
+ return { ok: false, logs, error: `Unsupported architecture: ${rawArch}` };
365
+ addLog(`Detected linux-${arch}`);
366
+ const tsEntry = resolve(__dirname, "../index.ts");
367
+ const jsEntry = resolve(__dirname, "../index.js");
368
+ const entry = existsSync(tsEntry) ? tsEntry : jsEntry;
369
+ const cacheDir = `${process.env.TMPDIR ?? "/tmp"}/jait-deploy`;
370
+ const outFile = `${cacheDir}/jait-gateway-${PKG_VERSION}-linux-${arch}`;
371
+ addLog("[1/4] Compiling gateway binary...");
372
+ await execAsync(`mkdir -p ${shellQuote(cacheDir)}`);
373
+ if (existsSync(outFile)) {
374
+ addLog(`Using cached binary (v${PKG_VERSION})`);
375
+ }
376
+ else {
377
+ await execAsync([
378
+ "bun", "build", "--compile",
379
+ shellQuote(`--target=bun-linux-${arch}`),
380
+ "--minify",
381
+ shellQuote(entry),
382
+ "--outfile",
383
+ shellQuote(outFile),
384
+ ].join(" "), { maxBuffer: 20 * 1024 * 1024 });
385
+ addLog("Binary compiled");
386
+ }
387
+ addLog("[2/4] Preparing remote directory...");
388
+ const mkdirResult = await runPtyCommand({
389
+ command: "ssh",
390
+ args: buildNonInteractiveSshArgs({
391
+ ip: input.ip,
392
+ username: input.username,
393
+ password: sshPassword,
394
+ command: "mkdir -p ~/.jait",
395
+ }),
396
+ password: sshPassword,
397
+ timeoutMs: 30_000,
398
+ });
399
+ if (mkdirResult.timedOut || mkdirResult.exitCode !== 0) {
400
+ return { ok: false, logs, error: mkdirResult.output || "Failed to prepare remote directory" };
401
+ }
402
+ addLog("[3/4] Transferring gateway binary...");
403
+ const scpResult = await runPtyCommand({
404
+ command: "scp",
405
+ args: buildNonInteractiveScpArgs({
406
+ ip: input.ip,
407
+ username: input.username,
408
+ password: sshPassword,
409
+ source: outFile,
410
+ target: "~/.jait/jait-gateway",
411
+ }),
412
+ password: sshPassword,
413
+ timeoutMs: 120_000,
414
+ });
415
+ if (scpResult.timedOut || scpResult.exitCode !== 0) {
416
+ return { ok: false, logs, error: scpResult.output || "Failed to transfer gateway binary" };
417
+ }
418
+ addLog("[4/4] Configuring service...");
419
+ const sudoCheck = await runPtyCommand({
420
+ command: "ssh",
421
+ args: buildNonInteractiveSshArgs({
422
+ ip: input.ip,
423
+ username: input.username,
424
+ password: sshPassword,
425
+ command: "if [ \"$(id -u)\" -eq 0 ]; then echo root; elif command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then echo nopass; elif command -v sudo >/dev/null 2>&1; then echo password; else echo none; fi",
426
+ }),
427
+ password: sshPassword,
428
+ timeoutMs: 30_000,
429
+ });
430
+ if (sudoCheck.timedOut || sudoCheck.exitCode !== 0) {
431
+ return { ok: false, logs, error: sudoCheck.output || "Failed to inspect sudo access" };
432
+ }
433
+ const sudoMode = sudoCheck.output.split("\n").map((line) => line.trim()).filter(Boolean).pop();
434
+ if (sudoMode === "none") {
435
+ return { ok: false, logs, error: `${input.username} is not root and sudo is unavailable on the target` };
436
+ }
437
+ let sudoPassword = sshPassword;
438
+ if (sudoMode === "password" && !sudoPassword) {
439
+ sudoPassword = await requestDeployPassword({
440
+ secretInput: input.secretInput,
441
+ sessionId: input.sessionId,
442
+ title: "Administrator password",
443
+ prompt: `Sudo password for ${input.username}@${input.ip}`,
444
+ requestedBy: "network.deploy",
445
+ secretKey: `sudo:${passwordKey}`,
446
+ });
447
+ if (!sudoPassword)
448
+ return { ok: false, logs, error: "Sudo password was not provided" };
449
+ }
450
+ const setupResult = await runPtyCommand({
451
+ command: "ssh",
452
+ args: buildNonInteractiveSshArgs({
453
+ ip: input.ip,
454
+ username: input.username,
455
+ password: sshPassword,
456
+ command: "bash -s",
457
+ }),
458
+ password: sshPassword,
459
+ stdinAfterAuth: `${buildRemoteSetupScript({
460
+ username: input.username,
461
+ sudoMode: sudoMode,
462
+ sudoPassword,
463
+ })}\x04`,
464
+ timeoutMs: 60_000,
465
+ onOutput: addLog,
466
+ });
467
+ if (setupResult.timedOut || setupResult.exitCode !== 0) {
468
+ return { ok: false, logs, error: setupResult.output || "Failed to configure remote service" };
469
+ }
470
+ const url = `http://${input.ip}:8000`;
471
+ addLog(`Gateway v${PKG_VERSION} deployed to ${input.ip}`);
472
+ addLog(`Dashboard: ${url}`);
473
+ return { ok: true, logs, url };
122
474
  }
123
475
  // ---------------------------------------------------------------------------
124
476
  // DB helpers — persist and read scanned hosts
@@ -237,7 +589,7 @@ async function getGatewayNode(providerRegistry) {
237
589
  cachedGateway = { node, builtAt: now };
238
590
  return node;
239
591
  }
240
- export function registerNetworkRoutes(app, ws, sqlite, providerRegistry, surfaceRegistry) {
592
+ export function registerNetworkRoutes(app, ws, sqlite, providerRegistry, secretInput, surfaceRegistry) {
241
593
  // ---- GET /api/network/interfaces — local NIC info ----
242
594
  app.get("/api/network/interfaces", async () => {
243
595
  const ifaces = networkInterfaces();
@@ -365,29 +717,48 @@ export function registerNetworkRoutes(app, ws, sqlite, providerRegistry, surface
365
717
  const body = (request.body ?? {});
366
718
  const ip = String(body["ip"] ?? "").trim();
367
719
  const username = String(body["username"] ?? "root").trim();
368
- const terminalId = String(body["terminalId"] ?? "").trim();
720
+ const sessionId = String(body["sessionId"] ?? "default").trim() || "default";
721
+ const terminalId = typeof body["terminalId"] === "string" ? body["terminalId"].trim() : "";
722
+ const authMethodValue = String(body["authMethod"] ?? "auto").trim();
723
+ const authMethod = authMethodValue === "password" || authMethodValue === "key"
724
+ ? authMethodValue
725
+ : "auto";
369
726
  if (!ip) {
370
727
  return reply.status(400).send({ error: "IP address is required" });
371
728
  }
372
- if (!terminalId) {
373
- return reply.status(400).send({ error: "Terminal ID is required" });
374
- }
375
- if (!surfaceRegistry) {
376
- return reply.status(503).send({ error: "Terminal support is unavailable" });
729
+ const ipError = validateSshTargetPart(ip, "IP address");
730
+ const usernameError = validateSshTargetPart(username, "username");
731
+ if (ipError || usernameError) {
732
+ return reply.status(400).send({ error: ipError ?? usernameError });
377
733
  }
378
- const surface = surfaceRegistry.getSurface(terminalId);
379
- if (!surface || surface.type !== "terminal") {
380
- return reply.status(404).send({ error: "Terminal not found" });
381
- }
382
- const terminal = surface;
383
734
  try {
384
- await terminal.waitForPrompt();
385
- terminal.write(buildInteractiveDeployCommand(ip, username) + "\r");
386
- return {
387
- ok: true,
388
- terminalId,
389
- message: `Deploy started in terminal ${terminalId}`,
390
- };
735
+ if (terminalId) {
736
+ const surface = surfaceRegistry?.getSurface(terminalId);
737
+ if (!surface || surface.type !== "terminal" || typeof surface.write !== "function") {
738
+ return reply.status(404).send({ error: "Deploy terminal not found" });
739
+ }
740
+ surface.write(`${buildInteractiveDeployCommand({ ip, username })}\r`);
741
+ return {
742
+ ok: true,
743
+ terminalId,
744
+ logs: [
745
+ `Deploy started for ${username}@${ip}.`,
746
+ "Continue in the embedded terminal.",
747
+ "SSH and sudo prompts will appear there.",
748
+ ],
749
+ };
750
+ }
751
+ const result = await runGuidedDeploy({
752
+ ip,
753
+ username,
754
+ authMethod,
755
+ sessionId,
756
+ secretInput,
757
+ });
758
+ if (!result.ok) {
759
+ return reply.status(500).send(result);
760
+ }
761
+ return result;
391
762
  }
392
763
  catch (err) {
393
764
  return reply.status(500).send({ error: err instanceof Error ? err.message : "Deployment failed" });