@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
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @artenx/dshb
|
|
2
|
+
|
|
3
|
+
DSH-HoneyBee routes DSH filesystem, shell, subprocess, and PTY operations to local, SSH, and Docker execution nodes. It includes a node registry, an authentication gate, credential custody, workspace bindings, and a mobile-aware management UI.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
dsh plugin --profile web add @artenx/dshb
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Restart the corresponding DSH Web profile after installation.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Node.js 22.13 or newer
|
|
16
|
+
- DeepSeek Harness 0.1.5-rc.1
|
|
17
|
+
|
|
18
|
+
Source code and complete documentation: https://github.com/Artenx/dsh-honeybee
|
package/cordis.patch.yml
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
- insert:
|
|
2
2
|
- id: dshb-auth
|
|
3
|
-
name: dshb
|
|
3
|
+
name: "@artenx/dshb/auth"
|
|
4
4
|
- insert:
|
|
5
5
|
- id: dshb-web-startup
|
|
6
|
-
name: dshb
|
|
6
|
+
name: "@artenx/dshb/auth/startup"
|
|
7
7
|
- insert:
|
|
8
8
|
- id: dshb-core
|
|
9
|
-
name: dshb
|
|
9
|
+
name: "@artenx/dshb/core"
|
|
10
10
|
- insert:
|
|
11
11
|
- id: dshb-router
|
|
12
|
-
name: dshb
|
|
12
|
+
name: "@artenx/dshb/router"
|
|
13
13
|
- insert:
|
|
14
14
|
- id: dshb-exec-ssh
|
|
15
|
-
name: dshb
|
|
16
|
-
- insert:
|
|
17
|
-
- id: dshb-ui
|
|
18
|
-
name: dshb-ui
|
|
15
|
+
name: "@artenx/dshb/exec-ssh"
|
|
19
16
|
- insert:
|
|
20
17
|
- id: dshb-exec-docker
|
|
21
|
-
name: dshb
|
|
18
|
+
name: "@artenx/dshb/exec-docker"
|
|
19
|
+
- insert:
|
|
20
|
+
- id: dshb-ui
|
|
21
|
+
name: "@artenx/dshb"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/auth/credentials.d.ts
|
|
2
|
+
interface AuthStoreData {
|
|
3
|
+
username: string;
|
|
4
|
+
salt: string;
|
|
5
|
+
hash: string;
|
|
6
|
+
sessionSecret: string;
|
|
7
|
+
}
|
|
8
|
+
declare function dshHome(): string;
|
|
9
|
+
declare function authFilePath(): string;
|
|
10
|
+
declare function sanitizeUsername(input: string): string;
|
|
11
|
+
declare class CredentialStore {
|
|
12
|
+
private data;
|
|
13
|
+
private loaded;
|
|
14
|
+
private loadedMtime;
|
|
15
|
+
private file;
|
|
16
|
+
private maybeReload;
|
|
17
|
+
isInitialized(): boolean;
|
|
18
|
+
get username(): string | null;
|
|
19
|
+
get sessionSecret(): Buffer | null;
|
|
20
|
+
register(username: string, password: string): void;
|
|
21
|
+
verify(password: string): boolean;
|
|
22
|
+
changePassword(username: string, password: string): void;
|
|
23
|
+
changeUsername(username: string): void;
|
|
24
|
+
private load;
|
|
25
|
+
private save;
|
|
26
|
+
}
|
|
27
|
+
declare function sharedCredentialStore(): CredentialStore;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { AuthStoreData, CredentialStore, authFilePath, dshHome, sanitizeUsername, sharedCredentialStore };
|
|
30
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
|
|
5
|
+
//#region src/auth/credentials.ts
|
|
6
|
+
function dshHome() {
|
|
7
|
+
return process.env.DSH_HOME ?? join(homedir(), ".dsh");
|
|
8
|
+
}
|
|
9
|
+
function authFilePath() {
|
|
10
|
+
return join(dshHome(), "dshb", "web-auth.json");
|
|
11
|
+
}
|
|
12
|
+
function sanitizeUsername(input) {
|
|
13
|
+
return input.replace(/[\x00-\x1f\x7f]/g, "").trim();
|
|
14
|
+
}
|
|
15
|
+
var CredentialStore = class {
|
|
16
|
+
data = null;
|
|
17
|
+
loaded = false;
|
|
18
|
+
loadedMtime = 0;
|
|
19
|
+
file() {
|
|
20
|
+
return authFilePath();
|
|
21
|
+
}
|
|
22
|
+
maybeReload() {
|
|
23
|
+
if (!this.loaded) {
|
|
24
|
+
this.load();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
if (statSync(this.file()).mtimeMs !== this.loadedMtime) this.load();
|
|
29
|
+
} catch {
|
|
30
|
+
this.data = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
isInitialized() {
|
|
34
|
+
this.maybeReload();
|
|
35
|
+
return this.data !== null;
|
|
36
|
+
}
|
|
37
|
+
get username() {
|
|
38
|
+
this.maybeReload();
|
|
39
|
+
return this.data?.username ?? null;
|
|
40
|
+
}
|
|
41
|
+
get sessionSecret() {
|
|
42
|
+
this.maybeReload();
|
|
43
|
+
return this.data ? Buffer.from(this.data.sessionSecret, "base64") : null;
|
|
44
|
+
}
|
|
45
|
+
register(username, password) {
|
|
46
|
+
this.maybeReload();
|
|
47
|
+
if (this.data) throw new Error("auth: already initialized");
|
|
48
|
+
const salt = randomBytes(16);
|
|
49
|
+
const hash = scryptSync(password, salt, 64);
|
|
50
|
+
this.data = {
|
|
51
|
+
username,
|
|
52
|
+
salt: salt.toString("base64"),
|
|
53
|
+
hash: hash.toString("base64"),
|
|
54
|
+
sessionSecret: randomBytes(32).toString("base64")
|
|
55
|
+
};
|
|
56
|
+
this.save();
|
|
57
|
+
}
|
|
58
|
+
verify(password) {
|
|
59
|
+
this.maybeReload();
|
|
60
|
+
if (!this.data) return false;
|
|
61
|
+
const hash = scryptSync(password, Buffer.from(this.data.salt, "base64"), 64);
|
|
62
|
+
const expected = Buffer.from(this.data.hash, "base64");
|
|
63
|
+
return timingSafeEqual(hash, expected);
|
|
64
|
+
}
|
|
65
|
+
changePassword(username, password) {
|
|
66
|
+
this.maybeReload();
|
|
67
|
+
if (!this.data) throw new Error("auth: not initialized");
|
|
68
|
+
const salt = randomBytes(16);
|
|
69
|
+
this.data = {
|
|
70
|
+
username,
|
|
71
|
+
salt: salt.toString("base64"),
|
|
72
|
+
hash: scryptSync(password, salt, 64).toString("base64"),
|
|
73
|
+
sessionSecret: randomBytes(32).toString("base64")
|
|
74
|
+
};
|
|
75
|
+
this.save();
|
|
76
|
+
}
|
|
77
|
+
changeUsername(username) {
|
|
78
|
+
this.maybeReload();
|
|
79
|
+
if (!this.data) throw new Error("auth: not initialized");
|
|
80
|
+
this.data = {
|
|
81
|
+
...this.data,
|
|
82
|
+
username,
|
|
83
|
+
sessionSecret: randomBytes(32).toString("base64")
|
|
84
|
+
};
|
|
85
|
+
this.save();
|
|
86
|
+
}
|
|
87
|
+
load() {
|
|
88
|
+
this.loaded = true;
|
|
89
|
+
const file = this.file();
|
|
90
|
+
try {
|
|
91
|
+
this.loadedMtime = statSync(file).mtimeMs;
|
|
92
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
93
|
+
if (parsed.username && parsed.salt && parsed.hash && parsed.sessionSecret) this.data = parsed;
|
|
94
|
+
} catch {
|
|
95
|
+
this.data = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
save() {
|
|
99
|
+
const file = this.file();
|
|
100
|
+
mkdirSync(join(file, ".."), {
|
|
101
|
+
recursive: true,
|
|
102
|
+
mode: 448
|
|
103
|
+
});
|
|
104
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
105
|
+
writeFileSync(tmp, JSON.stringify(this.data), { mode: 384 });
|
|
106
|
+
renameSync(tmp, file);
|
|
107
|
+
chmodSync(file, 384);
|
|
108
|
+
try {
|
|
109
|
+
this.loadedMtime = statSync(file).mtimeMs;
|
|
110
|
+
} catch {}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
let shared = null;
|
|
114
|
+
function sharedCredentialStore() {
|
|
115
|
+
if (!shared) shared = new CredentialStore();
|
|
116
|
+
return shared;
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { CredentialStore, authFilePath, dshHome, sanitizeUsername, sharedCredentialStore };
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","names":[],"sources":["../../src/auth/credentials.ts"],"sourcesContent":["import { chmodSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\nimport { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto'\n\nexport interface AuthStoreData {\n username: string\n salt: string\n hash: string\n sessionSecret: string\n}\n\nexport function dshHome(): string {\n return process.env.DSH_HOME ?? join(homedir(), '.dsh')\n}\n\nexport function authFilePath(): string {\n return join(dshHome(), 'dshb', 'web-auth.json')\n}\n\nexport function sanitizeUsername(input: string): string {\n return input.replace(/[\\x00-\\x1f\\x7f]/g, '').trim()\n}\n\nexport class CredentialStore {\n private data: AuthStoreData | null = null\n private loaded = false\n private loadedMtime = 0\n\n private file(): string {\n return authFilePath()\n }\n\n private maybeReload(): void {\n if (!this.loaded) {\n this.load()\n return\n }\n try {\n const mtime = statSync(this.file()).mtimeMs\n if (mtime !== this.loadedMtime) this.load()\n } catch {\n this.data = null\n }\n }\n\n isInitialized(): boolean {\n this.maybeReload()\n return this.data !== null\n }\n\n get username(): string | null {\n this.maybeReload()\n return this.data?.username ?? null\n }\n\n get sessionSecret(): Buffer | null {\n this.maybeReload()\n return this.data ? Buffer.from(this.data.sessionSecret, 'base64') : null\n }\n\n register(username: string, password: string): void {\n this.maybeReload()\n if (this.data) throw new Error('auth: already initialized')\n const salt = randomBytes(16)\n const hash = scryptSync(password, salt, 64)\n this.data = {\n username,\n salt: salt.toString('base64'),\n hash: hash.toString('base64'),\n sessionSecret: randomBytes(32).toString('base64'),\n }\n this.save()\n }\n\n verify(password: string): boolean {\n this.maybeReload()\n if (!this.data) return false\n const hash = scryptSync(password, Buffer.from(this.data.salt, 'base64'), 64)\n const expected = Buffer.from(this.data.hash, 'base64')\n return timingSafeEqual(hash, expected)\n }\n\n changePassword(username: string, password: string): void {\n this.maybeReload()\n if (!this.data) throw new Error('auth: not initialized')\n const salt = randomBytes(16)\n this.data = {\n username,\n salt: salt.toString('base64'),\n hash: scryptSync(password, salt, 64).toString('base64'),\n sessionSecret: randomBytes(32).toString('base64'),\n }\n this.save()\n }\n\n changeUsername(username: string): void {\n this.maybeReload()\n if (!this.data) throw new Error('auth: not initialized')\n this.data = { ...this.data, username, sessionSecret: randomBytes(32).toString('base64') }\n this.save()\n }\n\n private load(): void {\n this.loaded = true\n const file = this.file()\n try {\n this.loadedMtime = statSync(file).mtimeMs\n const parsed = JSON.parse(readFileSync(file, 'utf8')) as AuthStoreData\n if (parsed.username && parsed.salt && parsed.hash && parsed.sessionSecret) {\n this.data = parsed\n }\n } catch {\n this.data = null\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.data), { mode: 0o600 })\n renameSync(tmp, file)\n chmodSync(file, 0o600)\n try {\n this.loadedMtime = statSync(file).mtimeMs\n } catch {}\n }\n}\n\nlet shared: CredentialStore | null = null\n\nexport function sharedCredentialStore(): CredentialStore {\n if (!shared) shared = new CredentialStore()\n return shared\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CredentialStore } from "./credentials.js";
|
|
2
|
+
import WebServer from "@deepseek-ai/dsh-host-webserver";
|
|
3
|
+
//#region src/auth/decorate-webserver.d.ts
|
|
4
|
+
declare function decorateWebServer(webServer: WebServer, store: CredentialStore): () => void;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { decorateWebServer };
|
|
7
|
+
//# sourceMappingURL=decorate-webserver.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AuthGate, rewriteToLoopback } from "./gate.js";
|
|
2
|
+
//#region src/auth/decorate-webserver.ts
|
|
3
|
+
function decorateWebServer(webServer, store) {
|
|
4
|
+
const gate = new AuthGate(store);
|
|
5
|
+
const origRegister = webServer.register.bind(webServer);
|
|
6
|
+
const origRegisterFallback = webServer.registerFallback.bind(webServer);
|
|
7
|
+
const origRegisterUpgrade = webServer.registerUpgrade.bind(webServer);
|
|
8
|
+
webServer.register = (route) => {
|
|
9
|
+
return origRegister({
|
|
10
|
+
...route,
|
|
11
|
+
handler: (req, res) => guarded(route, req, res)
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
webServer.registerFallback = (handler) => {
|
|
15
|
+
return origRegisterFallback((req, res) => guarded({
|
|
16
|
+
kind: "exact",
|
|
17
|
+
path: "/",
|
|
18
|
+
handler
|
|
19
|
+
}, req, res));
|
|
20
|
+
};
|
|
21
|
+
webServer.registerUpgrade = (route) => {
|
|
22
|
+
return origRegisterUpgrade({
|
|
23
|
+
...route,
|
|
24
|
+
handler: (req, socket, head) => guardedUpgrade(route, req, socket, head)
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const ws = webServer;
|
|
28
|
+
const origFallback = ws.fallback;
|
|
29
|
+
if (typeof origFallback === "function" && !origFallback.__dshbGated) {
|
|
30
|
+
const wrapped = (req, res) => guarded({
|
|
31
|
+
kind: "exact",
|
|
32
|
+
path: "/",
|
|
33
|
+
handler: origFallback
|
|
34
|
+
}, req, res);
|
|
35
|
+
wrapped.__dshbGated = true;
|
|
36
|
+
ws.fallback = wrapped;
|
|
37
|
+
}
|
|
38
|
+
function guarded(route, req, res) {
|
|
39
|
+
const decision = gate.decide(req);
|
|
40
|
+
if (decision === "allow") {
|
|
41
|
+
rewriteToLoopback(req, webServer.port);
|
|
42
|
+
return route.handler(req, res);
|
|
43
|
+
}
|
|
44
|
+
if (decision === "exempt") return route.handler(req, res);
|
|
45
|
+
if (decision === "redirect-login") {
|
|
46
|
+
res.writeHead(302, { location: "/login" });
|
|
47
|
+
res.end();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
res.writeHead(401, { "content-type": "application/json" });
|
|
51
|
+
res.end(JSON.stringify({
|
|
52
|
+
ok: false,
|
|
53
|
+
error: "unauthorized"
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
function guardedUpgrade(route, req, socket, head) {
|
|
57
|
+
const decision = gate.decide(req);
|
|
58
|
+
if (decision === "allow" || decision === "exempt") {
|
|
59
|
+
if (decision === "allow") rewriteToLoopback(req, webServer.port);
|
|
60
|
+
return route.handler(req, socket, head);
|
|
61
|
+
}
|
|
62
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
63
|
+
socket.destroy();
|
|
64
|
+
}
|
|
65
|
+
return () => {
|
|
66
|
+
webServer.register = origRegister;
|
|
67
|
+
webServer.registerFallback = origRegisterFallback;
|
|
68
|
+
webServer.registerUpgrade = origRegisterUpgrade;
|
|
69
|
+
ws.fallback = origFallback;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { decorateWebServer };
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=decorate-webserver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorate-webserver.js","names":[],"sources":["../../src/auth/decorate-webserver.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport type { WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'\nimport type WebServer from '@deepseek-ai/dsh-host-webserver'\nimport { AuthGate, rewriteToLoopback } from './gate.js'\nimport type { CredentialStore } from './credentials.js'\n\nexport function decorateWebServer(\n webServer: WebServer,\n store: CredentialStore,\n): () => void {\n const gate = new AuthGate(store)\n\n const origRegister = webServer.register.bind(webServer)\n const origRegisterFallback = webServer.registerFallback.bind(webServer)\n const origRegisterUpgrade = webServer.registerUpgrade.bind(webServer)\n\n webServer.register = (route: WebRoute): (() => void) => {\n return origRegister({\n ...route,\n handler: (req, res) => guarded(route, req, res),\n })\n }\n\n webServer.registerFallback = (handler: WebRoute['handler']): (() => void) => {\n return origRegisterFallback((req, res) =>\n guarded({ kind: 'exact', path: '/', handler }, req, res),\n )\n }\n\n webServer.registerUpgrade = (route: WebUpgradeRoute): (() => void) => {\n return origRegisterUpgrade({\n ...route,\n handler: (req, socket, head) => guardedUpgrade(route, req, socket, head),\n })\n }\n\n // DSH 0.1.5+: the frontend-static plugin may have claimed the fallback seat\n // *before* this decoration runs (plugin apply order is not guaranteed). The\n // registerFallback wrap above only gates fallbacks registered later, so also\n // wrap an already-claimed seat in place. Idempotent via the __dshbGated mark.\n const ws = webServer as unknown as { fallback?: WebRoute['handler'] & { __dshbGated?: true } }\n const origFallback = ws.fallback\n if (typeof origFallback === 'function' && !origFallback.__dshbGated) {\n const wrapped: WebRoute['handler'] & { __dshbGated?: true } = (req, res) =>\n guarded({ kind: 'exact', path: '/', handler: origFallback }, req, res)\n wrapped.__dshbGated = true\n ws.fallback = wrapped\n }\n\n function guarded(\n route: WebRoute,\n req: IncomingMessage,\n res: ServerResponse,\n ): void | Promise<void> {\n const decision = gate.decide(req)\n if (decision === 'allow') {\n rewriteToLoopback(req, webServer.port)\n return route.handler(req, res)\n }\n if (decision === 'exempt') {\n return route.handler(req, res)\n }\n if (decision === 'redirect-login') {\n res.writeHead(302, { location: '/login' })\n res.end()\n return\n }\n res.writeHead(401, { 'content-type': 'application/json' })\n res.end(JSON.stringify({ ok: false, error: 'unauthorized' }))\n }\n\n function guardedUpgrade(\n route: WebUpgradeRoute,\n req: IncomingMessage,\n socket: Duplex,\n head: Buffer,\n ): void | Promise<void> {\n const decision = gate.decide(req)\n if (decision === 'allow' || decision === 'exempt') {\n if (decision === 'allow') rewriteToLoopback(req, webServer.port)\n return route.handler(req, socket, head)\n }\n socket.write('HTTP/1.1 401 Unauthorized\\r\\nConnection: close\\r\\n\\r\\n')\n socket.destroy()\n }\n\n return () => {\n webServer.register = origRegister\n webServer.registerFallback = origRegisterFallback\n webServer.registerUpgrade = origRegisterUpgrade\n ws.fallback = origFallback\n }\n}\n"],"mappings":";;AAOA,SAAgB,kBACd,WACA,OACY;CACZ,MAAM,OAAO,IAAI,SAAS,KAAK;CAE/B,MAAM,eAAe,UAAU,SAAS,KAAK,SAAS;CACtD,MAAM,uBAAuB,UAAU,iBAAiB,KAAK,SAAS;CACtE,MAAM,sBAAsB,UAAU,gBAAgB,KAAK,SAAS;CAEpE,UAAU,YAAY,UAAkC;EACtD,OAAO,aAAa;GAClB,GAAG;GACH,UAAU,KAAK,QAAQ,QAAQ,OAAO,KAAK,GAAG;EAChD,CAAC;CACH;CAEA,UAAU,oBAAoB,YAA+C;EAC3E,OAAO,sBAAsB,KAAK,QAChC,QAAQ;GAAE,MAAM;GAAS,MAAM;GAAK;EAAQ,GAAG,KAAK,GAAG,CACzD;CACF;CAEA,UAAU,mBAAmB,UAAyC;EACpE,OAAO,oBAAoB;GACzB,GAAG;GACH,UAAU,KAAK,QAAQ,SAAS,eAAe,OAAO,KAAK,QAAQ,IAAI;EACzE,CAAC;CACH;CAMA,MAAM,KAAK;CACX,MAAM,eAAe,GAAG;CACxB,IAAI,OAAO,iBAAiB,cAAc,CAAC,aAAa,aAAa;EACnE,MAAM,WAAyD,KAAK,QAClE,QAAQ;GAAE,MAAM;GAAS,MAAM;GAAK,SAAS;EAAa,GAAG,KAAK,GAAG;EACvE,QAAQ,cAAc;EACtB,GAAG,WAAW;CAChB;CAEA,SAAS,QACP,OACA,KACA,KACsB;EACtB,MAAM,WAAW,KAAK,OAAO,GAAG;EAChC,IAAI,aAAa,SAAS;GACxB,kBAAkB,KAAK,UAAU,IAAI;GACrC,OAAO,MAAM,QAAQ,KAAK,GAAG;EAC/B;EACA,IAAI,aAAa,UACf,OAAO,MAAM,QAAQ,KAAK,GAAG;EAE/B,IAAI,aAAa,kBAAkB;GACjC,IAAI,UAAU,KAAK,EAAE,UAAU,SAAS,CAAC;GACzC,IAAI,IAAI;GACR;EACF;EACA,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;EACzD,IAAI,IAAI,KAAK,UAAU;GAAE,IAAI;GAAO,OAAO;EAAe,CAAC,CAAC;CAC9D;CAEA,SAAS,eACP,OACA,KACA,QACA,MACsB;EACtB,MAAM,WAAW,KAAK,OAAO,GAAG;EAChC,IAAI,aAAa,WAAW,aAAa,UAAU;GACjD,IAAI,aAAa,SAAS,kBAAkB,KAAK,UAAU,IAAI;GAC/D,OAAO,MAAM,QAAQ,KAAK,QAAQ,IAAI;EACxC;EACA,OAAO,MAAM,wDAAwD;EACrE,OAAO,QAAQ;CACjB;CAEA,aAAa;EACX,UAAU,WAAW;EACrB,UAAU,mBAAmB;EAC7B,UAAU,kBAAkB;EAC5B,GAAG,WAAW;CAChB;AACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CredentialStore } from "./credentials.js";
|
|
2
|
+
import { IncomingMessage } from "node:http";
|
|
3
|
+
//#region src/auth/gate.d.ts
|
|
4
|
+
type GateDecision = 'allow' | 'exempt' | 'redirect-login' | 'reject';
|
|
5
|
+
declare class AuthGate {
|
|
6
|
+
private readonly store;
|
|
7
|
+
constructor(store: CredentialStore);
|
|
8
|
+
decide(req: IncomingMessage): GateDecision;
|
|
9
|
+
private isPageRequest;
|
|
10
|
+
}
|
|
11
|
+
declare function rewriteToLoopback(req: IncomingMessage, port: number): void;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { AuthGate, GateDecision, rewriteToLoopback };
|
|
14
|
+
//# sourceMappingURL=gate.d.ts.map
|
package/lib/auth/gate.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isLoopbackRequest } from "./loopback.js";
|
|
2
|
+
import { SESSION_COOKIE, readCookie, verifySession } from "./session.js";
|
|
3
|
+
//#region src/auth/gate.ts
|
|
4
|
+
var AuthGate = class {
|
|
5
|
+
store;
|
|
6
|
+
constructor(store) {
|
|
7
|
+
this.store = store;
|
|
8
|
+
}
|
|
9
|
+
decide(req) {
|
|
10
|
+
const path = new URL(req.url ?? "/", "http://x").pathname;
|
|
11
|
+
if (path === "/login" || path.startsWith("/api/auth/")) return "exempt";
|
|
12
|
+
if (isLoopbackRequest(req.socket.remoteAddress, req.headers.host)) return "allow";
|
|
13
|
+
const secret = this.store.sessionSecret;
|
|
14
|
+
const username = this.store.username;
|
|
15
|
+
if (secret && username) {
|
|
16
|
+
const value = readCookie(req.headers.cookie, SESSION_COOKIE);
|
|
17
|
+
if (verifySession(value, secret, username)) return "allow";
|
|
18
|
+
}
|
|
19
|
+
if (this.isPageRequest(req)) return "redirect-login";
|
|
20
|
+
return "reject";
|
|
21
|
+
}
|
|
22
|
+
isPageRequest(req) {
|
|
23
|
+
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
24
|
+
if (new URL(req.url ?? "/", "http://x").pathname.startsWith("/api/")) return false;
|
|
25
|
+
const accept = req.headers.accept ?? "";
|
|
26
|
+
return accept.includes("text/html") || accept.includes("*/*");
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
function rewriteToLoopback(req, port) {
|
|
30
|
+
const authority = `127.0.0.1:${port}`;
|
|
31
|
+
req.headers.host = authority;
|
|
32
|
+
for (const name of ["origin", "referer"]) {
|
|
33
|
+
const value = req.headers[name];
|
|
34
|
+
if (typeof value !== "string") continue;
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(value);
|
|
37
|
+
url.host = authority;
|
|
38
|
+
req.headers[name] = url.toString();
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { AuthGate, rewriteToLoopback };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.js","names":[],"sources":["../../src/auth/gate.ts"],"sourcesContent":["import type { IncomingMessage } from 'node:http'\nimport { isLoopbackRequest } from './loopback.js'\nimport { readCookie, SESSION_COOKIE, verifySession } from './session.js'\nimport type { CredentialStore } from './credentials.js'\n\nexport type GateDecision = 'allow' | 'exempt' | 'redirect-login' | 'reject'\n\nexport class AuthGate {\n constructor(private readonly store: CredentialStore) {}\n\n decide(req: IncomingMessage): GateDecision {\n const path = new URL(req.url ?? '/', 'http://x').pathname\n if (path === '/login' || path.startsWith('/api/auth/')) return 'exempt'\n if (isLoopbackRequest(req.socket.remoteAddress, req.headers.host)) return 'allow'\n const secret = this.store.sessionSecret\n const username = this.store.username\n if (secret && username) {\n const value = readCookie(req.headers.cookie, SESSION_COOKIE)\n if (verifySession(value, secret, username)) return 'allow'\n }\n if (this.isPageRequest(req)) return 'redirect-login'\n return 'reject'\n }\n\n private isPageRequest(req: IncomingMessage): boolean {\n if (req.method !== 'GET' && req.method !== 'HEAD') return false\n const path = new URL(req.url ?? '/', 'http://x').pathname\n if (path.startsWith('/api/')) return false\n const accept = req.headers.accept ?? ''\n return accept.includes('text/html') || accept.includes('*/*')\n }\n}\n\nexport function rewriteToLoopback(req: IncomingMessage, port: number): void {\n const authority = `127.0.0.1:${port}`\n req.headers.host = authority\n for (const name of ['origin', 'referer'] as const) {\n const value = req.headers[name]\n if (typeof value !== 'string') continue\n try {\n const url = new URL(value)\n url.host = authority\n req.headers[name] = url.toString()\n } catch {}\n }\n}\n"],"mappings":";;;AAOA,IAAa,WAAb,MAAsB;CACS;CAA7B,YAAY,OAAyC;EAAxB,KAAA,QAAA;CAAyB;CAEtD,OAAO,KAAoC;EACzC,MAAM,OAAO,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC;EACjD,IAAI,SAAS,YAAY,KAAK,WAAW,YAAY,GAAG,OAAO;EAC/D,IAAI,kBAAkB,IAAI,OAAO,eAAe,IAAI,QAAQ,IAAI,GAAG,OAAO;EAC1E,MAAM,SAAS,KAAK,MAAM;EAC1B,MAAM,WAAW,KAAK,MAAM;EAC5B,IAAI,UAAU,UAAU;GACtB,MAAM,QAAQ,WAAW,IAAI,QAAQ,QAAQ,cAAc;GAC3D,IAAI,cAAc,OAAO,QAAQ,QAAQ,GAAG,OAAO;EACrD;EACA,IAAI,KAAK,cAAc,GAAG,GAAG,OAAO;EACpC,OAAO;CACT;CAEA,cAAsB,KAA+B;EACnD,IAAI,IAAI,WAAW,SAAS,IAAI,WAAW,QAAQ,OAAO;EAE1D,IADa,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC,SACxC,WAAW,OAAO,GAAG,OAAO;EACrC,MAAM,SAAS,IAAI,QAAQ,UAAU;EACrC,OAAO,OAAO,SAAS,WAAW,KAAK,OAAO,SAAS,KAAK;CAC9D;AACF;AAEA,SAAgB,kBAAkB,KAAsB,MAAoB;CAC1E,MAAM,YAAY,aAAa;CAC/B,IAAI,QAAQ,OAAO;CACnB,KAAK,MAAM,QAAQ,CAAC,UAAU,SAAS,GAAY;EACjD,MAAM,QAAQ,IAAI,QAAQ;EAC1B,IAAI,OAAO,UAAU,UAAU;EAC/B,IAAI;GACF,MAAM,MAAM,IAAI,IAAI,KAAK;GACzB,IAAI,OAAO;GACX,IAAI,QAAQ,QAAQ,IAAI,SAAS;EACnC,QAAQ,CAAC;CACX;AACF"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
//#region src/auth/index.d.ts
|
|
3
|
+
declare const name = "dshb-auth";
|
|
4
|
+
declare const inject: string[];
|
|
5
|
+
declare function apply(ctx: Context): void;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { apply, inject, name };
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { sharedCredentialStore } from "./credentials.js";
|
|
2
|
+
import { decorateWebServer } from "./decorate-webserver.js";
|
|
3
|
+
import { installLoopbackCompat } from "./loopback-compat.js";
|
|
4
|
+
import { LoginRateLimiter } from "./ratelimit.js";
|
|
5
|
+
import { registerAuthRoutes } from "./routes.js";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
8
|
+
import { basename, dirname, join } from "node:path";
|
|
9
|
+
//#region src/auth/index.ts
|
|
10
|
+
const name = "dshb-auth";
|
|
11
|
+
const inject = ["webServer"];
|
|
12
|
+
function registerFrontendBootRaceWorkaround(ctx) {
|
|
13
|
+
try {
|
|
14
|
+
const manifestPath = createRequire(import.meta.url).resolve("@deepseek-ai/dsh-web-frontend/package.json", { paths: [process.cwd()] });
|
|
15
|
+
const assetDir = join(dirname(manifestPath), "dist", "assets");
|
|
16
|
+
const assetName = readdirSync(assetDir).find((name) => /^index-.*\.js$/.test(name));
|
|
17
|
+
if (!assetName) return;
|
|
18
|
+
const assetPath = join(assetDir, assetName);
|
|
19
|
+
const source = readFileSync(assetPath, "utf8");
|
|
20
|
+
const patched = source.replace("await i.await(),this.assertEntriesActive(t)", "await i.await(),await new Promise(r=>setTimeout(r,8000)),this.assertEntriesActive(t)");
|
|
21
|
+
if (patched === source) return;
|
|
22
|
+
ctx.effect(() => ctx.webServer.register({
|
|
23
|
+
kind: "exact",
|
|
24
|
+
path: `/assets/${basename(assetPath)}`,
|
|
25
|
+
handler: (_req, res) => {
|
|
26
|
+
res.writeHead(200, {
|
|
27
|
+
"cache-control": "no-store",
|
|
28
|
+
"content-type": "application/javascript; charset=utf-8"
|
|
29
|
+
});
|
|
30
|
+
res.end(patched);
|
|
31
|
+
}
|
|
32
|
+
}));
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
function apply(ctx) {
|
|
36
|
+
decorateWebServer(ctx.webServer, sharedCredentialStore());
|
|
37
|
+
registerFrontendBootRaceWorkaround(ctx);
|
|
38
|
+
ctx.webServer.tapIndex(installLoopbackCompat);
|
|
39
|
+
registerAuthRoutes(ctx, sharedCredentialStore(), new LoginRateLimiter());
|
|
40
|
+
ctx.inject(["connection"], (connCtx) => {
|
|
41
|
+
const auth = connCtx.connection?.browserAuth;
|
|
42
|
+
if (auth && typeof auth.isAuthenticated === "function" && !auth.__dshbNeutralized) {
|
|
43
|
+
auth.isAuthenticated = () => true;
|
|
44
|
+
auth.__dshbNeutralized = true;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { apply, inject, name };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/auth/index.ts"],"sourcesContent":["import { readFileSync, readdirSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { basename, dirname, join } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { sharedCredentialStore } from './credentials.js'\nimport { decorateWebServer } from './decorate-webserver.js'\nimport { installLoopbackCompat } from './loopback-compat.js'\nimport { LoginRateLimiter } from './ratelimit.js'\nimport { registerAuthRoutes } from './routes.js'\n\nexport const name = 'dshb-auth'\n\nexport const inject = ['webServer']\n\nfunction registerFrontendBootRaceWorkaround(ctx: Context): void {\n try {\n // Resolve from the active profile so the route patches the same hashed asset\n // that dsh-web-app's static fallback serves.\n const resolver = createRequire(import.meta.url)\n const manifestPath = resolver.resolve('@deepseek-ai/dsh-web-frontend/package.json', {\n paths: [process.cwd()],\n })\n const assetDir = join(dirname(manifestPath), 'dist', 'assets')\n const assetName = readdirSync(assetDir).find((name) => /^index-.*\\.js$/.test(name))\n if (!assetName) return\n\n const assetPath = join(assetDir, assetName)\n const source = readFileSync(assetPath, 'utf8')\n // DSH 0.1.5 asserts before api-remotes' dynamic remote namespace fibers\n // settle. Delay the assertion so mountApp sees the complete client graph;\n // real activation failures still throw after the bounded wait.\n const patched = source.replace(\n 'await i.await(),this.assertEntriesActive(t)',\n 'await i.await(),await new Promise(r=>setTimeout(r,8000)),this.assertEntriesActive(t)',\n )\n if (patched === source) return\n\n ctx.effect(() =>\n ctx.webServer.register({\n kind: 'exact',\n path: `/assets/${basename(assetPath)}`,\n handler: (_req, res) => {\n res.writeHead(200, {\n 'cache-control': 'no-store',\n 'content-type': 'application/javascript; charset=utf-8',\n })\n res.end(patched)\n },\n }),\n )\n } catch {\n // Upstream assets are optional from DSHB's perspective. A missing or changed\n // frontend layout simply skips this version-specific workaround.\n }\n}\n\nexport function apply(ctx: Context): void {\n decorateWebServer(ctx.webServer, sharedCredentialStore())\n registerFrontendBootRaceWorkaround(ctx)\n ctx.webServer.tapIndex(installLoopbackCompat)\n registerAuthRoutes(ctx, sharedCredentialStore(), new LoginRateLimiter())\n\n // DSH 0.1.5 introduced a browser-auth fence on the connection package: the SPA\n // fallback calls connection.authorizeIndex() on \"/\" (mints/checks DSH's own\n // launch-token cookie), and /api routes run requestRejection() which 401s when\n // that DSH cookie is absent. DSHB uses its own session cookie and must stay the\n // sole auth gate (DSH 0.1.1 had no such connection fence, so DSHB worked there).\n // Neutralize the connection's browserAuth.isAuthenticated check so DSH's fence\n // never rejects a DSHB-authenticated (or loopback) request; DSHB's gate still\n // rejects unauthenticated access to gated routes. Property is read at request\n // time, so installing it once after connection is ready suffices.\n ctx.inject(['connection'], (connCtx: Context) => {\n const conn = (connCtx as unknown as { connection?: { browserAuth?: { isAuthenticated?: (...a: unknown[]) => unknown; __dshbNeutralized?: true } } }).connection\n const auth = conn?.browserAuth\n if (auth && typeof auth.isAuthenticated === 'function' && !auth.__dshbNeutralized) {\n auth.isAuthenticated = () => true\n auth.__dshbNeutralized = true\n }\n })\n}\n"],"mappings":";;;;;;;;;AAUA,MAAa,OAAO;AAEpB,MAAa,SAAS,CAAC,WAAW;AAElC,SAAS,mCAAmC,KAAoB;CAC9D,IAAI;EAIF,MAAM,eADW,cAAc,YAAY,GACf,CAAC,CAAC,QAAQ,8CAA8C,EAClF,OAAO,CAAC,QAAQ,IAAI,CAAC,EACvB,CAAC;EACD,MAAM,WAAW,KAAK,QAAQ,YAAY,GAAG,QAAQ,QAAQ;EAC7D,MAAM,YAAY,YAAY,QAAQ,CAAC,CAAC,MAAM,SAAS,iBAAiB,KAAK,IAAI,CAAC;EAClF,IAAI,CAAC,WAAW;EAEhB,MAAM,YAAY,KAAK,UAAU,SAAS;EAC1C,MAAM,SAAS,aAAa,WAAW,MAAM;EAI7C,MAAM,UAAU,OAAO,QACrB,+CACA,sFACF;EACA,IAAI,YAAY,QAAQ;EAExB,IAAI,aACF,IAAI,UAAU,SAAS;GACrB,MAAM;GACN,MAAM,WAAW,SAAS,SAAS;GACnC,UAAU,MAAM,QAAQ;IACtB,IAAI,UAAU,KAAK;KACjB,iBAAiB;KACjB,gBAAgB;IAClB,CAAC;IACD,IAAI,IAAI,OAAO;GACjB;EACF,CAAC,CACH;CACF,QAAQ,CAGR;AACF;AAEA,SAAgB,MAAM,KAAoB;CACxC,kBAAkB,IAAI,WAAW,sBAAsB,CAAC;CACxD,mCAAmC,GAAG;CACtC,IAAI,UAAU,SAAS,qBAAqB;CAC5C,mBAAmB,KAAK,sBAAsB,GAAG,IAAI,iBAAiB,CAAC;CAWvE,IAAI,OAAO,CAAC,YAAY,IAAI,YAAqB;EAE/C,MAAM,OADQ,QAAuI,YAClI;EACnB,IAAI,QAAQ,OAAO,KAAK,oBAAoB,cAAc,CAAC,KAAK,mBAAmB;GACjF,KAAK,wBAAwB;GAC7B,KAAK,oBAAoB;EAC3B;CACF,CAAC;AACH"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
//#region src/auth/login-page.ts
|
|
2
|
+
function loginPageHtml() {
|
|
3
|
+
return `<!doctype html>
|
|
4
|
+
<html lang="zh-CN">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
8
|
+
<title>DSHB 登录</title>
|
|
9
|
+
<style>
|
|
10
|
+
:root { color-scheme: dark; }
|
|
11
|
+
body { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #0f1115; color: #e8eaf0; font-family: system-ui, -apple-system, "PingFang SC", sans-serif; }
|
|
12
|
+
.card { width: min(92vw, 360px); padding: 32px 28px; background: #171a21; border: 1px solid #262b36; border-radius: 12px; }
|
|
13
|
+
h1 { font-size: 20px; margin: 0 0 6px; }
|
|
14
|
+
p.sub { margin: 0 0 20px; font-size: 13px; color: #8b93a5; }
|
|
15
|
+
label { display: block; font-size: 13px; color: #aab1c0; margin: 14px 0 6px; }
|
|
16
|
+
input { width: 100%; box-sizing: border-box; padding: 12px; font-size: 16px; border-radius: 8px; border: 1px solid #2c3242; background: #0f1115; color: #e8eaf0; }
|
|
17
|
+
input:focus { outline: 2px solid #3b82f6; border-color: transparent; }
|
|
18
|
+
button { margin-top: 22px; width: 100%; padding: 12px; font-size: 15px; border: 0; border-radius: 8px; background: #3b82f6; color: #fff; cursor: pointer; }
|
|
19
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
20
|
+
.err { min-height: 18px; margin-top: 12px; font-size: 13px; color: #f87171; }
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<div class="card">
|
|
25
|
+
<h1 id="title">DSHB 登录</h1>
|
|
26
|
+
<p class="sub" id="subtitle">DeepSeek Harness 管理端</p>
|
|
27
|
+
<form id="form">
|
|
28
|
+
<label for="username">用户名</label>
|
|
29
|
+
<input id="username" name="username" autocomplete="username" required>
|
|
30
|
+
<label for="password">密码</label>
|
|
31
|
+
<input id="password" name="password" type="password" autocomplete="current-password" required>
|
|
32
|
+
<button id="submit" type="submit">登录</button>
|
|
33
|
+
<div class="err" id="error"></div>
|
|
34
|
+
</form>
|
|
35
|
+
</div>
|
|
36
|
+
<script>
|
|
37
|
+
(async () => {
|
|
38
|
+
const res = await fetch('/api/auth/status')
|
|
39
|
+
const status = await res.json()
|
|
40
|
+
const form = document.getElementById('form')
|
|
41
|
+
const title = document.getElementById('title')
|
|
42
|
+
const subtitle = document.getElementById('subtitle')
|
|
43
|
+
const submit = document.getElementById('submit')
|
|
44
|
+
const error = document.getElementById('error')
|
|
45
|
+
const password = document.getElementById('password')
|
|
46
|
+
let mode = 'login'
|
|
47
|
+
if (!status.initialized) {
|
|
48
|
+
mode = 'register'
|
|
49
|
+
title.textContent = '设置管理员账号'
|
|
50
|
+
subtitle.textContent = '首次使用,请创建管理员账号(密码至少 8 位)'
|
|
51
|
+
submit.textContent = '创建并登录'
|
|
52
|
+
password.autocomplete = 'new-password'
|
|
53
|
+
}
|
|
54
|
+
form.addEventListener('submit', async (ev) => {
|
|
55
|
+
ev.preventDefault()
|
|
56
|
+
submit.disabled = true
|
|
57
|
+
error.textContent = ''
|
|
58
|
+
try {
|
|
59
|
+
const resp = await fetch('/api/auth/' + mode, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'content-type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
username: document.getElementById('username').value,
|
|
64
|
+
password: password.value,
|
|
65
|
+
}),
|
|
66
|
+
})
|
|
67
|
+
const data = await resp.json()
|
|
68
|
+
if (resp.ok && data.ok) {
|
|
69
|
+
location.href = '/'
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
error.textContent = data.error || ('请求失败(' + resp.status + ')')
|
|
73
|
+
} catch {
|
|
74
|
+
error.textContent = '网络错误,请重试'
|
|
75
|
+
} finally {
|
|
76
|
+
submit.disabled = false
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
})()
|
|
80
|
+
<\/script>
|
|
81
|
+
</body>
|
|
82
|
+
</html>`;
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { loginPageHtml };
|
|
86
|
+
|
|
87
|
+
//# sourceMappingURL=login-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login-page.js","names":[],"sources":["../../src/auth/login-page.ts"],"sourcesContent":["export function loginPageHtml(): string {\n return `<!doctype html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<title>DSHB 登录</title>\n<style>\n:root { color-scheme: dark; }\nbody { margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #0f1115; color: #e8eaf0; font-family: system-ui, -apple-system, \"PingFang SC\", sans-serif; }\n.card { width: min(92vw, 360px); padding: 32px 28px; background: #171a21; border: 1px solid #262b36; border-radius: 12px; }\nh1 { font-size: 20px; margin: 0 0 6px; }\np.sub { margin: 0 0 20px; font-size: 13px; color: #8b93a5; }\nlabel { display: block; font-size: 13px; color: #aab1c0; margin: 14px 0 6px; }\ninput { width: 100%; box-sizing: border-box; padding: 12px; font-size: 16px; border-radius: 8px; border: 1px solid #2c3242; background: #0f1115; color: #e8eaf0; }\ninput:focus { outline: 2px solid #3b82f6; border-color: transparent; }\nbutton { margin-top: 22px; width: 100%; padding: 12px; font-size: 15px; border: 0; border-radius: 8px; background: #3b82f6; color: #fff; cursor: pointer; }\nbutton:disabled { opacity: .5; cursor: default; }\n.err { min-height: 18px; margin-top: 12px; font-size: 13px; color: #f87171; }\n</style>\n</head>\n<body>\n<div class=\"card\">\n<h1 id=\"title\">DSHB 登录</h1>\n<p class=\"sub\" id=\"subtitle\">DeepSeek Harness 管理端</p>\n<form id=\"form\">\n<label for=\"username\">用户名</label>\n<input id=\"username\" name=\"username\" autocomplete=\"username\" required>\n<label for=\"password\">密码</label>\n<input id=\"password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" required>\n<button id=\"submit\" type=\"submit\">登录</button>\n<div class=\"err\" id=\"error\"></div>\n</form>\n</div>\n<script>\n(async () => {\n const res = await fetch('/api/auth/status')\n const status = await res.json()\n const form = document.getElementById('form')\n const title = document.getElementById('title')\n const subtitle = document.getElementById('subtitle')\n const submit = document.getElementById('submit')\n const error = document.getElementById('error')\n const password = document.getElementById('password')\n let mode = 'login'\n if (!status.initialized) {\n mode = 'register'\n title.textContent = '设置管理员账号'\n subtitle.textContent = '首次使用,请创建管理员账号(密码至少 8 位)'\n submit.textContent = '创建并登录'\n password.autocomplete = 'new-password'\n }\n form.addEventListener('submit', async (ev) => {\n ev.preventDefault()\n submit.disabled = true\n error.textContent = ''\n try {\n const resp = await fetch('/api/auth/' + mode, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n username: document.getElementById('username').value,\n password: password.value,\n }),\n })\n const data = await resp.json()\n if (resp.ok && data.ok) {\n location.href = '/'\n return\n }\n error.textContent = data.error || ('请求失败(' + resp.status + ')')\n } catch {\n error.textContent = '网络错误,请重试'\n } finally {\n submit.disabled = false\n }\n })\n})()\n</script>\n</body>\n</html>`\n}\n"],"mappings":";AAAA,SAAgB,gBAAwB;CACtC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFT"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/auth/loopback-compat.d.ts
|
|
2
|
+
declare function loopbackCompatScript(): string;
|
|
3
|
+
declare function installLoopbackCompat(html: string): string;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { installLoopbackCompat, loopbackCompatScript };
|
|
6
|
+
//# sourceMappingURL=loopback-compat.d.ts.map
|