@poolzin/pool-bot 2026.2.21 → 2026.2.22

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 (369) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/agents/api-key-rotation.js +47 -0
  3. package/dist/agents/apply-patch-update.js +19 -9
  4. package/dist/agents/apply-patch.js +72 -47
  5. package/dist/agents/bash-tools.exec.js +141 -559
  6. package/dist/agents/cli-backends.js +49 -6
  7. package/dist/agents/cli-runner/helpers.js +69 -152
  8. package/dist/agents/cli-runner.js +70 -19
  9. package/dist/agents/identity.js +20 -1
  10. package/dist/agents/image-sanitization.js +9 -0
  11. package/dist/agents/live-auth-keys.js +123 -26
  12. package/dist/agents/live-model-filter.js +13 -4
  13. package/dist/agents/model-catalog.js +40 -9
  14. package/dist/agents/model-forward-compat.js +60 -23
  15. package/dist/agents/model-selection.js +134 -41
  16. package/dist/agents/pi-auth-json.js +2 -2
  17. package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
  18. package/dist/agents/pi-embedded-helpers/errors.js +140 -15
  19. package/dist/agents/pi-embedded-helpers/images.js +22 -12
  20. package/dist/agents/pi-embedded-helpers.js +2 -2
  21. package/dist/agents/pi-embedded-runner/abort.js +10 -3
  22. package/dist/agents/pi-embedded-runner/compact.js +230 -32
  23. package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
  24. package/dist/agents/pi-embedded-runner/google.js +109 -19
  25. package/dist/agents/pi-embedded-runner/history.js +35 -17
  26. package/dist/agents/pi-embedded-runner/run/attempt.js +386 -95
  27. package/dist/agents/pi-embedded-runner/run/images.js +81 -55
  28. package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
  29. package/dist/agents/pi-embedded-runner/run.js +193 -25
  30. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
  31. package/dist/agents/pi-embedded-runner/runs.js +17 -8
  32. package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
  33. package/dist/agents/pi-embedded-runner.js +1 -1
  34. package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
  35. package/dist/agents/pi-embedded-subscribe.js +37 -0
  36. package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
  37. package/dist/agents/pi-model-discovery.js +9 -2
  38. package/dist/agents/pi-tool-definition-adapter.js +60 -8
  39. package/dist/agents/pi-tools.before-tool-call.js +1 -1
  40. package/dist/agents/pi-tools.js +113 -94
  41. package/dist/agents/pi-tools.read.js +337 -38
  42. package/dist/agents/poolbot-tools.js +14 -5
  43. package/dist/agents/sandbox/docker.js +10 -5
  44. package/dist/agents/sandbox/registry.js +96 -46
  45. package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
  46. package/dist/agents/sandbox-paths.js +43 -10
  47. package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
  48. package/dist/agents/session-tool-result-guard.js +39 -39
  49. package/dist/agents/session-transcript-repair.js +36 -33
  50. package/dist/agents/session-write-lock.js +62 -44
  51. package/dist/agents/skills/frontmatter.js +49 -88
  52. package/dist/agents/skills/workspace.js +335 -28
  53. package/dist/agents/subagent-announce.js +508 -174
  54. package/dist/agents/subagent-registry.js +45 -4
  55. package/dist/agents/subagent-spawn.js +16 -33
  56. package/dist/agents/system-prompt-report.js +27 -10
  57. package/dist/agents/system-prompt.js +26 -32
  58. package/dist/agents/tool-call-id.js +69 -17
  59. package/dist/agents/tool-display-common.js +1 -1
  60. package/dist/agents/tool-images.js +64 -31
  61. package/dist/agents/tools/canvas-tool.js +17 -11
  62. package/dist/agents/tools/common.js +37 -19
  63. package/dist/agents/tools/cron-tool.js +40 -38
  64. package/dist/agents/tools/gateway.js +70 -2
  65. package/dist/agents/tools/message-tool.js +181 -40
  66. package/dist/agents/tools/nodes-tool.js +128 -36
  67. package/dist/agents/tools/nodes-utils.js +12 -38
  68. package/dist/agents/tools/session-status-tool.js +24 -71
  69. package/dist/agents/tools/sessions-helpers.js +38 -210
  70. package/dist/agents/tools/sessions-spawn-tool.js +28 -198
  71. package/dist/agents/tools/telegram-actions.js +58 -7
  72. package/dist/agents/tools/web-fetch-utils.js +112 -7
  73. package/dist/agents/tools/web-fetch.js +279 -175
  74. package/dist/agents/tools/web-shared.js +71 -8
  75. package/dist/agents/usage.js +25 -16
  76. package/dist/auto-reply/commands-registry.data.js +85 -11
  77. package/dist/auto-reply/dispatch.js +40 -21
  78. package/dist/auto-reply/reply/abort.js +102 -33
  79. package/dist/auto-reply/reply/commands-core.js +82 -33
  80. package/dist/auto-reply/reply/commands-export-session.js +1 -1
  81. package/dist/auto-reply/reply/commands-info.js +41 -12
  82. package/dist/auto-reply/reply/commands-subagents.js +352 -100
  83. package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
  84. package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
  85. package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
  86. package/dist/auto-reply/reply/inbound-meta.js +12 -1
  87. package/dist/auto-reply/reply/mentions.js +18 -11
  88. package/dist/auto-reply/reply/normalize-reply.js +17 -8
  89. package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
  90. package/dist/auto-reply/reply/session.js +102 -21
  91. package/dist/auto-reply/reply/streaming-directives.js +16 -5
  92. package/dist/auto-reply/status.js +73 -50
  93. package/dist/browser/extension-relay.js +3 -3
  94. package/dist/browser/http-auth.js +1 -1
  95. package/dist/browser/paths.js +2 -2
  96. package/dist/build-info.json +3 -3
  97. package/dist/channels/allowlist-match.js +20 -0
  98. package/dist/channels/allowlists/resolve-utils.js +65 -2
  99. package/dist/channels/chat-type.js +8 -4
  100. package/dist/channels/dock.js +127 -35
  101. package/dist/channels/draft-stream-loop.js +6 -2
  102. package/dist/channels/plugins/actions/telegram.js +42 -18
  103. package/dist/channels/plugins/allowlist-match.js +1 -1
  104. package/dist/channels/plugins/group-mentions.js +51 -41
  105. package/dist/channels/plugins/message-action-names.js +2 -0
  106. package/dist/channels/plugins/message-actions.js +24 -5
  107. package/dist/channels/plugins/normalize/discord.js +26 -4
  108. package/dist/channels/plugins/normalize/signal.js +35 -22
  109. package/dist/channels/plugins/onboarding/helpers.js +8 -26
  110. package/dist/channels/plugins/outbound/imessage.js +15 -14
  111. package/dist/channels/registry.js +20 -7
  112. package/dist/cli/acp-cli.js +7 -5
  113. package/dist/cli/browser-cli-extension.js +25 -12
  114. package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
  115. package/dist/cli/browser-cli-state.js +101 -145
  116. package/dist/cli/command-options.js +28 -0
  117. package/dist/cli/completion-cli.js +6 -6
  118. package/dist/cli/cron-cli/register.cron-add.js +25 -1
  119. package/dist/cli/cron-cli/register.cron-edit.js +44 -0
  120. package/dist/cli/cron-cli/shared.js +7 -1
  121. package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
  122. package/dist/cli/daemon-cli/lifecycle.js +23 -247
  123. package/dist/cli/daemon-cli/register-service-commands.js +25 -4
  124. package/dist/cli/daemon-cli.js +1 -0
  125. package/dist/cli/devices-cli.js +33 -20
  126. package/dist/cli/gateway-cli/register.js +37 -105
  127. package/dist/cli/gateway-cli/run.js +49 -11
  128. package/dist/cli/nodes-camera.js +59 -4
  129. package/dist/cli/nodes-cli/register.camera.js +27 -24
  130. package/dist/cli/nodes-cli/rpc.js +21 -38
  131. package/dist/cli/qr-cli.js +2 -2
  132. package/dist/cli/skills-cli.format.js +2 -2
  133. package/dist/cli/update-cli/progress.js +2 -2
  134. package/dist/cli/update-cli/restart-helper.js +28 -7
  135. package/dist/cli/update-cli/shared.js +7 -7
  136. package/dist/cli/update-cli/status.js +1 -1
  137. package/dist/cli/update-cli/update-command.js +14 -8
  138. package/dist/cli/update-cli/wizard.js +2 -2
  139. package/dist/cli/update-cli.js +21 -1027
  140. package/dist/commands/auth-choice.apply.anthropic.js +10 -2
  141. package/dist/commands/channels/add-mutators.js +3 -35
  142. package/dist/commands/channels/add.js +39 -51
  143. package/dist/commands/config-validation.js +1 -1
  144. package/dist/commands/configure.gateway-auth.js +52 -15
  145. package/dist/commands/configure.gateway.js +84 -40
  146. package/dist/commands/doctor-completion.js +3 -3
  147. package/dist/commands/doctor-config-flow.js +536 -16
  148. package/dist/commands/doctor-gateway-services.js +103 -79
  149. package/dist/commands/doctor-memory-search.js +9 -9
  150. package/dist/commands/doctor-platform-notes.js +57 -30
  151. package/dist/commands/doctor-prompter.js +26 -15
  152. package/dist/commands/doctor-session-locks.js +1 -1
  153. package/dist/commands/doctor.js +21 -9
  154. package/dist/commands/model-picker.js +120 -95
  155. package/dist/commands/models/set.js +2 -21
  156. package/dist/commands/models/shared.js +65 -37
  157. package/dist/commands/onboard-helpers.js +81 -39
  158. package/dist/commands/openai-codex-oauth.js +1 -1
  159. package/dist/commands/sessions.js +52 -53
  160. package/dist/commands/status.summary.js +52 -34
  161. package/dist/commands/test-wizard-helpers.js +2 -2
  162. package/dist/config/defaults.js +79 -42
  163. package/dist/config/group-policy.js +50 -18
  164. package/dist/config/includes.js +37 -10
  165. package/dist/config/schema.help.js +5 -4
  166. package/dist/config/schema.hints.js +2 -2
  167. package/dist/config/schema.labels.js +1 -0
  168. package/dist/config/sessions/group.js +12 -11
  169. package/dist/config/sessions/paths.js +137 -11
  170. package/dist/config/sessions/store.js +185 -65
  171. package/dist/config/sessions/types.js +15 -1
  172. package/dist/config/sessions.js +1 -0
  173. package/dist/config/telegram-custom-commands.js +3 -2
  174. package/dist/config/types.js +2 -0
  175. package/dist/config/zod-schema.agent-defaults.js +6 -27
  176. package/dist/config/zod-schema.agent-runtime.js +171 -79
  177. package/dist/config/zod-schema.providers-core.js +138 -65
  178. package/dist/config/zod-schema.session.js +49 -22
  179. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
  180. package/dist/cron/isolated-agent/run.js +224 -57
  181. package/dist/cron/normalize.js +48 -45
  182. package/dist/cron/run-log.js +14 -0
  183. package/dist/cron/service/jobs.js +190 -28
  184. package/dist/cron/service/normalize.js +29 -11
  185. package/dist/cron/service/store.js +30 -44
  186. package/dist/cron/service/timer.js +182 -96
  187. package/dist/cron/service.js +3 -0
  188. package/dist/cron/stagger.js +37 -0
  189. package/dist/daemon/inspect.js +132 -92
  190. package/dist/daemon/runtime-paths.js +25 -4
  191. package/dist/daemon/service-audit.js +47 -16
  192. package/dist/discord/accounts.js +23 -20
  193. package/dist/discord/monitor/agent-components.js +1115 -219
  194. package/dist/discord/monitor/allow-list.js +114 -34
  195. package/dist/discord/monitor/listeners.js +204 -97
  196. package/dist/discord/monitor/message-handler.js +21 -10
  197. package/dist/discord/monitor/message-handler.preflight.js +195 -101
  198. package/dist/discord/monitor/message-handler.process.js +384 -123
  199. package/dist/discord/monitor/message-utils.js +86 -23
  200. package/dist/discord/monitor/native-command.js +77 -57
  201. package/dist/discord/monitor/provider.js +122 -117
  202. package/dist/discord/monitor/reply-context.js +20 -16
  203. package/dist/discord/monitor/reply-delivery.js +40 -8
  204. package/dist/discord/monitor/rest-fetch.js +22 -0
  205. package/dist/discord/monitor/threading.js +117 -24
  206. package/dist/discord/send.js +2 -1
  207. package/dist/discord/send.outbound.js +124 -11
  208. package/dist/discord/send.shared.js +112 -72
  209. package/dist/discord/voice-message.js +3 -3
  210. package/dist/gateway/auth.js +119 -44
  211. package/dist/gateway/call.js +76 -34
  212. package/dist/gateway/channel-health-monitor.js +57 -50
  213. package/dist/gateway/client.js +63 -29
  214. package/dist/gateway/control-ui-contract.js +1 -1
  215. package/dist/gateway/gateway-config-prompts.shared.js +2 -2
  216. package/dist/gateway/net.js +109 -1
  217. package/dist/gateway/protocol/index.js +5 -8
  218. package/dist/gateway/protocol/schema/agent.js +19 -1
  219. package/dist/gateway/protocol/schema/channels.js +21 -0
  220. package/dist/gateway/protocol/schema/cron.js +43 -30
  221. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
  222. package/dist/gateway/protocol/schema/sessions.js +5 -1
  223. package/dist/gateway/protocol/schema.js +0 -1
  224. package/dist/gateway/server/presence-events.js +12 -0
  225. package/dist/gateway/server/ws-connection/message-handler.js +203 -212
  226. package/dist/gateway/server/ws-connection.js +58 -21
  227. package/dist/gateway/server-broadcast.js +18 -13
  228. package/dist/gateway/server-cron.js +177 -10
  229. package/dist/gateway/server-methods/agent-job.js +131 -38
  230. package/dist/gateway/server-methods/send.js +60 -14
  231. package/dist/gateway/server-methods/sessions.js +160 -96
  232. package/dist/gateway/server-methods/system.js +5 -7
  233. package/dist/gateway/server-methods-list.js +8 -0
  234. package/dist/gateway/server-methods.js +24 -8
  235. package/dist/gateway/server-node-events.js +278 -68
  236. package/dist/gateway/session-utils.fs.js +316 -75
  237. package/dist/gateway/session-utils.js +224 -70
  238. package/dist/gateway/sessions-patch.js +63 -20
  239. package/dist/gateway/test-temp-config.js +1 -1
  240. package/dist/gateway/tools-invoke-http.js +118 -70
  241. package/dist/gateway/ws-log.js +135 -107
  242. package/dist/hooks/frontmatter.js +36 -82
  243. package/dist/hooks/install.js +149 -139
  244. package/dist/hooks/internal-hooks.js +29 -4
  245. package/dist/hooks/plugin-hooks.js +2 -1
  246. package/dist/imessage/monitor/deliver.js +10 -4
  247. package/dist/imessage/monitor/monitor-provider.js +138 -375
  248. package/dist/imessage/monitor/runtime.js +4 -8
  249. package/dist/imessage/send.js +65 -19
  250. package/dist/infra/exec-approvals-allowlist.js +7 -0
  251. package/dist/infra/exec-approvals.js +35 -920
  252. package/dist/infra/exec-safe-bin-trust.js +64 -0
  253. package/dist/infra/heartbeat-runner.js +207 -134
  254. package/dist/infra/heartbeat-wake.js +183 -22
  255. package/dist/infra/install-source-utils.js +47 -0
  256. package/dist/infra/net/ssrf.js +170 -36
  257. package/dist/infra/outbound/deliver.js +224 -58
  258. package/dist/infra/outbound/message-action-spec.js +12 -5
  259. package/dist/infra/outbound/outbound-session.js +27 -25
  260. package/dist/infra/poolbot-root.js +32 -22
  261. package/dist/infra/ports.js +14 -11
  262. package/dist/infra/skills-remote.js +48 -37
  263. package/dist/infra/system-events.js +25 -11
  264. package/dist/infra/system-presence.js +26 -33
  265. package/dist/infra/tmp-poolbot-dir.js +81 -2
  266. package/dist/infra/wsl.js +37 -1
  267. package/dist/line/bot-message-context.js +163 -191
  268. package/dist/logging/subsystem.js +59 -22
  269. package/dist/markdown/ir.js +124 -50
  270. package/dist/media/store.js +1 -1
  271. package/dist/media-understanding/runner.entries.js +42 -25
  272. package/dist/media-understanding/runner.js +53 -488
  273. package/dist/memory/embeddings-gemini.js +53 -38
  274. package/dist/memory/manager-embedding-ops.js +48 -69
  275. package/dist/pairing/pairing-store.js +178 -119
  276. package/dist/plugin-sdk/index.js +34 -6
  277. package/dist/plugins/hooks.js +135 -14
  278. package/dist/plugins/install.js +190 -152
  279. package/dist/polls.js +11 -0
  280. package/dist/routing/resolve-route.js +190 -56
  281. package/dist/routing/session-key.js +38 -22
  282. package/dist/runtime.js +35 -9
  283. package/dist/security/audit-channel.js +1 -1
  284. package/dist/sessions/session-key-utils.js +29 -11
  285. package/dist/shared/frontmatter.js +5 -5
  286. package/dist/shared/node-list-types.js +1 -0
  287. package/dist/shared/string-normalization.js +15 -0
  288. package/dist/signal/monitor/event-handler.js +68 -36
  289. package/dist/signal/send.js +29 -37
  290. package/dist/slack/monitor/allow-list.js +10 -11
  291. package/dist/slack/monitor/commands.js +14 -3
  292. package/dist/slack/monitor/events/interactions.js +4 -4
  293. package/dist/slack/monitor/media.js +224 -16
  294. package/dist/slack/monitor/message-handler/dispatch.js +247 -13
  295. package/dist/slack/monitor/message-handler/prepare.js +128 -45
  296. package/dist/slack/monitor/slash.js +357 -144
  297. package/dist/slack/streaming.js +77 -0
  298. package/dist/telegram/accounts.js +40 -13
  299. package/dist/telegram/allowed-updates.js +3 -0
  300. package/dist/telegram/bot/delivery.js +129 -66
  301. package/dist/telegram/bot/helpers.js +136 -122
  302. package/dist/telegram/bot-handlers.js +600 -339
  303. package/dist/telegram/bot-message-context.js +115 -73
  304. package/dist/telegram/bot-message-dispatch.js +235 -104
  305. package/dist/telegram/bot-native-command-menu.js +3 -1
  306. package/dist/telegram/bot-native-commands.js +213 -193
  307. package/dist/telegram/bot.js +24 -132
  308. package/dist/telegram/draft-stream.js +84 -75
  309. package/dist/telegram/format.js +150 -6
  310. package/dist/telegram/send.js +415 -255
  311. package/dist/telegram/targets.js +21 -2
  312. package/dist/telegram/update-offset-store.js +19 -3
  313. package/dist/terminal/restore.js +5 -2
  314. package/dist/test-utils/fetch-mock.js +5 -0
  315. package/dist/version.js +18 -5
  316. package/dist/web/auto-reply/monitor/broadcast.js +7 -3
  317. package/dist/web/auto-reply/monitor/on-message.js +6 -3
  318. package/dist/web/inbound/media.js +34 -8
  319. package/dist/web/inbound/monitor.js +34 -17
  320. package/dist/web/inbound/send-api.js +18 -17
  321. package/dist/web/outbound.js +12 -5
  322. package/dist/wizard/clack-prompter.js +40 -7
  323. package/extensions/bluebubbles/package.json +1 -1
  324. package/extensions/copilot-proxy/package.json +1 -1
  325. package/extensions/diagnostics-otel/package.json +1 -1
  326. package/extensions/discord/package.json +1 -1
  327. package/extensions/feishu/package.json +1 -1
  328. package/extensions/google-antigravity-auth/package.json +1 -1
  329. package/extensions/google-gemini-cli-auth/package.json +1 -1
  330. package/extensions/googlechat/package.json +1 -1
  331. package/extensions/imessage/package.json +1 -1
  332. package/extensions/irc/package.json +1 -1
  333. package/extensions/line/package.json +1 -1
  334. package/extensions/llm-task/package.json +1 -1
  335. package/extensions/lobster/package.json +1 -1
  336. package/extensions/matrix/CHANGELOG.md +5 -0
  337. package/extensions/matrix/package.json +1 -1
  338. package/extensions/mattermost/package.json +1 -1
  339. package/extensions/memory-core/package.json +1 -1
  340. package/extensions/memory-lancedb/package.json +1 -1
  341. package/extensions/minimax-portal-auth/package.json +1 -1
  342. package/extensions/msteams/CHANGELOG.md +5 -0
  343. package/extensions/msteams/package.json +1 -1
  344. package/extensions/nextcloud-talk/package.json +1 -1
  345. package/extensions/nostr/CHANGELOG.md +5 -0
  346. package/extensions/nostr/package.json +1 -1
  347. package/extensions/open-prose/package.json +1 -1
  348. package/extensions/openai-codex-auth/package.json +1 -1
  349. package/extensions/signal/package.json +1 -1
  350. package/extensions/slack/package.json +1 -1
  351. package/extensions/telegram/package.json +1 -1
  352. package/extensions/tlon/package.json +1 -1
  353. package/extensions/twitch/CHANGELOG.md +5 -0
  354. package/extensions/twitch/package.json +1 -1
  355. package/extensions/voice-call/CHANGELOG.md +5 -0
  356. package/extensions/voice-call/package.json +1 -1
  357. package/extensions/whatsapp/package.json +1 -1
  358. package/extensions/zalo/CHANGELOG.md +5 -0
  359. package/extensions/zalo/package.json +1 -1
  360. package/extensions/zalouser/CHANGELOG.md +5 -0
  361. package/extensions/zalouser/package.json +1 -1
  362. package/package.json +1 -1
  363. package/skills/apple-reminders/SKILL.md +100 -49
  364. package/skills/coding-agent/SKILL.md +34 -28
  365. package/skills/github/SKILL.md +131 -16
  366. package/skills/imsg/SKILL.md +112 -15
  367. package/skills/openhue/SKILL.md +101 -19
  368. package/skills/tmux/SKILL.md +111 -79
  369. package/skills/weather/SKILL.md +88 -25
@@ -1,88 +1,33 @@
1
1
  import os from "node:os";
2
+ import { loadConfig } from "../../../config/config.js";
2
3
  import { deriveDeviceIdFromPublicKey, normalizeDevicePublicKeyBase64Url, verifyDeviceSignature, } from "../../../infra/device-identity.js";
3
4
  import { approveDevicePairing, ensureDeviceToken, getPairedDevice, requestDevicePairing, updatePairedDeviceMetadata, verifyDeviceToken, } from "../../../infra/device-pairing.js";
4
5
  import { updatePairedNodeMetadata } from "../../../infra/node-pairing.js";
5
6
  import { recordRemoteNodeInfo, refreshRemoteNodeBins } from "../../../infra/skills-remote.js";
6
- import { loadVoiceWakeConfig } from "../../../infra/voicewake.js";
7
7
  import { upsertPresence } from "../../../infra/system-presence.js";
8
+ import { loadVoiceWakeConfig } from "../../../infra/voicewake.js";
8
9
  import { rawDataToString } from "../../../infra/ws.js";
9
10
  import { isGatewayCliClient, isWebchatClient } from "../../../utils/message-channel.js";
11
+ import { resolveRuntimeServiceVersion } from "../../../version.js";
12
+ import { AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN, AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET, } from "../../auth-rate-limit.js";
10
13
  import { authorizeGatewayConnect, isLocalDirectRequest } from "../../auth.js";
11
- import { loadConfig } from "../../../config/config.js";
12
14
  import { buildDeviceAuthPayload } from "../../device-auth.js";
13
15
  import { isLoopbackAddress, isTrustedProxyAddress, resolveGatewayClientIp } from "../../net.js";
16
+ import { resolveHostName } from "../../net.js";
14
17
  import { resolveNodeCommandAllowlist } from "../../node-command-policy.js";
15
- import { ErrorCodes, errorShape, formatValidationErrors, PROTOCOL_VERSION, validateConnectParams, validateRequestFrame, } from "../../protocol/index.js";
18
+ import { checkBrowserOrigin } from "../../origin-check.js";
16
19
  import { GATEWAY_CLIENT_IDS } from "../../protocol/client-info.js";
20
+ import { ErrorCodes, errorShape, formatValidationErrors, PROTOCOL_VERSION, validateConnectParams, validateRequestFrame, } from "../../protocol/index.js";
17
21
  import { MAX_BUFFERED_BYTES, MAX_PAYLOAD_BYTES, TICK_INTERVAL_MS } from "../../server-constants.js";
18
22
  import { handleGatewayRequest } from "../../server-methods.js";
19
23
  import { formatError } from "../../server-utils.js";
20
24
  import { formatForLog, logWs } from "../../ws-log.js";
21
25
  import { truncateCloseReason } from "../close-reason.js";
22
26
  import { buildGatewaySnapshot, getHealthCache, getHealthVersion, incrementPresenceVersion, refreshGatewayHealthSnapshot, } from "../health-state.js";
27
+ import { formatGatewayAuthFailureMessage } from "./auth-messages.js";
23
28
  const DEVICE_SIGNATURE_SKEW_MS = 10 * 60 * 1000;
24
- function resolveHostName(hostHeader) {
25
- const host = (hostHeader ?? "").trim().toLowerCase();
26
- if (!host)
27
- return "";
28
- if (host.startsWith("[")) {
29
- const end = host.indexOf("]");
30
- if (end !== -1)
31
- return host.slice(1, end);
32
- }
33
- const [name] = host.split(":");
34
- return name ?? "";
35
- }
36
- function formatGatewayAuthFailureMessage(params) {
37
- const { authMode, authProvided, reason, client } = params;
38
- const isCli = isGatewayCliClient(client);
39
- const isControlUi = client?.id === GATEWAY_CLIENT_IDS.CONTROL_UI;
40
- const isWebchat = isWebchatClient(client);
41
- const uiHint = "open a tokenized dashboard URL or paste token in Control UI settings";
42
- const tokenHint = isCli
43
- ? "set gateway.remote.token to match gateway.auth.token"
44
- : isControlUi || isWebchat
45
- ? uiHint
46
- : "provide gateway auth token";
47
- const passwordHint = isCli
48
- ? "set gateway.remote.password to match gateway.auth.password"
49
- : isControlUi || isWebchat
50
- ? "enter the password in Control UI settings"
51
- : "provide gateway auth password";
52
- switch (reason) {
53
- case "token_missing":
54
- return `unauthorized: gateway token missing (${tokenHint})`;
55
- case "token_mismatch":
56
- return `unauthorized: gateway token mismatch (${tokenHint})`;
57
- case "token_missing_config":
58
- return "unauthorized: gateway token not configured on gateway (set gateway.auth.token)";
59
- case "password_missing":
60
- return `unauthorized: gateway password missing (${passwordHint})`;
61
- case "password_mismatch":
62
- return `unauthorized: gateway password mismatch (${passwordHint})`;
63
- case "password_missing_config":
64
- return "unauthorized: gateway password not configured on gateway (set gateway.auth.password)";
65
- case "tailscale_user_missing":
66
- return "unauthorized: tailscale identity missing (use Tailscale Serve auth or gateway token/password)";
67
- case "tailscale_proxy_missing":
68
- return "unauthorized: tailscale proxy headers missing (use Tailscale Serve or gateway token/password)";
69
- case "tailscale_whois_failed":
70
- return "unauthorized: tailscale identity check failed (use Tailscale Serve auth or gateway token/password)";
71
- case "tailscale_user_mismatch":
72
- return "unauthorized: tailscale identity mismatch (use Tailscale Serve auth or gateway token/password)";
73
- default:
74
- break;
75
- }
76
- if (authMode === "token" && authProvided === "none") {
77
- return `unauthorized: gateway token missing (${tokenHint})`;
78
- }
79
- if (authMode === "password" && authProvided === "none") {
80
- return `unauthorized: gateway password missing (${passwordHint})`;
81
- }
82
- return "unauthorized";
83
- }
84
29
  export function attachGatewayWsMessageHandler(params) {
85
- const { socket, upgradeReq, connId, remoteAddr, forwardedFor, realIp, requestHost, requestOrigin, requestUserAgent, canvasHostUrl, connectNonce, resolvedAuth, gatewayMethods, events, extraHandlers, buildRequestContext, send, close, isClosed, clearHandshakeTimer, getClient, setClient, setHandshakeState, setCloseCause, setLastFrameMeta, logGateway, logHealth, logWsControl, } = params;
30
+ const { socket, upgradeReq, connId, remoteAddr, forwardedFor, realIp, requestHost, requestOrigin, requestUserAgent, canvasHostUrl, connectNonce, resolvedAuth, rateLimiter, gatewayMethods, events, extraHandlers, buildRequestContext, send, close, isClosed, clearHandshakeTimer, getClient, setClient, setHandshakeState, setCloseCause, setLastFrameMeta, logGateway, logHealth, logWsControl, } = params;
86
31
  const configSnapshot = loadConfig();
87
32
  const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
88
33
  const clientIp = resolveGatewayClientIp({ remoteAddr, forwardedFor, realIp, trustedProxies });
@@ -115,8 +60,9 @@ export function attachGatewayWsMessageHandler(params) {
115
60
  }
116
61
  const isWebchatConnect = (p) => isWebchatClient(p?.client);
117
62
  socket.on("message", async (data) => {
118
- if (isClosed())
63
+ if (isClosed()) {
119
64
  return;
65
+ }
120
66
  const text = rawDataToString(data);
121
67
  try {
122
68
  const parsed = JSON.parse(text);
@@ -182,27 +128,35 @@ export function attachGatewayWsMessageHandler(params) {
182
128
  const frame = parsed;
183
129
  const connectParams = frame.params;
184
130
  const clientLabel = connectParams.client.displayName ?? connectParams.client.id;
131
+ const clientMeta = {
132
+ client: connectParams.client.id,
133
+ clientDisplayName: connectParams.client.displayName,
134
+ mode: connectParams.client.mode,
135
+ version: connectParams.client.version,
136
+ };
137
+ const markHandshakeFailure = (cause, meta) => {
138
+ setHandshakeState("failed");
139
+ setCloseCause(cause, { ...meta, ...clientMeta });
140
+ };
141
+ const sendHandshakeErrorResponse = (code, message, options) => {
142
+ send({
143
+ type: "res",
144
+ id: frame.id,
145
+ ok: false,
146
+ error: errorShape(code, message, options),
147
+ });
148
+ };
185
149
  // protocol negotiation
186
150
  const { minProtocol, maxProtocol } = connectParams;
187
151
  if (maxProtocol < PROTOCOL_VERSION || minProtocol > PROTOCOL_VERSION) {
188
- setHandshakeState("failed");
189
- logWsControl.warn(`protocol mismatch conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version}`);
190
- setCloseCause("protocol-mismatch", {
152
+ markHandshakeFailure("protocol-mismatch", {
191
153
  minProtocol,
192
154
  maxProtocol,
193
155
  expectedProtocol: PROTOCOL_VERSION,
194
- client: connectParams.client.id,
195
- clientDisplayName: connectParams.client.displayName,
196
- mode: connectParams.client.mode,
197
- version: connectParams.client.version,
198
156
  });
199
- send({
200
- type: "res",
201
- id: frame.id,
202
- ok: false,
203
- error: errorShape(ErrorCodes.INVALID_REQUEST, "protocol mismatch", {
204
- details: { expectedProtocol: PROTOCOL_VERSION },
205
- }),
157
+ logWsControl.warn(`protocol mismatch conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version}`);
158
+ sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, "protocol mismatch", {
159
+ details: { expectedProtocol: PROTOCOL_VERSION },
206
160
  });
207
161
  close(1002, "protocol mismatch");
208
162
  return;
@@ -210,76 +164,137 @@ export function attachGatewayWsMessageHandler(params) {
210
164
  const roleRaw = connectParams.role ?? "operator";
211
165
  const role = roleRaw === "operator" || roleRaw === "node" ? roleRaw : null;
212
166
  if (!role) {
213
- setHandshakeState("failed");
214
- setCloseCause("invalid-role", {
167
+ markHandshakeFailure("invalid-role", {
215
168
  role: roleRaw,
216
- client: connectParams.client.id,
217
- clientDisplayName: connectParams.client.displayName,
218
- mode: connectParams.client.mode,
219
- version: connectParams.client.version,
220
- });
221
- send({
222
- type: "res",
223
- id: frame.id,
224
- ok: false,
225
- error: errorShape(ErrorCodes.INVALID_REQUEST, "invalid role"),
226
169
  });
170
+ sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, "invalid role");
227
171
  close(1008, "invalid role");
228
172
  return;
229
173
  }
230
- const requestedScopes = Array.isArray(connectParams.scopes) ? connectParams.scopes : [];
231
- const scopes = requestedScopes.length > 0
232
- ? requestedScopes
233
- : role === "operator"
234
- ? ["operator.admin"]
235
- : [];
174
+ // Default-deny: scopes must be explicit. Empty/missing scopes means no permissions.
175
+ // Note: If the client does not present a device identity, we can't bind scopes to a paired
176
+ // device/token, so we will clear scopes after auth to avoid self-declared permissions.
177
+ let scopes = Array.isArray(connectParams.scopes) ? connectParams.scopes : [];
236
178
  connectParams.role = role;
237
179
  connectParams.scopes = scopes;
180
+ const isControlUi = connectParams.client.id === GATEWAY_CLIENT_IDS.CONTROL_UI;
181
+ const isWebchat = isWebchatConnect(connectParams);
182
+ if (isControlUi || isWebchat) {
183
+ const originCheck = checkBrowserOrigin({
184
+ requestHost,
185
+ origin: requestOrigin,
186
+ allowedOrigins: configSnapshot.gateway?.controlUi?.allowedOrigins,
187
+ });
188
+ if (!originCheck.ok) {
189
+ const errorMessage = "origin not allowed (open the Control UI from the gateway host or allow it in gateway.controlUi.allowedOrigins)";
190
+ markHandshakeFailure("origin-mismatch", {
191
+ origin: requestOrigin ?? "n/a",
192
+ host: requestHost ?? "n/a",
193
+ reason: originCheck.reason,
194
+ });
195
+ sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, errorMessage);
196
+ close(1008, truncateCloseReason(errorMessage));
197
+ return;
198
+ }
199
+ }
238
200
  const deviceRaw = connectParams.device;
239
201
  let devicePublicKey = null;
240
202
  const hasTokenAuth = Boolean(connectParams.auth?.token);
241
203
  const hasPasswordAuth = Boolean(connectParams.auth?.password);
242
204
  const hasSharedAuth = hasTokenAuth || hasPasswordAuth;
243
- const isControlUi = connectParams.client.id === GATEWAY_CLIENT_IDS.CONTROL_UI;
244
205
  const allowInsecureControlUi = isControlUi && configSnapshot.gateway?.controlUi?.allowInsecureAuth === true;
245
206
  const disableControlUiDeviceAuth = isControlUi && configSnapshot.gateway?.controlUi?.dangerouslyDisableDeviceAuth === true;
246
207
  const allowControlUiBypass = allowInsecureControlUi || disableControlUiDeviceAuth;
247
208
  const device = disableControlUiDeviceAuth ? null : deviceRaw;
209
+ const hasDeviceTokenCandidate = Boolean(connectParams.auth?.token && device);
210
+ let authResult = await authorizeGatewayConnect({
211
+ auth: resolvedAuth,
212
+ connectAuth: connectParams.auth,
213
+ req: upgradeReq,
214
+ trustedProxies,
215
+ rateLimiter: hasDeviceTokenCandidate ? undefined : rateLimiter,
216
+ clientIp,
217
+ rateLimitScope: AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET,
218
+ });
219
+ if (hasDeviceTokenCandidate &&
220
+ authResult.ok &&
221
+ rateLimiter &&
222
+ (authResult.method === "token" || authResult.method === "password")) {
223
+ const sharedRateCheck = rateLimiter.check(clientIp, AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET);
224
+ if (!sharedRateCheck.allowed) {
225
+ authResult = {
226
+ ok: false,
227
+ reason: "rate_limited",
228
+ rateLimited: true,
229
+ retryAfterMs: sharedRateCheck.retryAfterMs,
230
+ };
231
+ }
232
+ else {
233
+ rateLimiter.reset(clientIp, AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET);
234
+ }
235
+ }
236
+ let authOk = authResult.ok;
237
+ let authMethod = authResult.method ?? (resolvedAuth.mode === "password" ? "password" : "token");
238
+ const sharedAuthResult = hasSharedAuth
239
+ ? await authorizeGatewayConnect({
240
+ auth: { ...resolvedAuth, allowTailscale: false },
241
+ connectAuth: connectParams.auth,
242
+ req: upgradeReq,
243
+ trustedProxies,
244
+ // Shared-auth probe only; rate-limit side effects are handled in
245
+ // the primary auth flow (or deferred for device-token candidates).
246
+ rateLimitScope: AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET,
247
+ })
248
+ : null;
249
+ const sharedAuthOk = sharedAuthResult?.ok === true &&
250
+ (sharedAuthResult.method === "token" || sharedAuthResult.method === "password");
251
+ const rejectUnauthorized = (failedAuth) => {
252
+ markHandshakeFailure("unauthorized", {
253
+ authMode: resolvedAuth.mode,
254
+ authProvided: connectParams.auth?.token
255
+ ? "token"
256
+ : connectParams.auth?.password
257
+ ? "password"
258
+ : "none",
259
+ authReason: failedAuth.reason,
260
+ allowTailscale: resolvedAuth.allowTailscale,
261
+ });
262
+ logWsControl.warn(`unauthorized conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version} reason=${failedAuth.reason ?? "unknown"}`);
263
+ const authProvided = connectParams.auth?.token
264
+ ? "token"
265
+ : connectParams.auth?.password
266
+ ? "password"
267
+ : "none";
268
+ const authMessage = formatGatewayAuthFailureMessage({
269
+ authMode: resolvedAuth.mode,
270
+ authProvided,
271
+ reason: failedAuth.reason,
272
+ client: connectParams.client,
273
+ });
274
+ sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, authMessage);
275
+ close(1008, truncateCloseReason(authMessage));
276
+ };
248
277
  if (!device) {
249
- const canSkipDevice = allowControlUiBypass ? hasSharedAuth : hasTokenAuth;
278
+ if (scopes.length > 0 && !allowControlUiBypass) {
279
+ scopes = [];
280
+ connectParams.scopes = scopes;
281
+ }
282
+ const canSkipDevice = sharedAuthOk;
250
283
  if (isControlUi && !allowControlUiBypass) {
251
284
  const errorMessage = "control ui requires HTTPS or localhost (secure context)";
252
- setHandshakeState("failed");
253
- setCloseCause("control-ui-insecure-auth", {
254
- client: connectParams.client.id,
255
- clientDisplayName: connectParams.client.displayName,
256
- mode: connectParams.client.mode,
257
- version: connectParams.client.version,
258
- });
259
- send({
260
- type: "res",
261
- id: frame.id,
262
- ok: false,
263
- error: errorShape(ErrorCodes.INVALID_REQUEST, errorMessage),
264
- });
285
+ markHandshakeFailure("control-ui-insecure-auth");
286
+ sendHandshakeErrorResponse(ErrorCodes.INVALID_REQUEST, errorMessage);
265
287
  close(1008, errorMessage);
266
288
  return;
267
289
  }
268
- // Allow token-authenticated connections (e.g., control-ui) to skip device identity
290
+ // Allow shared-secret authenticated connections (e.g., control-ui) to skip device identity
269
291
  if (!canSkipDevice) {
270
- setHandshakeState("failed");
271
- setCloseCause("device-required", {
272
- client: connectParams.client.id,
273
- clientDisplayName: connectParams.client.displayName,
274
- mode: connectParams.client.mode,
275
- version: connectParams.client.version,
276
- });
277
- send({
278
- type: "res",
279
- id: frame.id,
280
- ok: false,
281
- error: errorShape(ErrorCodes.NOT_PAIRED, "device identity required"),
282
- });
292
+ if (!authOk && hasSharedAuth) {
293
+ rejectUnauthorized(authResult);
294
+ return;
295
+ }
296
+ markHandshakeFailure("device-required");
297
+ sendHandshakeErrorResponse(ErrorCodes.NOT_PAIRED, "device identity required");
283
298
  close(1008, "device identity required");
284
299
  return;
285
300
  }
@@ -359,12 +374,27 @@ export function attachGatewayWsMessageHandler(params) {
359
374
  clientId: connectParams.client.id,
360
375
  clientMode: connectParams.client.mode,
361
376
  role,
362
- scopes: requestedScopes,
377
+ scopes,
363
378
  signedAtMs: signedAt,
364
379
  token: connectParams.auth?.token ?? null,
365
380
  nonce: providedNonce || undefined,
366
381
  version: providedNonce ? "v2" : "v1",
367
382
  });
383
+ const rejectDeviceSignatureInvalid = () => {
384
+ setHandshakeState("failed");
385
+ setCloseCause("device-auth-invalid", {
386
+ reason: "device-signature",
387
+ client: connectParams.client.id,
388
+ deviceId: device.id,
389
+ });
390
+ send({
391
+ type: "res",
392
+ id: frame.id,
393
+ ok: false,
394
+ error: errorShape(ErrorCodes.INVALID_REQUEST, "device signature invalid"),
395
+ });
396
+ close(1008, "device signature invalid");
397
+ };
368
398
  const signatureOk = verifyDeviceSignature(device.publicKey, payload, device.signature);
369
399
  const allowLegacy = !nonceRequired && !providedNonce;
370
400
  if (!signatureOk && allowLegacy) {
@@ -373,7 +403,7 @@ export function attachGatewayWsMessageHandler(params) {
373
403
  clientId: connectParams.client.id,
374
404
  clientMode: connectParams.client.mode,
375
405
  role,
376
- scopes: requestedScopes,
406
+ scopes,
377
407
  signedAtMs: signedAt,
378
408
  token: connectParams.auth?.token ?? null,
379
409
  version: "v1",
@@ -382,36 +412,12 @@ export function attachGatewayWsMessageHandler(params) {
382
412
  // accepted legacy loopback signature
383
413
  }
384
414
  else {
385
- setHandshakeState("failed");
386
- setCloseCause("device-auth-invalid", {
387
- reason: "device-signature",
388
- client: connectParams.client.id,
389
- deviceId: device.id,
390
- });
391
- send({
392
- type: "res",
393
- id: frame.id,
394
- ok: false,
395
- error: errorShape(ErrorCodes.INVALID_REQUEST, "device signature invalid"),
396
- });
397
- close(1008, "device signature invalid");
415
+ rejectDeviceSignatureInvalid();
398
416
  return;
399
417
  }
400
418
  }
401
419
  else if (!signatureOk) {
402
- setHandshakeState("failed");
403
- setCloseCause("device-auth-invalid", {
404
- reason: "device-signature",
405
- client: connectParams.client.id,
406
- deviceId: device.id,
407
- });
408
- send({
409
- type: "res",
410
- id: frame.id,
411
- ok: false,
412
- error: errorShape(ErrorCodes.INVALID_REQUEST, "device signature invalid"),
413
- });
414
- close(1008, "device signature invalid");
420
+ rejectDeviceSignatureInvalid();
415
421
  return;
416
422
  }
417
423
  devicePublicKey = normalizeDevicePublicKeyBase64Url(device.publicKey);
@@ -432,60 +438,41 @@ export function attachGatewayWsMessageHandler(params) {
432
438
  return;
433
439
  }
434
440
  }
435
- const authResult = await authorizeGatewayConnect({
436
- auth: resolvedAuth,
437
- connectAuth: connectParams.auth,
438
- req: upgradeReq,
439
- trustedProxies,
440
- });
441
- let authOk = authResult.ok;
442
- let authMethod = authResult.method ?? (resolvedAuth.mode === "password" ? "password" : "token");
443
441
  if (!authOk && connectParams.auth?.token && device) {
444
- const tokenCheck = await verifyDeviceToken({
445
- deviceId: device.id,
446
- token: connectParams.auth.token,
447
- role,
448
- scopes,
449
- });
450
- if (tokenCheck.ok) {
451
- authOk = true;
452
- authMethod = "device-token";
442
+ if (rateLimiter) {
443
+ const deviceRateCheck = rateLimiter.check(clientIp, AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN);
444
+ if (!deviceRateCheck.allowed) {
445
+ authResult = {
446
+ ok: false,
447
+ reason: "rate_limited",
448
+ rateLimited: true,
449
+ retryAfterMs: deviceRateCheck.retryAfterMs,
450
+ };
451
+ }
452
+ }
453
+ if (!authResult.rateLimited) {
454
+ const tokenCheck = await verifyDeviceToken({
455
+ deviceId: device.id,
456
+ token: connectParams.auth.token,
457
+ role,
458
+ scopes,
459
+ });
460
+ if (tokenCheck.ok) {
461
+ authOk = true;
462
+ authMethod = "device-token";
463
+ rateLimiter?.reset(clientIp, AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN);
464
+ }
465
+ else {
466
+ authResult = { ok: false, reason: "device_token_mismatch" };
467
+ rateLimiter?.recordFailure(clientIp, AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN);
468
+ }
453
469
  }
454
470
  }
455
471
  if (!authOk) {
456
- setHandshakeState("failed");
457
- logWsControl.warn(`unauthorized conn=${connId} remote=${remoteAddr ?? "?"} client=${clientLabel} ${connectParams.client.mode} v${connectParams.client.version} reason=${authResult.reason ?? "unknown"}`);
458
- const authProvided = connectParams.auth?.token
459
- ? "token"
460
- : connectParams.auth?.password
461
- ? "password"
462
- : "none";
463
- const authMessage = formatGatewayAuthFailureMessage({
464
- authMode: resolvedAuth.mode,
465
- authProvided,
466
- reason: authResult.reason,
467
- client: connectParams.client,
468
- });
469
- setCloseCause("unauthorized", {
470
- authMode: resolvedAuth.mode,
471
- authProvided,
472
- authReason: authResult.reason,
473
- allowTailscale: resolvedAuth.allowTailscale,
474
- client: connectParams.client.id,
475
- clientDisplayName: connectParams.client.displayName,
476
- mode: connectParams.client.mode,
477
- version: connectParams.client.version,
478
- });
479
- send({
480
- type: "res",
481
- id: frame.id,
482
- ok: false,
483
- error: errorShape(ErrorCodes.INVALID_REQUEST, authMessage),
484
- });
485
- close(1008, truncateCloseReason(authMessage));
472
+ rejectUnauthorized(authResult);
486
473
  return;
487
474
  }
488
- const skipPairing = allowControlUiBypass && hasSharedAuth;
475
+ const skipPairing = allowControlUiBypass && sharedAuthOk;
489
476
  if (device && devicePublicKey && !skipPairing) {
490
477
  const requirePairing = async (reason, _paired) => {
491
478
  const pairing = await requestDevicePairing({
@@ -540,35 +527,40 @@ export function attachGatewayWsMessageHandler(params) {
540
527
  const isPaired = paired?.publicKey === devicePublicKey;
541
528
  if (!isPaired) {
542
529
  const ok = await requirePairing("not-paired");
543
- if (!ok)
530
+ if (!ok) {
544
531
  return;
532
+ }
545
533
  }
546
534
  else {
547
535
  const allowedRoles = new Set(Array.isArray(paired.roles) ? paired.roles : paired.role ? [paired.role] : []);
548
536
  if (allowedRoles.size === 0) {
549
537
  const ok = await requirePairing("role-upgrade", paired);
550
- if (!ok)
538
+ if (!ok) {
551
539
  return;
540
+ }
552
541
  }
553
542
  else if (!allowedRoles.has(role)) {
554
543
  const ok = await requirePairing("role-upgrade", paired);
555
- if (!ok)
544
+ if (!ok) {
556
545
  return;
546
+ }
557
547
  }
558
548
  const pairedScopes = Array.isArray(paired.scopes) ? paired.scopes : [];
559
549
  if (scopes.length > 0) {
560
550
  if (pairedScopes.length === 0) {
561
551
  const ok = await requirePairing("scope-upgrade", paired);
562
- if (!ok)
552
+ if (!ok) {
563
553
  return;
554
+ }
564
555
  }
565
556
  else {
566
557
  const allowedScopes = new Set(pairedScopes);
567
558
  const missingScope = scopes.find((scope) => !allowedScopes.has(scope));
568
559
  if (missingScope) {
569
560
  const ok = await requirePairing("scope-upgrade", paired);
570
- if (!ok)
561
+ if (!ok) {
571
562
  return;
563
+ }
572
564
  }
573
565
  }
574
566
  }
@@ -642,10 +634,7 @@ export function attachGatewayWsMessageHandler(params) {
642
634
  type: "hello-ok",
643
635
  protocol: PROTOCOL_VERSION,
644
636
  server: {
645
- version: process.env.POOLBOT_VERSION ??
646
- process.env.CLAWDBOT_VERSION ??
647
- process.env.npm_package_version ??
648
- "dev",
637
+ version: resolveRuntimeServiceVersion(process.env, "dev"),
649
638
  commit: process.env.GIT_COMMIT,
650
639
  host: os.hostname(),
651
640
  connId,
@@ -673,6 +662,7 @@ export function attachGatewayWsMessageHandler(params) {
673
662
  connect: connectParams,
674
663
  connId,
675
664
  presenceKey,
665
+ clientIp: reportedClientIp,
676
666
  };
677
667
  setClient(nextClient);
678
668
  setHandshakeState("connected");
@@ -684,8 +674,9 @@ export function attachGatewayWsMessageHandler(params) {
684
674
  const instanceIdRaw = connectParams.client.instanceId;
685
675
  const instanceId = typeof instanceIdRaw === "string" ? instanceIdRaw.trim() : "";
686
676
  const nodeIdsForPairing = new Set([nodeSession.nodeId]);
687
- if (instanceId)
677
+ if (instanceId) {
688
678
  nodeIdsForPairing.add(instanceId);
679
+ }
689
680
  for (const nodeId of nodeIdsForPairing) {
690
681
  void updatePairedNodeMetadata(nodeId, {
691
682
  lastConnectedAtMs: nodeSession.connectedAtMs,