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