@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.
@@ -1,7 +1,89 @@
1
1
  // src/server/index.ts
2
- import * as fs2 from "fs/promises";
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 fs from "fs/promises";
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 fs.mkdir(wpaths.globalRoot, { recursive: true });
131
- await fs.mkdir(wpaths.projectDir, { recursive: true });
132
- await fs.mkdir(wpaths.projectSessions, { recursive: true });
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
- process.stderr.write(`[WebUI] Encrypted ${migrated} plaintext secret(s) in ${file}
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: path.basename(projectRoot) || projectRoot,
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, resolve2] of pendingConfirms) {
1765
- resolve2("no");
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 resolve2 = pendingConfirms.get(id);
1856
- if (resolve2) {
1938
+ const resolve3 = pendingConfirms.get(id);
1939
+ if (resolve3) {
1857
1940
  pendingConfirms.delete(id);
1858
- resolve2(decision);
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 fs2.readFile(globalConfigPath, "utf8");
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 fs2.readdir(dir, { withFileTypes: true });
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(path.join(dir, e.name), childRel, depth + 1);
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 fs2.readFile(globalConfigPath, "utf8");
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 fs2.readFile(globalConfigPath, "utf8");
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 httpPort = Number.parseInt(process.env["PORT"] ?? "3456", 10);
2750
- const DIST_DIR = path.resolve(import.meta.dirname, "../../dist");
2751
- const mimeTypes = {
2752
- ".html": "text/html",
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
  });