@wrongstack/webui 0.8.6 → 0.9.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.
@@ -602,7 +602,6 @@ var WorktreeWebSocketHandler = class {
602
602
  };
603
603
 
604
604
  // src/server/index.ts
605
- var HTML_CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:; img-src 'self' data:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'";
606
605
  async function startWebUI(opts = {}) {
607
606
  const wsPort = opts.wsPort ?? 3457;
608
607
  const wsHost = opts.wsHost ?? "127.0.0.1";
@@ -612,7 +611,7 @@ async function startWebUI(opts = {}) {
612
611
  let config = baseConfig;
613
612
  let configWriteLock = Promise.resolve();
614
613
  console.log("[WebUI] Config loaded:", config.provider ?? "(none)", "/", config.model ?? "(none)");
615
- if (!config.provider && config.providers && Object.keys(config.providers).length > 0) {
614
+ if (!config.provider && config.providers && typeof config.providers === "object" && config.providers !== null && !Array.isArray(config.providers) && Object.keys(config.providers).length > 0) {
616
615
  const firstKey = Object.keys(config.providers)[0];
617
616
  config = patchConfig(config, { provider: firstKey });
618
617
  console.log("[WebUI] No active provider \u2014 auto-selected:", firstKey);
@@ -903,11 +902,12 @@ async function startWebUI(opts = {}) {
903
902
  const RATE_LIMIT_MESSAGES = 60;
904
903
  const RATE_LIMIT_WINDOW_MS = 6e4;
905
904
  const rateLimits = /* @__PURE__ */ new Map();
906
- function checkRateLimit(ws) {
905
+ function checkRateLimit(ws, client) {
907
906
  const now = Date.now();
908
- const limit = rateLimits.get(ws);
907
+ const key = client.sessionId ?? String(ws);
908
+ const limit = rateLimits.get(key);
909
909
  if (!limit || now > limit.resetAt) {
910
- rateLimits.set(ws, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
910
+ rateLimits.set(key, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
911
911
  return true;
912
912
  }
913
913
  if (limit.count >= RATE_LIMIT_MESSAGES) return false;
@@ -1036,7 +1036,7 @@ async function startWebUI(opts = {}) {
1036
1036
  autoPhaseHandler.addClient(ws);
1037
1037
  worktreeHandler.addClient(ws);
1038
1038
  ws.on("message", async (data) => {
1039
- if (!checkRateLimit(ws)) {
1039
+ if (!checkRateLimit(ws, client)) {
1040
1040
  send(ws, {
1041
1041
  type: "error",
1042
1042
  payload: {
@@ -1047,15 +1047,24 @@ async function startWebUI(opts = {}) {
1047
1047
  return;
1048
1048
  }
1049
1049
  try {
1050
- const msg = JSON.parse(data.toString());
1051
- await handleMessage(ws, client, msg);
1050
+ const rawObj = JSON.parse(data.toString());
1051
+ if (typeof rawObj === "object" && rawObj !== null) {
1052
+ const obj = rawObj;
1053
+ if ("__proto__" in obj || "constructor" in obj || "prototype" in obj) {
1054
+ send(ws, { type: "error", payload: { phase: "parse", message: "Invalid message object" } });
1055
+ } else {
1056
+ await handleMessage(ws, client, rawObj);
1057
+ }
1058
+ } else {
1059
+ await handleMessage(ws, client, rawObj);
1060
+ }
1052
1061
  } catch (err) {
1053
1062
  console.error("[WebUI] Failed to parse message", err);
1054
1063
  }
1055
1064
  });
1056
1065
  ws.on("close", () => {
1057
1066
  clients.delete(ws);
1058
- rateLimits.delete(ws);
1067
+ rateLimits.delete(String(ws));
1059
1068
  console.log("[WebUI] Client disconnected, total:", clients.size);
1060
1069
  if (pendingConfirms.size > 0) {
1061
1070
  for (const [id, resolve2] of pendingConfirms) {
@@ -2049,7 +2058,10 @@ async function startWebUI(opts = {}) {
2049
2058
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
2050
2059
  if (ext === ".html") {
2051
2060
  res.setHeader("Cache-Control", "no-cache");
2052
- res.setHeader("Content-Security-Policy", HTML_CSP);
2061
+ res.setHeader(
2062
+ "Content-Security-Policy",
2063
+ `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'`
2064
+ );
2053
2065
  }
2054
2066
  const fileContent = await fs2.readFile(resolvedPath);
2055
2067
  res.writeHead(200);
@@ -2065,7 +2077,7 @@ async function startWebUI(opts = {}) {
2065
2077
  "Referrer-Policy": "strict-origin-when-cross-origin",
2066
2078
  // SPA fallback previously shipped no CSP — apply the same policy as
2067
2079
  // the direct .html branch so deep-linked routes aren't unprotected.
2068
- "Content-Security-Policy": HTML_CSP
2080
+ "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'`
2069
2081
  });
2070
2082
  res.end(fileContent);
2071
2083
  } catch {