@wrongstack/webui 0.31.1 → 0.41.0
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/dist/server/entry.js +106 -88
- package/dist/server/entry.js.map +1 -1
- package/dist/server/index.js +106 -88
- package/dist/server/index.js.map +1 -1
- package/package.json +5 -5
package/dist/server/entry.js
CHANGED
|
@@ -1,8 +1,90 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// src/server/index.ts
|
|
3
|
-
import * as
|
|
3
|
+
import * as fs3 from "fs/promises";
|
|
4
|
+
import * as path2 from "path";
|
|
5
|
+
|
|
6
|
+
// src/server/http-server.ts
|
|
7
|
+
import * as fs from "fs/promises";
|
|
4
8
|
import * as http from "http";
|
|
5
9
|
import * as path from "path";
|
|
10
|
+
var MIME_TYPES = {
|
|
11
|
+
".html": "text/html",
|
|
12
|
+
".js": "application/javascript",
|
|
13
|
+
".css": "text/css",
|
|
14
|
+
".json": "application/json",
|
|
15
|
+
".svg": "image/svg+xml",
|
|
16
|
+
".png": "image/png",
|
|
17
|
+
".ico": "image/x-icon"
|
|
18
|
+
};
|
|
19
|
+
function buildCspHeader(wsPort2) {
|
|
20
|
+
return `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://127.0.0.1:${wsPort2} wss://127.0.0.1:${wsPort2} ws://[::1]:${wsPort2} wss://[::1]:${wsPort2}; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`;
|
|
21
|
+
}
|
|
22
|
+
function isInsideDist(candidate, distDir) {
|
|
23
|
+
const root = path.resolve(distDir);
|
|
24
|
+
const resolved = path.resolve(candidate);
|
|
25
|
+
return resolved === root || resolved.startsWith(root + path.sep);
|
|
26
|
+
}
|
|
27
|
+
function createHttpServer(opts) {
|
|
28
|
+
const port = opts.port ?? Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
29
|
+
const distDir = path.resolve(opts.distDir);
|
|
30
|
+
const wsPort2 = opts.wsPort;
|
|
31
|
+
return http.createServer(async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
|
|
34
|
+
let filePath;
|
|
35
|
+
if (url.pathname === "/" || url.pathname === "") {
|
|
36
|
+
filePath = path.join(distDir, "index.html");
|
|
37
|
+
} else if (url.pathname.startsWith("/assets/")) {
|
|
38
|
+
filePath = path.join(distDir, url.pathname);
|
|
39
|
+
} else if (url.pathname.startsWith("/")) {
|
|
40
|
+
filePath = path.join(distDir, url.pathname);
|
|
41
|
+
} else {
|
|
42
|
+
filePath = path.join(distDir, "index.html");
|
|
43
|
+
}
|
|
44
|
+
const resolvedPath = path.resolve(filePath);
|
|
45
|
+
if (!isInsideDist(resolvedPath, distDir)) {
|
|
46
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
47
|
+
res.end("Forbidden");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const ext = path.extname(resolvedPath);
|
|
51
|
+
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
52
|
+
res.setHeader("Content-Type", contentType);
|
|
53
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
54
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
55
|
+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
56
|
+
if (ext === ".html") {
|
|
57
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
58
|
+
res.setHeader("Content-Security-Policy", buildCspHeader(wsPort2));
|
|
59
|
+
}
|
|
60
|
+
const fileContent = await fs.readFile(resolvedPath);
|
|
61
|
+
res.writeHead(200);
|
|
62
|
+
res.end(fileContent);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err.code === "ENOENT") {
|
|
65
|
+
try {
|
|
66
|
+
const fileContent = await fs.readFile(path.join(distDir, "index.html"));
|
|
67
|
+
res.writeHead(200, {
|
|
68
|
+
"Content-Type": "text/html",
|
|
69
|
+
"X-Content-Type-Options": "nosniff",
|
|
70
|
+
"X-Frame-Options": "DENY",
|
|
71
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
72
|
+
"Content-Security-Policy": buildCspHeader(wsPort2)
|
|
73
|
+
});
|
|
74
|
+
res.end(fileContent);
|
|
75
|
+
} catch {
|
|
76
|
+
res.writeHead(404);
|
|
77
|
+
res.end("Not found");
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
res.writeHead(500);
|
|
81
|
+
res.end("Server error");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/server/index.ts
|
|
6
88
|
import {
|
|
7
89
|
Agent,
|
|
8
90
|
AutoCompactionMiddleware,
|
|
@@ -112,7 +194,7 @@ function createDefaultContainer(opts) {
|
|
|
112
194
|
}
|
|
113
195
|
|
|
114
196
|
// src/server/boot.ts
|
|
115
|
-
import * as
|
|
197
|
+
import * as fs2 from "fs/promises";
|
|
116
198
|
import * as os from "os";
|
|
117
199
|
import {
|
|
118
200
|
DefaultConfigLoader,
|
|
@@ -120,7 +202,8 @@ import {
|
|
|
120
202
|
DefaultPathResolver,
|
|
121
203
|
DefaultSecretVault,
|
|
122
204
|
migratePlaintextSecrets,
|
|
123
|
-
resolveWstackPaths
|
|
205
|
+
resolveWstackPaths,
|
|
206
|
+
writeErr
|
|
124
207
|
} from "@wrongstack/core";
|
|
125
208
|
async function bootConfig() {
|
|
126
209
|
const cwd = process.cwd();
|
|
@@ -128,15 +211,15 @@ async function bootConfig() {
|
|
|
128
211
|
const projectRoot = pathResolver.projectRoot;
|
|
129
212
|
const userHome = os.homedir();
|
|
130
213
|
const wpaths = resolveWstackPaths({ projectRoot, userHome });
|
|
131
|
-
await
|
|
132
|
-
await
|
|
133
|
-
await
|
|
214
|
+
await fs2.mkdir(wpaths.globalRoot, { recursive: true });
|
|
215
|
+
await fs2.mkdir(wpaths.projectDir, { recursive: true });
|
|
216
|
+
await fs2.mkdir(wpaths.projectSessions, { recursive: true });
|
|
134
217
|
const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey });
|
|
135
218
|
for (const file of [wpaths.globalConfig, wpaths.projectLocalConfig]) {
|
|
136
219
|
try {
|
|
137
220
|
const { migrated } = await migratePlaintextSecrets(file, vault);
|
|
138
221
|
if (migrated > 0) {
|
|
139
|
-
|
|
222
|
+
writeErr(`[WebUI] Encrypted ${migrated} plaintext secret(s) in ${file}
|
|
140
223
|
`);
|
|
141
224
|
}
|
|
142
225
|
} catch {
|
|
@@ -1528,7 +1611,7 @@ async function startWebUI(opts = {}) {
|
|
|
1528
1611
|
inputCost,
|
|
1529
1612
|
outputCost,
|
|
1530
1613
|
cacheReadCost,
|
|
1531
|
-
projectName:
|
|
1614
|
+
projectName: path2.basename(projectRoot) || projectRoot,
|
|
1532
1615
|
cwd: projectRoot,
|
|
1533
1616
|
mode: modeId,
|
|
1534
1617
|
contextMode: String(context.meta["contextWindowMode"] ?? DEFAULT_CONTEXT_WINDOW_MODE_ID),
|
|
@@ -1762,8 +1845,8 @@ async function startWebUI(opts = {}) {
|
|
|
1762
1845
|
rateLimits.delete(String(ws));
|
|
1763
1846
|
console.log("[WebUI] Client disconnected, total:", clients.size);
|
|
1764
1847
|
if (pendingConfirms.size > 0) {
|
|
1765
|
-
for (const [id,
|
|
1766
|
-
|
|
1848
|
+
for (const [id, resolve3] of pendingConfirms) {
|
|
1849
|
+
resolve3("no");
|
|
1767
1850
|
pendingConfirms.delete(id);
|
|
1768
1851
|
}
|
|
1769
1852
|
}
|
|
@@ -1853,10 +1936,10 @@ async function startWebUI(opts = {}) {
|
|
|
1853
1936
|
}
|
|
1854
1937
|
case "tool.confirm_result": {
|
|
1855
1938
|
const { id, decision } = msg.payload;
|
|
1856
|
-
const
|
|
1857
|
-
if (
|
|
1939
|
+
const resolve3 = pendingConfirms.get(id);
|
|
1940
|
+
if (resolve3) {
|
|
1858
1941
|
pendingConfirms.delete(id);
|
|
1859
|
-
|
|
1942
|
+
resolve3(decision);
|
|
1860
1943
|
}
|
|
1861
1944
|
break;
|
|
1862
1945
|
}
|
|
@@ -2118,7 +2201,7 @@ async function startWebUI(opts = {}) {
|
|
|
2118
2201
|
updateAutoCompactionMaxContext?.(newProv);
|
|
2119
2202
|
try {
|
|
2120
2203
|
configWriteLock = configWriteLock.then(async () => {
|
|
2121
|
-
const raw = await
|
|
2204
|
+
const raw = await fs3.readFile(globalConfigPath, "utf8");
|
|
2122
2205
|
const parsed = JSON.parse(raw);
|
|
2123
2206
|
parsed.provider = newProvider;
|
|
2124
2207
|
parsed.model = newModel;
|
|
@@ -2451,7 +2534,7 @@ async function startWebUI(opts = {}) {
|
|
|
2451
2534
|
if (depth > 8 || results.length >= 600) return;
|
|
2452
2535
|
let entries = [];
|
|
2453
2536
|
try {
|
|
2454
|
-
entries = await
|
|
2537
|
+
entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
2455
2538
|
} catch {
|
|
2456
2539
|
return;
|
|
2457
2540
|
}
|
|
@@ -2464,7 +2547,7 @@ async function startWebUI(opts = {}) {
|
|
|
2464
2547
|
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
2465
2548
|
if (e.isDirectory()) {
|
|
2466
2549
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
2467
|
-
await walk(
|
|
2550
|
+
await walk(path2.join(dir, e.name), childRel, depth + 1);
|
|
2468
2551
|
} else if (e.isFile()) {
|
|
2469
2552
|
results.push(childRel);
|
|
2470
2553
|
}
|
|
@@ -2594,7 +2677,7 @@ async function startWebUI(opts = {}) {
|
|
|
2594
2677
|
}
|
|
2595
2678
|
async function loadSavedProviders() {
|
|
2596
2679
|
try {
|
|
2597
|
-
const raw = await
|
|
2680
|
+
const raw = await fs3.readFile(globalConfigPath, "utf8");
|
|
2598
2681
|
const parsed = JSON.parse(raw);
|
|
2599
2682
|
if (!parsed.providers) return {};
|
|
2600
2683
|
return decryptConfigSecrets(parsed.providers, vault);
|
|
@@ -2606,7 +2689,7 @@ async function startWebUI(opts = {}) {
|
|
|
2606
2689
|
configWriteLock = configWriteLock.then(async () => {
|
|
2607
2690
|
let parsed;
|
|
2608
2691
|
try {
|
|
2609
|
-
const raw = await
|
|
2692
|
+
const raw = await fs3.readFile(globalConfigPath, "utf8");
|
|
2610
2693
|
parsed = JSON.parse(raw);
|
|
2611
2694
|
} catch {
|
|
2612
2695
|
parsed = {};
|
|
@@ -2747,77 +2830,12 @@ async function startWebUI(opts = {}) {
|
|
|
2747
2830
|
sendResult(ws, false, err instanceof Error ? err.message : String(err));
|
|
2748
2831
|
}
|
|
2749
2832
|
}
|
|
2750
|
-
const
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
".js": "application/javascript",
|
|
2755
|
-
".css": "text/css",
|
|
2756
|
-
".json": "application/json",
|
|
2757
|
-
".svg": "image/svg+xml",
|
|
2758
|
-
".png": "image/png",
|
|
2759
|
-
".ico": "image/x-icon"
|
|
2760
|
-
};
|
|
2761
|
-
const httpServer = http.createServer(async (req, res) => {
|
|
2762
|
-
try {
|
|
2763
|
-
const url = new URL(req.url ?? "/", `http://127.0.0.1:${httpPort}`);
|
|
2764
|
-
let filePath;
|
|
2765
|
-
if (url.pathname === "/" || url.pathname === "") {
|
|
2766
|
-
filePath = path.join(DIST_DIR, "index.html");
|
|
2767
|
-
} else if (url.pathname.startsWith("/assets/")) {
|
|
2768
|
-
filePath = path.join(DIST_DIR, url.pathname);
|
|
2769
|
-
} else if (url.pathname.startsWith("/")) {
|
|
2770
|
-
filePath = path.join(DIST_DIR, url.pathname);
|
|
2771
|
-
} else {
|
|
2772
|
-
filePath = path.join(DIST_DIR, "index.html");
|
|
2773
|
-
}
|
|
2774
|
-
const resolvedPath = path.resolve(filePath);
|
|
2775
|
-
const resolvedRoot = path.resolve(DIST_DIR);
|
|
2776
|
-
if (!resolvedPath.startsWith(resolvedRoot + path.sep) && resolvedPath !== resolvedRoot) {
|
|
2777
|
-
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
2778
|
-
res.end("Forbidden");
|
|
2779
|
-
return;
|
|
2780
|
-
}
|
|
2781
|
-
const ext = path.extname(resolvedPath);
|
|
2782
|
-
const contentType = mimeTypes[ext] ?? "application/octet-stream";
|
|
2783
|
-
res.setHeader("Content-Type", contentType);
|
|
2784
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
2785
|
-
res.setHeader("X-Frame-Options", "DENY");
|
|
2786
|
-
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
2787
|
-
if (ext === ".html") {
|
|
2788
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
2789
|
-
res.setHeader(
|
|
2790
|
-
"Content-Security-Policy",
|
|
2791
|
-
`default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://127.0.0.1:${wsPort2} wss://127.0.0.1:${wsPort2} ws://[::1]:${wsPort2} wss://[::1]:${wsPort2}; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`
|
|
2792
|
-
);
|
|
2793
|
-
}
|
|
2794
|
-
const fileContent = await fs2.readFile(resolvedPath);
|
|
2795
|
-
res.writeHead(200);
|
|
2796
|
-
res.end(fileContent);
|
|
2797
|
-
} catch (err) {
|
|
2798
|
-
if (err.code === "ENOENT") {
|
|
2799
|
-
try {
|
|
2800
|
-
const fileContent = await fs2.readFile(path.join(DIST_DIR, "index.html"));
|
|
2801
|
-
res.writeHead(200, {
|
|
2802
|
-
"Content-Type": "text/html",
|
|
2803
|
-
"X-Content-Type-Options": "nosniff",
|
|
2804
|
-
"X-Frame-Options": "DENY",
|
|
2805
|
-
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
2806
|
-
// SPA fallback previously shipped no CSP — apply the same policy as
|
|
2807
|
-
// the direct .html branch so deep-linked routes aren't unprotected.
|
|
2808
|
-
"Content-Security-Policy": `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://127.0.0.1:${wsPort2} wss://127.0.0.1:${wsPort2} ws://[::1]:${wsPort2} wss://[::1]:${wsPort2}; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'`
|
|
2809
|
-
});
|
|
2810
|
-
res.end(fileContent);
|
|
2811
|
-
} catch {
|
|
2812
|
-
res.writeHead(404);
|
|
2813
|
-
res.end("Not found");
|
|
2814
|
-
}
|
|
2815
|
-
} else {
|
|
2816
|
-
res.writeHead(500);
|
|
2817
|
-
res.end("Server error");
|
|
2818
|
-
}
|
|
2819
|
-
}
|
|
2833
|
+
const httpServer = createHttpServer({
|
|
2834
|
+
host: wsHost2,
|
|
2835
|
+
distDir: path2.resolve(import.meta.dirname, "../../dist"),
|
|
2836
|
+
wsPort: wsPort2
|
|
2820
2837
|
});
|
|
2838
|
+
const httpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
|
|
2821
2839
|
httpServer.listen(httpPort, wsHost2, () => {
|
|
2822
2840
|
console.log(`[WebUI] HTTP server running on http://${wsHost2}:${httpPort}`);
|
|
2823
2841
|
});
|