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/test.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createConnection } from "node:net";
|
|
2
|
+
//#region src/test.ts
|
|
3
|
+
function testReachability(host, port, timeoutMs = 5e3) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
let settled = false;
|
|
6
|
+
const finish = (report) => {
|
|
7
|
+
if (settled) return;
|
|
8
|
+
settled = true;
|
|
9
|
+
socket.destroy();
|
|
10
|
+
resolve(report);
|
|
11
|
+
};
|
|
12
|
+
const socket = createConnection({
|
|
13
|
+
host,
|
|
14
|
+
port
|
|
15
|
+
});
|
|
16
|
+
socket.setTimeout(timeoutMs);
|
|
17
|
+
socket.once("connect", () => finish({
|
|
18
|
+
ok: true,
|
|
19
|
+
reachable: true
|
|
20
|
+
}));
|
|
21
|
+
socket.once("timeout", () => finish({
|
|
22
|
+
ok: false,
|
|
23
|
+
reachable: false,
|
|
24
|
+
error: "connection timed out"
|
|
25
|
+
}));
|
|
26
|
+
socket.once("error", (err) => finish({
|
|
27
|
+
ok: false,
|
|
28
|
+
reachable: false,
|
|
29
|
+
error: err instanceof Error ? err.message : String(err)
|
|
30
|
+
}));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function testNode(registry, id, sshTester, overrides, dockerTester) {
|
|
34
|
+
const node = registry.get(id);
|
|
35
|
+
if (!node) return {
|
|
36
|
+
ok: false,
|
|
37
|
+
error: `node ${id} not found`
|
|
38
|
+
};
|
|
39
|
+
if (node.type === "local-host") {
|
|
40
|
+
registry.setStatus(id, {
|
|
41
|
+
reachable: true,
|
|
42
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
reachable: true
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (node.type === "local-docker") {
|
|
50
|
+
let report;
|
|
51
|
+
if (dockerTester) report = await dockerTester.test({
|
|
52
|
+
id: node.id,
|
|
53
|
+
type: node.type,
|
|
54
|
+
docker: node.docker
|
|
55
|
+
});
|
|
56
|
+
else report = {
|
|
57
|
+
ok: true,
|
|
58
|
+
reachable: true
|
|
59
|
+
};
|
|
60
|
+
registry.setStatus(id, {
|
|
61
|
+
reachable: report.reachable,
|
|
62
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
63
|
+
error: report.error
|
|
64
|
+
});
|
|
65
|
+
return report;
|
|
66
|
+
}
|
|
67
|
+
if (node.type === "remote-ssh" || node.type === "remote-docker") {
|
|
68
|
+
const ssh = overrides?.ssh ?? node.ssh;
|
|
69
|
+
if (!ssh) return {
|
|
70
|
+
ok: false,
|
|
71
|
+
error: "node has no ssh config"
|
|
72
|
+
};
|
|
73
|
+
let report;
|
|
74
|
+
if (sshTester) {
|
|
75
|
+
const savedSecrets = await registry.getSecrets(id) ?? {};
|
|
76
|
+
const secrets = overrides?.secrets ? {
|
|
77
|
+
password: overrides.secrets.password || savedSecrets.password,
|
|
78
|
+
privateKey: overrides.secrets.privateKey || savedSecrets.privateKey,
|
|
79
|
+
passphrase: overrides.secrets.passphrase || savedSecrets.passphrase
|
|
80
|
+
} : savedSecrets;
|
|
81
|
+
const t = await sshTester.test({
|
|
82
|
+
host: ssh.host,
|
|
83
|
+
port: ssh.port,
|
|
84
|
+
username: ssh.username,
|
|
85
|
+
auth: ssh.auth,
|
|
86
|
+
jump: ssh.jump
|
|
87
|
+
}, secrets);
|
|
88
|
+
report = {
|
|
89
|
+
ok: t.ok,
|
|
90
|
+
reachable: t.ok,
|
|
91
|
+
error: t.error,
|
|
92
|
+
...t.category ? { category: t.category } : {}
|
|
93
|
+
};
|
|
94
|
+
} else report = await testReachability(ssh.host, ssh.port);
|
|
95
|
+
if (report.ok && node.type === "remote-docker" && dockerTester) report = await dockerTester.test({
|
|
96
|
+
id: node.id,
|
|
97
|
+
type: node.type,
|
|
98
|
+
ssh,
|
|
99
|
+
docker: node.docker
|
|
100
|
+
});
|
|
101
|
+
registry.setStatus(id, {
|
|
102
|
+
reachable: report.reachable,
|
|
103
|
+
lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
104
|
+
error: report.error
|
|
105
|
+
});
|
|
106
|
+
return report;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
error: "unsupported node type"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
export { testNode, testReachability };
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=test.js.map
|
package/lib/test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.js","names":[],"sources":["../src/test.ts"],"sourcesContent":["import { createConnection } from 'node:net'\nimport type { NodeRegistry, NodeSsh, NodeTestReport } from './node-registry.js'\n\nexport interface SshHandshakeTester {\n test(\n target: { host: string; port: number; username: string; auth: NodeSsh['auth']; jump?: NodeSsh['jump'] },\n secrets: { password?: string; privateKey?: string; passphrase?: string },\n ): Promise<{ ok: boolean; error?: string; category?: string }>\n}\n\nexport function testReachability(host: string, port: number, timeoutMs = 5000): Promise<NodeTestReport> {\n return new Promise((resolve) => {\n let settled = false\n const finish = (report: NodeTestReport) => {\n if (settled) return\n settled = true\n socket.destroy()\n resolve(report)\n }\n const socket = createConnection({ host, port })\n socket.setTimeout(timeoutMs)\n socket.once('connect', () => finish({ ok: true, reachable: true }))\n socket.once('timeout', () => finish({ ok: false, reachable: false, error: 'connection timed out' }))\n socket.once('error', (err) =>\n finish({ ok: false, reachable: false, error: err instanceof Error ? err.message : String(err) }),\n )\n })\n}\n\nexport interface NodeTestOverrides {\n ssh?: NodeSsh\n secrets?: { password?: string; privateKey?: string; passphrase?: string }\n}\n\nexport interface DockerNodeTester {\n test(node: { id: string; type: string; ssh?: NodeSsh; docker?: { containerId?: string; image?: string } }): Promise<NodeTestReport>\n}\n\nexport async function testNode(\n registry: NodeRegistry,\n id: string,\n sshTester?: SshHandshakeTester,\n overrides?: NodeTestOverrides,\n dockerTester?: DockerNodeTester,\n): Promise<NodeTestReport> {\n const node = registry.get(id)\n if (!node) return { ok: false, error: `node ${id} not found` }\n if (node.type === 'local-host') {\n registry.setStatus(id, { reachable: true, lastCheckedAt: new Date().toISOString() })\n return { ok: true, reachable: true }\n }\n if (node.type === 'local-docker') {\n let report: NodeTestReport\n if (dockerTester) {\n report = await dockerTester.test({ id: node.id, type: node.type, docker: node.docker })\n } else {\n report = { ok: true, reachable: true }\n }\n registry.setStatus(id, {\n reachable: report.reachable,\n lastCheckedAt: new Date().toISOString(),\n error: report.error,\n })\n return report\n }\n if (node.type === 'remote-ssh' || node.type === 'remote-docker') {\n const ssh = overrides?.ssh ?? node.ssh\n if (!ssh) return { ok: false, error: 'node has no ssh config' }\n let report: NodeTestReport\n if (sshTester) {\n const savedSecrets = (await registry.getSecrets(id)) ?? {}\n const secrets = overrides?.secrets\n ? {\n password: overrides.secrets.password || savedSecrets.password,\n privateKey: overrides.secrets.privateKey || savedSecrets.privateKey,\n passphrase: overrides.secrets.passphrase || savedSecrets.passphrase,\n }\n : savedSecrets\n const t = await sshTester.test(\n { host: ssh.host, port: ssh.port, username: ssh.username, auth: ssh.auth, jump: ssh.jump },\n secrets,\n )\n report = { ok: t.ok, reachable: t.ok, error: t.error, ...(t.category ? { category: t.category } : {}) }\n } else {\n report = await testReachability(ssh.host, ssh.port)\n }\n if (report.ok && node.type === 'remote-docker' && dockerTester) {\n report = await dockerTester.test({ id: node.id, type: node.type, ssh, docker: node.docker })\n }\n registry.setStatus(id, {\n reachable: report.reachable,\n lastCheckedAt: new Date().toISOString(),\n error: report.error,\n })\n return report\n }\n return { ok: false, error: 'unsupported node type' }\n}\n"],"mappings":";;AAUA,SAAgB,iBAAiB,MAAc,MAAc,YAAY,KAA+B;CACtG,OAAO,IAAI,SAAS,YAAY;EAC9B,IAAI,UAAU;EACd,MAAM,UAAU,WAA2B;GACzC,IAAI,SAAS;GACb,UAAU;GACV,OAAO,QAAQ;GACf,QAAQ,MAAM;EAChB;EACA,MAAM,SAAS,iBAAiB;GAAE;GAAM;EAAK,CAAC;EAC9C,OAAO,WAAW,SAAS;EAC3B,OAAO,KAAK,iBAAiB,OAAO;GAAE,IAAI;GAAM,WAAW;EAAK,CAAC,CAAC;EAClE,OAAO,KAAK,iBAAiB,OAAO;GAAE,IAAI;GAAO,WAAW;GAAO,OAAO;EAAuB,CAAC,CAAC;EACnG,OAAO,KAAK,UAAU,QACpB,OAAO;GAAE,IAAI;GAAO,WAAW;GAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;EAAE,CAAC,CACjG;CACF,CAAC;AACH;AAWA,eAAsB,SACpB,UACA,IACA,WACA,WACA,cACyB;CACzB,MAAM,OAAO,SAAS,IAAI,EAAE;CAC5B,IAAI,CAAC,MAAM,OAAO;EAAE,IAAI;EAAO,OAAO,QAAQ,GAAG;CAAY;CAC7D,IAAI,KAAK,SAAS,cAAc;EAC9B,SAAS,UAAU,IAAI;GAAE,WAAW;GAAM,gCAAe,IAAI,KAAK,EAAA,CAAE,YAAY;EAAE,CAAC;EACnF,OAAO;GAAE,IAAI;GAAM,WAAW;EAAK;CACrC;CACA,IAAI,KAAK,SAAS,gBAAgB;EAChC,IAAI;EACJ,IAAI,cACF,SAAS,MAAM,aAAa,KAAK;GAAE,IAAI,KAAK;GAAI,MAAM,KAAK;GAAM,QAAQ,KAAK;EAAO,CAAC;OAEtF,SAAS;GAAE,IAAI;GAAM,WAAW;EAAK;EAEvC,SAAS,UAAU,IAAI;GACrB,WAAW,OAAO;GAClB,gCAAe,IAAI,KAAK,EAAA,CAAE,YAAY;GACtC,OAAO,OAAO;EAChB,CAAC;EACD,OAAO;CACT;CACA,IAAI,KAAK,SAAS,gBAAgB,KAAK,SAAS,iBAAiB;EAC/D,MAAM,MAAM,WAAW,OAAO,KAAK;EACnC,IAAI,CAAC,KAAK,OAAO;GAAE,IAAI;GAAO,OAAO;EAAyB;EAC9D,IAAI;EACJ,IAAI,WAAW;GACb,MAAM,eAAgB,MAAM,SAAS,WAAW,EAAE,KAAM,CAAC;GACzD,MAAM,UAAU,WAAW,UACvB;IACE,UAAU,UAAU,QAAQ,YAAY,aAAa;IACrD,YAAY,UAAU,QAAQ,cAAc,aAAa;IACzD,YAAY,UAAU,QAAQ,cAAc,aAAa;GAC3D,IACA;GACJ,MAAM,IAAI,MAAM,UAAU,KACxB;IAAE,MAAM,IAAI;IAAM,MAAM,IAAI;IAAM,UAAU,IAAI;IAAU,MAAM,IAAI;IAAM,MAAM,IAAI;GAAK,GACzF,OACF;GACA,SAAS;IAAE,IAAI,EAAE;IAAI,WAAW,EAAE;IAAI,OAAO,EAAE;IAAO,GAAI,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,IAAI,CAAC;GAAG;EACxG,OACE,SAAS,MAAM,iBAAiB,IAAI,MAAM,IAAI,IAAI;EAEpD,IAAI,OAAO,MAAM,KAAK,SAAS,mBAAmB,cAChD,SAAS,MAAM,aAAa,KAAK;GAAE,IAAI,KAAK;GAAI,MAAM,KAAK;GAAM;GAAK,QAAQ,KAAK;EAAO,CAAC;EAE7F,SAAS,UAAU,IAAI;GACrB,WAAW,OAAO;GAClB,gCAAe,IAAI,KAAK,EAAA,CAAE,YAAY;GACtC,OAAO,OAAO;EAChB,CAAC;EACD,OAAO;CACT;CACA,OAAO;EAAE,IAAI;EAAO,OAAO;CAAwB;AACrD"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/workspace-bindings.d.ts
|
|
2
|
+
interface WorkspaceBinding {
|
|
3
|
+
mirrorPath: string;
|
|
4
|
+
nodeId: string;
|
|
5
|
+
remotePath: string;
|
|
6
|
+
workspaceId?: string;
|
|
7
|
+
}
|
|
8
|
+
interface BindingHit {
|
|
9
|
+
nodeId: string;
|
|
10
|
+
remotePath: string;
|
|
11
|
+
}
|
|
12
|
+
declare class WorkspaceBindingsStore {
|
|
13
|
+
private bindings;
|
|
14
|
+
private loaded;
|
|
15
|
+
private file;
|
|
16
|
+
private load;
|
|
17
|
+
private save;
|
|
18
|
+
add(binding: Omit<WorkspaceBinding, 'mirrorPath'> & {
|
|
19
|
+
mirrorPath: string;
|
|
20
|
+
}): void;
|
|
21
|
+
remove(mirrorPath: string): void;
|
|
22
|
+
list(): WorkspaceBinding[];
|
|
23
|
+
resolve(path: string): BindingHit | undefined;
|
|
24
|
+
mirrorRoot(nodeId: string, slug: string): string;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { BindingHit, WorkspaceBinding, WorkspaceBindingsStore };
|
|
28
|
+
//# sourceMappingURL=workspace-bindings.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
//#region src/workspace-bindings.ts
|
|
5
|
+
function dshHome() {
|
|
6
|
+
return process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
7
|
+
}
|
|
8
|
+
function normalize(p) {
|
|
9
|
+
return p.replace(/\/+$/, "") || "/";
|
|
10
|
+
}
|
|
11
|
+
var WorkspaceBindingsStore = class {
|
|
12
|
+
bindings = [];
|
|
13
|
+
loaded = false;
|
|
14
|
+
file() {
|
|
15
|
+
return join(dshHome(), "dshb", "workspace-bindings.json");
|
|
16
|
+
}
|
|
17
|
+
load() {
|
|
18
|
+
if (this.loaded) return;
|
|
19
|
+
this.loaded = true;
|
|
20
|
+
try {
|
|
21
|
+
this.bindings = JSON.parse(readFileSync(this.file(), "utf8")).map((b) => ({
|
|
22
|
+
...b,
|
|
23
|
+
mirrorPath: normalize(b.mirrorPath)
|
|
24
|
+
}));
|
|
25
|
+
} catch {
|
|
26
|
+
this.bindings = [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
save() {
|
|
30
|
+
const file = this.file();
|
|
31
|
+
mkdirSync(join(file, ".."), {
|
|
32
|
+
recursive: true,
|
|
33
|
+
mode: 448
|
|
34
|
+
});
|
|
35
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
36
|
+
writeFileSync(tmp, JSON.stringify(this.bindings, null, 2), { mode: 384 });
|
|
37
|
+
renameSync(tmp, file);
|
|
38
|
+
chmodSync(file, 384);
|
|
39
|
+
}
|
|
40
|
+
add(binding) {
|
|
41
|
+
this.load();
|
|
42
|
+
const mirrorPath = normalize(binding.mirrorPath);
|
|
43
|
+
this.bindings = this.bindings.filter((b) => normalize(b.mirrorPath) !== mirrorPath);
|
|
44
|
+
this.bindings.push({
|
|
45
|
+
...binding,
|
|
46
|
+
mirrorPath
|
|
47
|
+
});
|
|
48
|
+
this.save();
|
|
49
|
+
}
|
|
50
|
+
remove(mirrorPath) {
|
|
51
|
+
this.load();
|
|
52
|
+
const target = normalize(mirrorPath);
|
|
53
|
+
this.bindings = this.bindings.filter((b) => normalize(b.mirrorPath) !== target);
|
|
54
|
+
this.save();
|
|
55
|
+
}
|
|
56
|
+
list() {
|
|
57
|
+
this.load();
|
|
58
|
+
return this.bindings.map((b) => ({ ...b }));
|
|
59
|
+
}
|
|
60
|
+
resolve(path) {
|
|
61
|
+
this.load();
|
|
62
|
+
const target = normalize(path);
|
|
63
|
+
let best;
|
|
64
|
+
for (const b of this.bindings) {
|
|
65
|
+
const root = normalize(b.mirrorPath);
|
|
66
|
+
if (target === root || target.startsWith(`${root}/`)) {
|
|
67
|
+
if (!best || root.length > normalize(best.mirrorPath).length) best = b;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!best) return void 0;
|
|
71
|
+
const root = normalize(best.mirrorPath);
|
|
72
|
+
const remoteRoot = normalize(best.remotePath);
|
|
73
|
+
const suffix = target === root ? "" : target.slice(root.length);
|
|
74
|
+
return {
|
|
75
|
+
nodeId: best.nodeId,
|
|
76
|
+
remotePath: `${remoteRoot}${suffix}`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
mirrorRoot(nodeId, slug) {
|
|
80
|
+
return join(dshHome(), "dshb", "mirrors", nodeId, slug);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
export { WorkspaceBindingsStore };
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=workspace-bindings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-bindings.js","names":[],"sources":["../src/workspace-bindings.ts"],"sourcesContent":["import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nexport interface WorkspaceBinding {\n mirrorPath: string\n nodeId: string\n remotePath: string\n workspaceId?: string\n}\n\nexport interface BindingHit {\n nodeId: string\n remotePath: string\n}\n\nfunction dshHome(): string {\n return process.env.DSH_HOME ?? join(homedir(), '.dsh')\n}\n\nfunction normalize(p: string): string {\n return p.replace(/\\/+$/, '') || '/'\n}\n\nexport class WorkspaceBindingsStore {\n private bindings: WorkspaceBinding[] = []\n private loaded = false\n\n private file(): string {\n return join(dshHome(), 'dshb', 'workspace-bindings.json')\n }\n\n private load(): void {\n if (this.loaded) return\n this.loaded = true\n try {\n this.bindings = (JSON.parse(readFileSync(this.file(), 'utf8')) as WorkspaceBinding[]).map((b) => ({\n ...b,\n mirrorPath: normalize(b.mirrorPath),\n }))\n } catch {\n this.bindings = []\n }\n }\n\n private save(): void {\n const file = this.file()\n mkdirSync(join(file, '..'), { recursive: true, mode: 0o700 })\n const tmp = `${file}.${process.pid}.tmp`\n writeFileSync(tmp, JSON.stringify(this.bindings, null, 2), { mode: 0o600 })\n renameSync(tmp, file)\n chmodSync(file, 0o600)\n }\n\n add(binding: Omit<WorkspaceBinding, 'mirrorPath'> & { mirrorPath: string }): void {\n this.load()\n const mirrorPath = normalize(binding.mirrorPath)\n this.bindings = this.bindings.filter((b) => normalize(b.mirrorPath) !== mirrorPath)\n this.bindings.push({ ...binding, mirrorPath })\n this.save()\n }\n\n remove(mirrorPath: string): void {\n this.load()\n const target = normalize(mirrorPath)\n this.bindings = this.bindings.filter((b) => normalize(b.mirrorPath) !== target)\n this.save()\n }\n\n list(): WorkspaceBinding[] {\n this.load()\n return this.bindings.map((b) => ({ ...b }))\n }\n\n resolve(path: string): BindingHit | undefined {\n this.load()\n const target = normalize(path)\n let best: WorkspaceBinding | undefined\n for (const b of this.bindings) {\n const root = normalize(b.mirrorPath)\n if (target === root || target.startsWith(`${root}/`)) {\n if (!best || root.length > normalize(best.mirrorPath).length) best = b\n }\n }\n if (!best) return undefined\n const root = normalize(best.mirrorPath)\n const remoteRoot = normalize(best.remotePath)\n const suffix = target === root ? '' : target.slice(root.length)\n return { nodeId: best.nodeId, remotePath: `${remoteRoot}${suffix}` }\n }\n\n mirrorRoot(nodeId: string, slug: string): string {\n return join(dshHome(), 'dshb', 'mirrors', nodeId, slug)\n }\n}\n"],"mappings":";;;;AAgBA,SAAS,UAAkB;CACzB,OAAO,QAAQ,IAAI,YAAY,KAAK,QAAQ,GAAG,MAAM;AACvD;AAEA,SAAS,UAAU,GAAmB;CACpC,OAAO,EAAE,QAAQ,QAAQ,EAAE,KAAK;AAClC;AAEA,IAAa,yBAAb,MAAoC;CAClC,WAAuC,CAAC;CACxC,SAAiB;CAEjB,OAAuB;EACrB,OAAO,KAAK,QAAQ,GAAG,QAAQ,yBAAyB;CAC1D;CAEA,OAAqB;EACnB,IAAI,KAAK,QAAQ;EACjB,KAAK,SAAS;EACd,IAAI;GACF,KAAK,WAAY,KAAK,MAAM,aAAa,KAAK,KAAK,GAAG,MAAM,CAAC,CAAC,CAAwB,KAAK,OAAO;IAChG,GAAG;IACH,YAAY,UAAU,EAAE,UAAU;GACpC,EAAE;EACJ,QAAQ;GACN,KAAK,WAAW,CAAC;EACnB;CACF;CAEA,OAAqB;EACnB,MAAM,OAAO,KAAK,KAAK;EACvB,UAAU,KAAK,MAAM,IAAI,GAAG;GAAE,WAAW;GAAM,MAAM;EAAM,CAAC;EAC5D,MAAM,MAAM,GAAG,KAAK,GAAG,QAAQ,IAAI;EACnC,cAAc,KAAK,KAAK,UAAU,KAAK,UAAU,MAAM,CAAC,GAAG,EAAE,MAAM,IAAM,CAAC;EAC1E,WAAW,KAAK,IAAI;EACpB,UAAU,MAAM,GAAK;CACvB;CAEA,IAAI,SAA8E;EAChF,KAAK,KAAK;EACV,MAAM,aAAa,UAAU,QAAQ,UAAU;EAC/C,KAAK,WAAW,KAAK,SAAS,QAAQ,MAAM,UAAU,EAAE,UAAU,MAAM,UAAU;EAClF,KAAK,SAAS,KAAK;GAAE,GAAG;GAAS;EAAW,CAAC;EAC7C,KAAK,KAAK;CACZ;CAEA,OAAO,YAA0B;EAC/B,KAAK,KAAK;EACV,MAAM,SAAS,UAAU,UAAU;EACnC,KAAK,WAAW,KAAK,SAAS,QAAQ,MAAM,UAAU,EAAE,UAAU,MAAM,MAAM;EAC9E,KAAK,KAAK;CACZ;CAEA,OAA2B;EACzB,KAAK,KAAK;EACV,OAAO,KAAK,SAAS,KAAK,OAAO,EAAE,GAAG,EAAE,EAAE;CAC5C;CAEA,QAAQ,MAAsC;EAC5C,KAAK,KAAK;EACV,MAAM,SAAS,UAAU,IAAI;EAC7B,IAAI;EACJ,KAAK,MAAM,KAAK,KAAK,UAAU;GAC7B,MAAM,OAAO,UAAU,EAAE,UAAU;GACnC,IAAI,WAAW,QAAQ,OAAO,WAAW,GAAG,KAAK,EAAE,GAC7C;QAAA,CAAC,QAAQ,KAAK,SAAS,UAAU,KAAK,UAAU,CAAC,CAAC,QAAQ,OAAO;GAAA;EAEzE;EACA,IAAI,CAAC,MAAM,OAAO,KAAA;EAClB,MAAM,OAAO,UAAU,KAAK,UAAU;EACtC,MAAM,aAAa,UAAU,KAAK,UAAU;EAC5C,MAAM,SAAS,WAAW,OAAO,KAAK,OAAO,MAAM,KAAK,MAAM;EAC9D,OAAO;GAAE,QAAQ,KAAK;GAAQ,YAAY,GAAG,aAAa;EAAS;CACrE;CAEA,WAAW,QAAgB,MAAsB;EAC/C,OAAO,KAAK,QAAQ,GAAG,QAAQ,WAAW,QAAQ,IAAI;CACxD;AACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { t as Context } from "./index-CLOUBKxU.js";
|
|
2
|
+
import { NodeRegistry } from "./node-registry.js";
|
|
3
|
+
import { WorkspaceBindingsStore } from "./workspace-bindings.js";
|
|
4
|
+
//#region src/workspaces.d.ts
|
|
5
|
+
declare function registerWorkspaceRoutes(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore): void;
|
|
6
|
+
declare function registerRemoteDownloadRoutes(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore): void;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { registerRemoteDownloadRoutes, registerWorkspaceRoutes };
|
|
9
|
+
//# sourceMappingURL=workspaces.d.ts.map
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
|
+
//#region src/workspaces.ts
|
|
4
|
+
const MAX_BODY = 65536;
|
|
5
|
+
function sendJson(res, status, body) {
|
|
6
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
7
|
+
res.end(JSON.stringify(body));
|
|
8
|
+
}
|
|
9
|
+
function readBody(req) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
if ((req.headers["content-type"] ?? "") !== "application/json") {
|
|
12
|
+
resolve(null);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let size = 0;
|
|
17
|
+
req.on("data", (chunk) => {
|
|
18
|
+
size += chunk.length;
|
|
19
|
+
if (size > MAX_BODY) {
|
|
20
|
+
resolve(null);
|
|
21
|
+
req.destroy();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
chunks.push(chunk);
|
|
25
|
+
});
|
|
26
|
+
req.on("end", () => {
|
|
27
|
+
try {
|
|
28
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
29
|
+
} catch {
|
|
30
|
+
resolve(null);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
req.on("error", () => resolve(null));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function pathSegments(req) {
|
|
37
|
+
return new URL(req.url ?? "/", "http://x").pathname.split("/").filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
function slugify(name) {
|
|
40
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9一-龥]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "workspace";
|
|
41
|
+
}
|
|
42
|
+
function registerWorkspaceRoutes(ctx, registry, bindings) {
|
|
43
|
+
const webServer = ctx.webServer;
|
|
44
|
+
ctx.effect(() => webServer.register({
|
|
45
|
+
kind: "prefix",
|
|
46
|
+
path: "/api/dshb/workspaces",
|
|
47
|
+
handler: async (req, res) => {
|
|
48
|
+
const worlds = ctx.get("dshbWorlds");
|
|
49
|
+
const action = pathSegments(req)[3];
|
|
50
|
+
if (req.method === "POST" && action === "bind") {
|
|
51
|
+
const body = await readBody(req);
|
|
52
|
+
if (!body || typeof body.nodeId !== "string" || typeof body.remotePath !== "string") {
|
|
53
|
+
sendJson(res, 400, {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: "需要 nodeId 与 remotePath"
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const node = registry.get(body.nodeId);
|
|
60
|
+
if (!node) {
|
|
61
|
+
sendJson(res, 404, {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: "node not found"
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const remotePath = String(body.remotePath);
|
|
68
|
+
if (node.type === "local-host") {
|
|
69
|
+
sendJson(res, 200, {
|
|
70
|
+
ok: true,
|
|
71
|
+
mirrorPath: remotePath
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const slug = slugify(String(body.name ?? remotePath.split("/").filter(Boolean).pop() ?? "workspace"));
|
|
76
|
+
const mirrorPath = bindings.mirrorRoot(node.id, slug);
|
|
77
|
+
try {
|
|
78
|
+
if (node.type === "local-docker" || node.type === "remote-docker") {
|
|
79
|
+
const dockerWorlds = ctx.get("dshbDockerWorldsGeneric");
|
|
80
|
+
if (!dockerWorlds) {
|
|
81
|
+
sendJson(res, 503, {
|
|
82
|
+
ok: false,
|
|
83
|
+
error: "Docker 执行世界不可用(dshb-exec-docker 未加载)"
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const world = await dockerWorlds.ensure(node.id);
|
|
88
|
+
if (!world) {
|
|
89
|
+
sendJson(res, 503, {
|
|
90
|
+
ok: false,
|
|
91
|
+
error: "容器未就绪,请先拉起容器"
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
await world.ensureDir(remotePath);
|
|
96
|
+
} else {
|
|
97
|
+
if (!worlds) {
|
|
98
|
+
sendJson(res, 503, {
|
|
99
|
+
ok: false,
|
|
100
|
+
error: "远程执行世界不可用"
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const world = await worlds.ensure(node.id);
|
|
105
|
+
if (!world) {
|
|
106
|
+
sendJson(res, 503, {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: "节点执行世界未就绪"
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await world.ensureDir(remotePath);
|
|
113
|
+
}
|
|
114
|
+
mkdirSync(mirrorPath, { recursive: true });
|
|
115
|
+
bindings.add({
|
|
116
|
+
mirrorPath,
|
|
117
|
+
nodeId: node.id,
|
|
118
|
+
remotePath
|
|
119
|
+
});
|
|
120
|
+
sendJson(res, 201, {
|
|
121
|
+
ok: true,
|
|
122
|
+
mirrorPath
|
|
123
|
+
});
|
|
124
|
+
} catch (err) {
|
|
125
|
+
sendJson(res, 502, {
|
|
126
|
+
ok: false,
|
|
127
|
+
error: err instanceof Error ? err.message : String(err)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (req.method === "GET" && action === void 0) {
|
|
133
|
+
sendJson(res, 200, {
|
|
134
|
+
ok: true,
|
|
135
|
+
bindings: bindings.list()
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
sendJson(res, 404, {
|
|
140
|
+
ok: false,
|
|
141
|
+
error: "not found"
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
function resolveRemoteWorld(ctx, node) {
|
|
147
|
+
if (node.type === "local-docker" || node.type === "remote-docker") return ctx.get("dshbDockerWorldsGeneric");
|
|
148
|
+
if (node.type === "remote-ssh") return ctx.get("dshbWorlds");
|
|
149
|
+
}
|
|
150
|
+
const REMOTE_MAX_BYTES = 536870912;
|
|
151
|
+
function registerRemoteDownloadRoutes(ctx, registry, bindings) {
|
|
152
|
+
const webServer = ctx.webServer;
|
|
153
|
+
ctx.effect(() => webServer.register({
|
|
154
|
+
kind: "prefix",
|
|
155
|
+
path: "/api/dshb/remote",
|
|
156
|
+
handler: async (req, res) => {
|
|
157
|
+
const action = pathSegments(req)[3];
|
|
158
|
+
if (req.method !== "GET" || action !== "bound" && action !== "file") {
|
|
159
|
+
sendJson(res, 404, {
|
|
160
|
+
ok: false,
|
|
161
|
+
error: "not found"
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const path = new URL(req.url ?? "/", "http://x").searchParams.get("path") ?? "";
|
|
166
|
+
if (!path) {
|
|
167
|
+
sendJson(res, 400, {
|
|
168
|
+
ok: false,
|
|
169
|
+
error: "需要 path"
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const hit = bindings.resolve(path);
|
|
174
|
+
if (!hit) {
|
|
175
|
+
sendJson(res, action === "bound" ? 200 : 404, {
|
|
176
|
+
ok: true,
|
|
177
|
+
bound: false
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const node = registry.get(hit.nodeId);
|
|
182
|
+
if (!node) {
|
|
183
|
+
sendJson(res, 502, {
|
|
184
|
+
ok: false,
|
|
185
|
+
bound: true,
|
|
186
|
+
error: "node not found"
|
|
187
|
+
});
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const worlds = resolveRemoteWorld(ctx, node);
|
|
191
|
+
if (!worlds) {
|
|
192
|
+
sendJson(res, 502, {
|
|
193
|
+
ok: false,
|
|
194
|
+
bound: true,
|
|
195
|
+
error: "远程执行世界不可用"
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const world = await worlds.ensure(node.id);
|
|
201
|
+
if (!world) {
|
|
202
|
+
sendJson(res, 502, {
|
|
203
|
+
ok: false,
|
|
204
|
+
bound: true,
|
|
205
|
+
error: "节点执行世界未就绪"
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const isFile = (await world.fs.stat(hit.remotePath))?.isFile ?? false;
|
|
210
|
+
if (action === "bound") {
|
|
211
|
+
sendJson(res, 200, {
|
|
212
|
+
ok: true,
|
|
213
|
+
bound: true,
|
|
214
|
+
nodeId: node.id,
|
|
215
|
+
remotePath: hit.remotePath,
|
|
216
|
+
kind: isFile ? "file" : "dir"
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!isFile) {
|
|
221
|
+
sendJson(res, 404, {
|
|
222
|
+
ok: false,
|
|
223
|
+
bound: true,
|
|
224
|
+
error: "目标不是文件"
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const data = await world.fs.readBytes(hit.remotePath, void 0, REMOTE_MAX_BYTES);
|
|
229
|
+
const name = basename(hit.remotePath) || "download";
|
|
230
|
+
let contentType = {
|
|
231
|
+
".txt": "text/plain",
|
|
232
|
+
".md": "text/markdown",
|
|
233
|
+
".json": "application/json",
|
|
234
|
+
".html": "text/html",
|
|
235
|
+
".htm": "text/html",
|
|
236
|
+
".css": "text/css",
|
|
237
|
+
".js": "text/javascript",
|
|
238
|
+
".mjs": "text/javascript",
|
|
239
|
+
".svg": "image/svg+xml",
|
|
240
|
+
".png": "image/png",
|
|
241
|
+
".jpg": "image/jpeg",
|
|
242
|
+
".jpeg": "image/jpeg",
|
|
243
|
+
".gif": "image/gif",
|
|
244
|
+
".webp": "image/webp",
|
|
245
|
+
".pdf": "application/pdf",
|
|
246
|
+
".zip": "application/zip",
|
|
247
|
+
".gz": "application/gzip",
|
|
248
|
+
".tar": "application/x-tar",
|
|
249
|
+
".csv": "text/csv",
|
|
250
|
+
".yaml": "text/yaml",
|
|
251
|
+
".yml": "text/yaml"
|
|
252
|
+
}[extname(name).toLowerCase()] ?? "application/octet-stream";
|
|
253
|
+
let body = Buffer.from(data);
|
|
254
|
+
if (contentType.startsWith("text/") || contentType === "application/json" || contentType === "application/javascript") {
|
|
255
|
+
contentType += "; charset=utf-8";
|
|
256
|
+
const bom = Buffer.from([
|
|
257
|
+
239,
|
|
258
|
+
187,
|
|
259
|
+
191
|
|
260
|
+
]);
|
|
261
|
+
if (!body.subarray(0, 3).equals(bom)) body = Buffer.concat([bom, body]);
|
|
262
|
+
}
|
|
263
|
+
const asciiFallback = name.replace(/[^\x20-\x7e]/g, "_");
|
|
264
|
+
res.writeHead(200, {
|
|
265
|
+
"content-type": contentType,
|
|
266
|
+
"content-disposition": `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encodeURIComponent(name).replace(/['()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`,
|
|
267
|
+
"cache-control": "no-store"
|
|
268
|
+
});
|
|
269
|
+
res.end(body);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
sendJson(res, 502, {
|
|
272
|
+
ok: false,
|
|
273
|
+
bound: true,
|
|
274
|
+
error: err instanceof Error ? err.message : String(err)
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
export { registerRemoteDownloadRoutes, registerWorkspaceRoutes };
|
|
282
|
+
|
|
283
|
+
//# sourceMappingURL=workspaces.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspaces.js","names":[],"sources":["../src/workspaces.ts"],"sourcesContent":["import { mkdirSync } from 'node:fs'\nimport { basename, extname } from 'node:path'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Context } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport type { NodeRegistry } from './node-registry.js'\nimport type { WorkspaceBindingsStore } from './workspace-bindings.js'\n\nconst MAX_BODY = 64 * 1024\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json' })\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 return new URL(req.url ?? '/', 'http://x').pathname.split('/').filter(Boolean)\n}\n\ninterface WorldsLike {\n ensure(nodeId: string): Promise<{ ensureDir(path: string): Promise<void> } | undefined>\n}\n\ninterface DockerProvisionLike {\n provision(nodeId: string): Promise<{ containerId: string } | undefined>\n}\n\nfunction slugify(name: string): string {\n return name\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9一-龥]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40) || 'workspace'\n}\n\nexport function registerWorkspaceRoutes(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore): void {\n const webServer = ctx.webServer\n\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/dshb/workspaces',\n handler: async (req, res) => {\n const worlds = ctx.get('dshbWorlds') as WorldsLike | undefined\n const segs = pathSegments(req)\n const action = segs[3]\n\n if (req.method === 'POST' && action === 'bind') {\n const body = await readBody(req)\n if (!body || typeof body.nodeId !== 'string' || typeof body.remotePath !== 'string') {\n sendJson(res, 400, { ok: false, error: '需要 nodeId 与 remotePath' })\n return\n }\n const node = registry.get(body.nodeId)\n if (!node) {\n sendJson(res, 404, { ok: false, error: 'node not found' })\n return\n }\n const remotePath = String(body.remotePath)\n if (node.type === 'local-host') {\n sendJson(res, 200, { ok: true, mirrorPath: remotePath })\n return\n }\n const slug = slugify(String(body.name ?? remotePath.split('/').filter(Boolean).pop() ?? 'workspace'))\n const mirrorPath = bindings.mirrorRoot(node.id, slug)\n try {\n const isDocker = node.type === 'local-docker' || node.type === 'remote-docker'\n if (isDocker) {\n const dockerWorlds = ctx.get('dshbDockerWorldsGeneric') as WorldsLike | undefined\n if (!dockerWorlds) {\n sendJson(res, 503, { ok: false, error: 'Docker 执行世界不可用(dshb-exec-docker 未加载)' })\n return\n }\n const world = await dockerWorlds.ensure(node.id)\n if (!world) {\n sendJson(res, 503, { ok: false, error: '容器未就绪,请先拉起容器' })\n return\n }\n await world.ensureDir(remotePath)\n } else {\n if (!worlds) {\n sendJson(res, 503, { ok: false, error: '远程执行世界不可用' })\n return\n }\n const world = await worlds.ensure(node.id)\n if (!world) {\n sendJson(res, 503, { ok: false, error: '节点执行世界未就绪' })\n return\n }\n await world.ensureDir(remotePath)\n }\n mkdirSync(mirrorPath, { recursive: true })\n bindings.add({ mirrorPath, nodeId: node.id, remotePath })\n sendJson(res, 201, { ok: true, mirrorPath })\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' && action === undefined) {\n sendJson(res, 200, { ok: true, bindings: bindings.list() })\n return\n }\n\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n\ninterface RemoteWorldLike {\n ensure(nodeId: string): Promise<{\n fs: {\n stat(path: string): Promise<{ isFile?: boolean; isDirectory?: boolean } | undefined>\n readBytes(path: string, signal?: AbortSignal, maxBytes?: number): Promise<Uint8Array>\n }\n } | undefined>\n}\n\nfunction resolveRemoteWorld(ctx: Context, node: { type: string }): RemoteWorldLike | undefined {\n if (node.type === 'local-docker' || node.type === 'remote-docker') {\n return ctx.get('dshbDockerWorldsGeneric') as RemoteWorldLike | undefined\n }\n if (node.type === 'remote-ssh') {\n return ctx.get('dshbWorlds') as RemoteWorldLike | undefined\n }\n return undefined\n}\n\nconst REMOTE_MAX_BYTES = 512 * 1024 * 1024\n\nexport function registerRemoteDownloadRoutes(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore): void {\n const webServer = ctx.webServer\n\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/dshb/remote',\n handler: async (req, res) => {\n const segs = pathSegments(req)\n const action = segs[3]\n if (req.method !== 'GET' || (action !== 'bound' && action !== 'file')) {\n sendJson(res, 404, { ok: false, error: 'not found' })\n return\n }\n const path = new URL(req.url ?? '/', 'http://x').searchParams.get('path') ?? ''\n if (!path) {\n sendJson(res, 400, { ok: false, error: '需要 path' })\n return\n }\n const hit = bindings.resolve(path)\n if (!hit) {\n sendJson(res, action === 'bound' ? 200 : 404, { ok: true, bound: false })\n return\n }\n const node = registry.get(hit.nodeId)\n if (!node) {\n sendJson(res, 502, { ok: false, bound: true, error: 'node not found' })\n return\n }\n const worlds = resolveRemoteWorld(ctx, node)\n if (!worlds) {\n sendJson(res, 502, { ok: false, bound: true, error: '远程执行世界不可用' })\n return\n }\n try {\n const world = await worlds.ensure(node.id)\n if (!world) {\n sendJson(res, 502, { ok: false, bound: true, error: '节点执行世界未就绪' })\n return\n }\n const info = await world.fs.stat(hit.remotePath)\n const isFile = info?.isFile ?? false\n if (action === 'bound') {\n sendJson(res, 200, { ok: true, bound: true, nodeId: node.id, remotePath: hit.remotePath, kind: isFile ? 'file' : 'dir' })\n return\n }\n if (!isFile) {\n sendJson(res, 404, { ok: false, bound: true, error: '目标不是文件' })\n return\n }\n const data = await world.fs.readBytes(hit.remotePath, undefined, REMOTE_MAX_BYTES)\n const name = basename(hit.remotePath) || 'download'\n const ext = extname(name).toLowerCase()\n const mime: Record<string, string> = {\n '.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json', '.html': 'text/html', '.htm': 'text/html',\n '.css': 'text/css', '.js': 'text/javascript', '.mjs': 'text/javascript', '.svg': 'image/svg+xml', '.png': 'image/png',\n '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.pdf': 'application/pdf',\n '.zip': 'application/zip', '.gz': 'application/gzip', '.tar': 'application/x-tar', '.csv': 'text/csv', '.yaml': 'text/yaml', '.yml': 'text/yaml',\n }\n let contentType = mime[ext] ?? 'application/octet-stream'\n let body = Buffer.from(data)\n const isTextual = contentType.startsWith('text/') || contentType === 'application/json' || contentType === 'application/javascript'\n if (isTextual) {\n contentType += '; charset=utf-8'\n const bom = Buffer.from([0xef, 0xbb, 0xbf])\n if (!body.subarray(0, 3).equals(bom)) body = Buffer.concat([bom, body])\n }\n const asciiFallback = name.replace(/[^\\x20-\\x7e]/g, '_')\n res.writeHead(200, {\n 'content-type': contentType,\n 'content-disposition': `attachment; filename=\"${asciiFallback}\"; filename*=UTF-8''${encodeURIComponent(name).replace(/['()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`,\n 'cache-control': 'no-store',\n })\n res.end(body)\n } catch (err) {\n sendJson(res, 502, { ok: false, bound: true, error: err instanceof Error ? err.message : String(err) })\n }\n },\n }),\n )\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dshb-core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "DSHB 节点注册表(ctx.nodeRegistry)与凭据托管",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./node-registry": "./lib/node-registry.js",
|
|
10
|
+
"./ssh-config": "./lib/ssh-config.js",
|
|
11
|
+
"./known-hosts": "./lib/known-hosts.js",
|
|
12
|
+
"./workspace-bindings": "./lib/workspace-bindings.js",
|
|
13
|
+
"./audit": "./lib/audit.js",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"lib",
|
|
18
|
+
"cordis.patch.yml"
|
|
19
|
+
],
|
|
20
|
+
"dsh": {
|
|
21
|
+
"bundle": {
|
|
22
|
+
"patch": "./cordis.patch.yml"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@deepseek-ai/dsh-credentials": "^0.1.1-rc.2",
|
|
27
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
|
|
28
|
+
"dshb-router": "0.0.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
32
|
+
},
|
|
33
|
+
"types": "lib/index.d.ts",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsdown",
|
|
36
|
+
"typecheck": "tsc --noEmit"
|
|
37
|
+
}
|
|
38
|
+
}
|