@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.
- package/dist/lib/network-scan.js +1 -1
- package/dist/lib/network-scan.js.map +1 -1
- package/dist/routes/network.d.ts +15 -3
- package/dist/routes/network.d.ts.map +1 -1
- package/dist/routes/network.js +454 -83
- package/dist/routes/network.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/web-dist/assets/{_basePickBy-BEMoZb9X.js → _basePickBy-DhrEJmuf.js} +1 -1
- package/web-dist/assets/{_baseUniq-BMu-Yyas.js → _baseUniq-B9lsiCrY.js} +1 -1
- package/web-dist/assets/{arc-DokX-xUf.js → arc-DQasnSqr.js} +1 -1
- package/web-dist/assets/{architectureDiagram-2XIMDMQ5-DTyrnzZO.js → architectureDiagram-2XIMDMQ5-CAhLzuSZ.js} +1 -1
- package/web-dist/assets/{blockDiagram-WCTKOSBZ-DIZsmYRn.js → blockDiagram-WCTKOSBZ-CEmZjcFP.js} +1 -1
- package/web-dist/assets/{c4Diagram-IC4MRINW-D4Xmw5b_.js → c4Diagram-IC4MRINW-BzrMrO0E.js} +1 -1
- package/web-dist/assets/channel-D-4ycc7r.js +1 -0
- package/web-dist/assets/{chunk-4BX2VUAB-ClZXuAzG.js → chunk-4BX2VUAB-B8YNTYoT.js} +1 -1
- package/web-dist/assets/{chunk-55IACEB6-le9at6Aj.js → chunk-55IACEB6-DpA7Ir5k.js} +1 -1
- package/web-dist/assets/{chunk-FMBD7UC4-D_9Z0dXn.js → chunk-FMBD7UC4-uwxI9lSc.js} +1 -1
- package/web-dist/assets/{chunk-JSJVCQXG-7Y9jBM6I.js → chunk-JSJVCQXG-CNLH80Cd.js} +1 -1
- package/web-dist/assets/{chunk-KX2RTZJC-Dbm3Qdpm.js → chunk-KX2RTZJC-CUOrdjCY.js} +1 -1
- package/web-dist/assets/{chunk-NQ4KR5QH-m6N1FujP.js → chunk-NQ4KR5QH-DXXe6pKB.js} +1 -1
- package/web-dist/assets/{chunk-QZHKN3VN-fmakBEY8.js → chunk-QZHKN3VN-C6GRquwf.js} +1 -1
- package/web-dist/assets/{chunk-WL4C6EOR-CI3Zs1lJ.js → chunk-WL4C6EOR-DXZVBqL-.js} +1 -1
- package/web-dist/assets/classDiagram-VBA2DB6C-Cdx9fpNb.js +1 -0
- package/web-dist/assets/classDiagram-v2-RAHNMMFH-Cdx9fpNb.js +1 -0
- package/web-dist/assets/clone-CIUyWx4d.js +1 -0
- package/web-dist/assets/{cose-bilkent-S5V4N54A-COos7fSJ.js → cose-bilkent-S5V4N54A-OUfHU879.js} +1 -1
- package/web-dist/assets/{dagre-KLK3FWXG-DbqtsZNn.js → dagre-KLK3FWXG-BDmShaFD.js} +1 -1
- package/web-dist/assets/{diagram-E7M64L7V-B_DcHEEb.js → diagram-E7M64L7V-DluFOBAK.js} +1 -1
- package/web-dist/assets/{diagram-IFDJBPK2-DlgkROoz.js → diagram-IFDJBPK2-CUW-K_M4.js} +1 -1
- package/web-dist/assets/{diagram-P4PSJMXO--eYjiGbF.js → diagram-P4PSJMXO-CRzFfjCy.js} +1 -1
- package/web-dist/assets/{erDiagram-INFDFZHY-DNIDHmAe.js → erDiagram-INFDFZHY-Dj_TgyFg.js} +1 -1
- package/web-dist/assets/{flowDiagram-PKNHOUZH-zyKrZI1m.js → flowDiagram-PKNHOUZH-CiTURYMj.js} +1 -1
- package/web-dist/assets/{ganttDiagram-A5KZAMGK-CkjWXRsE.js → ganttDiagram-A5KZAMGK-CzJL2HvO.js} +1 -1
- package/web-dist/assets/{gitGraphDiagram-K3NZZRJ6-DnYfRLO3.js → gitGraphDiagram-K3NZZRJ6-CHHFM6a3.js} +1 -1
- package/web-dist/assets/{graph-CdcnOrT4.js → graph-C5ciFCCm.js} +1 -1
- package/web-dist/assets/{index-ASt2_6mv.js → index-B5pTzjfu.js} +1 -1
- package/web-dist/assets/{index-C5d-5FBS.js → index-BomeC2u7.js} +77 -77
- package/web-dist/assets/{index-CRAyd4xj.js → index-ny6LkF9y.js} +1 -1
- package/web-dist/assets/{infoDiagram-LFFYTUFH-BMTD68N-.js → infoDiagram-LFFYTUFH-BGl8Z5dE.js} +1 -1
- package/web-dist/assets/{ishikawaDiagram-PHBUUO56-DWPjUN3w.js → ishikawaDiagram-PHBUUO56-DIgudH__.js} +1 -1
- package/web-dist/assets/{journeyDiagram-4ABVD52K-DO7dBE6b.js → journeyDiagram-4ABVD52K-DyU5seZb.js} +1 -1
- package/web-dist/assets/{kanban-definition-K7BYSVSG-Q6sxBc0r.js → kanban-definition-K7BYSVSG-CXX-Vfuy.js} +1 -1
- package/web-dist/assets/{layout-DBoV6KqC.js → layout-Fvvu44tp.js} +1 -1
- package/web-dist/assets/{linear-ByQ36XLK.js → linear-CKbEx1zZ.js} +1 -1
- package/web-dist/assets/{mindmap-definition-YRQLILUH-C94d2j1J.js → mindmap-definition-YRQLILUH-BAhVhvP4.js} +1 -1
- package/web-dist/assets/{pieDiagram-SKSYHLDU-iQ_s4rNZ.js → pieDiagram-SKSYHLDU-Codzr7Xq.js} +1 -1
- package/web-dist/assets/{quadrantDiagram-337W2JSQ-Bggp3_jC.js → quadrantDiagram-337W2JSQ-rBP93tu-.js} +1 -1
- package/web-dist/assets/{requirementDiagram-Z7DCOOCP-DEpYoXiQ.js → requirementDiagram-Z7DCOOCP-Cl-tIjbE.js} +1 -1
- package/web-dist/assets/{sankeyDiagram-WA2Y5GQK-CmcKKwux.js → sankeyDiagram-WA2Y5GQK-BjEbuAaJ.js} +1 -1
- package/web-dist/assets/{sequenceDiagram-2WXFIKYE-BP4ala3N.js → sequenceDiagram-2WXFIKYE-CjuDrNgv.js} +1 -1
- package/web-dist/assets/{stateDiagram-RAJIS63D-B63qNKRq.js → stateDiagram-RAJIS63D-VP-67XJS.js} +1 -1
- package/web-dist/assets/stateDiagram-v2-FVOUBMTO-xlGUplYt.js +1 -0
- package/web-dist/assets/{timeline-definition-YZTLITO2-yTT3dubh.js → timeline-definition-YZTLITO2-BYBaBs79.js} +1 -1
- package/web-dist/assets/{treemap-KZPCXAKY-H02GTJ0N.js → treemap-KZPCXAKY-Cql-h4_N.js} +1 -1
- package/web-dist/assets/{vennDiagram-LZ73GAT5-CZnlhpxS.js → vennDiagram-LZ73GAT5-C6IaEUPA.js} +1 -1
- package/web-dist/assets/{xychartDiagram-JWTSCODW-1mVqxtm_.js → xychartDiagram-JWTSCODW-D9VOxnGG.js} +1 -1
- package/web-dist/index.html +1 -1
- package/web-dist/assets/channel-BNGi7anu.js +0 -1
- package/web-dist/assets/classDiagram-VBA2DB6C-BwNok8Om.js +0 -1
- package/web-dist/assets/classDiagram-v2-RAHNMMFH-BwNok8Om.js +0 -1
- package/web-dist/assets/clone-4s7G4Vo3.js +0 -1
- package/web-dist/assets/stateDiagram-v2-FVOUBMTO-CsdHGXj3.js +0 -1
package/dist/routes/network.js
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
66
|
+
args.push(`${input.username}@${input.ip}`, input.command);
|
|
67
|
+
return args;
|
|
42
68
|
}
|
|
43
|
-
export function
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
"
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"",
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
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
|
-
"
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
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
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
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
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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" });
|