dshb-core 0.0.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/LICENSE +21 -0
- package/cordis.patch.yml +3 -0
- package/lib/audit.d.ts +16 -0
- package/lib/audit.js +38 -0
- package/lib/audit.js.map +1 -0
- package/lib/index-CLOUBKxU.d.ts +1125 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +77 -0
- package/lib/index.js.map +1 -0
- package/lib/known-hosts.d.ts +23 -0
- package/lib/known-hosts.js +76 -0
- package/lib/known-hosts.js.map +1 -0
- package/lib/node-registry.d.ts +108 -0
- package/lib/node-registry.js +191 -0
- package/lib/node-registry.js.map +1 -0
- package/lib/routes.d.ts +9 -0
- package/lib/routes.js +404 -0
- package/lib/routes.js.map +1 -0
- package/lib/ssh-config.d.ts +23 -0
- package/lib/ssh-config.js +70 -0
- package/lib/ssh-config.js.map +1 -0
- package/lib/test.d.ts +43 -0
- package/lib/test.js +116 -0
- package/lib/test.js.map +1 -0
- package/lib/workspace-bindings.d.ts +28 -0
- package/lib/workspace-bindings.js +86 -0
- package/lib/workspace-bindings.js.map +1 -0
- package/lib/workspaces.d.ts +9 -0
- package/lib/workspaces.js +283 -0
- package/lib/workspaces.js.map +1 -0
- package/package.json +38 -0
package/lib/routes.js
ADDED
|
@@ -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/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/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/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 { readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
//#region src/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/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"}
|
package/lib/test.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NodeRegistry, NodeSsh, NodeTestReport } from "./node-registry.js";
|
|
2
|
+
//#region src/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
|