@artenx/dshb 0.1.18 → 0.1.20
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/lib/auth/decorate-webserver.d.ts +2 -1
- package/lib/auth/decorate-webserver.js +21 -10
- package/lib/auth/decorate-webserver.js.map +1 -1
- package/lib/auth/gate.d.ts +3 -1
- package/lib/auth/gate.js +18 -1
- package/lib/auth/gate.js.map +1 -1
- package/lib/auth/index.js +11 -3
- package/lib/auth/index.js.map +1 -1
- package/lib/auth/webserver.d.ts +3 -1
- package/lib/auth/webserver.js +10 -1
- package/lib/auth/webserver.js.map +1 -1
- package/lib/client.js +2157 -567
- package/lib/client.js.map +1 -1
- package/lib/core/index.js +72 -4
- package/lib/core/index.js.map +1 -1
- package/lib/core/node-registry.d.ts +2 -108
- package/lib/core/node-registry.js +43 -2
- package/lib/core/node-registry.js.map +1 -1
- package/lib/core/routes.d.ts +4 -3
- package/lib/core/routes.js +67 -23
- package/lib/core/routes.js.map +1 -1
- package/lib/core/sync/coordinator.d.ts +48 -0
- package/lib/core/sync/coordinator.js +247 -0
- package/lib/core/sync/coordinator.js.map +1 -0
- package/lib/core/sync/host-key.d.ts +2 -0
- package/lib/core/sync/host-key.js +71 -0
- package/lib/core/sync/host-key.js.map +1 -0
- package/lib/core/sync/internal-routes.d.ts +9 -0
- package/lib/core/sync/internal-routes.js +236 -0
- package/lib/core/sync/internal-routes.js.map +1 -0
- package/lib/core/sync/model.d.ts +2 -0
- package/lib/core/sync/model.js +106 -0
- package/lib/core/sync/model.js.map +1 -0
- package/lib/core/sync/node-sync.d.ts +2 -0
- package/lib/core/sync/node-sync.js +660 -0
- package/lib/core/sync/node-sync.js.map +1 -0
- package/lib/core/sync/offline-archive.d.ts +55 -0
- package/lib/core/sync/offline-archive.js +673 -0
- package/lib/core/sync/offline-archive.js.map +1 -0
- package/lib/core/sync/offline-journal.d.ts +22 -0
- package/lib/core/sync/offline-journal.js +74 -0
- package/lib/core/sync/offline-journal.js.map +1 -0
- package/lib/core/sync/offline-routes.d.ts +7 -0
- package/lib/core/sync/offline-routes.js +192 -0
- package/lib/core/sync/offline-routes.js.map +1 -0
- package/lib/core/sync/peer-manager.d.ts +56 -0
- package/lib/core/sync/peer-manager.js +258 -0
- package/lib/core/sync/peer-manager.js.map +1 -0
- package/lib/core/sync/protocol.d.ts +2 -0
- package/lib/core/sync/protocol.js +124 -0
- package/lib/core/sync/protocol.js.map +1 -0
- package/lib/core/sync/remote-helper.d.ts +5 -0
- package/lib/core/sync/remote-helper.js +160 -0
- package/lib/core/sync/remote-helper.js.map +1 -0
- package/lib/core/sync/revision.d.ts +2 -0
- package/lib/core/sync/revision.js +77 -0
- package/lib/core/sync/revision.js.map +1 -0
- package/lib/core/sync/routes.d.ts +9 -0
- package/lib/core/sync/routes.js +339 -0
- package/lib/core/sync/routes.js.map +1 -0
- package/lib/core/sync/scheduler.d.ts +14 -0
- package/lib/core/sync/scheduler.js +81 -0
- package/lib/core/sync/scheduler.js.map +1 -0
- package/lib/core/sync/session-journal.d.ts +2 -0
- package/lib/core/sync/session-journal.js +191 -0
- package/lib/core/sync/session-journal.js.map +1 -0
- package/lib/core/sync/session-routes.d.ts +7 -0
- package/lib/core/sync/session-routes.js +67 -0
- package/lib/core/sync/session-routes.js.map +1 -0
- package/lib/core/sync/session-sync.d.ts +2 -0
- package/lib/core/sync/session-sync.js +2 -0
- package/lib/core/sync/ssh-transport.d.ts +2 -0
- package/lib/core/sync/ssh-transport.js +408 -0
- package/lib/core/sync/ssh-transport.js.map +1 -0
- package/lib/core/sync/stores.d.ts +3 -0
- package/lib/core/sync/stores.js +385 -0
- package/lib/core/sync/stores.js.map +1 -0
- package/lib/core/test.d.ts +1 -1
- package/lib/core/workspace-bindings.d.ts +2 -28
- package/lib/core/workspace-bindings.js +14 -14
- package/lib/core/workspace-bindings.js.map +1 -1
- package/lib/core/workspace-instruction-context.d.ts +3 -2260
- package/lib/core/workspace-instruction-context.js +104 -2
- package/lib/core/workspace-instruction-context.js.map +1 -0
- package/lib/core/workspace-instruction-routes.d.ts +1 -1
- package/lib/core/workspace-instructions.d.ts +2 -68
- package/lib/core/workspace-instructions.js +13 -0
- package/lib/core/workspace-instructions.js.map +1 -1
- package/lib/core/workspaces.d.ts +4 -3
- package/lib/core/workspaces.js +17 -1
- package/lib/core/workspaces.js.map +1 -1
- package/lib/host-key-C5afhAUd.d.ts +30 -0
- package/lib/index-VF8bcBeX.d.ts +2293 -0
- package/lib/{workspace-instruction-context-DytAmaPY.js → lib-gdtU_WKT.js} +159 -102
- package/lib/lib-gdtU_WKT.js.map +1 -0
- package/lib/model-BP7Gw1yk.d.ts +104 -0
- package/lib/node-registry-BTnnM5-k.d.ts +112 -0
- package/lib/node-sync-3Z9QLz2n.d.ts +89 -0
- package/lib/protocol-CV7LGE44.d.ts +35 -0
- package/lib/revision-C9sOlDZ6.d.ts +25 -0
- package/lib/router/router-fs.d.ts +12 -3
- package/lib/router/router-fs.js.map +1 -1
- package/lib/session-journal-pgPqcWn9.d.ts +40 -0
- package/lib/session-sync-EdfwUON5.js +2177 -0
- package/lib/session-sync-EdfwUON5.js.map +1 -0
- package/lib/session-sync-g6qKZzjV.d.ts +1252 -0
- package/lib/ssh-transport-BcIiLxsX.d.ts +55 -0
- package/lib/stores-CKyEt0yk.d.ts +31 -0
- package/lib/workspace-bindings-CsB0TIJB.d.ts +28 -0
- package/lib/workspace-instructions-DirYYiku.d.ts +69 -0
- package/package.json +20 -1
- package/lib/workspace-instruction-context-DytAmaPY.js.map +0 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CredentialStore } from "./credentials.js";
|
|
2
|
+
import { AuthGate } from "./gate.js";
|
|
2
3
|
import WebServer from "@deepseek-ai/dsh-host-webserver";
|
|
3
4
|
//#region src/auth/decorate-webserver.d.ts
|
|
4
|
-
declare function decorateWebServer(webServer: WebServer, store: CredentialStore): () => void;
|
|
5
|
+
declare function decorateWebServer(webServer: WebServer, store: CredentialStore, gate?: AuthGate): () => void;
|
|
5
6
|
//#endregion
|
|
6
7
|
export { decorateWebServer };
|
|
7
8
|
//# sourceMappingURL=decorate-webserver.d.ts.map
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { AuthGate, rewriteToLoopback } from "./gate.js";
|
|
2
2
|
//#region src/auth/decorate-webserver.ts
|
|
3
|
-
function
|
|
4
|
-
|
|
3
|
+
function isResourceFallback(fallback) {
|
|
4
|
+
return typeof fallback === "object" && fallback !== null && "resource" in fallback && typeof fallback.resource === "function";
|
|
5
|
+
}
|
|
6
|
+
function decorateWebServer(webServer, store, gate = new AuthGate(store)) {
|
|
5
7
|
const origRegister = webServer.register.bind(webServer);
|
|
6
|
-
const
|
|
8
|
+
const fallbackServer = webServer;
|
|
9
|
+
const origRegisterFallback = fallbackServer.registerFallback.bind(webServer);
|
|
7
10
|
const origRegisterUpgrade = webServer.registerUpgrade.bind(webServer);
|
|
8
11
|
webServer.register = (route) => {
|
|
9
12
|
return origRegister({
|
|
@@ -11,7 +14,8 @@ function decorateWebServer(webServer, store) {
|
|
|
11
14
|
handler: (req, res) => guarded(route, req, res)
|
|
12
15
|
});
|
|
13
16
|
};
|
|
14
|
-
|
|
17
|
+
fallbackServer.registerFallback = (handler) => {
|
|
18
|
+
if (isResourceFallback(handler)) return origRegisterFallback(handler);
|
|
15
19
|
return origRegisterFallback((req, res) => guarded({
|
|
16
20
|
kind: "exact",
|
|
17
21
|
path: "/",
|
|
@@ -24,8 +28,7 @@ function decorateWebServer(webServer, store) {
|
|
|
24
28
|
handler: (req, socket, head) => guardedUpgrade(route, req, socket, head)
|
|
25
29
|
});
|
|
26
30
|
};
|
|
27
|
-
const
|
|
28
|
-
const origFallback = ws.fallback;
|
|
31
|
+
const origFallback = fallbackServer.fallback;
|
|
29
32
|
if (typeof origFallback === "function" && !origFallback.__dshbGated) {
|
|
30
33
|
const wrapped = (req, res) => guarded({
|
|
31
34
|
kind: "exact",
|
|
@@ -33,7 +36,7 @@ function decorateWebServer(webServer, store) {
|
|
|
33
36
|
handler: origFallback
|
|
34
37
|
}, req, res);
|
|
35
38
|
wrapped.__dshbGated = true;
|
|
36
|
-
|
|
39
|
+
fallbackServer.fallback = wrapped;
|
|
37
40
|
}
|
|
38
41
|
function guarded(route, req, res) {
|
|
39
42
|
const decision = gate.decide(req);
|
|
@@ -47,6 +50,14 @@ function decorateWebServer(webServer, store) {
|
|
|
47
50
|
res.end();
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
53
|
+
if (decision === "forbidden") {
|
|
54
|
+
res.writeHead(403, { "content-type": "application/json" });
|
|
55
|
+
res.end(JSON.stringify({
|
|
56
|
+
ok: false,
|
|
57
|
+
error: "forbidden"
|
|
58
|
+
}));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
50
61
|
res.writeHead(401, { "content-type": "application/json" });
|
|
51
62
|
res.end(JSON.stringify({
|
|
52
63
|
ok: false,
|
|
@@ -59,14 +70,14 @@ function decorateWebServer(webServer, store) {
|
|
|
59
70
|
if (decision === "allow") rewriteToLoopback(req, webServer.port);
|
|
60
71
|
return route.handler(req, socket, head);
|
|
61
72
|
}
|
|
62
|
-
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
73
|
+
socket.write(decision === "forbidden" ? "HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n" : "HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
63
74
|
socket.destroy();
|
|
64
75
|
}
|
|
65
76
|
return () => {
|
|
66
77
|
webServer.register = origRegister;
|
|
67
|
-
|
|
78
|
+
fallbackServer.registerFallback = origRegisterFallback;
|
|
68
79
|
webServer.registerUpgrade = origRegisterUpgrade;
|
|
69
|
-
|
|
80
|
+
fallbackServer.fallback = origFallback;
|
|
70
81
|
};
|
|
71
82
|
}
|
|
72
83
|
//#endregion
|
|
@@ -1 +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
|
|
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\ntype ResourceFallback = { resource: unknown }\ntype CompatibleFallback = WebRoute['handler'] | ResourceFallback\n\nfunction isResourceFallback(fallback: CompatibleFallback): fallback is ResourceFallback {\n return typeof fallback === 'object'\n && fallback !== null\n && 'resource' in fallback\n && typeof fallback.resource === 'function'\n}\n\nexport function decorateWebServer(\n webServer: WebServer,\n store: CredentialStore,\n gate = new AuthGate(store),\n): () => void {\n const origRegister = webServer.register.bind(webServer)\n const fallbackServer = webServer as unknown as {\n registerFallback: (fallback: CompatibleFallback) => () => void\n fallback?: CompatibleFallback & { __dshbGated?: true }\n }\n const origRegisterFallback = fallbackServer.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 fallbackServer.registerFallback = (handler: CompatibleFallback): (() => void) => {\n // Newer DSH releases register SPA fallbacks as resources. Their resource\n // callback owns index authorization and must keep its original contract.\n if (isResourceFallback(handler)) return origRegisterFallback(handler)\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 origFallback = fallbackServer.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 fallbackServer.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 if (decision === 'forbidden') {\n res.writeHead(403, { 'content-type': 'application/json' })\n res.end(JSON.stringify({ ok: false, error: 'forbidden' }))\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(decision === 'forbidden'\n ? 'HTTP/1.1 403 Forbidden\\r\\nConnection: close\\r\\n\\r\\n'\n : 'HTTP/1.1 401 Unauthorized\\r\\nConnection: close\\r\\n\\r\\n')\n socket.destroy()\n }\n\n return () => {\n webServer.register = origRegister\n fallbackServer.registerFallback = origRegisterFallback\n webServer.registerUpgrade = origRegisterUpgrade\n fallbackServer.fallback = origFallback\n }\n}\n"],"mappings":";;AAUA,SAAS,mBAAmB,UAA4D;CACtF,OAAO,OAAO,aAAa,YACtB,aAAa,QACb,cAAc,YACd,OAAO,SAAS,aAAa;AACpC;AAEA,SAAgB,kBACd,WACA,OACA,OAAO,IAAI,SAAS,KAAK,GACb;CACZ,MAAM,eAAe,UAAU,SAAS,KAAK,SAAS;CACtD,MAAM,iBAAiB;CAIvB,MAAM,uBAAuB,eAAe,iBAAiB,KAAK,SAAS;CAC3E,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,eAAe,oBAAoB,YAA8C;EAG/E,IAAI,mBAAmB,OAAO,GAAG,OAAO,qBAAqB,OAAO;EACpE,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,eAAe,eAAe;CACpC,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,eAAe,WAAW;CAC5B;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,aAAa,aAAa;GAC5B,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;GACzD,IAAI,IAAI,KAAK,UAAU;IAAE,IAAI;IAAO,OAAO;GAAY,CAAC,CAAC;GACzD;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,aAAa,cACtB,wDACA,wDAAwD;EAC5D,OAAO,QAAQ;CACjB;CAEA,aAAa;EACX,UAAU,WAAW;EACrB,eAAe,mBAAmB;EAClC,UAAU,kBAAkB;EAC5B,eAAe,WAAW;CAC5B;AACF"}
|
package/lib/auth/gate.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { CredentialStore } from "./credentials.js";
|
|
2
2
|
import { IncomingMessage } from "node:http";
|
|
3
3
|
//#region src/auth/gate.d.ts
|
|
4
|
-
type GateDecision = 'allow' | 'exempt' | 'redirect-login' | 'reject';
|
|
4
|
+
type GateDecision = 'allow' | 'exempt' | 'redirect-login' | 'reject' | 'forbidden';
|
|
5
5
|
declare class AuthGate {
|
|
6
6
|
private readonly store;
|
|
7
|
+
private isTrustedAuthRequest;
|
|
7
8
|
constructor(store: CredentialStore);
|
|
9
|
+
setAuthRequestTrustCheck(check: (req: IncomingMessage) => boolean): void;
|
|
8
10
|
decide(req: IncomingMessage): GateDecision;
|
|
9
11
|
private isPageRequest;
|
|
10
12
|
}
|
package/lib/auth/gate.js
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import { isLoopbackRequest } from "./loopback.js";
|
|
2
2
|
import { SESSION_COOKIE, readCookie, verifySession } from "./session.js";
|
|
3
3
|
//#region src/auth/gate.ts
|
|
4
|
+
function isTrustedLoopbackAuthRequest(req) {
|
|
5
|
+
if (!isLoopbackRequest(req.socket.remoteAddress, req.headers.host)) return false;
|
|
6
|
+
if (req.headers["sec-fetch-site"] === "cross-site") return false;
|
|
7
|
+
const origin = req.headers.origin;
|
|
8
|
+
if (origin === void 0) return true;
|
|
9
|
+
if (typeof origin !== "string" || !req.headers.host) return false;
|
|
10
|
+
try {
|
|
11
|
+
return new URL(origin).host === new URL(`http://${req.headers.host}`).host;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
4
16
|
var AuthGate = class {
|
|
5
17
|
store;
|
|
18
|
+
isTrustedAuthRequest;
|
|
6
19
|
constructor(store) {
|
|
7
20
|
this.store = store;
|
|
8
21
|
}
|
|
22
|
+
setAuthRequestTrustCheck(check) {
|
|
23
|
+
this.isTrustedAuthRequest = check;
|
|
24
|
+
}
|
|
9
25
|
decide(req) {
|
|
10
26
|
const path = new URL(req.url ?? "/", "http://x").pathname;
|
|
11
|
-
if (path === "/login"
|
|
27
|
+
if (path === "/login") return "exempt";
|
|
28
|
+
if (path === "/api/auth" || path.startsWith("/api/auth/")) return (this.isTrustedAuthRequest ? this.isTrustedAuthRequest(req) : isTrustedLoopbackAuthRequest(req)) ? "exempt" : "forbidden";
|
|
12
29
|
if (isLoopbackRequest(req.socket.remoteAddress, req.headers.host)) return "allow";
|
|
13
30
|
const secret = this.store.sessionSecret;
|
|
14
31
|
const username = this.store.username;
|
package/lib/auth/gate.js.map
CHANGED
|
@@ -1 +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 localPort = port ?? req.socket.localPort\n if (typeof localPort !== 'number') return\n const authority = `127.0.0.1:${localPort}`\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;
|
|
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' | 'forbidden'\n\nfunction isTrustedLoopbackAuthRequest(req: IncomingMessage): boolean {\n if (!isLoopbackRequest(req.socket.remoteAddress, req.headers.host)) return false\n if (req.headers['sec-fetch-site'] === 'cross-site') return false\n const origin = req.headers.origin\n if (origin === undefined) return true\n if (typeof origin !== 'string' || !req.headers.host) return false\n try {\n return new URL(origin).host === new URL(`http://${req.headers.host}`).host\n } catch {\n return false\n }\n}\n\nexport class AuthGate {\n private isTrustedAuthRequest: ((req: IncomingMessage) => boolean) | undefined\n\n constructor(private readonly store: CredentialStore) {}\n\n setAuthRequestTrustCheck(check: (req: IncomingMessage) => boolean): void {\n this.isTrustedAuthRequest = check\n }\n\n decide(req: IncomingMessage): GateDecision {\n const path = new URL(req.url ?? '/', 'http://x').pathname\n if (path === '/login') return 'exempt'\n if (path === '/api/auth' || path.startsWith('/api/auth/')) {\n const trusted = this.isTrustedAuthRequest\n ? this.isTrustedAuthRequest(req)\n : isTrustedLoopbackAuthRequest(req)\n return trusted ? 'exempt' : 'forbidden'\n }\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 localPort = port ?? req.socket.localPort\n if (typeof localPort !== 'number') return\n const authority = `127.0.0.1:${localPort}`\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,SAAS,6BAA6B,KAA+B;CACnE,IAAI,CAAC,kBAAkB,IAAI,OAAO,eAAe,IAAI,QAAQ,IAAI,GAAG,OAAO;CAC3E,IAAI,IAAI,QAAQ,sBAAsB,cAAc,OAAO;CAC3D,MAAM,SAAS,IAAI,QAAQ;CAC3B,IAAI,WAAW,KAAA,GAAW,OAAO;CACjC,IAAI,OAAO,WAAW,YAAY,CAAC,IAAI,QAAQ,MAAM,OAAO;CAC5D,IAAI;EACF,OAAO,IAAI,IAAI,MAAM,CAAC,CAAC,SAAS,IAAI,IAAI,UAAU,IAAI,QAAQ,MAAM,CAAC,CAAC;CACxE,QAAQ;EACN,OAAO;CACT;AACF;AAEA,IAAa,WAAb,MAAsB;CAGS;CAF7B;CAEA,YAAY,OAAyC;EAAxB,KAAA,QAAA;CAAyB;CAEtD,yBAAyB,OAAgD;EACvE,KAAK,uBAAuB;CAC9B;CAEA,OAAO,KAAoC;EACzC,MAAM,OAAO,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC;EACjD,IAAI,SAAS,UAAU,OAAO;EAC9B,IAAI,SAAS,eAAe,KAAK,WAAW,YAAY,GAItD,QAHgB,KAAK,uBACjB,KAAK,qBAAqB,GAAG,IAC7B,6BAA6B,GAAG,KACnB,WAAW;EAE9B,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,MAAqB;CAC3E,MAAM,YAAY,QAAQ,IAAI,OAAO;CACrC,IAAI,OAAO,cAAc,UAAU;CACnC,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"}
|
package/lib/auth/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sharedCredentialStore } from "./credentials.js";
|
|
2
|
+
import { AuthGate } from "./gate.js";
|
|
2
3
|
import { decorateWebServer } from "./decorate-webserver.js";
|
|
3
4
|
import { installLoopbackCompat } from "./loopback-compat.js";
|
|
4
5
|
import { LoginRateLimiter } from "./ratelimit.js";
|
|
@@ -41,16 +42,23 @@ function registerFrontendBootRaceWorkaround(ctx) {
|
|
|
41
42
|
} catch {}
|
|
42
43
|
}
|
|
43
44
|
function apply(ctx) {
|
|
44
|
-
|
|
45
|
+
const store = sharedCredentialStore();
|
|
46
|
+
const gate = new AuthGate(store);
|
|
47
|
+
decorateWebServer(ctx.webServer, store, gate);
|
|
45
48
|
registerFrontendBootRaceWorkaround(ctx);
|
|
46
49
|
ctx.webServer.tapIndex(installLoopbackCompat);
|
|
47
|
-
registerAuthRoutes(ctx,
|
|
50
|
+
registerAuthRoutes(ctx, store, new LoginRateLimiter());
|
|
48
51
|
ctx.inject(["connection"], (connCtx) => {
|
|
49
|
-
const
|
|
52
|
+
const conn = connCtx.connection;
|
|
53
|
+
const auth = conn?.browserAuth;
|
|
50
54
|
if (auth && typeof auth.isAuthenticated === "function" && !auth.__dshbNeutralized) {
|
|
51
55
|
auth.isAuthenticated = () => true;
|
|
52
56
|
auth.__dshbNeutralized = true;
|
|
53
57
|
}
|
|
58
|
+
if (conn && typeof conn.requestRejection === "function") gate.setAuthRequestTrustCheck((req) => {
|
|
59
|
+
const rejection = conn.requestRejection(req);
|
|
60
|
+
return rejection === void 0 || rejection === 401;
|
|
61
|
+
});
|
|
54
62
|
});
|
|
55
63
|
}
|
|
56
64
|
//#endregion
|
package/lib/auth/index.js.map
CHANGED
|
@@ -1 +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 the frontend through the same `dsh-web-app` bundle the webserver\n // loads, so the patched hashed asset is the one the static fallback serves.\n // `process.cwd()` may hold a different installation whose asset filename\n // the server never requests, leaving the served bundle unpatched.\n const resolver = createRequire(import.meta.url)\n let manifestPath: string\n try {\n const webAppManifest = resolver.resolve('@deepseek-ai/dsh-web-app/package.json')\n manifestPath = createRequire(webAppManifest).resolve('@deepseek-ai/dsh-web-frontend/package.json')\n } catch {\n manifestPath = resolver.resolve('@deepseek-ai/dsh-web-frontend/package.json', {\n paths: [process.cwd()],\n })\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 asserts before api-remotes' dynamic remote namespace fibers settle.\n // Delay the assertion so mountApp sees the complete client graph; real\n // activation failures still throw after the bounded wait. The minified\n // identifiers vary between builds, and 0.1.7 moved the assertion into nE.\n const legacyPatch = source.replace(\n /await ([A-Za-z_$][\\w$]*)\\.await\\(\\),this\\.assertEntriesActive\\(([A-Za-z_$][\\w$]*)\\)/,\n 'await $1.await(),await new Promise(r=>setTimeout(r,8000)),this.assertEntriesActive($2)',\n )\n const patched = legacyPatch !== source\n ? legacyPatch\n : source.replace(\n /await ([A-Za-z_$][\\w$]*)\\.await\\(\\),([A-Za-z_$][\\w$]*)\\(([A-Za-z_$][\\w$]*),([A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)*)\\)/,\n 'await $1.await(),await new Promise(r=>setTimeout(r,8000)),$2($3,$4)',\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,
|
|
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 { IncomingMessage } from 'node:http'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { sharedCredentialStore } from './credentials.js'\nimport { decorateWebServer } from './decorate-webserver.js'\nimport { AuthGate } from './gate.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 the frontend through the same `dsh-web-app` bundle the webserver\n // loads, so the patched hashed asset is the one the static fallback serves.\n // `process.cwd()` may hold a different installation whose asset filename\n // the server never requests, leaving the served bundle unpatched.\n const resolver = createRequire(import.meta.url)\n let manifestPath: string\n try {\n const webAppManifest = resolver.resolve('@deepseek-ai/dsh-web-app/package.json')\n manifestPath = createRequire(webAppManifest).resolve('@deepseek-ai/dsh-web-frontend/package.json')\n } catch {\n manifestPath = resolver.resolve('@deepseek-ai/dsh-web-frontend/package.json', {\n paths: [process.cwd()],\n })\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 asserts before api-remotes' dynamic remote namespace fibers settle.\n // Delay the assertion so mountApp sees the complete client graph; real\n // activation failures still throw after the bounded wait. The minified\n // identifiers vary between builds, and 0.1.7 moved the assertion into nE.\n const legacyPatch = source.replace(\n /await ([A-Za-z_$][\\w$]*)\\.await\\(\\),this\\.assertEntriesActive\\(([A-Za-z_$][\\w$]*)\\)/,\n 'await $1.await(),await new Promise(r=>setTimeout(r,8000)),this.assertEntriesActive($2)',\n )\n const patched = legacyPatch !== source\n ? legacyPatch\n : source.replace(\n /await ([A-Za-z_$][\\w$]*)\\.await\\(\\),([A-Za-z_$][\\w$]*)\\(([A-Za-z_$][\\w$]*),([A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*)*)\\)/,\n 'await $1.await(),await new Promise(r=>setTimeout(r,8000)),$2($3,$4)',\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 const store = sharedCredentialStore()\n const gate = new AuthGate(store)\n decorateWebServer(ctx.webServer, store, gate)\n registerFrontendBootRaceWorkaround(ctx)\n ctx.webServer.tapIndex(installLoopbackCompat)\n registerAuthRoutes(ctx, store, 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 {\n connection?: {\n browserAuth?: { isAuthenticated?: (...a: unknown[]) => unknown; __dshbNeutralized?: true }\n requestRejection?: (request: IncomingMessage) => number | undefined\n }\n }).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 if (conn && typeof conn.requestRejection === 'function') {\n gate.setAuthRequestTrustCheck((req) => {\n const rejection = conn.requestRejection!(req)\n // DSH 401 is its browser-cookie gate; DSHB owns that check for these routes.\n return rejection === undefined || rejection === 401\n })\n }\n })\n}\n"],"mappings":";;;;;;;;;;AAYA,MAAa,OAAO;AAEpB,MAAa,SAAS,CAAC,WAAW;AAElC,SAAS,mCAAmC,KAAoB;CAC9D,IAAI;EAKF,MAAM,WAAW,cAAc,YAAY,GAAG;EAC9C,IAAI;EACJ,IAAI;GACF,MAAM,iBAAiB,SAAS,QAAQ,uCAAuC;GAC/E,eAAe,cAAc,cAAc,CAAC,CAAC,QAAQ,4CAA4C;EACnG,QAAQ;GACN,eAAe,SAAS,QAAQ,8CAA8C,EAC5E,OAAO,CAAC,QAAQ,IAAI,CAAC,EACvB,CAAC;EACH;EACA,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;EAK7C,MAAM,cAAc,OAAO,QACzB,uFACA,wFACF;EACA,MAAM,UAAU,gBAAgB,SAC5B,cACA,OAAO,QACL,0HACA,qEACF;EACJ,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,MAAM,QAAQ,sBAAsB;CACpC,MAAM,OAAO,IAAI,SAAS,KAAK;CAC/B,kBAAkB,IAAI,WAAW,OAAO,IAAI;CAC5C,mCAAmC,GAAG;CACtC,IAAI,UAAU,SAAS,qBAAqB;CAC5C,mBAAmB,KAAK,OAAO,IAAI,iBAAiB,CAAC;CAWrD,IAAI,OAAO,CAAC,YAAY,IAAI,YAAqB;EAC/C,MAAM,OAAQ,QAKX;EACH,MAAM,OAAO,MAAM;EACnB,IAAI,QAAQ,OAAO,KAAK,oBAAoB,cAAc,CAAC,KAAK,mBAAmB;GACjF,KAAK,wBAAwB;GAC7B,KAAK,oBAAoB;EAC3B;EACA,IAAI,QAAQ,OAAO,KAAK,qBAAqB,YAC3C,KAAK,0BAA0B,QAAQ;GACrC,MAAM,YAAY,KAAK,iBAAkB,GAAG;GAE5C,OAAO,cAAc,KAAA,KAAa,cAAc;EAClD,CAAC;CAEL,CAAC;AACH"}
|
package/lib/auth/webserver.d.ts
CHANGED
|
@@ -8,7 +8,9 @@ declare class AuthedWebServer extends WebServer {
|
|
|
8
8
|
port: number;
|
|
9
9
|
});
|
|
10
10
|
register(route: WebRoute): () => void;
|
|
11
|
-
registerFallback(handler: WebRoute['handler']
|
|
11
|
+
registerFallback(handler: WebRoute['handler'] | {
|
|
12
|
+
resource: unknown;
|
|
13
|
+
}): () => void;
|
|
12
14
|
registerUpgrade(route: WebUpgradeRoute): () => void;
|
|
13
15
|
private guarded;
|
|
14
16
|
private guardedUpgrade;
|
package/lib/auth/webserver.js
CHANGED
|
@@ -14,6 +14,7 @@ var AuthedWebServer = class extends WebServer {
|
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
registerFallback(handler) {
|
|
17
|
+
if (typeof handler === "object" && handler !== null && "resource" in handler && typeof handler.resource === "function") return super.registerFallback(handler);
|
|
17
18
|
return super.registerFallback((req, res) => this.guarded({
|
|
18
19
|
kind: "exact",
|
|
19
20
|
path: "/",
|
|
@@ -38,6 +39,14 @@ var AuthedWebServer = class extends WebServer {
|
|
|
38
39
|
res.end();
|
|
39
40
|
return;
|
|
40
41
|
}
|
|
42
|
+
if (decision === "forbidden") {
|
|
43
|
+
res.writeHead(403, { "content-type": "application/json" });
|
|
44
|
+
res.end(JSON.stringify({
|
|
45
|
+
ok: false,
|
|
46
|
+
error: "forbidden"
|
|
47
|
+
}));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
41
50
|
res.writeHead(401, { "content-type": "application/json" });
|
|
42
51
|
res.end(JSON.stringify({
|
|
43
52
|
ok: false,
|
|
@@ -50,7 +59,7 @@ var AuthedWebServer = class extends WebServer {
|
|
|
50
59
|
if (decision === "allow") rewriteToLoopback(req, this.port);
|
|
51
60
|
return route.handler(req, socket, head);
|
|
52
61
|
}
|
|
53
|
-
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
62
|
+
socket.write(decision === "forbidden" ? "HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n" : "HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
54
63
|
socket.destroy();
|
|
55
64
|
}
|
|
56
65
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webserver.js","names":[],"sources":["../../src/auth/webserver.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport WebServer from '@deepseek-ai/dsh-host-webserver'\nimport type { WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { sharedCredentialStore } from './credentials.js'\nimport { AuthGate, rewriteToLoopback } from './gate.js'\n\nexport default class AuthedWebServer extends WebServer {\n private readonly gate = new AuthGate(sharedCredentialStore())\n\n constructor(ctx: Context, config: { host: string; port: number }) {\n super(ctx, config)\n }\n\n override register(route: WebRoute): () => void {\n return super.register({ ...route, handler: (req, res) => this.guarded(route, req, res) })\n }\n\n override registerFallback(handler: WebRoute['handler']): () => void {\n return super.registerFallback((req, res) =>\n this.guarded({ kind: 'exact', path: '/', handler }, req, res),\n )\n }\n\n override registerUpgrade(route: WebUpgradeRoute): () => void {\n return super.registerUpgrade({\n ...route,\n handler: (req, socket, head) => this.guardedUpgrade(route, req, socket, head),\n })\n }\n\n private guarded(route: WebRoute, req: IncomingMessage, res: ServerResponse): void | Promise<void> {\n const decision = this.gate.decide(req)\n if (decision === 'allow') {\n rewriteToLoopback(req, this.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 private guardedUpgrade(\n route: WebUpgradeRoute,\n req: IncomingMessage,\n socket: Duplex,\n head: Buffer,\n ): void | Promise<void> {\n const decision = this.gate.decide(req)\n if (decision === 'allow' || decision === 'exempt') {\n if (decision === 'allow') rewriteToLoopback(req, this.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"],"mappings":";;;;AAQA,IAAqB,kBAArB,cAA6C,UAAU;CACrD,OAAwB,IAAI,SAAS,sBAAsB,CAAC;CAE5D,YAAY,KAAc,QAAwC;EAChE,MAAM,KAAK,MAAM;CACnB;CAEA,SAAkB,OAA6B;EAC7C,OAAO,MAAM,SAAS;GAAE,GAAG;GAAO,UAAU,KAAK,QAAQ,KAAK,QAAQ,OAAO,KAAK,GAAG;EAAE,CAAC;CAC1F;CAEA,iBAA0B,
|
|
1
|
+
{"version":3,"file":"webserver.js","names":[],"sources":["../../src/auth/webserver.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport WebServer from '@deepseek-ai/dsh-host-webserver'\nimport type { WebRoute, WebUpgradeRoute } from '@deepseek-ai/dsh-host-webserver'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { sharedCredentialStore } from './credentials.js'\nimport { AuthGate, rewriteToLoopback } from './gate.js'\n\nexport default class AuthedWebServer extends WebServer {\n private readonly gate = new AuthGate(sharedCredentialStore())\n\n constructor(ctx: Context, config: { host: string; port: number }) {\n super(ctx, config)\n }\n\n override register(route: WebRoute): () => void {\n return super.register({ ...route, handler: (req, res) => this.guarded(route, req, res) })\n }\n\n override registerFallback(handler: WebRoute['handler'] | { resource: unknown }): () => void {\n if (typeof handler === 'object'\n && handler !== null\n && 'resource' in handler\n && typeof handler.resource === 'function') {\n return super.registerFallback(handler as unknown as WebRoute['handler'])\n }\n return super.registerFallback((req, res) =>\n this.guarded({ kind: 'exact', path: '/', handler: handler as WebRoute['handler'] }, req, res),\n )\n }\n\n override registerUpgrade(route: WebUpgradeRoute): () => void {\n return super.registerUpgrade({\n ...route,\n handler: (req, socket, head) => this.guardedUpgrade(route, req, socket, head),\n })\n }\n\n private guarded(route: WebRoute, req: IncomingMessage, res: ServerResponse): void | Promise<void> {\n const decision = this.gate.decide(req)\n if (decision === 'allow') {\n rewriteToLoopback(req, this.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 if (decision === 'forbidden') {\n res.writeHead(403, { 'content-type': 'application/json' })\n res.end(JSON.stringify({ ok: false, error: 'forbidden' }))\n return\n }\n res.writeHead(401, { 'content-type': 'application/json' })\n res.end(JSON.stringify({ ok: false, error: 'unauthorized' }))\n }\n\n private guardedUpgrade(\n route: WebUpgradeRoute,\n req: IncomingMessage,\n socket: Duplex,\n head: Buffer,\n ): void | Promise<void> {\n const decision = this.gate.decide(req)\n if (decision === 'allow' || decision === 'exempt') {\n if (decision === 'allow') rewriteToLoopback(req, this.port)\n return route.handler(req, socket, head)\n }\n socket.write(decision === 'forbidden'\n ? 'HTTP/1.1 403 Forbidden\\r\\nConnection: close\\r\\n\\r\\n'\n : 'HTTP/1.1 401 Unauthorized\\r\\nConnection: close\\r\\n\\r\\n')\n socket.destroy()\n }\n}\n"],"mappings":";;;;AAQA,IAAqB,kBAArB,cAA6C,UAAU;CACrD,OAAwB,IAAI,SAAS,sBAAsB,CAAC;CAE5D,YAAY,KAAc,QAAwC;EAChE,MAAM,KAAK,MAAM;CACnB;CAEA,SAAkB,OAA6B;EAC7C,OAAO,MAAM,SAAS;GAAE,GAAG;GAAO,UAAU,KAAK,QAAQ,KAAK,QAAQ,OAAO,KAAK,GAAG;EAAE,CAAC;CAC1F;CAEA,iBAA0B,SAAkE;EAC1F,IAAI,OAAO,YAAY,YAClB,YAAY,QACZ,cAAc,WACd,OAAO,QAAQ,aAAa,YAC/B,OAAO,MAAM,iBAAiB,OAAyC;EAEzE,OAAO,MAAM,kBAAkB,KAAK,QAClC,KAAK,QAAQ;GAAE,MAAM;GAAS,MAAM;GAAc;EAA+B,GAAG,KAAK,GAAG,CAC9F;CACF;CAEA,gBAAyB,OAAoC;EAC3D,OAAO,MAAM,gBAAgB;GAC3B,GAAG;GACH,UAAU,KAAK,QAAQ,SAAS,KAAK,eAAe,OAAO,KAAK,QAAQ,IAAI;EAC9E,CAAC;CACH;CAEA,QAAgB,OAAiB,KAAsB,KAA2C;EAChG,MAAM,WAAW,KAAK,KAAK,OAAO,GAAG;EACrC,IAAI,aAAa,SAAS;GACxB,kBAAkB,KAAK,KAAK,IAAI;GAChC,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,aAAa,aAAa;GAC5B,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;GACzD,IAAI,IAAI,KAAK,UAAU;IAAE,IAAI;IAAO,OAAO;GAAY,CAAC,CAAC;GACzD;EACF;EACA,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;EACzD,IAAI,IAAI,KAAK,UAAU;GAAE,IAAI;GAAO,OAAO;EAAe,CAAC,CAAC;CAC9D;CAEA,eACE,OACA,KACA,QACA,MACsB;EACtB,MAAM,WAAW,KAAK,KAAK,OAAO,GAAG;EACrC,IAAI,aAAa,WAAW,aAAa,UAAU;GACjD,IAAI,aAAa,SAAS,kBAAkB,KAAK,KAAK,IAAI;GAC1D,OAAO,MAAM,QAAQ,KAAK,QAAQ,IAAI;EACxC;EACA,OAAO,MAAM,aAAa,cACtB,wDACA,wDAAwD;EAC5D,OAAO,QAAQ;CACjB;AACF"}
|