@artenx/dshb 0.0.5 → 0.1.1
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/README.md +18 -0
- package/cordis.patch.yml +9 -9
- package/lib/auth/credentials.d.ts +30 -0
- package/lib/auth/credentials.js +121 -0
- package/lib/auth/credentials.js.map +1 -0
- package/lib/auth/decorate-webserver.d.ts +7 -0
- package/lib/auth/decorate-webserver.js +75 -0
- package/lib/auth/decorate-webserver.js.map +1 -0
- package/lib/auth/gate.d.ts +14 -0
- package/lib/auth/gate.js +45 -0
- package/lib/auth/gate.js.map +1 -0
- package/lib/auth/index.d.ts +8 -0
- package/lib/auth/index.js +51 -0
- package/lib/auth/index.js.map +1 -0
- package/lib/auth/login-page.d.ts +5 -0
- package/lib/auth/login-page.js +87 -0
- package/lib/auth/login-page.js.map +1 -0
- package/lib/auth/loopback-compat.d.ts +6 -0
- package/lib/auth/loopback-compat.js +94 -0
- package/lib/auth/loopback-compat.js.map +1 -0
- package/lib/auth/loopback.d.ts +7 -0
- package/lib/auth/loopback.js +24 -0
- package/lib/auth/loopback.js.map +1 -0
- package/lib/auth/ratelimit.d.ts +11 -0
- package/lib/auth/ratelimit.js +37 -0
- package/lib/auth/ratelimit.js.map +1 -0
- package/lib/auth/routes.d.ts +8 -0
- package/lib/auth/routes.js +224 -0
- package/lib/auth/routes.js.map +1 -0
- package/lib/auth/session.d.ts +11 -0
- package/lib/auth/session.js +52 -0
- package/lib/auth/session.js.map +1 -0
- package/lib/auth/startup.d.ts +15 -0
- package/lib/auth/startup.js +68 -0
- package/lib/auth/startup.js.map +1 -0
- package/lib/auth/webserver.d.ts +18 -0
- package/lib/auth/webserver.js +60 -0
- package/lib/auth/webserver.js.map +1 -0
- package/lib/client.js +2770 -0
- package/lib/client.js.map +1 -0
- package/lib/connection-sIX25pkS.d.ts +46 -0
- package/lib/core/audit.d.ts +16 -0
- package/lib/core/audit.js +38 -0
- package/lib/core/audit.js.map +1 -0
- package/lib/core/index.d.ts +8 -0
- package/lib/core/index.js +77 -0
- package/lib/core/index.js.map +1 -0
- package/lib/core/known-hosts.d.ts +23 -0
- package/lib/core/known-hosts.js +76 -0
- package/lib/core/known-hosts.js.map +1 -0
- package/lib/core/node-registry.d.ts +108 -0
- package/lib/core/node-registry.js +191 -0
- package/lib/core/node-registry.js.map +1 -0
- package/lib/core/routes.d.ts +9 -0
- package/lib/core/routes.js +404 -0
- package/lib/core/routes.js.map +1 -0
- package/lib/core/ssh-config.d.ts +23 -0
- package/lib/core/ssh-config.js +70 -0
- package/lib/core/ssh-config.js.map +1 -0
- package/lib/core/test.d.ts +43 -0
- package/lib/core/test.js +116 -0
- package/lib/core/test.js.map +1 -0
- package/lib/core/workspace-bindings.d.ts +28 -0
- package/lib/core/workspace-bindings.js +86 -0
- package/lib/core/workspace-bindings.js.map +1 -0
- package/lib/core/workspaces.d.ts +9 -0
- package/lib/core/workspaces.js +283 -0
- package/lib/core/workspaces.js.map +1 -0
- package/lib/exec-docker/daemon-runner.d.ts +21 -0
- package/lib/exec-docker/daemon-runner.js +326 -0
- package/lib/exec-docker/daemon-runner.js.map +1 -0
- package/lib/exec-docker/docker-backend.d.ts +48 -0
- package/lib/exec-docker/docker-backend.js +1 -0
- package/lib/exec-docker/docker-client.d.ts +46 -0
- package/lib/exec-docker/docker-client.js +302 -0
- package/lib/exec-docker/docker-client.js.map +1 -0
- package/lib/exec-docker/docker-routes.d.ts +6 -0
- package/lib/exec-docker/docker-routes.js +215 -0
- package/lib/exec-docker/docker-routes.js.map +1 -0
- package/lib/exec-docker/fs-provider.d.ts +50 -0
- package/lib/exec-docker/fs-provider.js +92 -0
- package/lib/exec-docker/fs-provider.js.map +1 -0
- package/lib/exec-docker/host-runner.d.ts +29 -0
- package/lib/exec-docker/host-runner.js +17 -0
- package/lib/exec-docker/host-runner.js.map +1 -0
- package/lib/exec-docker/index.d.ts +21 -0
- package/lib/exec-docker/index.js +137 -0
- package/lib/exec-docker/index.js.map +1 -0
- package/lib/exec-docker/process-provider.d.ts +20 -0
- package/lib/exec-docker/process-provider.js +75 -0
- package/lib/exec-docker/process-provider.js.map +1 -0
- package/lib/exec-docker/provision.d.ts +24 -0
- package/lib/exec-docker/provision.js +103 -0
- package/lib/exec-docker/provision.js.map +1 -0
- package/lib/exec-docker/remote-docker-cli.d.ts +52 -0
- package/lib/exec-docker/remote-docker-cli.js +155 -0
- package/lib/exec-docker/remote-docker-cli.js.map +1 -0
- package/lib/exec-docker/world.d.ts +37 -0
- package/lib/exec-docker/world.js +62 -0
- package/lib/exec-docker/world.js.map +1 -0
- package/lib/exec-ssh/connection-pool.d.ts +25 -0
- package/lib/exec-ssh/connection-pool.js +54 -0
- package/lib/exec-ssh/connection-pool.js.map +1 -0
- package/lib/exec-ssh/connection.d.ts +2 -0
- package/lib/exec-ssh/connection.js +125 -0
- package/lib/exec-ssh/connection.js.map +1 -0
- package/lib/exec-ssh/env-scrub.d.ts +7 -0
- package/lib/exec-ssh/env-scrub.js +40 -0
- package/lib/exec-ssh/env-scrub.js.map +1 -0
- package/lib/exec-ssh/executor.d.ts +46 -0
- package/lib/exec-ssh/executor.js +212 -0
- package/lib/exec-ssh/executor.js.map +1 -0
- package/lib/exec-ssh/fs-provider.d.ts +50 -0
- package/lib/exec-ssh/fs-provider.js +92 -0
- package/lib/exec-ssh/fs-provider.js.map +1 -0
- package/lib/exec-ssh/index.d.ts +16 -0
- package/lib/exec-ssh/index.js +30 -0
- package/lib/exec-ssh/index.js.map +1 -0
- package/lib/exec-ssh/process-provider.d.ts +20 -0
- package/lib/exec-ssh/process-provider.js +75 -0
- package/lib/exec-ssh/process-provider.js.map +1 -0
- package/lib/exec-ssh/runner.d.ts +13 -0
- package/lib/exec-ssh/runner.js +62 -0
- package/lib/exec-ssh/runner.js.map +1 -0
- package/lib/exec-ssh/world.d.ts +79 -0
- package/lib/exec-ssh/world.js +95 -0
- package/lib/exec-ssh/world.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/router/decorate-fs.d.ts +6 -0
- package/lib/router/decorate-fs.js +78 -0
- package/lib/router/decorate-fs.js.map +1 -0
- package/lib/router/decorate-shell.d.ts +6 -0
- package/lib/router/decorate-shell.js +35 -0
- package/lib/router/decorate-shell.js.map +1 -0
- package/lib/router/decorate-subprocess.d.ts +6 -0
- package/lib/router/decorate-subprocess.js +35 -0
- package/lib/router/decorate-subprocess.js.map +1 -0
- package/lib/router/index.d.ts +10 -0
- package/lib/router/index.js +21 -0
- package/lib/router/index.js.map +1 -0
- package/lib/router/resolve.d.ts +22 -0
- package/lib/router/resolve.js +45 -0
- package/lib/router/resolve.js.map +1 -0
- package/lib/router/router-fs.d.ts +25 -0
- package/lib/router/router-fs.js +64 -0
- package/lib/router/router-fs.js.map +1 -0
- package/lib/router/router-shell.d.ts +14 -0
- package/lib/router/router-shell.js +36 -0
- package/lib/router/router-shell.js.map +1 -0
- package/lib/router/router-subprocess.d.ts +13 -0
- package/lib/router/router-subprocess.js +33 -0
- package/lib/router/router-subprocess.js.map +1 -0
- package/lib/router/types.d.ts +2 -0
- package/lib/router/types.js +1 -0
- package/lib/types-C0TtEO7z.d.ts +67 -0
- package/lib/ui/decorate-picker.d.ts +6 -0
- package/lib/ui/decorate-picker.js +58 -0
- package/lib/ui/decorate-picker.js.map +1 -0
- package/lib/ui/index.d.ts +8 -0
- package/lib/ui/index.js +11 -0
- package/lib/ui/index.js.map +1 -0
- package/lib/ui/picker.d.ts +10 -0
- package/lib/ui/picker.js +58 -0
- package/lib/ui/picker.js.map +1 -0
- package/package.json +146 -13
- package/LICENSE +0 -21
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { readSshConfig, resolveSshConfigEntry } from "./ssh-config.js";
|
|
2
|
+
import { testNode } from "./test.js";
|
|
3
|
+
import { mkdirSync, readdirSync } from "node:fs";
|
|
4
|
+
//#region src/core/routes.ts
|
|
5
|
+
function resolveWorlds(ctx, nodeType) {
|
|
6
|
+
if (nodeType === "local-docker" || nodeType === "remote-docker") return ctx.get("dshbDockerWorldsGeneric");
|
|
7
|
+
return ctx.get("dshbWorlds");
|
|
8
|
+
}
|
|
9
|
+
const MAX_BODY = 65536;
|
|
10
|
+
function sendJson(res, status, body, headers = {}) {
|
|
11
|
+
res.writeHead(status, {
|
|
12
|
+
"content-type": "application/json",
|
|
13
|
+
...headers
|
|
14
|
+
});
|
|
15
|
+
res.end(JSON.stringify(body));
|
|
16
|
+
}
|
|
17
|
+
function readBody(req) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
if ((req.headers["content-type"] ?? "") !== "application/json") {
|
|
20
|
+
resolve(null);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let size = 0;
|
|
25
|
+
req.on("data", (chunk) => {
|
|
26
|
+
size += chunk.length;
|
|
27
|
+
if (size > MAX_BODY) {
|
|
28
|
+
resolve(null);
|
|
29
|
+
req.destroy();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
chunks.push(chunk);
|
|
33
|
+
});
|
|
34
|
+
req.on("end", () => {
|
|
35
|
+
try {
|
|
36
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
37
|
+
} catch {
|
|
38
|
+
resolve(null);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
req.on("error", () => resolve(null));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function pathSegments(req) {
|
|
45
|
+
return new URL(req.url ?? "/", "http://x").pathname.split("/").filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
const VALID_TYPES = [
|
|
48
|
+
"local-host",
|
|
49
|
+
"local-docker",
|
|
50
|
+
"remote-ssh",
|
|
51
|
+
"remote-docker"
|
|
52
|
+
];
|
|
53
|
+
function maybeProvision(ctx, node) {
|
|
54
|
+
if (node.type !== "local-docker" && node.type !== "remote-docker") return false;
|
|
55
|
+
if (!node.docker?.image) return false;
|
|
56
|
+
const provisioner = ctx.get("dshbDockerProvisioner");
|
|
57
|
+
if (!provisioner) return false;
|
|
58
|
+
provisioner.provision(node.id).catch(() => {});
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
async function registerNodeRoutes(ctx, registry) {
|
|
62
|
+
const webServer = ctx.webServer;
|
|
63
|
+
ctx.effect(() => webServer.register({
|
|
64
|
+
kind: "prefix",
|
|
65
|
+
path: "/api/dshb/nodes",
|
|
66
|
+
handler: async (req, res) => {
|
|
67
|
+
const segs = pathSegments(req);
|
|
68
|
+
const resourceId = segs[3];
|
|
69
|
+
if (req.method === "GET" && segs[4] === "browse" && resourceId) {
|
|
70
|
+
const node = registry.get(resourceId);
|
|
71
|
+
if (!node) {
|
|
72
|
+
sendJson(res, 404, {
|
|
73
|
+
ok: false,
|
|
74
|
+
error: "node not found"
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const path = new URL(req.url ?? "/", "http://x").searchParams.get("path") ?? (node.type === "local-host" ? process.env.HOME ?? "/" : "/");
|
|
79
|
+
try {
|
|
80
|
+
if (node.type === "local-host") {
|
|
81
|
+
sendJson(res, 200, {
|
|
82
|
+
ok: true,
|
|
83
|
+
path,
|
|
84
|
+
entries: readdirSync(path, { withFileTypes: true }).map((e) => ({
|
|
85
|
+
name: e.name,
|
|
86
|
+
path: `${path.replace(/\/$/, "")}/${e.name}`,
|
|
87
|
+
isDirectory: e.isDirectory(),
|
|
88
|
+
isFile: e.isFile()
|
|
89
|
+
}))
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const worlds = resolveWorlds(ctx, node.type);
|
|
94
|
+
if (!worlds) {
|
|
95
|
+
sendJson(res, 503, {
|
|
96
|
+
ok: false,
|
|
97
|
+
error: "远程执行世界不可用"
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const world = await worlds.ensure(resourceId);
|
|
102
|
+
if (!world) {
|
|
103
|
+
sendJson(res, 503, {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: "节点执行世界未就绪"
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
sendJson(res, 200, {
|
|
110
|
+
ok: true,
|
|
111
|
+
path,
|
|
112
|
+
entries: await world.fs.listDir(path)
|
|
113
|
+
});
|
|
114
|
+
} catch (err) {
|
|
115
|
+
sendJson(res, 502, {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: err instanceof Error ? err.message : String(err)
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (req.method === "POST" && segs[4] === "mkdir" && resourceId) {
|
|
123
|
+
const node = registry.get(resourceId);
|
|
124
|
+
if (!node) {
|
|
125
|
+
sendJson(res, 404, {
|
|
126
|
+
ok: false,
|
|
127
|
+
error: "node not found"
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const body = await readBody(req);
|
|
132
|
+
const path = body && typeof body.path === "string" ? body.path : void 0;
|
|
133
|
+
if (!path) {
|
|
134
|
+
sendJson(res, 400, {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: "需要 path"
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
if (node.type === "local-host") {
|
|
142
|
+
mkdirSync(path, { recursive: true });
|
|
143
|
+
sendJson(res, 200, {
|
|
144
|
+
ok: true,
|
|
145
|
+
path
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const worlds = resolveWorlds(ctx, node.type);
|
|
150
|
+
if (!worlds) {
|
|
151
|
+
sendJson(res, 503, {
|
|
152
|
+
ok: false,
|
|
153
|
+
error: "远程执行世界不可用"
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const world = await worlds.ensure(resourceId);
|
|
158
|
+
if (!world) {
|
|
159
|
+
sendJson(res, 503, {
|
|
160
|
+
ok: false,
|
|
161
|
+
error: "节点执行世界未就绪"
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
await world.ensureDir(path);
|
|
166
|
+
sendJson(res, 200, {
|
|
167
|
+
ok: true,
|
|
168
|
+
path
|
|
169
|
+
});
|
|
170
|
+
} catch (err) {
|
|
171
|
+
sendJson(res, 502, {
|
|
172
|
+
ok: false,
|
|
173
|
+
error: err instanceof Error ? err.message : String(err)
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (req.method === "GET" && resourceId === void 0) {
|
|
179
|
+
const provisioner = ctx.get("dshbDockerProvisioner");
|
|
180
|
+
const nodes = registry.list();
|
|
181
|
+
sendJson(res, 200, {
|
|
182
|
+
ok: true,
|
|
183
|
+
nodes: await Promise.all(nodes.map(async (n) => ({
|
|
184
|
+
...n,
|
|
185
|
+
hasSecret: await registry.hasSecret(n.id),
|
|
186
|
+
status: registry.status(n.id),
|
|
187
|
+
provision: provisioner?.status(n.id)
|
|
188
|
+
})))
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (req.method === "POST" && resourceId === void 0) {
|
|
193
|
+
const body = await readBody(req);
|
|
194
|
+
if (!body) {
|
|
195
|
+
sendJson(res, 400, {
|
|
196
|
+
ok: false,
|
|
197
|
+
error: "请求体必须是 JSON"
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const type = String(body.type ?? "");
|
|
202
|
+
if (!VALID_TYPES.includes(type)) {
|
|
203
|
+
sendJson(res, 400, {
|
|
204
|
+
ok: false,
|
|
205
|
+
error: "type 必须是 local-host/local-docker/remote-ssh/remote-docker"
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const node = await registry.create({
|
|
211
|
+
name: String(body.name ?? ""),
|
|
212
|
+
type,
|
|
213
|
+
ssh: body.ssh,
|
|
214
|
+
docker: body.docker,
|
|
215
|
+
secrets: body.secrets
|
|
216
|
+
});
|
|
217
|
+
const provision = maybeProvision(ctx, node);
|
|
218
|
+
sendJson(res, 201, {
|
|
219
|
+
ok: true,
|
|
220
|
+
node: {
|
|
221
|
+
...node,
|
|
222
|
+
hasSecret: await registry.hasSecret(node.id)
|
|
223
|
+
},
|
|
224
|
+
provisioning: provision
|
|
225
|
+
});
|
|
226
|
+
} catch (err) {
|
|
227
|
+
sendJson(res, 409, {
|
|
228
|
+
ok: false,
|
|
229
|
+
error: err instanceof Error ? err.message : String(err)
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (resourceId === void 0) {
|
|
235
|
+
sendJson(res, 405, {
|
|
236
|
+
ok: false,
|
|
237
|
+
error: "method not allowed"
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (req.method === "GET" && segs[4] === void 0) {
|
|
242
|
+
const node = registry.get(resourceId);
|
|
243
|
+
if (!node) {
|
|
244
|
+
sendJson(res, 404, {
|
|
245
|
+
ok: false,
|
|
246
|
+
error: "node not found"
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
sendJson(res, 200, {
|
|
251
|
+
ok: true,
|
|
252
|
+
node: {
|
|
253
|
+
...node,
|
|
254
|
+
hasSecret: await registry.hasSecret(node.id),
|
|
255
|
+
status: registry.status(resourceId)
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (req.method === "PATCH" && segs[4] === void 0) {
|
|
261
|
+
const body = await readBody(req);
|
|
262
|
+
if (!body) {
|
|
263
|
+
sendJson(res, 400, {
|
|
264
|
+
ok: false,
|
|
265
|
+
error: "请求体必须是 JSON"
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const node = await registry.update(resourceId, {
|
|
271
|
+
name: body.name,
|
|
272
|
+
ssh: body.ssh,
|
|
273
|
+
docker: body.docker,
|
|
274
|
+
secrets: body.secrets
|
|
275
|
+
});
|
|
276
|
+
const provision = maybeProvision(ctx, node);
|
|
277
|
+
sendJson(res, 200, {
|
|
278
|
+
ok: true,
|
|
279
|
+
node: {
|
|
280
|
+
...node,
|
|
281
|
+
hasSecret: await registry.hasSecret(node.id)
|
|
282
|
+
},
|
|
283
|
+
provisioning: provision
|
|
284
|
+
});
|
|
285
|
+
} catch (err) {
|
|
286
|
+
sendJson(res, 404, {
|
|
287
|
+
ok: false,
|
|
288
|
+
error: err instanceof Error ? err.message : String(err)
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (req.method === "DELETE" && segs[4] === void 0) {
|
|
294
|
+
await registry.remove(resourceId);
|
|
295
|
+
sendJson(res, 200, { ok: true });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (req.method === "POST" && resourceId === "test-unsaved") {
|
|
299
|
+
const body = await readBody(req);
|
|
300
|
+
if (!body || !body.ssh) {
|
|
301
|
+
sendJson(res, 400, {
|
|
302
|
+
ok: false,
|
|
303
|
+
error: "需要 ssh 配置"
|
|
304
|
+
});
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const sshTester = ctx.get("dshbSshPool");
|
|
308
|
+
if (!sshTester) {
|
|
309
|
+
sendJson(res, 503, {
|
|
310
|
+
ok: false,
|
|
311
|
+
error: "SSH 执行通道不可用"
|
|
312
|
+
});
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const ssh = body.ssh;
|
|
316
|
+
const secrets = body.secrets ?? {};
|
|
317
|
+
const t = await sshTester.test({
|
|
318
|
+
host: ssh.host,
|
|
319
|
+
port: Number(ssh.port) || 22,
|
|
320
|
+
username: ssh.username,
|
|
321
|
+
auth: ssh.auth,
|
|
322
|
+
jump: ssh.jump
|
|
323
|
+
}, secrets);
|
|
324
|
+
sendJson(res, 200, {
|
|
325
|
+
ok: true,
|
|
326
|
+
report: {
|
|
327
|
+
ok: t.ok,
|
|
328
|
+
reachable: t.ok,
|
|
329
|
+
error: t.error,
|
|
330
|
+
...t.category ? { category: t.category } : {}
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (req.method === "POST" && segs[4] === "test") {
|
|
336
|
+
const body = await readBody(req);
|
|
337
|
+
const sshTester = ctx.get("dshbSshPool");
|
|
338
|
+
const dockerTester = ctx.get("dshbDockerTester");
|
|
339
|
+
const overrides = body && (body.ssh || body.secrets) ? {
|
|
340
|
+
ssh: body.ssh,
|
|
341
|
+
secrets: body.secrets
|
|
342
|
+
} : void 0;
|
|
343
|
+
sendJson(res, 200, {
|
|
344
|
+
ok: true,
|
|
345
|
+
report: await testNode(registry, resourceId, sshTester, overrides, dockerTester)
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
sendJson(res, 404, {
|
|
350
|
+
ok: false,
|
|
351
|
+
error: "not found"
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
async function registerSshConfigRoutes(ctx, knownHosts) {
|
|
357
|
+
const webServer = ctx.webServer;
|
|
358
|
+
ctx.effect(() => webServer.register({
|
|
359
|
+
kind: "exact",
|
|
360
|
+
path: "/api/dshb/ssh-config",
|
|
361
|
+
handler: (_req, res) => {
|
|
362
|
+
sendJson(res, 200, {
|
|
363
|
+
ok: true,
|
|
364
|
+
entries: readSshConfig().map(resolveSshConfigEntry)
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}));
|
|
368
|
+
ctx.effect(() => webServer.register({
|
|
369
|
+
kind: "prefix",
|
|
370
|
+
path: "/api/dshb/known-hosts",
|
|
371
|
+
handler: async (req, res) => {
|
|
372
|
+
const action = pathSegments(req)[3];
|
|
373
|
+
if (req.method === "GET" && action === void 0) {
|
|
374
|
+
sendJson(res, 200, {
|
|
375
|
+
ok: true,
|
|
376
|
+
hosts: knownHosts.list()
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (req.method === "POST" && action === "forget") {
|
|
381
|
+
const body = await readBody(req);
|
|
382
|
+
if (!body || typeof body.host !== "string") {
|
|
383
|
+
sendJson(res, 400, {
|
|
384
|
+
ok: false,
|
|
385
|
+
error: "需要 host"
|
|
386
|
+
});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const port = Number(body.port ?? 22);
|
|
390
|
+
knownHosts.forget(String(body.host), port);
|
|
391
|
+
sendJson(res, 200, { ok: true });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
sendJson(res, 404, {
|
|
395
|
+
ok: false,
|
|
396
|
+
error: "not found"
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
//#endregion
|
|
402
|
+
export { registerNodeRoutes, registerSshConfigRoutes };
|
|
403
|
+
|
|
404
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","names":[],"sources":["../../src/core/routes.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { mkdirSync, readdirSync } from 'node:fs'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { NodeRegistry, type NodeProfile, type NodeType } from './node-registry.js'\nimport { readSshConfig, resolveSshConfigEntry } from './ssh-config.js'\nimport type { KnownHostsStore } from './known-hosts.js'\nimport { testNode, type SshHandshakeTester, type DockerNodeTester } from './test.js'\n\ninterface WorldsLike {\n ensure(nodeId: string): Promise<{\n ensureDir(path: string): Promise<void>\n fs: { listDir(path: string): Promise<Array<{ name: string; path: string; isDirectory: boolean; isFile: boolean }>> }\n } | undefined>\n}\n\nfunction resolveWorlds(ctx: Context, nodeType: string): WorldsLike | undefined {\n if (nodeType === 'local-docker' || nodeType === 'remote-docker') {\n return ctx.get('dshbDockerWorldsGeneric') as WorldsLike | undefined\n }\n return ctx.get('dshbWorlds') as WorldsLike | undefined\n}\n\nconst MAX_BODY = 64 * 1024\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown, headers: Record<string, string> = {}): void {\n res.writeHead(status, { 'content-type': 'application/json', ...headers })\n res.end(JSON.stringify(body))\n}\n\nfunction readBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n return new Promise((resolve) => {\n if ((req.headers['content-type'] ?? '') !== 'application/json') {\n resolve(null)\n return\n }\n const chunks: Buffer[] = []\n let size = 0\n req.on('data', (chunk: Buffer) => {\n size += chunk.length\n if (size > MAX_BODY) {\n resolve(null)\n req.destroy()\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>)\n } catch {\n resolve(null)\n }\n })\n req.on('error', () => resolve(null))\n })\n}\n\nfunction pathSegments(req: IncomingMessage): string[] {\n const path = new URL(req.url ?? '/', 'http://x').pathname\n return path.split('/').filter(Boolean)\n}\n\nconst VALID_TYPES: NodeType[] = ['local-host', 'local-docker', 'remote-ssh', 'remote-docker']\n\ninterface DockerProvisionerLike {\n provision(nodeId: string): Promise<unknown>\n status(nodeId: string): { state: string; error?: string; containerId?: string; updatedAt: string } | undefined\n}\n\nfunction maybeProvision(ctx: Context, node: NodeProfile): boolean {\n if (node.type !== 'local-docker' && node.type !== 'remote-docker') return false\n if (!node.docker?.image) return false\n const provisioner = ctx.get('dshbDockerProvisioner') as DockerProvisionerLike | undefined\n if (!provisioner) return false\n void provisioner.provision(node.id).catch(() => {})\n return true\n}\n\nexport async function registerNodeRoutes(ctx: Context, registry: NodeRegistry): Promise<void> {\n const webServer = ctx.webServer\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/dshb/nodes',\n handler: async (req, res) => {\n const segs = pathSegments(req)\n const resourceId = segs[3]\n\n if (req.method === 'GET' && segs[4] === 'browse' && resourceId) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n const path = new URL(req.url ?? '/', 'http://x').searchParams.get('path') ?? (node.type === 'local-host' ? process.env.HOME ?? '/' : '/')\n try {\n if (node.type === 'local-host') {\n const entries = readdirSync(path, { withFileTypes: true }).map((e) => ({\n name: e.name,\n path: `${path.replace(/\\/$/, '')}/${e.name}`,\n isDirectory: e.isDirectory(),\n isFile: e.isFile(),\n }))\n sendJson(res, 200, { ok: true, path, entries })\n return\n }\n const worlds = resolveWorlds(ctx, node.type)\n if (!worlds) {\n sendJson(res, 503, { ok: false, error: '远程执行世界不可用' })\n return\n }\n const world = await worlds.ensure(resourceId)\n if (!world) {\n sendJson(res, 503, { ok: false, error: '节点执行世界未就绪' })\n return\n }\n const entries = await world.fs.listDir(path)\n sendJson(res, 200, { ok: true, path, entries })\n } catch (err) {\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'POST' && segs[4] === 'mkdir' && resourceId) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n const body = await readBody(req)\n const path = body && typeof body.path === 'string' ? body.path : undefined\n if (!path) {\n sendJson(res, 400, { ok: false, error: '需要 path' })\n return\n }\n try {\n if (node.type === 'local-host') {\n mkdirSync(path, { recursive: true })\n sendJson(res, 200, { ok: true, path })\n return\n }\n const worlds = resolveWorlds(ctx, node.type)\n if (!worlds) {\n sendJson(res, 503, { ok: false, error: '远程执行世界不可用' })\n return\n }\n const world = await worlds.ensure(resourceId)\n if (!world) {\n sendJson(res, 503, { ok: false, error: '节点执行世界未就绪' })\n return\n }\n await world.ensureDir(path)\n sendJson(res, 200, { ok: true, path })\n } catch (err) {\n sendJson(res, 502, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'GET' && resourceId === undefined) {\n const provisioner = ctx.get('dshbDockerProvisioner') as DockerProvisionerLike | undefined\n const nodes = registry.list()\n const withFlags = await Promise.all(\n nodes.map(async (n) => ({\n ...n,\n hasSecret: await registry.hasSecret(n.id),\n status: registry.status(n.id),\n provision: provisioner?.status(n.id),\n })),\n )\n sendJson(res, 200, { ok: true, nodes: withFlags })\n return\n }\n\n if (req.method === 'POST' && resourceId === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: '请求体必须是 JSON' })\n return\n }\n const type = String(body.type ?? '') as NodeType\n if (!VALID_TYPES.includes(type)) {\n sendJson(res, 400, { ok: false, error: 'type 必须是 local-host/local-docker/remote-ssh/remote-docker' })\n return\n }\n try {\n const node: NodeProfile = await registry.create({\n name: String(body.name ?? ''),\n type,\n ssh: body.ssh as never,\n docker: body.docker as never,\n secrets: body.secrets as never,\n })\n const provision = maybeProvision(ctx, node)\n sendJson(res, 201, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id) }, provisioning: provision })\n } catch (err) {\n sendJson(res, 409, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (resourceId === undefined) {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n\n if (req.method === 'GET' && segs[4] === undefined) {\n const node = registry.get(resourceId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n sendJson(res, 200, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id), status: registry.status(resourceId) } })\n return\n }\n\n if (req.method === 'PATCH' && segs[4] === undefined) {\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: '请求体必须是 JSON' })\n return\n }\n try {\n const node = await registry.update(resourceId, {\n name: body.name as string | undefined,\n ssh: body.ssh as never,\n docker: body.docker as never,\n secrets: body.secrets as never,\n })\n const provision = maybeProvision(ctx, node)\n sendJson(res, 200, { ok: true, node: { ...node, hasSecret: await registry.hasSecret(node.id) }, provisioning: provision })\n } catch (err) {\n sendJson(res, 404, { ok: false, error: err instanceof Error ? err.message : String(err) })\n }\n return\n }\n\n if (req.method === 'DELETE' && segs[4] === undefined) {\n await registry.remove(resourceId)\n sendJson(res, 200, { ok: true })\n return\n }\n\n if (req.method === 'POST' && resourceId === 'test-unsaved') {\n const body = await readBody(req)\n if (!body || !body.ssh) {\n sendJson(res, 400, { ok: false, error: '需要 ssh 配置' })\n return\n }\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n if (!sshTester) {\n sendJson(res, 503, { ok: false, error: 'SSH 执行通道不可用' })\n return\n }\n const ssh = body.ssh as { host: string; port: number; username: string; auth: never; jump?: never }\n const secrets = (body.secrets ?? {}) as { password?: string; privateKey?: string; passphrase?: string }\n const t = await sshTester.test(\n { host: ssh.host, port: Number(ssh.port) || 22, username: ssh.username, auth: ssh.auth, jump: ssh.jump },\n secrets,\n )\n sendJson(res, 200, { ok: true, report: { ok: t.ok, reachable: t.ok, error: t.error, ...(t.category ? { category: t.category } : {}) } })\n return\n }\n\n if (req.method === 'POST' && segs[4] === 'test') {\n const body = await readBody(req)\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n const dockerTester = ctx.get('dshbDockerTester') as DockerNodeTester | undefined\n const overrides = body && (body.ssh || body.secrets)\n ? { ssh: body.ssh as never, secrets: body.secrets as never }\n : undefined\n const report = await testNode(registry, resourceId, sshTester, overrides, dockerTester)\n sendJson(res, 200, { ok: true, report })\n return\n }\n\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n\nexport async function registerSshConfigRoutes(ctx: Context, knownHosts: KnownHostsStore): Promise<void> {\n const webServer = ctx.webServer\n\n ctx.effect(() =>\n webServer.register({\n kind: 'exact',\n path: '/api/dshb/ssh-config',\n handler: (_req, res) => {\n const entries = readSshConfig().map(resolveSshConfigEntry)\n sendJson(res, 200, { ok: true, entries })\n },\n }),\n )\n\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/dshb/known-hosts',\n handler: async (req, res) => {\n const segs = pathSegments(req)\n const action = segs[3]\n\n if (req.method === 'GET' && action === undefined) {\n sendJson(res, 200, { ok: true, hosts: knownHosts.list() })\n return\n }\n\n if (req.method === 'POST' && action === 'forget') {\n const body = await readBody(req)\n if (!body || typeof body.host !== 'string') {\n sendJson(res, 400, { ok: false, error: '需要 host' })\n return\n }\n const port = Number(body.port ?? 22)\n knownHosts.forget(String(body.host), port)\n sendJson(res, 200, { ok: true })\n return\n }\n\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/core/ssh-config.d.ts
|
|
2
|
+
interface SshConfigEntry {
|
|
3
|
+
alias: string;
|
|
4
|
+
host?: string;
|
|
5
|
+
port?: number;
|
|
6
|
+
username?: string;
|
|
7
|
+
identityFile?: string;
|
|
8
|
+
proxyJump?: string;
|
|
9
|
+
}
|
|
10
|
+
declare function parseSshConfig(content: string): SshConfigEntry[];
|
|
11
|
+
declare function readSshConfig(): SshConfigEntry[];
|
|
12
|
+
declare function resolveSshConfigEntry(entry: SshConfigEntry): {
|
|
13
|
+
host: string;
|
|
14
|
+
port: number;
|
|
15
|
+
username: string;
|
|
16
|
+
identityFile?: string;
|
|
17
|
+
proxyJump?: string;
|
|
18
|
+
hasProxyJump: boolean;
|
|
19
|
+
hasIdentityFile: boolean;
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { SshConfigEntry, parseSshConfig, readSshConfig, resolveSshConfigEntry };
|
|
23
|
+
//# sourceMappingURL=ssh-config.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
//#region src/core/ssh-config.ts
|
|
5
|
+
function sshConfigPath() {
|
|
6
|
+
return process.env.DSH_REMOTE_SSH_CONFIG ?? join(homedir(), ".ssh", "config");
|
|
7
|
+
}
|
|
8
|
+
function parsePort(value, fallback) {
|
|
9
|
+
if (value === void 0) return fallback;
|
|
10
|
+
const n = Number(value);
|
|
11
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
12
|
+
}
|
|
13
|
+
function parseSshConfig(content) {
|
|
14
|
+
const entries = [];
|
|
15
|
+
const lines = content.split(/\r?\n/);
|
|
16
|
+
let current = null;
|
|
17
|
+
for (const raw of lines) {
|
|
18
|
+
let line = raw.trim();
|
|
19
|
+
if (!line || line.startsWith("#")) continue;
|
|
20
|
+
const spaceIdx = line.search(/\s+/);
|
|
21
|
+
if (spaceIdx < 0) continue;
|
|
22
|
+
const keyword = line.slice(0, spaceIdx).toLowerCase();
|
|
23
|
+
const value = line.slice(spaceIdx).trim();
|
|
24
|
+
if (keyword === "host") {
|
|
25
|
+
if (current) entries.push(current);
|
|
26
|
+
current = { alias: value };
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!current) continue;
|
|
30
|
+
switch (keyword) {
|
|
31
|
+
case "hostname":
|
|
32
|
+
current.host = value;
|
|
33
|
+
break;
|
|
34
|
+
case "port":
|
|
35
|
+
current.port = parsePort(value, 22);
|
|
36
|
+
break;
|
|
37
|
+
case "user":
|
|
38
|
+
current.username = value;
|
|
39
|
+
break;
|
|
40
|
+
case "identityfile":
|
|
41
|
+
current.identityFile = value.replace(/^~/, homedir());
|
|
42
|
+
break;
|
|
43
|
+
case "proxyjump": current.proxyJump = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (current) entries.push(current);
|
|
47
|
+
return entries.filter((e) => !e.alias.includes("*") && !e.alias.includes("?") && !e.alias.includes("!"));
|
|
48
|
+
}
|
|
49
|
+
function readSshConfig() {
|
|
50
|
+
try {
|
|
51
|
+
return parseSshConfig(readFileSync(sshConfigPath(), "utf8"));
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function resolveSshConfigEntry(entry) {
|
|
57
|
+
return {
|
|
58
|
+
host: entry.host ?? entry.alias,
|
|
59
|
+
port: entry.port ?? 22,
|
|
60
|
+
username: entry.username ?? "",
|
|
61
|
+
identityFile: entry.identityFile,
|
|
62
|
+
proxyJump: entry.proxyJump,
|
|
63
|
+
hasProxyJump: Boolean(entry.proxyJump),
|
|
64
|
+
hasIdentityFile: Boolean(entry.identityFile)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { parseSshConfig, readSshConfig, resolveSshConfigEntry };
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=ssh-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh-config.js","names":[],"sources":["../../src/core/ssh-config.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface SshConfigEntry {\n alias: string\n host?: string\n port?: number\n username?: string\n identityFile?: string\n proxyJump?: string\n}\n\nfunction sshConfigPath(): string {\n return process.env.DSH_REMOTE_SSH_CONFIG ?? join(homedir(), '.ssh', 'config')\n}\n\nfunction parsePort(value: string | undefined, fallback: number): number {\n if (value === undefined) return fallback\n const n = Number(value)\n return Number.isInteger(n) && n > 0 ? n : fallback\n}\n\nexport function parseSshConfig(content: string): SshConfigEntry[] {\n const entries: SshConfigEntry[] = []\n const lines = content.split(/\\r?\\n/)\n let current: SshConfigEntry | null = null\n\n for (const raw of lines) {\n let line = raw.trim()\n if (!line || line.startsWith('#')) continue\n const spaceIdx = line.search(/\\s+/)\n if (spaceIdx < 0) continue\n const keyword = line.slice(0, spaceIdx).toLowerCase()\n const value = line.slice(spaceIdx).trim()\n\n if (keyword === 'host') {\n if (current) entries.push(current)\n current = { alias: value }\n continue\n }\n if (!current) continue\n switch (keyword) {\n case 'hostname':\n current.host = value\n break\n case 'port':\n current.port = parsePort(value, 22)\n break\n case 'user':\n current.username = value\n break\n case 'identityfile':\n current.identityFile = value.replace(/^~/, homedir())\n break\n case 'proxyjump':\n current.proxyJump = value\n break\n }\n }\n if (current) entries.push(current)\n return entries.filter((e) => !e.alias.includes('*') && !e.alias.includes('?') && !e.alias.includes('!'))\n}\n\nexport function readSshConfig(): SshConfigEntry[] {\n try {\n return parseSshConfig(readFileSync(sshConfigPath(), 'utf8'))\n } catch {\n return []\n }\n}\n\nexport function resolveSshConfigEntry(entry: SshConfigEntry): {\n host: string\n port: number\n username: string\n identityFile?: string\n proxyJump?: string\n hasProxyJump: boolean\n hasIdentityFile: boolean\n} {\n return {\n host: entry.host ?? entry.alias,\n port: entry.port ?? 22,\n username: entry.username ?? '',\n identityFile: entry.identityFile,\n proxyJump: entry.proxyJump,\n hasProxyJump: Boolean(entry.proxyJump),\n hasIdentityFile: Boolean(entry.identityFile),\n }\n}\n"],"mappings":";;;;AAaA,SAAS,gBAAwB;CAC/B,OAAO,QAAQ,IAAI,yBAAyB,KAAK,QAAQ,GAAG,QAAQ,QAAQ;AAC9E;AAEA,SAAS,UAAU,OAA2B,UAA0B;CACtE,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,MAAM,IAAI,OAAO,KAAK;CACtB,OAAO,OAAO,UAAU,CAAC,KAAK,IAAI,IAAI,IAAI;AAC5C;AAEA,SAAgB,eAAe,SAAmC;CAChE,MAAM,UAA4B,CAAC;CACnC,MAAM,QAAQ,QAAQ,MAAM,OAAO;CACnC,IAAI,UAAiC;CAErC,KAAK,MAAM,OAAO,OAAO;EACvB,IAAI,OAAO,IAAI,KAAK;EACpB,IAAI,CAAC,QAAQ,KAAK,WAAW,GAAG,GAAG;EACnC,MAAM,WAAW,KAAK,OAAO,KAAK;EAClC,IAAI,WAAW,GAAG;EAClB,MAAM,UAAU,KAAK,MAAM,GAAG,QAAQ,CAAC,CAAC,YAAY;EACpD,MAAM,QAAQ,KAAK,MAAM,QAAQ,CAAC,CAAC,KAAK;EAExC,IAAI,YAAY,QAAQ;GACtB,IAAI,SAAS,QAAQ,KAAK,OAAO;GACjC,UAAU,EAAE,OAAO,MAAM;GACzB;EACF;EACA,IAAI,CAAC,SAAS;EACd,QAAQ,SAAR;GACE,KAAK;IACH,QAAQ,OAAO;IACf;GACF,KAAK;IACH,QAAQ,OAAO,UAAU,OAAO,EAAE;IAClC;GACF,KAAK;IACH,QAAQ,WAAW;IACnB;GACF,KAAK;IACH,QAAQ,eAAe,MAAM,QAAQ,MAAM,QAAQ,CAAC;IACpD;GACF,KAAK,aACH,QAAQ,YAAY;EAExB;CACF;CACA,IAAI,SAAS,QAAQ,KAAK,OAAO;CACjC,OAAO,QAAQ,QAAQ,MAAM,CAAC,EAAE,MAAM,SAAS,GAAG,KAAK,CAAC,EAAE,MAAM,SAAS,GAAG,KAAK,CAAC,EAAE,MAAM,SAAS,GAAG,CAAC;AACzG;AAEA,SAAgB,gBAAkC;CAChD,IAAI;EACF,OAAO,eAAe,aAAa,cAAc,GAAG,MAAM,CAAC;CAC7D,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAgB,sBAAsB,OAQpC;CACA,OAAO;EACL,MAAM,MAAM,QAAQ,MAAM;EAC1B,MAAM,MAAM,QAAQ;EACpB,UAAU,MAAM,YAAY;EAC5B,cAAc,MAAM;EACpB,WAAW,MAAM;EACjB,cAAc,QAAQ,MAAM,SAAS;EACrC,iBAAiB,QAAQ,MAAM,YAAY;CAC7C;AACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NodeRegistry, NodeSsh, NodeTestReport } from "./node-registry.js";
|
|
2
|
+
//#region src/core/test.d.ts
|
|
3
|
+
interface SshHandshakeTester {
|
|
4
|
+
test(target: {
|
|
5
|
+
host: string;
|
|
6
|
+
port: number;
|
|
7
|
+
username: string;
|
|
8
|
+
auth: NodeSsh['auth'];
|
|
9
|
+
jump?: NodeSsh['jump'];
|
|
10
|
+
}, secrets: {
|
|
11
|
+
password?: string;
|
|
12
|
+
privateKey?: string;
|
|
13
|
+
passphrase?: string;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
ok: boolean;
|
|
16
|
+
error?: string;
|
|
17
|
+
category?: string;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
declare function testReachability(host: string, port: number, timeoutMs?: number): Promise<NodeTestReport>;
|
|
21
|
+
interface NodeTestOverrides {
|
|
22
|
+
ssh?: NodeSsh;
|
|
23
|
+
secrets?: {
|
|
24
|
+
password?: string;
|
|
25
|
+
privateKey?: string;
|
|
26
|
+
passphrase?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
interface DockerNodeTester {
|
|
30
|
+
test(node: {
|
|
31
|
+
id: string;
|
|
32
|
+
type: string;
|
|
33
|
+
ssh?: NodeSsh;
|
|
34
|
+
docker?: {
|
|
35
|
+
containerId?: string;
|
|
36
|
+
image?: string;
|
|
37
|
+
};
|
|
38
|
+
}): Promise<NodeTestReport>;
|
|
39
|
+
}
|
|
40
|
+
declare function testNode(registry: NodeRegistry, id: string, sshTester?: SshHandshakeTester, overrides?: NodeTestOverrides, dockerTester?: DockerNodeTester): Promise<NodeTestReport>;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { DockerNodeTester, NodeTestOverrides, SshHandshakeTester, testNode, testReachability };
|
|
43
|
+
//# sourceMappingURL=test.d.ts.map
|