@poolzin/pool-bot 2026.2.0 → 2026.2.2

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 (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README-header.png +0 -0
  3. package/dist/agents/bash-tools.exec.js +76 -25
  4. package/dist/agents/cli-runner/helpers.js +9 -11
  5. package/dist/agents/context.js +1 -1
  6. package/dist/agents/identity.js +47 -7
  7. package/dist/agents/memory-search.js +25 -8
  8. package/dist/agents/model-catalog.js +1 -1
  9. package/dist/agents/model-selection.js +21 -0
  10. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  11. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  12. package/dist/agents/pi-embedded-helpers.js +1 -1
  13. package/dist/agents/pi-embedded-runner/compact.js +8 -10
  14. package/dist/agents/pi-embedded-runner/model.js +62 -3
  15. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  16. package/dist/agents/pi-embedded-runner/run.js +199 -46
  17. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  18. package/dist/agents/pi-embedded-subscribe.js +118 -29
  19. package/dist/agents/pi-tools.js +10 -5
  20. package/dist/agents/poolbot-tools.js +15 -10
  21. package/dist/agents/sandbox-paths.js +31 -0
  22. package/dist/agents/session-tool-result-guard.js +94 -15
  23. package/dist/agents/shell-utils.js +51 -0
  24. package/dist/agents/skills/bundled-context.js +23 -0
  25. package/dist/agents/skills/bundled-dir.js +41 -7
  26. package/dist/agents/skills-install.js +60 -23
  27. package/dist/agents/subagent-announce.js +79 -34
  28. package/dist/agents/tool-policy.conformance.js +14 -0
  29. package/dist/agents/tool-policy.js +24 -0
  30. package/dist/agents/tools/cron-tool.js +166 -19
  31. package/dist/agents/tools/discord-actions-presence.js +78 -0
  32. package/dist/agents/tools/image-tool.js +1 -1
  33. package/dist/agents/tools/message-tool.js +56 -2
  34. package/dist/agents/tools/sessions-history-tool.js +69 -1
  35. package/dist/agents/tools/web-search.js +211 -42
  36. package/dist/agents/usage.js +23 -1
  37. package/dist/agents/workspace-run.js +67 -0
  38. package/dist/agents/workspace-templates.js +44 -0
  39. package/dist/auto-reply/command-auth.js +121 -6
  40. package/dist/auto-reply/envelope.js +74 -82
  41. package/dist/auto-reply/reply/commands-compact.js +1 -0
  42. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  43. package/dist/auto-reply/reply/commands-context.js +1 -0
  44. package/dist/auto-reply/reply/commands-models.js +107 -60
  45. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  46. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  47. package/dist/auto-reply/reply/inbound-context.js +5 -1
  48. package/dist/auto-reply/reply/mentions.js +1 -1
  49. package/dist/auto-reply/reply/model-selection.js +3 -3
  50. package/dist/auto-reply/thinking.js +88 -43
  51. package/dist/browser/bridge-server.js +13 -0
  52. package/dist/browser/cdp.helpers.js +38 -24
  53. package/dist/browser/client-fetch.js +50 -7
  54. package/dist/browser/config.js +1 -10
  55. package/dist/browser/extension-relay.js +101 -40
  56. package/dist/browser/pw-ai.js +1 -1
  57. package/dist/browser/pw-session.js +143 -8
  58. package/dist/browser/pw-tools-core.interactions.js +125 -27
  59. package/dist/browser/pw-tools-core.responses.js +1 -1
  60. package/dist/browser/pw-tools-core.state.js +1 -1
  61. package/dist/browser/routes/agent.act.js +86 -41
  62. package/dist/browser/routes/dispatcher.js +4 -4
  63. package/dist/browser/screenshot.js +1 -1
  64. package/dist/browser/server.js +13 -0
  65. package/dist/build-info.json +3 -3
  66. package/dist/canvas-host/a2ui/index.html +28 -28
  67. package/dist/channels/reply-prefix.js +8 -1
  68. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  69. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  70. package/dist/cli/cron-cli/shared.js +56 -41
  71. package/dist/cli/dns-cli.js +26 -14
  72. package/dist/cli/gateway-cli/register.js +37 -19
  73. package/dist/cli/memory-cli.js +5 -5
  74. package/dist/cli/parse-bytes.js +37 -0
  75. package/dist/cli/update-cli.js +173 -52
  76. package/dist/commands/agent.js +1 -0
  77. package/dist/commands/auth-choice.apply.oauth.js +1 -1
  78. package/dist/commands/doctor-config-flow.js +61 -5
  79. package/dist/commands/doctor-state-migrations.js +1 -1
  80. package/dist/commands/health.js +1 -1
  81. package/dist/commands/model-allowlist.js +29 -0
  82. package/dist/commands/model-picker.js +2 -1
  83. package/dist/commands/models/list.registry.js +1 -1
  84. package/dist/commands/models/list.status-command.js +43 -23
  85. package/dist/commands/models/shared.js +15 -0
  86. package/dist/commands/onboard-custom.js +384 -0
  87. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  88. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  89. package/dist/commands/onboard-skills.js +63 -38
  90. package/dist/commands/openai-model-default.js +41 -0
  91. package/dist/compat/legacy-names.js +2 -0
  92. package/dist/config/defaults.js +3 -2
  93. package/dist/config/paths.js +136 -35
  94. package/dist/config/plugin-auto-enable.js +21 -5
  95. package/dist/config/redact-snapshot.js +153 -0
  96. package/dist/config/schema.field-metadata.js +590 -0
  97. package/dist/config/schema.js +2 -2
  98. package/dist/config/sessions/store.js +291 -23
  99. package/dist/config/zod-schema.agent-defaults.js +3 -0
  100. package/dist/config/zod-schema.agent-runtime.js +13 -2
  101. package/dist/config/zod-schema.providers-core.js +142 -0
  102. package/dist/config/zod-schema.session.js +3 -0
  103. package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
  104. package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
  105. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
  106. package/dist/control-ui/index.html +4 -4
  107. package/dist/cron/delivery.js +57 -0
  108. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  109. package/dist/cron/isolated-agent/helpers.js +22 -5
  110. package/dist/cron/isolated-agent/run.js +172 -63
  111. package/dist/cron/isolated-agent/session.js +2 -0
  112. package/dist/cron/normalize.js +356 -28
  113. package/dist/cron/parse.js +10 -5
  114. package/dist/cron/run-log.js +35 -10
  115. package/dist/cron/schedule.js +41 -6
  116. package/dist/cron/service/jobs.js +208 -35
  117. package/dist/cron/service/ops.js +72 -16
  118. package/dist/cron/service/state.js +2 -0
  119. package/dist/cron/service/store.js +386 -14
  120. package/dist/cron/service/timer.js +390 -147
  121. package/dist/cron/session-reaper.js +86 -0
  122. package/dist/cron/store.js +23 -8
  123. package/dist/cron/validate-timestamp.js +43 -0
  124. package/dist/discord/monitor/agent-components.js +438 -0
  125. package/dist/discord/monitor/allow-list.js +28 -5
  126. package/dist/discord/monitor/gateway-registry.js +29 -0
  127. package/dist/discord/monitor/native-command.js +44 -23
  128. package/dist/discord/monitor/sender-identity.js +45 -0
  129. package/dist/discord/pluralkit.js +27 -0
  130. package/dist/discord/send.outbound.js +92 -5
  131. package/dist/discord/send.shared.js +60 -23
  132. package/dist/discord/targets.js +84 -1
  133. package/dist/entry.js +15 -9
  134. package/dist/extensionAPI.js +8 -0
  135. package/dist/gateway/control-ui.js +8 -1
  136. package/dist/gateway/hooks-mapping.js +3 -0
  137. package/dist/gateway/hooks.js +65 -0
  138. package/dist/gateway/net.js +96 -31
  139. package/dist/gateway/node-command-policy.js +50 -15
  140. package/dist/gateway/origin-check.js +56 -0
  141. package/dist/gateway/protocol/client-info.js +9 -0
  142. package/dist/gateway/protocol/index.js +9 -2
  143. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  144. package/dist/gateway/protocol/schema/cron.js +22 -10
  145. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  146. package/dist/gateway/protocol/schema/sessions.js +12 -0
  147. package/dist/gateway/server/hooks.js +1 -1
  148. package/dist/gateway/server-broadcast.js +26 -9
  149. package/dist/gateway/server-chat.js +112 -23
  150. package/dist/gateway/server-discovery-runtime.js +10 -2
  151. package/dist/gateway/server-http.js +109 -11
  152. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  153. package/dist/gateway/server-methods/agents.js +321 -2
  154. package/dist/gateway/server-methods/usage.js +559 -16
  155. package/dist/gateway/server-runtime-state.js +22 -8
  156. package/dist/gateway/server-startup-memory.js +16 -0
  157. package/dist/gateway/server.impl.js +5 -1
  158. package/dist/gateway/session-utils.fs.js +23 -25
  159. package/dist/gateway/session-utils.js +20 -10
  160. package/dist/gateway/sessions-patch.js +7 -22
  161. package/dist/gateway/test-helpers.mocks.js +11 -7
  162. package/dist/gateway/test-helpers.server.js +35 -2
  163. package/dist/imessage/constants.js +2 -0
  164. package/dist/imessage/monitor/deliver.js +4 -1
  165. package/dist/imessage/monitor/monitor-provider.js +51 -1
  166. package/dist/infra/bonjour-discovery.js +131 -70
  167. package/dist/infra/control-ui-assets.js +134 -12
  168. package/dist/infra/errors.js +12 -0
  169. package/dist/infra/exec-approvals.js +266 -57
  170. package/dist/infra/format-time/format-datetime.js +79 -0
  171. package/dist/infra/format-time/format-duration.js +81 -0
  172. package/dist/infra/format-time/format-relative.js +80 -0
  173. package/dist/infra/heartbeat-runner.js +140 -49
  174. package/dist/infra/home-dir.js +54 -0
  175. package/dist/infra/net/fetch-guard.js +122 -0
  176. package/dist/infra/net/ssrf.js +65 -29
  177. package/dist/infra/outbound/abort.js +14 -0
  178. package/dist/infra/outbound/message-action-runner.js +77 -13
  179. package/dist/infra/outbound/outbound-session.js +143 -37
  180. package/dist/infra/poolbot-root.js +43 -1
  181. package/dist/infra/session-cost-usage.js +631 -41
  182. package/dist/infra/state-migrations.js +317 -47
  183. package/dist/infra/update-global.js +35 -0
  184. package/dist/infra/update-runner.js +149 -43
  185. package/dist/infra/warning-filter.js +65 -0
  186. package/dist/infra/widearea-dns.js +30 -9
  187. package/dist/logging/redact-identifier.js +12 -0
  188. package/dist/media/fetch.js +81 -58
  189. package/dist/media/store.js +2 -0
  190. package/dist/media-understanding/apply.js +403 -3
  191. package/dist/media-understanding/attachments.js +38 -27
  192. package/dist/media-understanding/defaults.js +16 -0
  193. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  194. package/dist/media-understanding/providers/google/audio.js +24 -17
  195. package/dist/media-understanding/providers/google/video.js +24 -17
  196. package/dist/media-understanding/providers/image.js +3 -3
  197. package/dist/media-understanding/providers/index.js +4 -1
  198. package/dist/media-understanding/providers/openai/audio.js +22 -14
  199. package/dist/media-understanding/providers/shared.js +16 -11
  200. package/dist/media-understanding/providers/zai/index.js +6 -0
  201. package/dist/media-understanding/runner.js +158 -90
  202. package/dist/memory/batch-voyage.js +277 -0
  203. package/dist/memory/embeddings-voyage.js +75 -0
  204. package/dist/memory/embeddings.js +28 -16
  205. package/dist/memory/internal.js +101 -18
  206. package/dist/memory/manager.js +154 -48
  207. package/dist/memory/search-manager.js +173 -0
  208. package/dist/memory/session-files.js +9 -3
  209. package/dist/node-host/runner.js +34 -24
  210. package/dist/node-host/with-timeout.js +27 -0
  211. package/dist/plugins/commands.js +5 -1
  212. package/dist/plugins/config-state.js +86 -7
  213. package/dist/plugins/source-display.js +51 -0
  214. package/dist/process/exec.js +20 -2
  215. package/dist/routing/resolve-route.js +12 -0
  216. package/dist/routing/session-key.js +15 -0
  217. package/dist/runtime.js +2 -0
  218. package/dist/security/audit-extra.async.js +601 -0
  219. package/dist/security/audit-extra.js +2 -830
  220. package/dist/security/audit-extra.sync.js +505 -0
  221. package/dist/security/channel-metadata.js +34 -0
  222. package/dist/security/external-content.js +88 -6
  223. package/dist/security/skill-scanner.js +330 -0
  224. package/dist/sessions/session-key-utils.js +7 -0
  225. package/dist/signal/monitor/event-handler.js +80 -1
  226. package/dist/slack/monitor/media.js +85 -15
  227. package/dist/tailscale/detect.js +1 -2
  228. package/dist/telegram/bot/helpers.js +109 -28
  229. package/dist/telegram/bot-handlers.js +144 -3
  230. package/dist/telegram/bot-message-context.js +37 -10
  231. package/dist/telegram/bot-message-dispatch.js +54 -17
  232. package/dist/telegram/bot-native-commands.js +86 -29
  233. package/dist/telegram/bot.js +30 -29
  234. package/dist/telegram/model-buttons.js +163 -0
  235. package/dist/telegram/monitor.js +110 -85
  236. package/dist/telegram/send.js +129 -47
  237. package/dist/terminal/restore.js +45 -0
  238. package/dist/test-helpers/state-dir-env.js +16 -0
  239. package/dist/tts/tts.js +12 -6
  240. package/dist/tui/tui-session-actions.js +166 -54
  241. package/dist/utils/fetch-timeout.js +20 -0
  242. package/dist/utils/normalize-secret-input.js +19 -0
  243. package/dist/utils/transcript-tools.js +58 -0
  244. package/dist/utils.js +45 -14
  245. package/dist/version.js +42 -5
  246. package/dist/wizard/clack-prompter.js +9 -6
  247. package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
  248. package/extensions/googlechat/package.json +2 -2
  249. package/extensions/line/node_modules/.bin/poolbot +21 -0
  250. package/extensions/line/package.json +1 -1
  251. package/extensions/matrix/node_modules/.bin/poolbot +21 -0
  252. package/extensions/matrix/package.json +1 -1
  253. package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
  254. package/extensions/memory-core/package.json +4 -1
  255. package/extensions/twitch/node_modules/.bin/poolbot +21 -0
  256. package/extensions/twitch/package.json +1 -1
  257. package/package.json +183 -24
  258. package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
@@ -23,7 +23,18 @@ function enhanceBrowserFetchError(url, err, timeoutMs) {
23
23
  async function fetchHttpJson(url, init) {
24
24
  const timeoutMs = init.timeoutMs ?? 5000;
25
25
  const ctrl = new AbortController();
26
- const t = setTimeout(() => ctrl.abort(), timeoutMs);
26
+ const upstreamSignal = init.signal;
27
+ let upstreamAbortListener;
28
+ if (upstreamSignal) {
29
+ if (upstreamSignal.aborted) {
30
+ ctrl.abort(upstreamSignal.reason);
31
+ }
32
+ else {
33
+ upstreamAbortListener = () => ctrl.abort(upstreamSignal.reason);
34
+ upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
35
+ }
36
+ }
37
+ const t = setTimeout(() => ctrl.abort(new Error("timed out")), timeoutMs);
27
38
  try {
28
39
  const res = await fetch(url, { ...init, signal: ctrl.signal });
29
40
  if (!res.ok) {
@@ -34,6 +45,9 @@ async function fetchHttpJson(url, init) {
34
45
  }
35
46
  finally {
36
47
  clearTimeout(t);
48
+ if (upstreamSignal && upstreamAbortListener) {
49
+ upstreamSignal.removeEventListener("abort", upstreamAbortListener);
50
+ }
37
51
  }
38
52
  }
39
53
  export async function fetchBrowserJson(url, init) {
@@ -61,6 +75,29 @@ export async function fetchBrowserJson(url, init) {
61
75
  // keep as string
62
76
  }
63
77
  }
78
+ const abortCtrl = new AbortController();
79
+ const upstreamSignal = init?.signal;
80
+ let upstreamAbortListener;
81
+ if (upstreamSignal) {
82
+ if (upstreamSignal.aborted) {
83
+ abortCtrl.abort(upstreamSignal.reason);
84
+ }
85
+ else {
86
+ upstreamAbortListener = () => abortCtrl.abort(upstreamSignal.reason);
87
+ upstreamSignal.addEventListener("abort", upstreamAbortListener, { once: true });
88
+ }
89
+ }
90
+ let abortListener;
91
+ const abortPromise = abortCtrl.signal.aborted
92
+ ? Promise.reject(abortCtrl.signal.reason ?? new Error("aborted"))
93
+ : new Promise((_, reject) => {
94
+ abortListener = () => reject(abortCtrl.signal.reason ?? new Error("aborted"));
95
+ abortCtrl.signal.addEventListener("abort", abortListener, { once: true });
96
+ });
97
+ let timer;
98
+ if (timeoutMs) {
99
+ timer = setTimeout(() => abortCtrl.abort(new Error("timed out")), timeoutMs);
100
+ }
64
101
  const dispatchPromise = dispatcher.dispatch({
65
102
  method: init?.method?.toUpperCase() === "DELETE"
66
103
  ? "DELETE"
@@ -70,13 +107,19 @@ export async function fetchBrowserJson(url, init) {
70
107
  path: parsed.pathname,
71
108
  query,
72
109
  body,
110
+ signal: abortCtrl.signal,
111
+ });
112
+ const result = await Promise.race([dispatchPromise, abortPromise]).finally(() => {
113
+ if (timer) {
114
+ clearTimeout(timer);
115
+ }
116
+ if (abortListener) {
117
+ abortCtrl.signal.removeEventListener("abort", abortListener);
118
+ }
119
+ if (upstreamSignal && upstreamAbortListener) {
120
+ upstreamSignal.removeEventListener("abort", upstreamAbortListener);
121
+ }
73
122
  });
74
- const result = await (timeoutMs
75
- ? Promise.race([
76
- dispatchPromise,
77
- new Promise((_, reject) => setTimeout(() => reject(new Error("timed out")), timeoutMs)),
78
- ])
79
- : dispatchPromise);
80
123
  if (result.status >= 400) {
81
124
  const message = result.body && typeof result.body === "object" && "error" in result.body
82
125
  ? String(result.body.error)
@@ -1,17 +1,8 @@
1
1
  import { deriveDefaultBrowserCdpPortRange, deriveDefaultBrowserControlPort, DEFAULT_BROWSER_CONTROL_PORT, } from "../config/port-defaults.js";
2
2
  import { resolveGatewayPort } from "../config/paths.js";
3
+ import { isLoopbackHost } from "../gateway/net.js";
3
4
  import { DEFAULT_CLAWD_BROWSER_COLOR, DEFAULT_CLAWD_BROWSER_ENABLED, DEFAULT_BROWSER_EVALUATE_ENABLED, DEFAULT_BROWSER_DEFAULT_PROFILE_NAME, DEFAULT_CLAWD_BROWSER_PROFILE_NAME, } from "./constants.js";
4
5
  import { CDP_PORT_RANGE_START, getUsedPorts } from "./profiles.js";
5
- function isLoopbackHost(host) {
6
- const h = host.trim().toLowerCase();
7
- return (h === "localhost" ||
8
- h === "127.0.0.1" ||
9
- h === "0.0.0.0" ||
10
- h === "[::1]" ||
11
- h === "::1" ||
12
- h === "[::]" ||
13
- h === "::");
14
- }
15
6
  function normalizeHexColor(raw) {
16
7
  const value = (raw ?? "").trim();
17
8
  if (!value)
@@ -1,28 +1,20 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import { createServer } from "node:http";
2
3
  import WebSocket, { WebSocketServer } from "ws";
4
+ import { isLoopbackAddress, isLoopbackHost } from "../gateway/net.js";
3
5
  import { rawDataToString } from "../infra/ws.js";
4
- function isLoopbackHost(host) {
5
- const h = host.trim().toLowerCase();
6
- return (h === "localhost" ||
7
- h === "127.0.0.1" ||
8
- h === "0.0.0.0" ||
9
- h === "[::1]" ||
10
- h === "::1" ||
11
- h === "[::]" ||
12
- h === "::");
6
+ const RELAY_AUTH_HEADER = "x-poolbot-relay-token";
7
+ function headerValue(value) {
8
+ if (!value) {
9
+ return undefined;
10
+ }
11
+ if (Array.isArray(value)) {
12
+ return value[0];
13
+ }
14
+ return value;
13
15
  }
14
- function isLoopbackAddress(ip) {
15
- if (!ip)
16
- return false;
17
- if (ip === "127.0.0.1")
18
- return true;
19
- if (ip.startsWith("127."))
20
- return true;
21
- if (ip === "::1")
22
- return true;
23
- if (ip.startsWith("::ffff:127."))
24
- return true;
25
- return false;
16
+ function getHeader(req, name) {
17
+ return headerValue(req.headers[name.toLowerCase()]);
26
18
  }
27
19
  function parseBaseUrl(raw) {
28
20
  const parsed = new URL(raw.trim().replace(/\/$/, ""));
@@ -56,14 +48,43 @@ function rejectUpgrade(socket, status, bodyText) {
56
48
  }
57
49
  }
58
50
  const serversByPort = new Map();
51
+ const relayAuthByPort = new Map();
52
+ function relayAuthTokenForUrl(url) {
53
+ try {
54
+ const parsed = new URL(url);
55
+ if (!isLoopbackHost(parsed.hostname)) {
56
+ return null;
57
+ }
58
+ const port = parsed.port?.trim() !== ""
59
+ ? Number(parsed.port)
60
+ : parsed.protocol === "https:" || parsed.protocol === "wss:"
61
+ ? 443
62
+ : 80;
63
+ if (!Number.isFinite(port)) {
64
+ return null;
65
+ }
66
+ return relayAuthByPort.get(port) ?? null;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ export function getChromeExtensionRelayAuthHeaders(url) {
73
+ const token = relayAuthTokenForUrl(url);
74
+ if (!token) {
75
+ return {};
76
+ }
77
+ return { [RELAY_AUTH_HEADER]: token };
78
+ }
59
79
  export async function ensureChromeExtensionRelayServer(opts) {
60
80
  const info = parseBaseUrl(opts.cdpUrl);
61
81
  if (!isLoopbackHost(info.host)) {
62
82
  throw new Error(`extension relay requires loopback cdpUrl host (got ${info.host})`);
63
83
  }
64
84
  const existing = serversByPort.get(info.port);
65
- if (existing)
85
+ if (existing) {
66
86
  return existing;
87
+ }
67
88
  let extensionWs = null;
68
89
  const cdpClients = new Set();
69
90
  const connectedTargets = new Map();
@@ -86,14 +107,16 @@ export async function ensureChromeExtensionRelayServer(opts) {
86
107
  const broadcastToCdpClients = (evt) => {
87
108
  const msg = JSON.stringify(evt);
88
109
  for (const ws of cdpClients) {
89
- if (ws.readyState !== WebSocket.OPEN)
110
+ if (ws.readyState !== WebSocket.OPEN) {
90
111
  continue;
112
+ }
91
113
  ws.send(msg);
92
114
  }
93
115
  };
94
116
  const sendResponseToCdp = (ws, res) => {
95
- if (ws.readyState !== WebSocket.OPEN)
117
+ if (ws.readyState !== WebSocket.OPEN) {
96
118
  return;
119
+ }
97
120
  ws.send(JSON.stringify(res));
98
121
  };
99
122
  const ensureTargetEventsForClient = (ws, mode) => {
@@ -143,14 +166,16 @@ export async function ensureChromeExtensionRelayServer(opts) {
143
166
  const targetId = typeof params.targetId === "string" ? params.targetId : undefined;
144
167
  if (targetId) {
145
168
  for (const t of connectedTargets.values()) {
146
- if (t.targetId === targetId)
169
+ if (t.targetId === targetId) {
147
170
  return { targetInfo: t.targetInfo };
171
+ }
148
172
  }
149
173
  }
150
174
  if (cmd.sessionId && connectedTargets.has(cmd.sessionId)) {
151
175
  const t = connectedTargets.get(cmd.sessionId);
152
- if (t)
176
+ if (t) {
153
177
  return { targetInfo: t.targetInfo };
178
+ }
154
179
  }
155
180
  const first = Array.from(connectedTargets.values())[0];
156
181
  return { targetInfo: first?.targetInfo };
@@ -158,11 +183,13 @@ export async function ensureChromeExtensionRelayServer(opts) {
158
183
  case "Target.attachToTarget": {
159
184
  const params = (cmd.params ?? {});
160
185
  const targetId = typeof params.targetId === "string" ? params.targetId : undefined;
161
- if (!targetId)
186
+ if (!targetId) {
162
187
  throw new Error("targetId required");
188
+ }
163
189
  for (const t of connectedTargets.values()) {
164
- if (t.targetId === targetId)
190
+ if (t.targetId === targetId) {
165
191
  return { sessionId: t.sessionId };
192
+ }
166
193
  }
167
194
  throw new Error("target not found");
168
195
  }
@@ -180,9 +207,18 @@ export async function ensureChromeExtensionRelayServer(opts) {
180
207
  }
181
208
  }
182
209
  };
210
+ const relayAuthToken = randomBytes(32).toString("base64url");
183
211
  const server = createServer((req, res) => {
184
212
  const url = new URL(req.url ?? "/", info.baseUrl);
185
213
  const path = url.pathname;
214
+ if (path.startsWith("/json")) {
215
+ const token = getHeader(req, RELAY_AUTH_HEADER);
216
+ if (!token || token !== relayAuthToken) {
217
+ res.writeHead(401);
218
+ res.end("Unauthorized");
219
+ return;
220
+ }
221
+ }
186
222
  if (req.method === "HEAD" && path === "/") {
187
223
  res.writeHead(200);
188
224
  res.end();
@@ -208,8 +244,9 @@ export async function ensureChromeExtensionRelayServer(opts) {
208
244
  "Protocol-Version": "1.3",
209
245
  };
210
246
  // Only advertise the WS URL if a real extension is connected.
211
- if (extensionWs)
247
+ if (extensionWs) {
212
248
  payload.webSocketDebuggerUrl = cdpWsUrl;
249
+ }
213
250
  res.writeHead(200, { "Content-Type": "application/json" });
214
251
  res.end(JSON.stringify(payload));
215
252
  return;
@@ -290,6 +327,11 @@ export async function ensureChromeExtensionRelayServer(opts) {
290
327
  rejectUpgrade(socket, 403, "Forbidden");
291
328
  return;
292
329
  }
330
+ const origin = headerValue(req.headers.origin);
331
+ if (origin && !origin.startsWith("chrome-extension://")) {
332
+ rejectUpgrade(socket, 403, "Forbidden: invalid origin");
333
+ return;
334
+ }
293
335
  if (pathname === "/extension") {
294
336
  if (extensionWs) {
295
337
  rejectUpgrade(socket, 409, "Extension already connected");
@@ -301,6 +343,11 @@ export async function ensureChromeExtensionRelayServer(opts) {
301
343
  return;
302
344
  }
303
345
  if (pathname === "/cdp") {
346
+ const token = getHeader(req, RELAY_AUTH_HEADER);
347
+ if (!token || token !== relayAuthToken) {
348
+ rejectUpgrade(socket, 401, "Unauthorized");
349
+ return;
350
+ }
304
351
  if (!extensionWs) {
305
352
  rejectUpgrade(socket, 503, "Extension not connected");
306
353
  return;
@@ -315,8 +362,9 @@ export async function ensureChromeExtensionRelayServer(opts) {
315
362
  wssExtension.on("connection", (ws) => {
316
363
  extensionWs = ws;
317
364
  const ping = setInterval(() => {
318
- if (ws.readyState !== WebSocket.OPEN)
365
+ if (ws.readyState !== WebSocket.OPEN) {
319
366
  return;
367
+ }
320
368
  ws.send(JSON.stringify({ method: "ping" }));
321
369
  }, 5000);
322
370
  ws.on("message", (data) => {
@@ -329,8 +377,9 @@ export async function ensureChromeExtensionRelayServer(opts) {
329
377
  }
330
378
  if (parsed && typeof parsed === "object" && "id" in parsed && typeof parsed.id === "number") {
331
379
  const pending = pendingExtension.get(parsed.id);
332
- if (!pending)
380
+ if (!pending) {
333
381
  return;
382
+ }
334
383
  pendingExtension.delete(parsed.id);
335
384
  clearTimeout(pending.timer);
336
385
  if ("error" in parsed && typeof parsed.error === "string" && parsed.error.trim()) {
@@ -342,21 +391,25 @@ export async function ensureChromeExtensionRelayServer(opts) {
342
391
  return;
343
392
  }
344
393
  if (parsed && typeof parsed === "object" && "method" in parsed) {
345
- if (parsed.method === "pong")
394
+ if (parsed.method === "pong") {
346
395
  return;
347
- if (parsed.method !== "forwardCDPEvent")
396
+ }
397
+ if (parsed.method !== "forwardCDPEvent") {
348
398
  return;
399
+ }
349
400
  const evt = parsed;
350
401
  const method = evt.params?.method;
351
402
  const params = evt.params?.params;
352
403
  const sessionId = evt.params?.sessionId;
353
- if (!method || typeof method !== "string")
404
+ if (!method || typeof method !== "string") {
354
405
  return;
406
+ }
355
407
  if (method === "Target.attachedToTarget") {
356
408
  const attached = (params ?? {});
357
409
  const targetType = attached?.targetInfo?.type ?? "page";
358
- if (targetType !== "page")
410
+ if (targetType !== "page") {
359
411
  return;
412
+ }
360
413
  if (attached?.sessionId && attached?.targetInfo?.targetId) {
361
414
  const prev = connectedTargets.get(attached.sessionId);
362
415
  const nextTargetId = attached.targetInfo.targetId;
@@ -382,8 +435,9 @@ export async function ensureChromeExtensionRelayServer(opts) {
382
435
  }
383
436
  if (method === "Target.detachedFromTarget") {
384
437
  const detached = (params ?? {});
385
- if (detached?.sessionId)
438
+ if (detached?.sessionId) {
386
439
  connectedTargets.delete(detached.sessionId);
440
+ }
387
441
  broadcastToCdpClients({ method, params, sessionId });
388
442
  return;
389
443
  }
@@ -395,8 +449,9 @@ export async function ensureChromeExtensionRelayServer(opts) {
395
449
  const targetId = targetInfo?.targetId;
396
450
  if (targetId && (targetInfo?.type ?? "page") === "page") {
397
451
  for (const [sid, target] of connectedTargets) {
398
- if (target.targetId !== targetId)
452
+ if (target.targetId !== targetId) {
399
453
  continue;
454
+ }
400
455
  connectedTargets.set(sid, {
401
456
  ...target,
402
457
  targetInfo: { ...target.targetInfo, ...targetInfo },
@@ -437,10 +492,12 @@ export async function ensureChromeExtensionRelayServer(opts) {
437
492
  catch {
438
493
  return;
439
494
  }
440
- if (!cmd || typeof cmd !== "object")
495
+ if (!cmd || typeof cmd !== "object") {
441
496
  return;
442
- if (typeof cmd.id !== "number" || typeof cmd.method !== "string")
497
+ }
498
+ if (typeof cmd.id !== "number" || typeof cmd.method !== "string") {
443
499
  return;
500
+ }
444
501
  if (!extensionWs) {
445
502
  sendResponseToCdp(ws, {
446
503
  id: cmd.id,
@@ -507,6 +564,7 @@ export async function ensureChromeExtensionRelayServer(opts) {
507
564
  extensionConnected: () => Boolean(extensionWs),
508
565
  stop: async () => {
509
566
  serversByPort.delete(port);
567
+ relayAuthByPort.delete(port);
510
568
  try {
511
569
  extensionWs?.close(1001, "server stopping");
512
570
  }
@@ -528,14 +586,17 @@ export async function ensureChromeExtensionRelayServer(opts) {
528
586
  wssCdp.close();
529
587
  },
530
588
  };
589
+ relayAuthByPort.set(port, relayAuthToken);
531
590
  serversByPort.set(port, relay);
532
591
  return relay;
533
592
  }
534
593
  export async function stopChromeExtensionRelayServer(opts) {
535
594
  const info = parseBaseUrl(opts.cdpUrl);
536
595
  const existing = serversByPort.get(info.port);
537
- if (!existing)
596
+ if (!existing) {
538
597
  return false;
598
+ }
539
599
  await existing.stop();
600
+ relayAuthByPort.delete(info.port);
540
601
  return true;
541
602
  }
@@ -1,2 +1,2 @@
1
- export { closePageByTargetIdViaPlaywright, closePlaywrightBrowserConnection, createPageViaPlaywright, ensurePageState, focusPageByTargetIdViaPlaywright, getPageForTargetId, listPagesViaPlaywright, refLocator, } from "./pw-session.js";
1
+ export { closePageByTargetIdViaPlaywright, closePlaywrightBrowserConnection, createPageViaPlaywright, ensurePageState, forceDisconnectPlaywrightForTarget, focusPageByTargetIdViaPlaywright, getPageForTargetId, listPagesViaPlaywright, refLocator, } from "./pw-session.js";
2
2
  export { armDialogViaPlaywright, armFileUploadViaPlaywright, clickViaPlaywright, closePageViaPlaywright, cookiesClearViaPlaywright, cookiesGetViaPlaywright, cookiesSetViaPlaywright, downloadViaPlaywright, dragViaPlaywright, emulateMediaViaPlaywright, evaluateViaPlaywright, fillFormViaPlaywright, getConsoleMessagesViaPlaywright, getNetworkRequestsViaPlaywright, getPageErrorsViaPlaywright, highlightViaPlaywright, hoverViaPlaywright, navigateViaPlaywright, pdfViaPlaywright, pressKeyViaPlaywright, resizeViewportViaPlaywright, responseBodyViaPlaywright, scrollIntoViewViaPlaywright, selectOptionViaPlaywright, setDeviceViaPlaywright, setExtraHTTPHeadersViaPlaywright, setGeolocationViaPlaywright, setHttpCredentialsViaPlaywright, setInputFilesViaPlaywright, setLocaleViaPlaywright, setOfflineViaPlaywright, setTimezoneViaPlaywright, snapshotAiViaPlaywright, snapshotAriaViaPlaywright, snapshotRoleViaPlaywright, screenshotWithLabelsViaPlaywright, storageClearViaPlaywright, storageGetViaPlaywright, storageSetViaPlaywright, takeScreenshotViaPlaywright, traceStartViaPlaywright, traceStopViaPlaywright, typeViaPlaywright, waitForDownloadViaPlaywright, waitForViaPlaywright, } from "./pw-tools-core.js";
@@ -1,6 +1,7 @@
1
1
  import { chromium } from "playwright-core";
2
2
  import { formatErrorMessage } from "../infra/errors.js";
3
- import { getHeadersWithAuth } from "./cdp.helpers.js";
3
+ import { appendCdpPath, fetchJson, getHeadersWithAuth, withCdpSocket } from "./cdp.helpers.js";
4
+ import { normalizeCdpWsUrl } from "./cdp.js";
4
5
  import { getChromeWebSocketUrl } from "./chrome.js";
5
6
  const pageStates = new WeakMap();
6
7
  const contextStates = new WeakMap();
@@ -196,13 +197,15 @@ async function connectBrowser(cdpUrl) {
196
197
  const endpoint = wsUrl ?? normalized;
197
198
  const headers = getHeadersWithAuth(endpoint);
198
199
  const browser = await chromium.connectOverCDP(endpoint, { timeout, headers });
199
- const connected = { browser, cdpUrl: normalized };
200
+ const onDisconnected = () => {
201
+ if (cached?.browser === browser) {
202
+ cached = null;
203
+ }
204
+ };
205
+ const connected = { browser, cdpUrl: normalized, onDisconnected };
200
206
  cached = connected;
207
+ browser.on("disconnected", onDisconnected);
201
208
  observeBrowser(browser);
202
- browser.on("disconnected", () => {
203
- if (cached?.browser === browser)
204
- cached = null;
205
- });
206
209
  return connected;
207
210
  }
208
211
  catch (err) {
@@ -254,7 +257,8 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
254
257
  .replace(/\/+$/, "")
255
258
  .replace(/^ws:/, "http:")
256
259
  .replace(/\/cdp$/, "");
257
- const response = await fetch(`${baseUrl}/json/list`);
260
+ const listUrl = `${baseUrl}/json/list`;
261
+ const response = await fetch(listUrl, { headers: getHeadersWithAuth(listUrl) });
258
262
  if (response.ok) {
259
263
  const targets = (await response.json());
260
264
  const target = targets.find((t) => t.id === targetId);
@@ -335,10 +339,141 @@ export function refLocator(page, ref) {
335
339
  export async function closePlaywrightBrowserConnection() {
336
340
  const cur = cached;
337
341
  cached = null;
338
- if (!cur)
342
+ connecting = null;
343
+ if (!cur) {
339
344
  return;
345
+ }
346
+ if (cur.onDisconnected && typeof cur.browser.off === "function") {
347
+ cur.browser.off("disconnected", cur.onDisconnected);
348
+ }
340
349
  await cur.browser.close().catch(() => { });
341
350
  }
351
+ function normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) {
352
+ try {
353
+ const url = new URL(cdpUrl);
354
+ if (url.protocol === "ws:") {
355
+ url.protocol = "http:";
356
+ }
357
+ else if (url.protocol === "wss:") {
358
+ url.protocol = "https:";
359
+ }
360
+ url.pathname = url.pathname.replace(/\/devtools\/browser\/.*$/, "");
361
+ url.pathname = url.pathname.replace(/\/cdp$/, "");
362
+ return url.toString().replace(/\/$/, "");
363
+ }
364
+ catch {
365
+ // Best-effort fallback for non-URL-ish inputs.
366
+ return cdpUrl
367
+ .replace(/^ws:/, "http:")
368
+ .replace(/^wss:/, "https:")
369
+ .replace(/\/devtools\/browser\/.*$/, "")
370
+ .replace(/\/cdp$/, "")
371
+ .replace(/\/$/, "");
372
+ }
373
+ }
374
+ function cdpSocketNeedsAttach(wsUrl) {
375
+ try {
376
+ const pathname = new URL(wsUrl).pathname;
377
+ return (pathname === "/cdp" || pathname.endsWith("/cdp") || pathname.includes("/devtools/browser/"));
378
+ }
379
+ catch {
380
+ return false;
381
+ }
382
+ }
383
+ async function tryTerminateExecutionViaCdp(opts) {
384
+ const cdpHttpBase = normalizeCdpHttpBaseForJsonEndpoints(opts.cdpUrl);
385
+ const listUrl = appendCdpPath(cdpHttpBase, "/json/list");
386
+ const pages = await fetchJson(listUrl, 2000).catch(() => null);
387
+ if (!pages || pages.length === 0) {
388
+ return;
389
+ }
390
+ const target = pages.find((p) => String(p.id ?? "").trim() === opts.targetId);
391
+ const wsUrlRaw = String(target?.webSocketDebuggerUrl ?? "").trim();
392
+ if (!wsUrlRaw) {
393
+ return;
394
+ }
395
+ const wsUrl = normalizeCdpWsUrl(wsUrlRaw, cdpHttpBase);
396
+ const needsAttach = cdpSocketNeedsAttach(wsUrl);
397
+ const runWithTimeout = async (work, ms) => {
398
+ let timer;
399
+ const timeoutPromise = new Promise((_, reject) => {
400
+ timer = setTimeout(() => reject(new Error("CDP command timed out")), ms);
401
+ });
402
+ try {
403
+ return await Promise.race([work, timeoutPromise]);
404
+ }
405
+ finally {
406
+ if (timer) {
407
+ clearTimeout(timer);
408
+ }
409
+ }
410
+ };
411
+ await withCdpSocket(wsUrl, async (send) => {
412
+ let sessionId;
413
+ try {
414
+ if (needsAttach) {
415
+ const attached = (await runWithTimeout(send("Target.attachToTarget", { targetId: opts.targetId, flatten: true }), 1500));
416
+ if (typeof attached?.sessionId === "string" && attached.sessionId.trim()) {
417
+ sessionId = attached.sessionId;
418
+ }
419
+ }
420
+ await runWithTimeout(send("Runtime.terminateExecution", undefined, sessionId), 1500);
421
+ if (sessionId) {
422
+ // Best-effort cleanup; not required for termination to take effect.
423
+ void send("Target.detachFromTarget", { sessionId }).catch(() => { });
424
+ }
425
+ }
426
+ catch {
427
+ // Best-effort; ignore
428
+ }
429
+ }, { handshakeTimeoutMs: 2000 }).catch(() => { });
430
+ }
431
+ /**
432
+ * Best-effort cancellation for stuck page operations.
433
+ *
434
+ * Playwright serializes CDP commands per page; a long-running or stuck operation (notably evaluate)
435
+ * can block all subsequent commands. We cannot safely "cancel" an individual command, and we do
436
+ * not want to close the actual Chromium tab. Instead, we disconnect Playwright's CDP connection
437
+ * so in-flight commands fail fast and the next request reconnects transparently.
438
+ *
439
+ * IMPORTANT: We CANNOT call Connection.close() because Playwright shares a single Connection
440
+ * across all objects (BrowserType, Browser, etc.). Closing it corrupts the entire Playwright
441
+ * instance, preventing reconnection.
442
+ *
443
+ * Instead we:
444
+ * 1. Null out `cached` so the next call triggers a fresh connectOverCDP
445
+ * 2. Fire-and-forget browser.close() — it may hang but won't block us
446
+ * 3. The next connectBrowser() creates a completely new CDP WebSocket connection
447
+ *
448
+ * The old browser.close() eventually resolves when the in-browser evaluate timeout fires,
449
+ * or the old connection gets GC'd. Either way, it doesn't affect the fresh connection.
450
+ */
451
+ export async function forceDisconnectPlaywrightForTarget(opts) {
452
+ const normalized = normalizeCdpUrl(opts.cdpUrl);
453
+ if (cached?.cdpUrl !== normalized) {
454
+ return;
455
+ }
456
+ const cur = cached;
457
+ cached = null;
458
+ // Also clear `connecting` so the next call does a fresh connectOverCDP
459
+ // rather than awaiting a stale promise.
460
+ connecting = null;
461
+ if (cur) {
462
+ // Remove the "disconnected" listener to prevent the old browser's teardown
463
+ // from racing with a fresh connection and nulling the new `cached`.
464
+ if (cur.onDisconnected && typeof cur.browser.off === "function") {
465
+ cur.browser.off("disconnected", cur.onDisconnected);
466
+ }
467
+ // Best-effort: kill any stuck JS to unblock the target's execution context before we
468
+ // disconnect Playwright's CDP connection.
469
+ const targetId = opts.targetId?.trim() || "";
470
+ if (targetId) {
471
+ await tryTerminateExecutionViaCdp({ cdpUrl: normalized, targetId }).catch(() => { });
472
+ }
473
+ // Fire-and-forget: don't await because browser.close() may hang on the stuck CDP pipe.
474
+ cur.browser.close().catch(() => { });
475
+ }
476
+ }
342
477
  /**
343
478
  * List all pages/tabs from the persistent Playwright connection.
344
479
  * Used for remote profiles where HTTP-based /json/list is ephemeral.