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