quadwork 1.10.1 → 1.11.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.
Files changed (71) hide show
  1. package/bin/quadwork.js +84 -56
  2. package/out/404.html +1 -1
  3. package/out/__next.__PAGE__.txt +1 -1
  4. package/out/__next._full.txt +1 -1
  5. package/out/__next._head.txt +1 -1
  6. package/out/__next._index.txt +1 -1
  7. package/out/__next._tree.txt +1 -1
  8. package/out/_next/static/chunks/{16g.ca89g7fib.js → 0n~dq4kpx9xxx.js} +1 -1
  9. package/out/_next/static/chunks/turbopack-0qm-e3ifrz~2u.js +1 -0
  10. package/out/_not-found/__next._full.txt +1 -1
  11. package/out/_not-found/__next._head.txt +1 -1
  12. package/out/_not-found/__next._index.txt +1 -1
  13. package/out/_not-found/__next._not-found.__PAGE__.txt +1 -1
  14. package/out/_not-found/__next._not-found.txt +1 -1
  15. package/out/_not-found/__next._tree.txt +1 -1
  16. package/out/_not-found.html +1 -1
  17. package/out/_not-found.txt +1 -1
  18. package/out/app-shell/__next._full.txt +1 -1
  19. package/out/app-shell/__next._head.txt +1 -1
  20. package/out/app-shell/__next._index.txt +1 -1
  21. package/out/app-shell/__next._tree.txt +1 -1
  22. package/out/app-shell/__next.app-shell.__PAGE__.txt +1 -1
  23. package/out/app-shell/__next.app-shell.txt +1 -1
  24. package/out/app-shell.html +1 -1
  25. package/out/app-shell.txt +1 -1
  26. package/out/index.html +1 -1
  27. package/out/index.txt +1 -1
  28. package/out/project/_/__next._full.txt +1 -1
  29. package/out/project/_/__next._head.txt +1 -1
  30. package/out/project/_/__next._index.txt +1 -1
  31. package/out/project/_/__next._tree.txt +1 -1
  32. package/out/project/_/__next.project.$d$id.__PAGE__.txt +1 -1
  33. package/out/project/_/__next.project.$d$id.txt +1 -1
  34. package/out/project/_/__next.project.txt +1 -1
  35. package/out/project/_/queue/__next._full.txt +1 -1
  36. package/out/project/_/queue/__next._head.txt +1 -1
  37. package/out/project/_/queue/__next._index.txt +1 -1
  38. package/out/project/_/queue/__next._tree.txt +1 -1
  39. package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +1 -1
  40. package/out/project/_/queue/__next.project.$d$id.queue.txt +1 -1
  41. package/out/project/_/queue/__next.project.$d$id.txt +1 -1
  42. package/out/project/_/queue/__next.project.txt +1 -1
  43. package/out/project/_/queue.html +1 -1
  44. package/out/project/_/queue.txt +1 -1
  45. package/out/project/_.html +1 -1
  46. package/out/project/_.txt +1 -1
  47. package/out/settings/__next._full.txt +1 -1
  48. package/out/settings/__next._head.txt +1 -1
  49. package/out/settings/__next._index.txt +1 -1
  50. package/out/settings/__next._tree.txt +1 -1
  51. package/out/settings/__next.settings.__PAGE__.txt +1 -1
  52. package/out/settings/__next.settings.txt +1 -1
  53. package/out/settings.html +1 -1
  54. package/out/settings.txt +1 -1
  55. package/out/setup/__next._full.txt +1 -1
  56. package/out/setup/__next._head.txt +1 -1
  57. package/out/setup/__next._index.txt +1 -1
  58. package/out/setup/__next._tree.txt +1 -1
  59. package/out/setup/__next.setup.__PAGE__.txt +1 -1
  60. package/out/setup/__next.setup.txt +1 -1
  61. package/out/setup.html +1 -1
  62. package/out/setup.txt +1 -1
  63. package/package.json +2 -2
  64. package/server/config.js +29 -5
  65. package/server/index.js +84 -25
  66. package/server/install-agentchattr.js +12 -11
  67. package/server/routes.js +27 -30
  68. package/out/_next/static/chunks/turbopack-0lcwh84lrj9gi.js +0 -1
  69. /package/out/_next/static/{MA2-1YByee5M0-bbLgqQD → a1_CwwdhUp5-lHCPnFaTw}/_buildManifest.js +0 -0
  70. /package/out/_next/static/{MA2-1YByee5M0-bbLgqQD → a1_CwwdhUp5-lHCPnFaTw}/_clientMiddlewareManifest.js +0 -0
  71. /package/out/_next/static/{MA2-1YByee5M0-bbLgqQD → a1_CwwdhUp5-lHCPnFaTw}/_ssgManifest.js +0 -0
package/server/index.js CHANGED
@@ -6,7 +6,7 @@ const os = require("os");
6
6
  const { WebSocketServer } = require("ws");
7
7
  const pty = require("node-pty");
8
8
  const { spawn } = require("child_process");
9
- const { readConfig, resolveAgentCwd, resolveAgentCommand, resolveProjectChattr, resolveChattrSpawn, syncChattrToken, CONFIG_PATH } = require("./config");
9
+ const { readConfig, resolveAgentCwd, resolveAgentCommand, resolveProjectChattr, resolveChattrSpawn, syncChattrToken, CONFIG_PATH, ensureSecureDir, writeSecureFile, writeConfig } = require("./config");
10
10
  const routes = require("./routes");
11
11
  const {
12
12
  patchAgentchattrConfigForDiscordBridge,
@@ -43,11 +43,11 @@ app.get("/api/health", (_req, res) => {
43
43
 
44
44
  // --- CLI status detection ---
45
45
 
46
- const { execSync } = require("child_process");
46
+ const { execFileSync } = require("child_process");
47
47
 
48
48
  function isCliInstalled(cmd) {
49
49
  try {
50
- execSync(`which ${cmd}`, { encoding: "utf-8", stdio: "pipe" });
50
+ execFileSync("which", [cmd], { encoding: "utf-8", stdio: "pipe" });
51
51
  return true;
52
52
  } catch {
53
53
  return false;
@@ -264,8 +264,8 @@ function readPersistedAgentToken(projectId, agentId) {
264
264
  function writePersistedAgentToken(projectId, agentId, token) {
265
265
  try {
266
266
  const configDir = path.join(os.homedir(), ".quadwork", projectId);
267
- if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
268
- fs.writeFileSync(_agentTokenPath(projectId, agentId), token, { mode: 0o600 });
267
+ ensureSecureDir(configDir);
268
+ writeSecureFile(_agentTokenPath(projectId, agentId), token);
269
269
  } catch {
270
270
  // non-fatal — stale-slot reclaim will degrade but registration still works
271
271
  }
@@ -278,7 +278,7 @@ function clearPersistedAgentToken(projectId, agentId) {
278
278
  function writeMcpConfigFile(projectId, agentId, mcpHttpPort, token) {
279
279
  const os = require("os");
280
280
  const configDir = path.join(os.homedir(), ".quadwork", projectId);
281
- if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
281
+ ensureSecureDir(configDir);
282
282
  const filePath = path.join(configDir, `mcp-${agentId}.json`);
283
283
  const url = `http://127.0.0.1:${mcpHttpPort}/mcp`;
284
284
  const config = {
@@ -290,7 +290,7 @@ function writeMcpConfigFile(projectId, agentId, mcpHttpPort, token) {
290
290
  },
291
291
  },
292
292
  };
293
- fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
293
+ writeSecureFile(filePath, JSON.stringify(config, null, 2));
294
294
  return filePath;
295
295
  }
296
296
 
@@ -431,7 +431,7 @@ function buildAgentEnv(projectId, agentId) {
431
431
  if (cliBase === "gemini" && project.mcp_http_port) {
432
432
  const os = require("os");
433
433
  const configDir = path.join(os.homedir(), ".quadwork", projectId);
434
- if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
434
+ ensureSecureDir(configDir);
435
435
  const settingsPath = path.join(configDir, `mcp-${agentId}-settings.json`);
436
436
  const url = `http://127.0.0.1:${project.mcp_http_port}/mcp`;
437
437
  const settings = {
@@ -443,7 +443,7 @@ function buildAgentEnv(projectId, agentId) {
443
443
  },
444
444
  },
445
445
  };
446
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
446
+ writeSecureFile(settingsPath, JSON.stringify(settings, null, 2));
447
447
  env.GEMINI_CLI_SYSTEM_SETTINGS_PATH = settingsPath;
448
448
  }
449
449
 
@@ -555,6 +555,7 @@ async function spawnAgentPty(project, agent) {
555
555
  queueWatcherHandle: null,
556
556
  // #418: ring buffer of recent PTY output so reconnecting WS
557
557
  // clients see the terminal state instead of a blank panel.
558
+ // #538: scrollback is scrubbed of likely secrets before replay.
558
559
  scrollback: Buffer.alloc(0),
559
560
  };
560
561
  agentSessions.set(key, session);
@@ -730,7 +731,7 @@ const HISTORY_SNAPSHOT_LIMIT = 5;
730
731
  async function snapshotProjectHistory(projectId) {
731
732
  try {
732
733
  const snapDir = path.join(require("os").homedir(), ".quadwork", projectId, "history-snapshots");
733
- if (!fs.existsSync(snapDir)) fs.mkdirSync(snapDir, { recursive: true });
734
+ ensureSecureDir(snapDir);
734
735
  const res = await fetch(`http://127.0.0.1:${PORT}/api/project-history?project=${encodeURIComponent(projectId)}`, {
735
736
  signal: AbortSignal.timeout(30000),
736
737
  });
@@ -809,7 +810,7 @@ async function handleAgentChattr(req, res) {
809
810
  try {
810
811
  let content = fs.readFileSync(projectConfigToml, "utf-8");
811
812
  content = content.replace(/^port = \d+/m, `port = ${chattrPort}`);
812
- fs.writeFileSync(projectConfigToml, content);
813
+ writeSecureFile(projectConfigToml, content);
813
814
  } catch {}
814
815
  }
815
816
 
@@ -866,7 +867,7 @@ async function handleAgentChattr(req, res) {
866
867
  // lose their tracked reference when the Node process recycles).
867
868
  function killProcessOnPort(port) {
868
869
  try {
869
- const pids = execSync(`lsof -ti TCP:${port} -sTCP:LISTEN`, {
870
+ const pids = execFileSync("lsof", ["-ti", `TCP:${port}`, "-sTCP:LISTEN"], {
870
871
  encoding: "utf-8",
871
872
  timeout: 5000,
872
873
  stdio: ["pipe", "pipe", "pipe"],
@@ -889,7 +890,7 @@ async function handleAgentChattr(req, res) {
889
890
  return new Promise((resolve) => {
890
891
  function check() {
891
892
  try {
892
- execSync(`lsof -ti TCP:${port} -sTCP:LISTEN`, {
893
+ execFileSync("lsof", ["-ti", `TCP:${port}`, "-sTCP:LISTEN"], {
893
894
  encoding: "utf-8",
894
895
  timeout: 2000,
895
896
  stdio: ["pipe", "pipe", "pipe"],
@@ -1045,8 +1046,6 @@ async function handleAgentChattr(req, res) {
1045
1046
  return res.status(400).json({ ok: false, error: "AgentChattr not installed at " + (acDir || "unknown") });
1046
1047
  }
1047
1048
  try {
1048
- const { execSync } = require("child_process");
1049
-
1050
1049
  // Stop running process before pulling. Snapshot first so a
1051
1050
  // botched git pull can still be rolled back from disk.
1052
1051
  // #424 / quadwork#304: best-effort.
@@ -1070,14 +1069,14 @@ async function handleAgentChattr(req, res) {
1070
1069
  await waitForPortFree(chattrPort, 3000);
1071
1070
  }
1072
1071
 
1073
- const pullResult = execSync("git pull 2>&1", { cwd: acDir, encoding: "utf-8", timeout: 30000 }).trim();
1072
+ const pullResult = execFileSync("git", ["pull"], { cwd: acDir, encoding: "utf-8", timeout: 30000, stdio: "pipe" }).trim();
1074
1073
  // #388: re-apply sender-overflow CSS patch after git pull
1075
1074
  patchAgentchattrCss(acDir);
1076
1075
  const venvPython = path.join(acDir, ".venv", "bin", "python");
1077
1076
  let pipResult = "";
1078
1077
  const reqFile = path.join(acDir, "requirements.txt");
1079
1078
  if (fs.existsSync(venvPython) && fs.existsSync(reqFile)) {
1080
- pipResult = execSync(`"${venvPython}" -m pip install -r requirements.txt 2>&1`, { cwd: acDir, encoding: "utf-8", timeout: 120000 }).trim();
1079
+ pipResult = execFileSync(venvPython, ["-m", "pip", "install", "-r", "requirements.txt"], { cwd: acDir, encoding: "utf-8", timeout: 120000, stdio: "pipe" }).trim();
1081
1080
  }
1082
1081
 
1083
1082
  // Restart if it was running before the update
@@ -1533,7 +1532,7 @@ app.post("/api/triggers/:project/start", (req, res) => {
1533
1532
  if (typeof message === "string" && message.length > 0) entry.trigger_message = message;
1534
1533
  if (Number.isFinite(interval) && interval > 0) entry.trigger_interval_min = interval;
1535
1534
  if (Number.isFinite(duration) && duration >= 0) entry.trigger_duration_min = duration;
1536
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
1535
+ writeConfig(cfg);
1537
1536
  }
1538
1537
  } catch (e) { /* non-fatal — timer still runs with its in-memory values */ }
1539
1538
 
@@ -1624,7 +1623,7 @@ app.put("/api/queue", express.json({ limit: "512kb" }), (req, res) => {
1624
1623
  if (content === null) return res.status(400).json({ error: "Missing content" });
1625
1624
  const p = queuePathFor(projectId);
1626
1625
  try {
1627
- fs.mkdirSync(path.dirname(p), { recursive: true });
1626
+ ensureSecureDir(path.dirname(p));
1628
1627
  fs.writeFileSync(p, content);
1629
1628
  return res.json({ ok: true });
1630
1629
  } catch (e) { return res.status(500).json({ error: e.message }); }
@@ -1642,7 +1641,7 @@ app.post("/api/queue", (req, res) => {
1642
1641
  let content = fs.readFileSync(tpl, "utf-8");
1643
1642
  content = content.replace(/\{\{project_name\}\}/g, project.name || projectId);
1644
1643
  content = content.replace(/\{\{repo\}\}/g, project.repo || "");
1645
- fs.mkdirSync(path.dirname(p), { recursive: true });
1644
+ ensureSecureDir(path.dirname(p));
1646
1645
  fs.writeFileSync(p, content);
1647
1646
  return res.json({ ok: true, existed: false });
1648
1647
  } catch (e) { return res.status(500).json({ error: e.message }); }
@@ -1719,6 +1718,56 @@ app.use((req, res, next) => {
1719
1718
  }
1720
1719
  });
1721
1720
 
1721
+ // --- #538: PTY output secret scrubbing ---
1722
+ // Redact likely secrets from both live PTY streaming and scrollback
1723
+ // replay so echoed credentials are not exposed to dashboard clients.
1724
+ //
1725
+ // Threat model: QuadWork binds to 127.0.0.1 only. The scrub is
1726
+ // defense-in-depth — it reduces exposure if a secret is accidentally
1727
+ // echoed, but cannot catch every possible format. Operators who handle
1728
+ // highly sensitive credentials should avoid echoing them in agent
1729
+ // terminals.
1730
+ //
1731
+ // Live chunks from term.onData() are typically line-aligned (shell
1732
+ // flushes on newline), so per-chunk scrubbing catches the vast majority
1733
+ // of secrets. A secret split across two chunks is a theoretical edge
1734
+ // case that the scrollback scrub (which sees the full buffer) covers
1735
+ // on reconnect.
1736
+
1737
+ // Patterns that indicate a line contains a secret value.
1738
+ const _SECRET_NAME_RE = /\b\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|PASSPHRASE|AUTH)\w*\s*[=:]/i;
1739
+ // Known API key prefixes (Anthropic, GitHub, OpenAI, etc.).
1740
+ const _API_KEY_PREFIX_RE = /\b(sk-ant-api\d{2}-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{36,}|ghu_[A-Za-z0-9]{36,}|ghs_[A-Za-z0-9]{36,}|sk-[A-Za-z0-9]{20,}|xoxb-[A-Za-z0-9-]{20,}|xoxp-[A-Za-z0-9-]{20,})\b/;
1741
+ // Bearer authorization headers.
1742
+ const _BEARER_RE = /\bBearer\s+[A-Za-z0-9_.+/=-]{20,}/i;
1743
+ const _REDACTED = "[REDACTED]";
1744
+
1745
+ function scrubSecrets(text) {
1746
+ if (!text) return text;
1747
+ return text.split("\n").map((line) => {
1748
+ // Strip ANSI escape codes for pattern matching, but redact the
1749
+ // original line (preserves terminal formatting around non-secret
1750
+ // lines while ensuring secrets inside styled output are caught).
1751
+ const plain = line.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
1752
+ if (_SECRET_NAME_RE.test(plain)) {
1753
+ // Redact the value portion after the = or : delimiter.
1754
+ return line.replace(/([=:])\s*\S.*/, `$1 ${_REDACTED}`);
1755
+ }
1756
+ if (_API_KEY_PREFIX_RE.test(plain)) {
1757
+ return line.replace(_API_KEY_PREFIX_RE, _REDACTED);
1758
+ }
1759
+ if (_BEARER_RE.test(plain)) {
1760
+ return line.replace(/\bBearer\s+[A-Za-z0-9_.+/=-]{20,}/gi, `Bearer ${_REDACTED}`);
1761
+ }
1762
+ return line;
1763
+ }).join("\n");
1764
+ }
1765
+
1766
+ function scrubScrollback(buf) {
1767
+ if (!buf || buf.length === 0) return buf;
1768
+ return Buffer.from(scrubSecrets(buf.toString("utf-8")), "utf-8");
1769
+ }
1770
+
1722
1771
  // --- WebSocket + PTY ---
1723
1772
  // WS connects to an existing PTY session (started via lifecycle API)
1724
1773
  // or spawns a new one if none exists.
@@ -1760,10 +1809,10 @@ wss.on("connection", async (ws, req) => {
1760
1809
  // {"type":"replay"} to avoid the timing race where eager replay
1761
1810
  // arrived before the client's onmessage handler was registered.
1762
1811
 
1763
- // PTY → client
1812
+ // PTY → client (#538: scrub secrets from live output)
1764
1813
  const dataHandler = session.term.onData((data) => {
1765
1814
  if (ws.readyState === ws.OPEN) {
1766
- ws.send(data);
1815
+ ws.send(scrubSecrets(data));
1767
1816
  }
1768
1817
  });
1769
1818
 
@@ -1773,8 +1822,17 @@ wss.on("connection", async (ws, req) => {
1773
1822
  const str = msg.toString();
1774
1823
  try {
1775
1824
  const parsed = JSON.parse(str);
1776
- if (parsed.type === "resize" && parsed.cols && parsed.rows) {
1777
- session.term.resize(parsed.cols, parsed.rows);
1825
+ if (parsed.type === "resize") {
1826
+ // #541: strict numeric type check and bounds validation before
1827
+ // passing to PTY. The dashboard client (TerminalPanel.tsx) sends
1828
+ // xterm.js cols/rows which are always numbers. Reject anything
1829
+ // else at the boundary.
1830
+ if (typeof parsed.cols === "number" && typeof parsed.rows === "number" &&
1831
+ Number.isFinite(parsed.cols) && Number.isFinite(parsed.rows) &&
1832
+ parsed.cols >= 1 && parsed.cols <= 500 &&
1833
+ parsed.rows >= 1 && parsed.rows <= 500) {
1834
+ session.term.resize(parsed.cols, parsed.rows);
1835
+ }
1778
1836
  return;
1779
1837
  }
1780
1838
  // #461: client requests scrollback replay after xterm is fully
@@ -1784,7 +1842,8 @@ wss.on("connection", async (ws, req) => {
1784
1842
  // synthetic status line so the terminal isn't completely blank.
1785
1843
  if (parsed.type === "replay") {
1786
1844
  if (session.scrollback && session.scrollback.length > 0) {
1787
- ws.send(session.scrollback);
1845
+ // #538: scrub likely secrets before replaying accumulated output.
1846
+ ws.send(scrubScrollback(session.scrollback));
1788
1847
  } else {
1789
1848
  ws.send(`\x1b[2m[agent online — waiting for input]\x1b[0m\r\n`);
1790
1849
  }
@@ -14,7 +14,7 @@
14
14
  // Self-contained — depends only on Node built-ins so it's safe to require
15
15
  // from anywhere in the project (CLI bin, server routes, future tests).
16
16
 
17
- const { execSync } = require("child_process");
17
+ const { execFileSync } = require("child_process");
18
18
  const fs = require("fs");
19
19
  const path = require("path");
20
20
 
@@ -28,8 +28,8 @@ const INSTALL_LOCK_STALE_MS = 10 * 60 * 1000; // 10 min
28
28
  const INSTALL_LOCK_WAIT_TOTAL_MS = 30 * 1000; // wait up to 30s for a peer
29
29
  const INSTALL_LOCK_POLL_MS = 500;
30
30
 
31
- function _run(cmd, opts = {}) {
32
- try { return execSync(cmd, { encoding: "utf-8", stdio: "pipe", ...opts }).trim(); }
31
+ function _run(cmd, args = [], opts = {}) {
32
+ try { return execFileSync(cmd, args, { encoding: "utf-8", stdio: "pipe", ...opts }).trim(); }
33
33
  catch { return null; }
34
34
  }
35
35
 
@@ -102,14 +102,15 @@ function installAgentChattr(dir) {
102
102
 
103
103
  // --- Per-target lock ---
104
104
  const lockFile = `${dir}.install.lock`;
105
- try { fs.mkdirSync(path.dirname(lockFile), { recursive: true }); }
105
+ try { fs.mkdirSync(path.dirname(lockFile), { recursive: true, mode: 0o700 }); }
106
106
  catch (e) { return setError(`Cannot create parent of ${dir}: ${e.message}`); }
107
107
 
108
108
  let acquired = false;
109
109
  const deadline = Date.now() + INSTALL_LOCK_WAIT_TOTAL_MS;
110
110
  while (!acquired) {
111
111
  try {
112
- fs.writeFileSync(lockFile, `${process.pid}:${Date.now()}`, { flag: "wx" });
112
+ fs.writeFileSync(lockFile, `${process.pid}:${Date.now()}`, { mode: 0o600, flag: "wx" });
113
+ try { fs.chmodSync(lockFile, 0o600); } catch {}
113
114
  acquired = true;
114
115
  } catch (e) {
115
116
  if (e.code !== "EEXIST") return setError(`Cannot create install lock ${lockFile}: ${e.message}`);
@@ -129,7 +130,7 @@ function installAgentChattr(dir) {
129
130
  const info = _readLock(lockFile) || { pid: "?", ts: 0 };
130
131
  return setError(`Another install is in progress at ${dir} (pid ${info.pid}); timed out after ${INSTALL_LOCK_WAIT_TOTAL_MS}ms. Re-run after it finishes, or remove ${lockFile} if stale.`);
131
132
  }
132
- try { execSync(`sleep ${INSTALL_LOCK_POLL_MS / 1000}`); }
133
+ try { execFileSync("sleep", [String(INSTALL_LOCK_POLL_MS / 1000)], { stdio: "pipe" }); }
133
134
  catch { /* sleep interrupted; loop will recheck */ }
134
135
  }
135
136
  }
@@ -158,7 +159,7 @@ function _installAgentChattrLocked(dir, setError) {
158
159
  try { fs.rmSync(dir, { recursive: true, force: true }); }
159
160
  catch (e) { return setError(`Cannot remove empty dir ${dir}: ${e.message}`); }
160
161
  } else if (fs.existsSync(path.join(dir, ".git"))) {
161
- const remote = _run(`git -C "${dir}" remote get-url origin 2>/dev/null`);
162
+ const remote = _run("git", ["-C", dir, "remote", "get-url", "origin"]);
162
163
  if (remote && remote.includes("agentchattr")) {
163
164
  try { fs.rmSync(dir, { recursive: true, force: true }); }
164
165
  catch (e) { return setError(`Cannot remove failed clone at ${dir}: ${e.message}`); }
@@ -169,16 +170,16 @@ function _installAgentChattrLocked(dir, setError) {
169
170
  return setError(`Refusing to overwrite ${dir}: directory exists with unrelated content`);
170
171
  }
171
172
  }
172
- try { fs.mkdirSync(path.dirname(dir), { recursive: true }); }
173
+ try { fs.mkdirSync(path.dirname(dir), { recursive: true, mode: 0o700 }); }
173
174
  catch (e) { return setError(`Cannot create parent of ${dir}: ${e.message}`); }
174
- const cloneResult = _run(`git clone "${AGENTCHATTR_REPO}" "${dir}" 2>&1`, { timeout: 60000 });
175
+ const cloneResult = _run("git", ["clone", AGENTCHATTR_REPO, dir], { timeout: 60000 });
175
176
  if (cloneResult === null) return setError(`git clone of ${AGENTCHATTR_REPO} into ${dir} failed`);
176
177
  if (!fs.existsSync(runPy)) return setError(`Clone completed but run.py missing at ${dir}`);
177
178
  }
178
179
 
179
180
  // 2. Create venv if missing.
180
181
  if (!fs.existsSync(venvPython)) {
181
- const venvResult = _run(`python3 -m venv "${path.join(dir, ".venv")}" 2>&1`, { timeout: 60000 });
182
+ const venvResult = _run("python3", ["-m", "venv", path.join(dir, ".venv")], { timeout: 60000 });
182
183
  if (venvResult === null) return setError(`python3 -m venv failed at ${dir}/.venv (is python3 installed?)`);
183
184
  if (!fs.existsSync(venvPython)) return setError(`venv created but ${venvPython} missing`);
184
185
  venvJustCreated = true;
@@ -188,7 +189,7 @@ function _installAgentChattrLocked(dir, setError) {
188
189
  if (venvJustCreated) {
189
190
  const reqFile = path.join(dir, "requirements.txt");
190
191
  if (fs.existsSync(reqFile)) {
191
- const pipResult = _run(`"${venvPython}" -m pip install -r "${reqFile}" 2>&1`, { timeout: 120000 });
192
+ const pipResult = _run(venvPython, ["-m", "pip", "install", "-r", reqFile], { timeout: 120000 });
192
193
  if (pipResult === null) return setError(`pip install -r ${reqFile} failed`);
193
194
  }
194
195
  }
package/server/routes.js CHANGED
@@ -36,8 +36,8 @@ function readConfigFile() {
36
36
 
37
37
  function writeConfigFile(cfg) {
38
38
  const dir = path.dirname(CONFIG_PATH);
39
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
40
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
39
+ ensureSecureDir(dir);
40
+ writeConfig(cfg);
41
41
  }
42
42
 
43
43
  // ─── Config ────────────────────────────────────────────────────────────────
@@ -66,8 +66,8 @@ router.put("/api/config", (req, res) => {
66
66
  try {
67
67
  const body = req.body;
68
68
  const dir = path.dirname(CONFIG_PATH);
69
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
70
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(body, null, 2));
69
+ ensureSecureDir(dir);
70
+ writeConfig(body);
71
71
  // Trigger sync is handled internally since we're in the same process now
72
72
  if (typeof req.app.get("syncTriggers") === "function") {
73
73
  req.app.get("syncTriggers")();
@@ -80,7 +80,7 @@ router.put("/api/config", (req, res) => {
80
80
 
81
81
  // ─── Chat (AgentChattr proxy) ──────────────────────────────────────────────
82
82
 
83
- const { resolveProjectChattr, sanitizeOperatorName } = require("./config");
83
+ const { resolveProjectChattr, sanitizeOperatorName, ensureSecureDir, writeSecureFile, writeConfig } = require("./config");
84
84
  const { installAgentChattr, findAgentChattr } = require("./install-agentchattr");
85
85
 
86
86
  /**
@@ -96,7 +96,7 @@ function writeOvernightQueueFileSafe(projectId, projectName, repo) {
96
96
  if (fs.existsSync(queuePath)) return;
97
97
  const tpl = path.join(TEMPLATES_DIR, "OVERNIGHT-QUEUE.md");
98
98
  if (!fs.existsSync(tpl)) return;
99
- fs.mkdirSync(path.dirname(queuePath), { recursive: true });
99
+ ensureSecureDir(path.dirname(queuePath));
100
100
  let content = fs.readFileSync(tpl, "utf-8");
101
101
  content = content.replace(/\{\{project_name\}\}/g, projectName || projectId || "");
102
102
  content = content.replace(/\{\{repo\}\}/g, repo || "");
@@ -404,7 +404,7 @@ router.put("/api/loop-guard", async (req, res) => {
404
404
  const trailing = content.endsWith("\n") ? "" : "\n";
405
405
  content += `${trailing}\n[routing]\ndefault = "none"\nmax_agent_hops = ${value}\n`;
406
406
  }
407
- fs.writeFileSync(tomlPath, content);
407
+ writeSecureFile(tomlPath, content);
408
408
  } catch (err) {
409
409
  return res.status(500).json({ error: "Failed to write config.toml", detail: err.message });
410
410
  }
@@ -838,7 +838,7 @@ router.post("/api/activity/log", (req, res) => {
838
838
  const row = { agent, start, end: ts, duration_ms: Math.max(0, ts - start) };
839
839
  try {
840
840
  const p = activityLogPath(project);
841
- fs.mkdirSync(path.dirname(p), { recursive: true });
841
+ ensureSecureDir(path.dirname(p));
842
842
  fs.appendFileSync(p, JSON.stringify(row) + "\n");
843
843
  // Invalidate the stats cache so the next read sees the new row.
844
844
  _activityStatsCache.ts = 0;
@@ -1029,7 +1029,7 @@ const uploadStorage = multer.diskStorage({
1029
1029
  const projectId = req.query.project || "";
1030
1030
  if (!projectId || /[/\\]/.test(projectId)) return cb(new Error("Invalid project"));
1031
1031
  const dir = path.join(CONFIG_DIR, projectId, "uploads");
1032
- fs.mkdirSync(dir, { recursive: true });
1032
+ ensureSecureDir(dir);
1033
1033
  cb(null, dir);
1034
1034
  },
1035
1035
  filename: (_req, file, cb) => {
@@ -1340,7 +1340,7 @@ function readBatchSnapshot(projectId) {
1340
1340
  function writeBatchSnapshot(projectId, snapshot) {
1341
1341
  try {
1342
1342
  const p = batchSnapshotPath(projectId);
1343
- fs.mkdirSync(path.dirname(p), { recursive: true });
1343
+ ensureSecureDir(path.dirname(p));
1344
1344
  fs.writeFileSync(p, JSON.stringify(snapshot));
1345
1345
  } catch {
1346
1346
  // Non-fatal — panel still works from the live parse.
@@ -1850,8 +1850,8 @@ router.post("/api/setup/save-token", (req, res) => {
1850
1850
  if (!token) return res.status(400).json({ error: "Missing token" });
1851
1851
  const tokenPath = path.join(os.homedir(), ".quadwork", "reviewer-token");
1852
1852
  const dir = path.dirname(tokenPath);
1853
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1854
- fs.writeFileSync(tokenPath, token.trim() + "\n", { mode: 0o600 });
1853
+ ensureSecureDir(dir);
1854
+ writeSecureFile(tokenPath, token.trim() + "\n");
1855
1855
  try { fs.chmodSync(tokenPath, 0o600); } catch {}
1856
1856
  res.json({ ok: true, path: tokenPath });
1857
1857
  });
@@ -1890,7 +1890,7 @@ router.post("/api/setup", (req, res) => {
1890
1890
  const workingDir = body.workingDir;
1891
1891
  if (!workingDir) return res.json({ ok: false, error: "Missing working directory" });
1892
1892
  if (!fs.existsSync(path.join(workingDir, ".git"))) {
1893
- if (!fs.existsSync(workingDir)) fs.mkdirSync(workingDir, { recursive: true });
1893
+ if (!fs.existsSync(workingDir)) ensureSecureDir(workingDir);
1894
1894
  if (!REPO_RE.test(body.repo)) return res.json({ ok: false, error: "Invalid repo" });
1895
1895
  const clone = exec("gh", ["repo", "clone", body.repo, workingDir]);
1896
1896
  if (!clone.ok) return res.json({ ok: false, error: `Clone failed: ${clone.output}` });
@@ -2026,7 +2026,7 @@ router.post("/api/setup", (req, res) => {
2026
2026
  }
2027
2027
  }
2028
2028
  const dataDir = path.join(projectConfigDir, "data");
2029
- fs.mkdirSync(dataDir, { recursive: true });
2029
+ ensureSecureDir(dataDir);
2030
2030
  const tomlPath = path.join(projectConfigDir, "config.toml");
2031
2031
 
2032
2032
  // Resolve per-project ports: prefer explicit body params (from setup wizard),
@@ -2067,7 +2067,7 @@ router.post("/api/setup", (req, res) => {
2067
2067
  // operator to type /continue. AC clamps to [1, 50] internally.
2068
2068
  content += `[routing]\ndefault = "none"\nmax_agent_hops = 30\n\n`;
2069
2069
  content += `[mcp]\nhttp_port = ${mcp_http}\nsse_port = ${mcp_sse}\n`;
2070
- fs.writeFileSync(tomlPath, content);
2070
+ writeSecureFile(tomlPath, content);
2071
2071
 
2072
2072
  // Restart this project's AgentChattr instance (not global)
2073
2073
  try {
@@ -2158,8 +2158,8 @@ router.post("/api/setup", (req, res) => {
2158
2158
  agentchattr_dir: perProjectDir,
2159
2159
  });
2160
2160
  const dir = path.dirname(CONFIG_PATH);
2161
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
2162
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
2161
+ ensureSecureDir(dir);
2162
+ writeConfig(cfg);
2163
2163
 
2164
2164
  // Batch 25 / #204: seed the per-project OVERNIGHT-QUEUE.md at
2165
2165
  // ~/.quadwork/{id}/OVERNIGHT-QUEUE.md.
@@ -2495,8 +2495,7 @@ function writeEnvToken(key, value) {
2495
2495
  const line = `${key}=${value}`;
2496
2496
  if (regex.test(content)) content = content.replace(regex, line);
2497
2497
  else content = content.trimEnd() + (content ? "\n" : "") + line + "\n";
2498
- fs.writeFileSync(ENV_PATH, content, { mode: 0o600 });
2499
- fs.chmodSync(ENV_PATH, 0o600);
2498
+ writeSecureFile(ENV_PATH, content);
2500
2499
  }
2501
2500
 
2502
2501
  function resolveToken(value) {
@@ -2558,7 +2557,7 @@ router.get("/api/telegram", async (req, res) => {
2558
2557
  if (data && data.ok && data.result && typeof data.result.username === "string") {
2559
2558
  botUsername = data.result.username;
2560
2559
  project.telegram.bot_username = botUsername;
2561
- try { fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2)); } catch {}
2560
+ try { writeConfig(cfg); } catch {}
2562
2561
  }
2563
2562
  }
2564
2563
  } catch { /* non-fatal — widget will just show no username */ }
@@ -2718,8 +2717,7 @@ router.post("/api/telegram", async (req, res) => {
2718
2717
  // #383 Bug 2: write agentchattr_url inside [telegram]; the
2719
2718
  // bridge's load_config only reads from that section.
2720
2719
  const tomlContent = buildTelegramBridgeToml(tg, projectId);
2721
- fs.writeFileSync(tomlPath, tomlContent, { mode: 0o600 });
2722
- fs.chmodSync(tomlPath, 0o600);
2720
+ writeSecureFile(tomlPath, tomlContent);
2723
2721
  // #353: pre-flight import check so a fresh install with no
2724
2722
  // `requests` module produces a readable error instead of the
2725
2723
  // Start → Running → Stopped flicker that the v1 code path
@@ -2852,7 +2850,7 @@ router.post("/api/telegram", async (req, res) => {
2852
2850
  const project = cfg.projects?.find((p) => p.id === projectId);
2853
2851
  if (project?.telegram) {
2854
2852
  project.telegram.bot_token = `env:${envKey}`;
2855
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
2853
+ writeConfig(cfg);
2856
2854
  }
2857
2855
  } catch {}
2858
2856
  return res.json({ ok: true, env_key: envKey });
@@ -2885,7 +2883,7 @@ router.post("/api/telegram", async (req, res) => {
2885
2883
  // will re-fetch it from Telegram's getMe for the new token.
2886
2884
  bot_username: "",
2887
2885
  };
2888
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
2886
+ writeConfig(cfg);
2889
2887
  return res.json({ ok: true, env_key: envKey });
2890
2888
  } catch (err) {
2891
2889
  return res.json({ ok: false, error: err.message || "Config write failed" });
@@ -3030,7 +3028,7 @@ router.get("/api/discord", async (req, res) => {
3030
3028
  if (r.ok && data.username) {
3031
3029
  botUsername = data.username;
3032
3030
  project.discord.bot_username = botUsername;
3033
- try { fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2)); } catch {}
3031
+ try { writeConfig(cfg); } catch {}
3034
3032
  }
3035
3033
  }
3036
3034
  } catch { /* non-fatal — widget will just show no username */ }
@@ -3088,7 +3086,7 @@ router.post("/api/discord", async (req, res) => {
3088
3086
  // #506: always copy bundled bridge files (not just on first install)
3089
3087
  // so re-installing after a QuadWork upgrade refreshes the script.
3090
3088
  if (!fs.existsSync(DISCORD_BRIDGE_DIR)) {
3091
- fs.mkdirSync(DISCORD_BRIDGE_DIR, { recursive: true });
3089
+ ensureSecureDir(DISCORD_BRIDGE_DIR);
3092
3090
  }
3093
3091
  fs.cpSync(
3094
3092
  path.join(DISCORD_BRIDGE_SRC, "discord_bridge.py"),
@@ -3165,8 +3163,7 @@ router.post("/api/discord", async (req, res) => {
3165
3163
  if (!dc || !dc.bot_token || !dc.channel_id) return res.json({ ok: false, error: "Save bot_token and channel_id in project settings first." });
3166
3164
  const tomlPath = discordConfigToml(projectId);
3167
3165
  const tomlContent = buildDiscordBridgeToml(dc, projectId);
3168
- fs.writeFileSync(tomlPath, tomlContent, { mode: 0o600 });
3169
- fs.chmodSync(tomlPath, 0o600);
3166
+ writeSecureFile(tomlPath, tomlContent);
3170
3167
  const depCheck = checkDiscordBridgePythonDeps(venvPython);
3171
3168
  if (!depCheck.ok) {
3172
3169
  const msg =
@@ -3273,7 +3270,7 @@ router.post("/api/discord", async (req, res) => {
3273
3270
  channel_id,
3274
3271
  bot_username: "",
3275
3272
  };
3276
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
3273
+ writeConfig(cfg);
3277
3274
  return res.json({ ok: true, env_key: envKey });
3278
3275
  } catch (err) {
3279
3276
  return res.json({ ok: false, error: err.message || "Config write failed" });
@@ -3345,7 +3342,7 @@ router.put("/api/project/:projectId/agent-models/:agentId", (req, res) => {
3345
3342
  else a.reasoning_effort = reasoning;
3346
3343
  }
3347
3344
  project.agents[agentId] = a;
3348
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2));
3345
+ writeConfig(cfg);
3349
3346
  return res.json({ ok: true, agent: { agent_id: agentId, model: a.model || "", reasoning_effort: a.reasoning_effort || "" } });
3350
3347
  } catch (err) {
3351
3348
  return res.json({ ok: false, error: err.message || "write failed" });
@@ -1 +0,0 @@
1
- (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,{otherChunks:["static/chunks/0ze4gu236oq96.js","static/chunks/0.bbxho1vnxin.js","static/chunks/16g.ca89g7fib.js","static/chunks/0zfotsowwll1x.js","static/chunks/0pqt~8bl3ukh4.js"],runtimeModuleIds:[94553]}]),(()=>{let e;if(!Array.isArray(globalThis.TURBOPACK))return;let t="/_next/",r=(self.TURBOPACK_ASSET_SUFFIX??document?.currentScript?.getAttribute?.("src")?.replace(/^(.*(?=\?)|^.*$)/,""))||"",n=["NEXT_DEPLOYMENT_ID","NEXT_CLIENT_ASSET_SUFFIX"];var o,i=((o=i||{})[o.Runtime=0]="Runtime",o[o.Parent=1]="Parent",o[o.Update=2]="Update",o);let l=new WeakMap;function s(e,t){this.m=e,this.e=t}let u=s.prototype,a=Object.prototype.hasOwnProperty,c="u">typeof Symbol&&Symbol.toStringTag;function f(e,t,r){a.call(e,t)||Object.defineProperty(e,t,r)}function p(e,t){let r=e[t];return r||(r=h(t),e[t]=r),r}function h(e){return{exports:{},error:void 0,id:e,namespaceObject:void 0}}function d(e,t){f(e,"__esModule",{value:!0}),c&&f(e,c,{value:"Module"});let r=0;for(;r<t.length;){let n=t[r++],o=t[r++];if("number"==typeof o)if(0===o)f(e,n,{value:t[r++],enumerable:!0,writable:!1});else throw Error(`unexpected tag: ${o}`);else"function"==typeof t[r]?f(e,n,{get:o,set:t[r++],enumerable:!0}):f(e,n,{get:o,enumerable:!0})}Object.seal(e)}function m(e,t){(null!=t?p(this.c,t):this.m).exports=e}u.s=function(e,t){let r,n;null!=t?n=(r=p(this.c,t)).exports:(r=this.m,n=this.e),r.namespaceObject=n,d(n,e)},u.j=function(e,t){var r,n;let o,i,s;null!=t?i=(o=p(this.c,t)).exports:(o=this.m,i=this.e);let u=(r=o,n=i,(s=l.get(r))||(l.set(r,s=[]),r.exports=r.namespaceObject=new Proxy(n,{get(e,t){if(a.call(e,t)||"default"===t||"__esModule"===t)return Reflect.get(e,t);for(let e of s){let r=Reflect.get(e,t);if(void 0!==r)return r}},ownKeys(e){let t=Reflect.ownKeys(e);for(let e of s)for(let r of Reflect.ownKeys(e))"default"===r||t.includes(r)||t.push(r);return t}})),s);"object"==typeof e&&null!==e&&u.push(e)},u.v=m,u.n=function(e,t){let r;(r=null!=t?p(this.c,t):this.m).exports=r.namespaceObject=e};let b=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,y=[null,b({}),b([]),b(b)];function g(e,t,r){let n=[],o=-1;for(let t=e;("object"==typeof t||"function"==typeof t)&&!y.includes(t);t=b(t))for(let r of Object.getOwnPropertyNames(t))n.push(r,function(e,t){return()=>e[t]}(e,r)),-1===o&&"default"===r&&(o=n.length-1);return r&&o>=0||(o>=0?n.splice(o,1,0,e):n.push("default",0,e)),d(t,n),t}function w(e){let t=W(e,this.m);if(t.namespaceObject)return t.namespaceObject;let r=t.exports;return t.namespaceObject=g(r,"function"==typeof r?function(...e){return r.apply(this,e)}:Object.create(null),r&&r.__esModule)}function O(e){let t=e.indexOf("#");-1!==t&&(e=e.substring(0,t));let r=e.indexOf("?");return -1!==r&&(e=e.substring(0,r)),e}function k(){let e,t;return{promise:new Promise((r,n)=>{t=n,e=r}),resolve:e,reject:t}}u.i=w,u.A=function(e){return this.r(e)(w.bind(this))},u.t="function"==typeof require?require:function(){throw Error("Unexpected use of runtime require")},u.r=function(e){return W(e,this.m).exports},u.f=function(e){function t(t){if(t=O(t),a.call(e,t))return e[t].module();let r=Error(`Cannot find module '${t}'`);throw r.code="MODULE_NOT_FOUND",r}return t.keys=()=>Object.keys(e),t.resolve=t=>{if(t=O(t),a.call(e,t))return e[t].id();let r=Error(`Cannot find module '${t}'`);throw r.code="MODULE_NOT_FOUND",r},t.import=async e=>await t(e),t};let j=Symbol("turbopack queues"),v=Symbol("turbopack exports"),C=Symbol("turbopack error");function P(e){e&&1!==e.status&&(e.status=1,e.forEach(e=>e.queueCount--),e.forEach(e=>e.queueCount--?e.queueCount++:e()))}u.a=function(e,t){let r=this.m,n=t?Object.assign([],{status:-1}):void 0,o=new Set,{resolve:i,reject:l,promise:s}=k(),u=Object.assign(s,{[v]:r.exports,[j]:e=>{n&&e(n),o.forEach(e),u.catch(()=>{})}}),a={get:()=>u,set(e){e!==u&&(u[v]=e)}};Object.defineProperty(r,"exports",a),Object.defineProperty(r,"namespaceObject",a),e(function(e){let t=e.map(e=>{if(null!==e&&"object"==typeof e){if(j in e)return e;if(null!=e&&"object"==typeof e&&"then"in e&&"function"==typeof e.then){let t=Object.assign([],{status:0}),r={[v]:{},[j]:e=>e(t)};return e.then(e=>{r[v]=e,P(t)},e=>{r[C]=e,P(t)}),r}}return{[v]:e,[j]:()=>{}}}),r=()=>t.map(e=>{if(e[C])throw e[C];return e[v]}),{promise:i,resolve:l}=k(),s=Object.assign(()=>l(r),{queueCount:0});function u(e){e!==n&&!o.has(e)&&(o.add(e),e&&0===e.status&&(s.queueCount++,e.push(s)))}return t.map(e=>e[j](u)),s.queueCount?i:r()},function(e){e?l(u[C]=e):i(u[v]),P(n)}),n&&-1===n.status&&(n.status=0)};let U=function(e){let t=new URL(e,"x:/"),r={};for(let e in t)r[e]=t[e];for(let t in r.href=e,r.pathname=e.replace(/[?#].*/,""),r.origin=r.protocol="",r.toString=r.toJSON=(...t)=>e,r)Object.defineProperty(this,t,{enumerable:!0,configurable:!0,value:r[t]})};function $(e,t){throw Error(`Invariant: ${t(e)}`)}U.prototype=URL.prototype,u.U=U,u.z=function(e){throw Error("dynamic usage of require is not supported")},u.g=globalThis;let R=s.prototype,S=new Map;u.M=S;let E=new Map,_=new Map;async function T(e,t,r){let n;if("string"==typeof r)return M(e,t,q(r));let o=r.included||[],i=o.map(e=>!!S.has(e)||E.get(e));if(i.length>0&&i.every(e=>e))return void await Promise.all(i);let l=r.moduleChunks||[],s=l.map(e=>_.get(e)).filter(e=>e);if(s.length>0){if(s.length===l.length)return void await Promise.all(s);let r=new Set;for(let e of l)_.has(e)||r.add(e);for(let n of r){let r=M(e,t,q(n));_.set(n,r),s.push(r)}n=Promise.all(s)}else{for(let o of(n=M(e,t,q(r.path)),l))_.has(o)||_.set(o,n)}for(let e of o)E.has(e)||E.set(e,n);await n}R.l=function(e){return T(i.Parent,this.m.id,e)};let x=Promise.resolve(void 0),A=new WeakMap;function M(t,r,n){let o=e.loadChunkCached(t,n),l=A.get(o);if(void 0===l){let e=A.set.bind(A,o,x);l=o.then(e).catch(e=>{let o;switch(t){case i.Runtime:o=`as a runtime dependency of chunk ${r}`;break;case i.Parent:o=`from module ${r}`;break;case i.Update:o="from an HMR update";break;default:$(t,e=>`Unknown source type: ${e}`)}let l=Error(`Failed to load chunk ${n} ${o}${e?`: ${e}`:""}`,e?{cause:e}:void 0);throw l.name="ChunkLoadError",l}),A.set(o,l)}return l}function q(e){return`${t}${e.split("/").map(e=>encodeURIComponent(e)).join("/")}${r}`}R.L=function(e){return M(i.Parent,this.m.id,e)},R.R=function(e){let t=this.r(e);return t?.default??t},R.P=function(e){return`/ROOT/${e??""}`},R.q=function(e,t){m.call(this,`${e}${r}`,t)},R.b=function(e,t,o,i){let l="SharedWorker"===e.name,s=[o.map(e=>q(e)).reverse(),r];for(let e of n)s.push(globalThis[e]);let u=new URL(q(t),location.origin),a=JSON.stringify(s);return l?u.searchParams.set("params",a):u.hash="#params="+encodeURIComponent(a),new e(u,i?{...i,type:void 0}:void 0)};let N=/\.js(?:\?[^#]*)?(?:#.*)?$/,K=/\.css(?:\?[^#]*)?(?:#.*)?$/;function L(e){return K.test(e)}u.w=function(t,r,n){return e.loadWebAssembly(i.Parent,this.m.id,t,r,n)},u.u=function(t,r){return e.loadWebAssemblyModule(i.Parent,this.m.id,t,r)};let I={};u.c=I;let W=(e,t)=>{let r=I[e];if(r){if(r.error)throw r.error;return r}return B(e,i.Parent,t.id)};function B(e,t,r){let n=S.get(e);if("function"!=typeof n)throw Error(function(e,t,r){let n;switch(t){case 0:n=`as a runtime entry of chunk ${r}`;break;case 1:n=`because it was required from module ${r}`;break;case 2:n="because of an HMR update";break;default:$(t,e=>`Unknown source type: ${e}`)}return`Module ${e} was instantiated ${n}, but the module factory is not available.`}(e,t,r));let o=h(e),i=o.exports;I[e]=o;let l=new s(o,i);try{n(l,o,i)}catch(e){throw o.error=e,e}return o.namespaceObject&&o.exports!==o.namespaceObject&&g(o.exports,o.namespaceObject),o}function F(t){let r,n=function(e){if("string"==typeof e)return e;if(e)return{src:e.getAttribute("src")};if("u">typeof TURBOPACK_NEXT_CHUNK_URLS)return{src:TURBOPACK_NEXT_CHUNK_URLS.pop()};throw Error("chunk path empty but not in a worker")}(t[0]);return 2===t.length?r=t[1]:(r=void 0,!function(e,t){let r=1;for(;r<e.length;){let n,o=r+1;for(;o<e.length&&"function"!=typeof e[o];)o++;if(o===e.length)throw Error("malformed chunk format, expected a factory function");let i=e[o];for(let i=r;i<o;i++){let r=e[i],o=t.get(r);if(o){n=o;break}}let l=n??i,s=!1;for(let n=r;n<o;n++){let r=e[n];t.has(r)||(s||(l===i&&Object.defineProperty(i,"name",{value:"module evaluation"}),s=!0),t.set(r,l))}r=o+1}}(t,S)),e.registerChunk(n,r)}let X=new Map;function D(e){let t=X.get(e);if(!t){let r,n;t={resolved:!1,loadingStarted:!1,promise:new Promise((e,t)=>{r=e,n=t}),resolve:()=>{t.resolved=!0,r()},reject:n},X.set(e,t)}return t}e={async registerChunk(e,r){let n=function(e){if("string"==typeof e)return e;let r=decodeURIComponent(e.src.replace(/[?#].*$/,""));return r.startsWith(t)?r.slice(t.length):r}(e);if(D("string"==typeof e?q(e):e.src).resolve(),null!=r){for(let e of r.otherChunks)D(q("string"==typeof e?e:e.path));if(await Promise.all(r.otherChunks.map(e=>T(i.Runtime,n,e))),r.runtimeModuleIds.length>0)for(let e of r.runtimeModuleIds)!function(e,t){let r=I[t];if(r){if(r.error)throw r.error;return}B(t,i.Runtime,e)}(n,e)}},loadChunkCached:(e,t)=>(function(e,t){let r=D(t);if(r.loadingStarted)return r.promise;if(e===i.Runtime)return r.loadingStarted=!0,L(t)&&r.resolve(),r.promise;if("function"==typeof importScripts)if(L(t));else if(N.test(t))self.TURBOPACK_NEXT_CHUNK_URLS.push(t),importScripts(t);else throw Error(`can't infer type of chunk from URL ${t} in worker`);else{let e=decodeURI(t);if(L(t))if(document.querySelectorAll(`link[rel=stylesheet][href="${t}"],link[rel=stylesheet][href^="${t}?"],link[rel=stylesheet][href="${e}"],link[rel=stylesheet][href^="${e}?"]`).length>0)r.resolve();else{let e=document.createElement("link");e.rel="stylesheet",e.href=t,e.onerror=()=>{r.reject()},e.onload=()=>{r.resolve()},document.head.appendChild(e)}else if(N.test(t)){let n=document.querySelectorAll(`script[src="${t}"],script[src^="${t}?"],script[src="${e}"],script[src^="${e}?"]`);if(n.length>0)for(let e of Array.from(n))e.addEventListener("error",()=>{r.reject()});else{let e=document.createElement("script");e.src=t,e.onerror=()=>{r.reject()},document.head.appendChild(e)}}else throw Error(`can't infer type of chunk from URL ${t}`)}return r.loadingStarted=!0,r.promise})(e,t),async loadWebAssembly(e,t,r,n,o){let i=fetch(q(r)),{instance:l}=await WebAssembly.instantiateStreaming(i,o);return l.exports},async loadWebAssemblyModule(e,t,r,n){let o=fetch(q(r));return await WebAssembly.compileStreaming(o)}};let H=globalThis.TURBOPACK;globalThis.TURBOPACK={push:F},H.forEach(F)})();