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