@poolzin/pool-bot 2026.2.21 → 2026.2.23

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 (378) hide show
  1. package/CHANGELOG.md +25 -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/device-pair/index.ts +2 -2
  326. package/extensions/diagnostics-otel/package.json +1 -1
  327. package/extensions/discord/package.json +1 -1
  328. package/extensions/feishu/package.json +1 -1
  329. package/extensions/google-antigravity-auth/package.json +1 -1
  330. package/extensions/google-gemini-cli-auth/package.json +1 -1
  331. package/extensions/googlechat/package.json +1 -1
  332. package/extensions/imessage/package.json +1 -1
  333. package/extensions/irc/package.json +1 -1
  334. package/extensions/irc/src/accounts.ts +1 -1
  335. package/extensions/irc/src/onboarding.ts +4 -4
  336. package/extensions/line/package.json +1 -1
  337. package/extensions/llm-task/package.json +1 -1
  338. package/extensions/lobster/package.json +1 -1
  339. package/extensions/matrix/CHANGELOG.md +10 -0
  340. package/extensions/matrix/package.json +1 -1
  341. package/extensions/mattermost/package.json +1 -1
  342. package/extensions/memory-core/package.json +1 -1
  343. package/extensions/memory-lancedb/package.json +1 -1
  344. package/extensions/minimax-portal-auth/package.json +1 -1
  345. package/extensions/msteams/CHANGELOG.md +10 -0
  346. package/extensions/msteams/package.json +1 -1
  347. package/extensions/nextcloud-talk/package.json +1 -1
  348. package/extensions/nostr/CHANGELOG.md +10 -0
  349. package/extensions/nostr/package.json +1 -1
  350. package/extensions/open-prose/package.json +1 -1
  351. package/extensions/openai-codex-auth/package.json +1 -1
  352. package/extensions/signal/package.json +1 -1
  353. package/extensions/slack/package.json +1 -1
  354. package/extensions/telegram/package.json +1 -1
  355. package/extensions/tlon/package.json +1 -1
  356. package/extensions/twitch/CHANGELOG.md +10 -0
  357. package/extensions/twitch/package.json +1 -1
  358. package/extensions/voice-call/CHANGELOG.md +10 -0
  359. package/extensions/voice-call/package.json +1 -1
  360. package/extensions/whatsapp/package.json +1 -1
  361. package/extensions/zalo/CHANGELOG.md +10 -0
  362. package/extensions/zalo/package.json +1 -1
  363. package/extensions/zalouser/CHANGELOG.md +10 -0
  364. package/extensions/zalouser/package.json +1 -1
  365. package/package.json +1 -1
  366. package/skills/apple-reminders/SKILL.md +100 -49
  367. package/skills/coding-agent/SKILL.md +34 -28
  368. package/skills/github/SKILL.md +131 -16
  369. package/skills/imsg/SKILL.md +112 -15
  370. package/skills/openhue/SKILL.md +101 -19
  371. package/skills/tmux/SKILL.md +111 -79
  372. package/skills/weather/SKILL.md +88 -25
  373. package/dist/agents/openclaw-tools.js +0 -151
  374. package/dist/agents/tool-security.js +0 -96
  375. package/dist/gateway/url-validation.js +0 -94
  376. package/dist/infra/openclaw-root.js +0 -109
  377. package/dist/infra/tmp-openclaw-dir.js +0 -81
  378. package/dist/media/path-sanitization.js +0 -78
@@ -1,55 +1,21 @@
1
- import { RequestClient } from "@buape/carbon";
1
+ import { Embed, serializePayload, } from "@buape/carbon";
2
2
  import { PollLayoutType } from "discord-api-types/payloads/v10";
3
3
  import { Routes } from "discord-api-types/v10";
4
4
  import { loadConfig } from "../config/config.js";
5
- import { createDiscordRetryRunner } from "../infra/retry-policy.js";
6
5
  import { normalizePollDurationHours, normalizePollInput } from "../polls.js";
7
6
  import { loadWebMedia } from "../web/media.js";
8
7
  import { resolveDiscordAccount } from "./accounts.js";
9
8
  import { chunkDiscordTextWithMode } from "./chunk.js";
9
+ import { createDiscordClient, resolveDiscordRest } from "./client.js";
10
10
  import { fetchChannelPermissionsDiscord, isThreadChannelType } from "./send.permissions.js";
11
11
  import { DiscordSendError } from "./send.types.js";
12
12
  import { parseDiscordTarget, resolveDiscordTarget } from "./targets.js";
13
- import { normalizeDiscordToken } from "./token.js";
14
13
  const DISCORD_TEXT_LIMIT = 2000;
15
14
  const DISCORD_MAX_STICKERS = 3;
16
15
  const DISCORD_POLL_MAX_ANSWERS = 10;
17
16
  const DISCORD_POLL_MAX_DURATION_HOURS = 32 * 24;
18
17
  const DISCORD_MISSING_PERMISSIONS = 50013;
19
18
  const DISCORD_CANNOT_DM = 50007;
20
- export const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12;
21
- function resolveToken(params) {
22
- const explicit = normalizeDiscordToken(params.explicit);
23
- if (explicit) {
24
- return explicit;
25
- }
26
- const fallback = normalizeDiscordToken(params.fallbackToken);
27
- if (!fallback) {
28
- throw new Error(`Discord bot token missing for account "${params.accountId}" (set discord.accounts.${params.accountId}.token or DISCORD_BOT_TOKEN for default).`);
29
- }
30
- return fallback;
31
- }
32
- function resolveRest(token, rest) {
33
- return rest ?? new RequestClient(token);
34
- }
35
- function createDiscordClient(opts, cfg = loadConfig()) {
36
- const account = resolveDiscordAccount({ cfg, accountId: opts.accountId });
37
- const token = resolveToken({
38
- explicit: opts.token,
39
- accountId: account.accountId,
40
- fallbackToken: account.token,
41
- });
42
- const rest = resolveRest(token, opts.rest);
43
- const request = createDiscordRetryRunner({
44
- retry: opts.retry,
45
- configRetry: account.config.retry,
46
- verbose: opts.verbose,
47
- });
48
- return { token, rest, request };
49
- }
50
- function resolveDiscordRest(opts) {
51
- return createDiscordClient(opts).rest;
52
- }
53
19
  function normalizeReactionEmoji(raw) {
54
20
  const trimmed = raw.trim();
55
21
  if (!trimmed) {
@@ -202,6 +168,8 @@ async function resolveChannelId(rest, recipient, request) {
202
168
  }
203
169
  return { channelId: dmChannel.id, dm: true };
204
170
  }
171
+ // Discord message flag for silent/suppress notifications
172
+ export const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12;
205
173
  export function buildDiscordTextChunks(text, opts = {}) {
206
174
  if (!text) {
207
175
  return [];
@@ -216,62 +184,137 @@ export function buildDiscordTextChunks(text, opts = {}) {
216
184
  }
217
185
  return chunks;
218
186
  }
219
- async function sendDiscordText(rest, channelId, text, replyTo, request, maxLinesPerMessage, embeds, chunkMode) {
187
+ function hasV2Components(components) {
188
+ return Boolean(components?.some((component) => "isV2" in component && component.isV2));
189
+ }
190
+ export function resolveDiscordSendComponents(params) {
191
+ if (!params.components || !params.isFirst) {
192
+ return undefined;
193
+ }
194
+ return typeof params.components === "function"
195
+ ? params.components(params.text)
196
+ : params.components;
197
+ }
198
+ function normalizeDiscordEmbeds(embeds) {
199
+ if (!embeds?.length) {
200
+ return undefined;
201
+ }
202
+ return embeds.map((embed) => (embed instanceof Embed ? embed : new Embed(embed)));
203
+ }
204
+ export function resolveDiscordSendEmbeds(params) {
205
+ if (!params.embeds || !params.isFirst) {
206
+ return undefined;
207
+ }
208
+ return normalizeDiscordEmbeds(params.embeds);
209
+ }
210
+ export function buildDiscordMessagePayload(params) {
211
+ const payload = {};
212
+ const hasV2 = hasV2Components(params.components);
213
+ const trimmed = params.text.trim();
214
+ if (!hasV2 && trimmed) {
215
+ payload.content = params.text;
216
+ }
217
+ if (params.components?.length) {
218
+ payload.components = params.components;
219
+ }
220
+ if (!hasV2 && params.embeds?.length) {
221
+ payload.embeds = params.embeds;
222
+ }
223
+ if (params.flags !== undefined) {
224
+ payload.flags = params.flags;
225
+ }
226
+ if (params.files?.length) {
227
+ payload.files = params.files;
228
+ }
229
+ return payload;
230
+ }
231
+ export function stripUndefinedFields(value) {
232
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
233
+ }
234
+ async function sendDiscordText(rest, channelId, text, replyTo, request, maxLinesPerMessage, components, embeds, chunkMode, silent) {
220
235
  if (!text.trim()) {
221
236
  throw new Error("Message must be non-empty for Discord sends");
222
237
  }
223
238
  const messageReference = replyTo ? { message_id: replyTo, fail_if_not_exists: false } : undefined;
239
+ const flags = silent ? SUPPRESS_NOTIFICATIONS_FLAG : undefined;
224
240
  const chunks = buildDiscordTextChunks(text, { maxLinesPerMessage, chunkMode });
225
- if (chunks.length === 1) {
226
- const res = (await request(() => rest.post(Routes.channelMessages(channelId), {
227
- body: {
228
- content: chunks[0],
229
- message_reference: messageReference,
230
- ...(embeds?.length ? { embeds } : {}),
231
- },
241
+ const sendChunk = async (chunk, isFirst) => {
242
+ const chunkComponents = resolveDiscordSendComponents({
243
+ components,
244
+ text: chunk,
245
+ isFirst,
246
+ });
247
+ const chunkEmbeds = resolveDiscordSendEmbeds({ embeds, isFirst });
248
+ const payload = buildDiscordMessagePayload({
249
+ text: chunk,
250
+ components: chunkComponents,
251
+ embeds: chunkEmbeds,
252
+ flags,
253
+ });
254
+ const body = stripUndefinedFields({
255
+ ...serializePayload(payload),
256
+ ...(messageReference ? { message_reference: messageReference } : {}),
257
+ });
258
+ return (await request(() => rest.post(Routes.channelMessages(channelId), {
259
+ body,
232
260
  }), "text"));
233
- return res;
261
+ };
262
+ if (chunks.length === 1) {
263
+ return await sendChunk(chunks[0], true);
234
264
  }
235
265
  let last = null;
236
- let isFirst = true;
237
- for (const chunk of chunks) {
238
- last = (await request(() => rest.post(Routes.channelMessages(channelId), {
239
- body: {
240
- content: chunk,
241
- message_reference: isFirst ? messageReference : undefined,
242
- ...(isFirst && embeds?.length ? { embeds } : {}),
243
- },
244
- }), "text"));
245
- isFirst = false;
266
+ for (const [index, chunk] of chunks.entries()) {
267
+ last = await sendChunk(chunk, index === 0);
246
268
  }
247
269
  if (!last) {
248
270
  throw new Error("Discord send failed (empty chunk result)");
249
271
  }
250
272
  return last;
251
273
  }
252
- async function sendDiscordMedia(rest, channelId, text, mediaUrl, replyTo, request, maxLinesPerMessage, embeds, chunkMode) {
253
- const media = await loadWebMedia(mediaUrl);
274
+ async function sendDiscordMedia(rest, channelId, text, mediaUrl, mediaLocalRoots, replyTo, request, maxLinesPerMessage, components, embeds, chunkMode, silent) {
275
+ const media = await loadWebMedia(mediaUrl, { localRoots: mediaLocalRoots });
254
276
  const chunks = text ? buildDiscordTextChunks(text, { maxLinesPerMessage, chunkMode }) : [];
255
277
  const caption = chunks[0] ?? "";
256
278
  const messageReference = replyTo ? { message_id: replyTo, fail_if_not_exists: false } : undefined;
279
+ const flags = silent ? SUPPRESS_NOTIFICATIONS_FLAG : undefined;
280
+ let fileData;
281
+ if (media.buffer instanceof Blob) {
282
+ fileData = media.buffer;
283
+ }
284
+ else {
285
+ const arrayBuffer = new ArrayBuffer(media.buffer.byteLength);
286
+ new Uint8Array(arrayBuffer).set(media.buffer);
287
+ fileData = new Blob([arrayBuffer]);
288
+ }
289
+ const captionComponents = resolveDiscordSendComponents({
290
+ components,
291
+ text: caption,
292
+ isFirst: true,
293
+ });
294
+ const captionEmbeds = resolveDiscordSendEmbeds({ embeds, isFirst: true });
295
+ const payload = buildDiscordMessagePayload({
296
+ text: caption,
297
+ components: captionComponents,
298
+ embeds: captionEmbeds,
299
+ flags,
300
+ files: [
301
+ {
302
+ data: fileData,
303
+ name: media.fileName ?? "upload",
304
+ },
305
+ ],
306
+ });
257
307
  const res = (await request(() => rest.post(Routes.channelMessages(channelId), {
258
- body: {
259
- content: caption || undefined,
260
- message_reference: messageReference,
261
- ...(embeds?.length ? { embeds } : {}),
262
- files: [
263
- {
264
- data: media.buffer,
265
- name: media.fileName ?? "upload",
266
- },
267
- ],
268
- },
308
+ body: stripUndefinedFields({
309
+ ...serializePayload(payload),
310
+ ...(messageReference ? { message_reference: messageReference } : {}),
311
+ }),
269
312
  }), "media"));
270
313
  for (const chunk of chunks.slice(1)) {
271
314
  if (!chunk.trim()) {
272
315
  continue;
273
316
  }
274
- await sendDiscordText(rest, channelId, chunk, undefined, request, maxLinesPerMessage, undefined, chunkMode);
317
+ await sendDiscordText(rest, channelId, chunk, replyTo, request, maxLinesPerMessage, undefined, undefined, chunkMode, silent);
275
318
  }
276
319
  return res;
277
320
  }
@@ -284,7 +327,4 @@ function buildReactionIdentifier(emoji) {
284
327
  function formatReactionEmoji(emoji) {
285
328
  return buildReactionIdentifier(emoji);
286
329
  }
287
- export function stripUndefinedFields(obj) {
288
- return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
289
- }
290
330
  export { buildDiscordSendError, buildReactionIdentifier, createDiscordClient, formatReactionEmoji, normalizeDiscordPollInput, normalizeEmojiName, normalizeReactionEmoji, normalizeStickerIds, parseRecipient, resolveChannelId, resolveDiscordRest, sendDiscordMedia, sendDiscordText, };
@@ -14,7 +14,7 @@ import crypto from "node:crypto";
14
14
  import fs from "node:fs/promises";
15
15
  import path from "node:path";
16
16
  import { promisify } from "node:util";
17
- import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-poolbot-dir.js";
17
+ import { resolvePreferredPoolbotTmpDir } from "../infra/tmp-poolbot-dir.js";
18
18
  const execFileAsync = promisify(execFile);
19
19
  const DISCORD_VOICE_MESSAGE_FLAG = 1 << 13;
20
20
  const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12;
@@ -62,7 +62,7 @@ export async function generateWaveform(filePath) {
62
62
  * Generate waveform by extracting raw PCM data and sampling amplitudes
63
63
  */
64
64
  async function generateWaveformFromPcm(filePath) {
65
- const tempDir = resolvePreferredOpenClawTmpDir();
65
+ const tempDir = resolvePreferredPoolbotTmpDir();
66
66
  const tempPcm = path.join(tempDir, `waveform-${crypto.randomUUID()}.raw`);
67
67
  try {
68
68
  // Convert to raw 16-bit signed PCM, mono, 8kHz
@@ -161,7 +161,7 @@ export async function ensureOggOpus(filePath) {
161
161
  }
162
162
  }
163
163
  // Convert to OGG/Opus
164
- const tempDir = resolvePreferredOpenClawTmpDir();
164
+ const tempDir = resolvePreferredPoolbotTmpDir();
165
165
  const outputPath = path.join(tempDir, `voice-${crypto.randomUUID()}.ogg`);
166
166
  await execFileAsync("ffmpeg", [
167
167
  "-y",
@@ -1,46 +1,24 @@
1
- import { safeEqualSecret } from "../security/secret-equal.js";
2
1
  import { readTailscaleWhoisIdentity } from "../infra/tailscale.js";
3
- import { isTrustedProxyAddress, parseForwardedForClientIp, resolveGatewayClientIp } from "./net.js";
2
+ import { safeEqualSecret } from "../security/secret-equal.js";
3
+ import { AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET, } from "./auth-rate-limit.js";
4
+ import { isLoopbackAddress, isTrustedProxyAddress, resolveHostName, parseForwardedForClientIp, resolveGatewayClientIp, } from "./net.js";
4
5
  function normalizeLogin(login) {
5
6
  return login.trim().toLowerCase();
6
7
  }
7
- function isLoopbackAddress(ip) {
8
- if (!ip)
9
- return false;
10
- if (ip === "127.0.0.1")
11
- return true;
12
- if (ip.startsWith("127."))
13
- return true;
14
- if (ip === "::1")
15
- return true;
16
- if (ip.startsWith("::ffff:127."))
17
- return true;
18
- return false;
19
- }
20
- function getHostName(hostHeader) {
21
- const host = (hostHeader ?? "").trim().toLowerCase();
22
- if (!host)
23
- return "";
24
- if (host.startsWith("[")) {
25
- const end = host.indexOf("]");
26
- if (end !== -1)
27
- return host.slice(1, end);
28
- }
29
- const [name] = host.split(":");
30
- return name ?? "";
31
- }
32
8
  function headerValue(value) {
33
9
  return Array.isArray(value) ? value[0] : value;
34
10
  }
35
11
  function resolveTailscaleClientIp(req) {
36
- if (!req)
12
+ if (!req) {
37
13
  return undefined;
14
+ }
38
15
  const forwardedFor = headerValue(req.headers?.["x-forwarded-for"]);
39
16
  return forwardedFor ? parseForwardedForClientIp(forwardedFor) : undefined;
40
17
  }
41
18
  function resolveRequestClientIp(req, trustedProxies) {
42
- if (!req)
19
+ if (!req) {
43
20
  return undefined;
21
+ }
44
22
  return resolveGatewayClientIp({
45
23
  remoteAddr: req.socket?.remoteAddress ?? "",
46
24
  forwardedFor: headerValue(req.headers?.["x-forwarded-for"]),
@@ -49,12 +27,14 @@ function resolveRequestClientIp(req, trustedProxies) {
49
27
  });
50
28
  }
51
29
  export function isLocalDirectRequest(req, trustedProxies) {
52
- if (!req)
30
+ if (!req) {
53
31
  return false;
32
+ }
54
33
  const clientIp = resolveRequestClientIp(req, trustedProxies) ?? "";
55
- if (!isLoopbackAddress(clientIp))
34
+ if (!isLoopbackAddress(clientIp)) {
56
35
  return false;
57
- const host = getHostName(req.headers?.host);
36
+ }
37
+ const host = resolveHostName(req.headers?.host);
58
38
  const hostIsLocal = host === "localhost" || host === "127.0.0.1" || host === "::1";
59
39
  const hostIsTailscaleServe = host.endsWith(".ts.net");
60
40
  const hasForwarded = Boolean(req.headers?.["x-forwarded-for"] ||
@@ -64,11 +44,13 @@ export function isLocalDirectRequest(req, trustedProxies) {
64
44
  return (hostIsLocal || hostIsTailscaleServe) && (!hasForwarded || remoteIsTrustedProxy);
65
45
  }
66
46
  function getTailscaleUser(req) {
67
- if (!req)
47
+ if (!req) {
68
48
  return null;
49
+ }
69
50
  const login = req.headers["tailscale-user-login"];
70
- if (typeof login !== "string" || !login.trim())
51
+ if (typeof login !== "string" || !login.trim()) {
71
52
  return null;
53
+ }
72
54
  const nameRaw = req.headers["tailscale-user-name"];
73
55
  const profilePic = req.headers["tailscale-user-profile-pic"];
74
56
  const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : login.trim();
@@ -79,15 +61,17 @@ function getTailscaleUser(req) {
79
61
  };
80
62
  }
81
63
  function hasTailscaleProxyHeaders(req) {
82
- if (!req)
64
+ if (!req) {
83
65
  return false;
66
+ }
84
67
  return Boolean(req.headers["x-forwarded-for"] &&
85
68
  req.headers["x-forwarded-proto"] &&
86
69
  req.headers["x-forwarded-host"]);
87
70
  }
88
71
  function isTailscaleProxyRequest(req) {
89
- if (!req)
72
+ if (!req) {
90
73
  return false;
74
+ }
91
75
  return isLoopbackAddress(req.socket?.remoteAddress) && hasTailscaleProxyHeaders(req);
92
76
  }
93
77
  async function resolveVerifiedTailscaleUser(params) {
@@ -122,40 +106,124 @@ async function resolveVerifiedTailscaleUser(params) {
122
106
  export function resolveGatewayAuth(params) {
123
107
  const authConfig = params.authConfig ?? {};
124
108
  const env = params.env ?? process.env;
125
- const token = authConfig.token ?? env.POOLBOT_GATEWAY_TOKEN ?? env.CLAWDBOT_GATEWAY_TOKEN ?? undefined;
126
- const password = authConfig.password ??
127
- env.POOLBOT_GATEWAY_PASSWORD ??
128
- env.CLAWDBOT_GATEWAY_PASSWORD ??
129
- undefined;
130
- const mode = authConfig.mode ?? (password ? "password" : "token");
131
- const allowTailscale = authConfig.allowTailscale ?? (params.tailscaleMode === "serve" && mode !== "password");
109
+ const token = authConfig.token ?? env.POOLBOT_GATEWAY_TOKEN ?? undefined;
110
+ const password = authConfig.password ?? env.POOLBOT_GATEWAY_PASSWORD ?? undefined;
111
+ const trustedProxy = authConfig.trustedProxy;
112
+ let mode;
113
+ if (authConfig.mode) {
114
+ mode = authConfig.mode;
115
+ }
116
+ else if (password) {
117
+ mode = "password";
118
+ }
119
+ else if (token) {
120
+ mode = "token";
121
+ }
122
+ else {
123
+ mode = "none";
124
+ }
125
+ const allowTailscale = authConfig.allowTailscale ??
126
+ (params.tailscaleMode === "serve" && mode !== "password" && mode !== "trusted-proxy");
132
127
  return {
133
128
  mode,
134
129
  token,
135
130
  password,
136
131
  allowTailscale,
132
+ trustedProxy,
137
133
  };
138
134
  }
139
135
  export function assertGatewayAuthConfigured(auth) {
140
136
  if (auth.mode === "token" && !auth.token) {
141
- if (auth.allowTailscale)
137
+ if (auth.allowTailscale) {
142
138
  return;
139
+ }
143
140
  throw new Error("gateway auth mode is token, but no token was configured (set gateway.auth.token or POOLBOT_GATEWAY_TOKEN)");
144
141
  }
145
142
  if (auth.mode === "password" && !auth.password) {
146
143
  throw new Error("gateway auth mode is password, but no password was configured");
147
144
  }
145
+ if (auth.mode === "trusted-proxy") {
146
+ if (!auth.trustedProxy) {
147
+ throw new Error("gateway auth mode is trusted-proxy, but no trustedProxy config was provided (set gateway.auth.trustedProxy)");
148
+ }
149
+ if (!auth.trustedProxy.userHeader || auth.trustedProxy.userHeader.trim() === "") {
150
+ throw new Error("gateway auth mode is trusted-proxy, but trustedProxy.userHeader is empty (set gateway.auth.trustedProxy.userHeader)");
151
+ }
152
+ }
153
+ }
154
+ /**
155
+ * Check if the request came from a trusted proxy and extract user identity.
156
+ * Returns the user identity if valid, or null with a reason if not.
157
+ */
158
+ function authorizeTrustedProxy(params) {
159
+ const { req, trustedProxies, trustedProxyConfig } = params;
160
+ if (!req) {
161
+ return { reason: "trusted_proxy_no_request" };
162
+ }
163
+ const remoteAddr = req.socket?.remoteAddress;
164
+ if (!remoteAddr || !isTrustedProxyAddress(remoteAddr, trustedProxies)) {
165
+ return { reason: "trusted_proxy_untrusted_source" };
166
+ }
167
+ const requiredHeaders = trustedProxyConfig.requiredHeaders ?? [];
168
+ for (const header of requiredHeaders) {
169
+ const value = headerValue(req.headers[header.toLowerCase()]);
170
+ if (!value || value.trim() === "") {
171
+ return { reason: `trusted_proxy_missing_header_${header}` };
172
+ }
173
+ }
174
+ const userHeaderValue = headerValue(req.headers[trustedProxyConfig.userHeader.toLowerCase()]);
175
+ if (!userHeaderValue || userHeaderValue.trim() === "") {
176
+ return { reason: "trusted_proxy_user_missing" };
177
+ }
178
+ const user = userHeaderValue.trim();
179
+ const allowUsers = trustedProxyConfig.allowUsers ?? [];
180
+ if (allowUsers.length > 0 && !allowUsers.includes(user)) {
181
+ return { reason: "trusted_proxy_user_not_allowed" };
182
+ }
183
+ return { user };
148
184
  }
149
185
  export async function authorizeGatewayConnect(params) {
150
186
  const { auth, connectAuth, req, trustedProxies } = params;
151
187
  const tailscaleWhois = params.tailscaleWhois ?? readTailscaleWhoisIdentity;
152
188
  const localDirect = isLocalDirectRequest(req, trustedProxies);
189
+ if (auth.mode === "trusted-proxy") {
190
+ if (!auth.trustedProxy) {
191
+ return { ok: false, reason: "trusted_proxy_config_missing" };
192
+ }
193
+ if (!trustedProxies || trustedProxies.length === 0) {
194
+ return { ok: false, reason: "trusted_proxy_no_proxies_configured" };
195
+ }
196
+ const result = authorizeTrustedProxy({
197
+ req,
198
+ trustedProxies,
199
+ trustedProxyConfig: auth.trustedProxy,
200
+ });
201
+ if ("user" in result) {
202
+ return { ok: true, method: "trusted-proxy", user: result.user };
203
+ }
204
+ return { ok: false, reason: result.reason };
205
+ }
206
+ const limiter = params.rateLimiter;
207
+ const ip = params.clientIp ?? resolveRequestClientIp(req, trustedProxies) ?? req?.socket?.remoteAddress;
208
+ const rateLimitScope = params.rateLimitScope ?? AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET;
209
+ if (limiter) {
210
+ const rlCheck = limiter.check(ip, rateLimitScope);
211
+ if (!rlCheck.allowed) {
212
+ return {
213
+ ok: false,
214
+ reason: "rate_limited",
215
+ rateLimited: true,
216
+ retryAfterMs: rlCheck.retryAfterMs,
217
+ };
218
+ }
219
+ }
153
220
  if (auth.allowTailscale && !localDirect) {
154
221
  const tailscaleCheck = await resolveVerifiedTailscaleUser({
155
222
  req,
156
223
  tailscaleWhois,
157
224
  });
158
225
  if (tailscaleCheck.ok) {
226
+ limiter?.reset(ip, rateLimitScope);
159
227
  return {
160
228
  ok: true,
161
229
  method: "tailscale",
@@ -168,11 +236,14 @@ export async function authorizeGatewayConnect(params) {
168
236
  return { ok: false, reason: "token_missing_config" };
169
237
  }
170
238
  if (!connectAuth?.token) {
239
+ limiter?.recordFailure(ip, rateLimitScope);
171
240
  return { ok: false, reason: "token_missing" };
172
241
  }
173
242
  if (!safeEqualSecret(connectAuth.token, auth.token)) {
243
+ limiter?.recordFailure(ip, rateLimitScope);
174
244
  return { ok: false, reason: "token_mismatch" };
175
245
  }
246
+ limiter?.reset(ip, rateLimitScope);
176
247
  return { ok: true, method: "token" };
177
248
  }
178
249
  if (auth.mode === "password") {
@@ -181,12 +252,16 @@ export async function authorizeGatewayConnect(params) {
181
252
  return { ok: false, reason: "password_missing_config" };
182
253
  }
183
254
  if (!password) {
255
+ limiter?.recordFailure(ip, rateLimitScope);
184
256
  return { ok: false, reason: "password_missing" };
185
257
  }
186
258
  if (!safeEqualSecret(password, auth.password)) {
259
+ limiter?.recordFailure(ip, rateLimitScope);
187
260
  return { ok: false, reason: "password_mismatch" };
188
261
  }
262
+ limiter?.reset(ip, rateLimitScope);
189
263
  return { ok: true, method: "password" };
190
264
  }
265
+ limiter?.recordFailure(ip, rateLimitScope);
191
266
  return { ok: false, reason: "unauthorized" };
192
267
  }