@openclaw/discord 2026.3.13 → 2026.5.1-beta.1

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 (498) hide show
  1. package/account-inspect-api.ts +6 -0
  2. package/action-runtime-api.ts +1 -0
  3. package/api.ts +132 -0
  4. package/channel-config-api.ts +1 -0
  5. package/channel-plugin-api.ts +3 -0
  6. package/config-api.ts +4 -0
  7. package/configured-state.ts +6 -0
  8. package/contract-api.ts +21 -0
  9. package/directory-contract-api.ts +4 -0
  10. package/doctor-contract-api.ts +1 -0
  11. package/index.test.ts +13 -0
  12. package/index.ts +18 -13
  13. package/openclaw.plugin.json +3282 -1
  14. package/package.json +67 -2
  15. package/runtime-api.actions.ts +15 -0
  16. package/runtime-api.lookup.ts +22 -0
  17. package/runtime-api.monitor.ts +50 -0
  18. package/runtime-api.send.ts +79 -0
  19. package/runtime-api.threads.ts +30 -0
  20. package/runtime-api.ts +180 -0
  21. package/runtime-setter-api.ts +3 -0
  22. package/secret-contract-api.ts +4 -0
  23. package/security-audit-contract-api.ts +1 -0
  24. package/security-contract-api.ts +4 -0
  25. package/session-key-api.ts +1 -0
  26. package/setup-entry.ts +9 -0
  27. package/setup-plugin-api.ts +3 -0
  28. package/src/account-inspect.test.ts +126 -0
  29. package/src/account-inspect.ts +132 -0
  30. package/src/accounts.test.ts +247 -0
  31. package/src/accounts.ts +196 -0
  32. package/src/actions/handle-action.guild-admin.ts +411 -0
  33. package/src/actions/handle-action.test.ts +185 -0
  34. package/src/actions/handle-action.ts +332 -0
  35. package/src/actions/runtime.guild.ts +446 -0
  36. package/src/actions/runtime.messaging.messages.ts +205 -0
  37. package/src/actions/runtime.messaging.reactions.ts +67 -0
  38. package/src/actions/runtime.messaging.runtime.ts +69 -0
  39. package/src/actions/runtime.messaging.send.ts +244 -0
  40. package/src/actions/runtime.messaging.shared.ts +92 -0
  41. package/src/actions/runtime.messaging.ts +37 -0
  42. package/src/actions/runtime.moderation-shared.ts +48 -0
  43. package/src/actions/runtime.moderation.authz.test.ts +151 -0
  44. package/src/actions/runtime.moderation.ts +116 -0
  45. package/src/actions/runtime.presence.test.ts +160 -0
  46. package/src/actions/runtime.presence.ts +117 -0
  47. package/src/actions/runtime.shared.ts +83 -0
  48. package/src/actions/runtime.test.ts +1056 -0
  49. package/src/actions/runtime.ts +81 -0
  50. package/src/api-barrel.test.ts +80 -0
  51. package/src/api.test.ts +130 -0
  52. package/src/api.ts +169 -0
  53. package/src/approval-handler.runtime.test.ts +41 -0
  54. package/src/approval-handler.runtime.ts +632 -0
  55. package/src/approval-native.test.ts +330 -0
  56. package/src/approval-native.ts +219 -0
  57. package/src/approval-runtime.ts +14 -0
  58. package/src/approval-shared.ts +53 -0
  59. package/src/audit-core.ts +141 -0
  60. package/src/audit.test.ts +145 -0
  61. package/src/audit.ts +32 -0
  62. package/src/channel-actions.contract.test.ts +45 -0
  63. package/src/channel-actions.runtime.ts +1 -0
  64. package/src/channel-actions.test.ts +236 -0
  65. package/src/channel-actions.ts +198 -0
  66. package/src/channel-api.ts +28 -0
  67. package/src/channel.conversation.ts +159 -0
  68. package/src/channel.loaders.ts +47 -0
  69. package/src/channel.runtime.ts +1 -0
  70. package/src/channel.setup.ts +12 -0
  71. package/src/channel.test.ts +539 -12
  72. package/src/channel.ts +596 -430
  73. package/src/chunk.test.ts +157 -0
  74. package/src/chunk.ts +321 -0
  75. package/src/client.proxy.test.ts +176 -0
  76. package/src/client.test.ts +76 -0
  77. package/src/client.ts +139 -0
  78. package/src/component-custom-id.ts +72 -0
  79. package/src/components-registry.ts +356 -0
  80. package/src/components.builders.ts +409 -0
  81. package/src/components.modal.ts +124 -0
  82. package/src/components.parse.ts +407 -0
  83. package/src/components.test.ts +312 -0
  84. package/src/components.ts +54 -0
  85. package/src/components.types.ts +187 -0
  86. package/src/config-schema.test.ts +325 -0
  87. package/src/config-schema.ts +6 -0
  88. package/src/config-ui-hints.ts +249 -0
  89. package/src/conversation-identity.ts +58 -0
  90. package/src/delivery-retry.ts +56 -0
  91. package/src/directory-cache.ts +116 -0
  92. package/src/directory-config.ts +58 -0
  93. package/src/directory-contract.test.ts +129 -0
  94. package/src/directory-live.test.ts +126 -0
  95. package/src/directory-live.ts +135 -0
  96. package/src/doctor-contract.ts +477 -0
  97. package/src/doctor-shared.ts +5 -0
  98. package/src/doctor.test.ts +405 -0
  99. package/src/doctor.ts +340 -0
  100. package/src/draft-chunking.test.ts +64 -0
  101. package/src/draft-chunking.ts +43 -0
  102. package/src/draft-stream.test.ts +159 -0
  103. package/src/draft-stream.ts +154 -0
  104. package/src/error-body.ts +38 -0
  105. package/src/exec-approvals.test.ts +88 -0
  106. package/src/exec-approvals.ts +110 -0
  107. package/src/gateway-logging.test.ts +98 -0
  108. package/src/gateway-logging.ts +67 -0
  109. package/src/group-policy.ts +113 -0
  110. package/src/guilds.ts +29 -0
  111. package/src/inbound-context.contract.test.ts +11 -0
  112. package/src/interactive-dispatch.ts +104 -0
  113. package/src/internal/api.commands.ts +51 -0
  114. package/src/internal/api.guild.ts +164 -0
  115. package/src/internal/api.interactions.ts +53 -0
  116. package/src/internal/api.messages.ts +113 -0
  117. package/src/internal/api.reactions.ts +38 -0
  118. package/src/internal/api.test.ts +262 -0
  119. package/src/internal/api.ts +61 -0
  120. package/src/internal/api.users.ts +19 -0
  121. package/src/internal/api.webhooks.ts +13 -0
  122. package/src/internal/client.test.ts +297 -0
  123. package/src/internal/client.ts +246 -0
  124. package/src/internal/command-deploy.ts +202 -0
  125. package/src/internal/commands.ts +188 -0
  126. package/src/internal/components.base.ts +65 -0
  127. package/src/internal/components.message.ts +279 -0
  128. package/src/internal/components.modal.ts +95 -0
  129. package/src/internal/components.ts +31 -0
  130. package/src/internal/discord.ts +11 -0
  131. package/src/internal/embeds.ts +35 -0
  132. package/src/internal/entity-cache.ts +98 -0
  133. package/src/internal/event-queue.ts +162 -0
  134. package/src/internal/gateway-close-codes.ts +25 -0
  135. package/src/internal/gateway-dispatch.ts +96 -0
  136. package/src/internal/gateway-identify-limiter.ts +26 -0
  137. package/src/internal/gateway-lifecycle.ts +61 -0
  138. package/src/internal/gateway-rate-limit.ts +104 -0
  139. package/src/internal/gateway.test.ts +475 -0
  140. package/src/internal/gateway.ts +437 -0
  141. package/src/internal/interaction-dispatch.test.ts +148 -0
  142. package/src/internal/interaction-dispatch.ts +130 -0
  143. package/src/internal/interaction-options.ts +98 -0
  144. package/src/internal/interaction-response.ts +53 -0
  145. package/src/internal/interactions.test.ts +253 -0
  146. package/src/internal/interactions.ts +337 -0
  147. package/src/internal/listeners.ts +85 -0
  148. package/src/internal/live-smoke.live.test.ts +26 -0
  149. package/src/internal/modal-fields.ts +95 -0
  150. package/src/internal/payload.ts +69 -0
  151. package/src/internal/rest-body.ts +115 -0
  152. package/src/internal/rest-errors.ts +88 -0
  153. package/src/internal/rest-routes.ts +50 -0
  154. package/src/internal/rest-scheduler.ts +412 -0
  155. package/src/internal/rest.test.ts +437 -0
  156. package/src/internal/rest.ts +213 -0
  157. package/src/internal/schemas.ts +36 -0
  158. package/src/internal/structures.ts +278 -0
  159. package/src/internal/test-builders.test-support.ts +163 -0
  160. package/src/internal/voice.ts +49 -0
  161. package/src/media-detection.ts +28 -0
  162. package/src/mentions.test.ts +111 -0
  163. package/src/mentions.ts +147 -0
  164. package/src/monitor/access-groups.ts +55 -0
  165. package/src/monitor/ack-reactions.ts +70 -0
  166. package/src/monitor/acp-bind-here.integration.test.ts +211 -0
  167. package/src/monitor/agent-components-auth.ts +7 -0
  168. package/src/monitor/agent-components-context.ts +144 -0
  169. package/src/monitor/agent-components-data.ts +224 -0
  170. package/src/monitor/agent-components-dm-auth.ts +221 -0
  171. package/src/monitor/agent-components-guild-auth.ts +322 -0
  172. package/src/monitor/agent-components-helpers.runtime.ts +5 -0
  173. package/src/monitor/agent-components-helpers.ts +34 -0
  174. package/src/monitor/agent-components-reply.ts +10 -0
  175. package/src/monitor/agent-components.deps.runtime.ts +2 -0
  176. package/src/monitor/agent-components.dispatch.ts +366 -0
  177. package/src/monitor/agent-components.handlers.ts +303 -0
  178. package/src/monitor/agent-components.modal.ts +160 -0
  179. package/src/monitor/agent-components.plugin-interactive.ts +187 -0
  180. package/src/monitor/agent-components.runtime.ts +14 -0
  181. package/src/monitor/agent-components.system-controls.ts +211 -0
  182. package/src/monitor/agent-components.ts +70 -0
  183. package/src/monitor/agent-components.types.ts +57 -0
  184. package/src/monitor/agent-components.wildcard-controls.ts +168 -0
  185. package/src/monitor/agent-components.wildcard.test.ts +71 -0
  186. package/src/monitor/allow-list.ts +623 -0
  187. package/src/monitor/auto-presence.test.ts +156 -0
  188. package/src/monitor/auto-presence.ts +356 -0
  189. package/src/monitor/channel-access.ts +70 -0
  190. package/src/monitor/commands.test.ts +24 -0
  191. package/src/monitor/commands.ts +9 -0
  192. package/src/monitor/dm-command-auth.test.ts +197 -0
  193. package/src/monitor/dm-command-auth.ts +158 -0
  194. package/src/monitor/dm-command-decision.test.ts +113 -0
  195. package/src/monitor/dm-command-decision.ts +49 -0
  196. package/src/monitor/exec-approvals.test.ts +226 -0
  197. package/src/monitor/exec-approvals.ts +158 -0
  198. package/src/monitor/format.ts +45 -0
  199. package/src/monitor/gateway-handle.ts +34 -0
  200. package/src/monitor/gateway-metadata.test.ts +29 -0
  201. package/src/monitor/gateway-metadata.ts +298 -0
  202. package/src/monitor/gateway-plugin.test.ts +297 -0
  203. package/src/monitor/gateway-plugin.ts +294 -0
  204. package/src/monitor/gateway-registry.ts +37 -0
  205. package/src/monitor/gateway-supervisor.test.ts +150 -0
  206. package/src/monitor/gateway-supervisor.ts +206 -0
  207. package/src/monitor/inbound-context.test-helpers.ts +37 -0
  208. package/src/monitor/inbound-context.test.ts +106 -0
  209. package/src/monitor/inbound-context.ts +103 -0
  210. package/src/monitor/inbound-dedupe.ts +79 -0
  211. package/src/monitor/inbound-job.test.ts +203 -0
  212. package/src/monitor/inbound-job.ts +118 -0
  213. package/src/monitor/listeners.queue.ts +91 -0
  214. package/src/monitor/listeners.reactions.ts +610 -0
  215. package/src/monitor/listeners.test.ts +200 -0
  216. package/src/monitor/listeners.ts +150 -0
  217. package/src/monitor/message-channel-info.ts +96 -0
  218. package/src/monitor/message-forwarded.ts +107 -0
  219. package/src/monitor/message-handler.batch-gate.test.ts +22 -0
  220. package/src/monitor/message-handler.batch-gate.ts +19 -0
  221. package/src/monitor/message-handler.bot-self-filter.test.ts +68 -0
  222. package/src/monitor/message-handler.context.ts +393 -0
  223. package/src/monitor/message-handler.dm-preflight.ts +123 -0
  224. package/src/monitor/message-handler.draft-preview.ts +246 -0
  225. package/src/monitor/message-handler.hydration.test.ts +80 -0
  226. package/src/monitor/message-handler.hydration.ts +198 -0
  227. package/src/monitor/message-handler.inbound-context.test.ts +59 -0
  228. package/src/monitor/message-handler.module-test-helpers.ts +31 -0
  229. package/src/monitor/message-handler.preflight-channel-access.ts +86 -0
  230. package/src/monitor/message-handler.preflight-channel-context.ts +55 -0
  231. package/src/monitor/message-handler.preflight-context.ts +54 -0
  232. package/src/monitor/message-handler.preflight-helpers.ts +164 -0
  233. package/src/monitor/message-handler.preflight-history.ts +23 -0
  234. package/src/monitor/message-handler.preflight-logging.ts +36 -0
  235. package/src/monitor/message-handler.preflight-pluralkit.ts +27 -0
  236. package/src/monitor/message-handler.preflight-runtime.ts +28 -0
  237. package/src/monitor/message-handler.preflight-thread.ts +49 -0
  238. package/src/monitor/message-handler.preflight.acp-bindings.test.ts +369 -0
  239. package/src/monitor/message-handler.preflight.test-helpers.ts +111 -0
  240. package/src/monitor/message-handler.preflight.test.ts +1544 -0
  241. package/src/monitor/message-handler.preflight.ts +680 -0
  242. package/src/monitor/message-handler.preflight.types.ts +109 -0
  243. package/src/monitor/message-handler.process.test.ts +1301 -0
  244. package/src/monitor/message-handler.process.ts +684 -0
  245. package/src/monitor/message-handler.queue.test.ts +496 -0
  246. package/src/monitor/message-handler.routing-preflight.ts +112 -0
  247. package/src/monitor/message-handler.test-harness.ts +99 -0
  248. package/src/monitor/message-handler.test-helpers.ts +75 -0
  249. package/src/monitor/message-handler.ts +274 -0
  250. package/src/monitor/message-media.ts +507 -0
  251. package/src/monitor/message-run-queue.ts +101 -0
  252. package/src/monitor/message-text.ts +171 -0
  253. package/src/monitor/message-utils.test.ts +1151 -0
  254. package/src/monitor/message-utils.ts +32 -0
  255. package/src/monitor/model-picker-preferences.test.ts +67 -0
  256. package/src/monitor/model-picker-preferences.ts +184 -0
  257. package/src/monitor/model-picker.state.ts +364 -0
  258. package/src/monitor/model-picker.test-utils.ts +26 -0
  259. package/src/monitor/model-picker.test.ts +794 -0
  260. package/src/monitor/model-picker.ts +38 -0
  261. package/src/monitor/model-picker.view.ts +695 -0
  262. package/src/monitor/monitor.agent-components.test.ts +375 -0
  263. package/src/monitor/monitor.test.ts +849 -0
  264. package/src/monitor/monitor.threading-utils.test.ts +598 -0
  265. package/src/monitor/native-command-agent-reply.ts +123 -0
  266. package/src/monitor/native-command-arg-ui.ts +233 -0
  267. package/src/monitor/native-command-auth.ts +308 -0
  268. package/src/monitor/native-command-bypass.ts +13 -0
  269. package/src/monitor/native-command-context.test.ts +98 -0
  270. package/src/monitor/native-command-context.ts +103 -0
  271. package/src/monitor/native-command-dispatch.ts +35 -0
  272. package/src/monitor/native-command-model-picker-apply.ts +177 -0
  273. package/src/monitor/native-command-model-picker-interaction.ts +461 -0
  274. package/src/monitor/native-command-model-picker-ui.ts +368 -0
  275. package/src/monitor/native-command-reply.test.ts +68 -0
  276. package/src/monitor/native-command-reply.ts +183 -0
  277. package/src/monitor/native-command-route.ts +91 -0
  278. package/src/monitor/native-command-status.ts +76 -0
  279. package/src/monitor/native-command-ui.ts +26 -0
  280. package/src/monitor/native-command-ui.types.ts +20 -0
  281. package/src/monitor/native-command.args.ts +45 -0
  282. package/src/monitor/native-command.command-arg.test.ts +99 -0
  283. package/src/monitor/native-command.commands-allowfrom.test.ts +490 -0
  284. package/src/monitor/native-command.model-picker.test.ts +767 -0
  285. package/src/monitor/native-command.options.test.ts +369 -0
  286. package/src/monitor/native-command.options.ts +153 -0
  287. package/src/monitor/native-command.plugin-dispatch.test.ts +879 -0
  288. package/src/monitor/native-command.runtime.ts +50 -0
  289. package/src/monitor/native-command.status-direct.test.ts +272 -0
  290. package/src/monitor/native-command.test-helpers.ts +64 -0
  291. package/src/monitor/native-command.think-autocomplete.test.ts +416 -0
  292. package/src/monitor/native-command.ts +699 -0
  293. package/src/monitor/native-command.types.ts +9 -0
  294. package/src/monitor/native-interaction-channel-context.ts +50 -0
  295. package/src/monitor/preflight-audio.runtime.ts +9 -0
  296. package/src/monitor/preflight-audio.test.ts +157 -0
  297. package/src/monitor/preflight-audio.ts +130 -0
  298. package/src/monitor/presence-cache.ts +61 -0
  299. package/src/monitor/presence.test.ts +44 -0
  300. package/src/monitor/presence.ts +50 -0
  301. package/src/monitor/provider-session.runtime.ts +12 -0
  302. package/src/monitor/provider.acp.ts +89 -0
  303. package/src/monitor/provider.allowlist.test.ts +149 -0
  304. package/src/monitor/provider.allowlist.ts +394 -0
  305. package/src/monitor/provider.cleanup.ts +41 -0
  306. package/src/monitor/provider.commands.ts +129 -0
  307. package/src/monitor/provider.config-log.ts +45 -0
  308. package/src/monitor/provider.deploy-errors.ts +362 -0
  309. package/src/monitor/provider.deploy.ts +221 -0
  310. package/src/monitor/provider.interactions.ts +160 -0
  311. package/src/monitor/provider.lifecycle.test.ts +658 -0
  312. package/src/monitor/provider.lifecycle.ts +545 -0
  313. package/src/monitor/provider.proxy.test.ts +745 -0
  314. package/src/monitor/provider.rest-proxy.test.ts +121 -0
  315. package/src/monitor/provider.runtime.ts +1 -0
  316. package/src/monitor/provider.skill-dedupe.test.ts +42 -0
  317. package/src/monitor/provider.startup-log.ts +32 -0
  318. package/src/monitor/provider.startup.test.ts +426 -0
  319. package/src/monitor/provider.startup.ts +323 -0
  320. package/src/monitor/provider.test.ts +1111 -0
  321. package/src/monitor/provider.ts +713 -0
  322. package/src/monitor/reply-context.ts +64 -0
  323. package/src/monitor/reply-delivery.test.ts +244 -0
  324. package/src/monitor/reply-delivery.ts +203 -0
  325. package/src/monitor/rest-fetch.ts +43 -0
  326. package/src/monitor/route-resolution.test.ts +204 -0
  327. package/src/monitor/route-resolution.ts +140 -0
  328. package/src/monitor/sender-identity.ts +81 -0
  329. package/src/monitor/startup-status.test.ts +30 -0
  330. package/src/monitor/startup-status.ts +10 -0
  331. package/src/monitor/status.ts +22 -0
  332. package/src/monitor/system-events.ts +55 -0
  333. package/src/monitor/thread-bindings.config.ts +35 -0
  334. package/src/monitor/thread-bindings.discord-api.test.ts +229 -0
  335. package/src/monitor/thread-bindings.discord-api.ts +318 -0
  336. package/src/monitor/thread-bindings.lifecycle.test.ts +1871 -0
  337. package/src/monitor/thread-bindings.lifecycle.ts +354 -0
  338. package/src/monitor/thread-bindings.manager.ts +553 -0
  339. package/src/monitor/thread-bindings.messages.ts +6 -0
  340. package/src/monitor/thread-bindings.persona.test.ts +34 -0
  341. package/src/monitor/thread-bindings.persona.ts +25 -0
  342. package/src/monitor/thread-bindings.session-adapter.ts +229 -0
  343. package/src/monitor/thread-bindings.session-shared.ts +59 -0
  344. package/src/monitor/thread-bindings.session-updates.ts +35 -0
  345. package/src/monitor/thread-bindings.shared-state.test.ts +36 -0
  346. package/src/monitor/thread-bindings.state.ts +540 -0
  347. package/src/monitor/thread-bindings.ts +48 -0
  348. package/src/monitor/thread-bindings.types.ts +83 -0
  349. package/src/monitor/thread-channel-context.ts +112 -0
  350. package/src/monitor/thread-session-close.test.ts +180 -0
  351. package/src/monitor/thread-session-close.ts +63 -0
  352. package/src/monitor/thread-title.generate.test.ts +197 -0
  353. package/src/monitor/thread-title.test.ts +31 -0
  354. package/src/monitor/thread-title.ts +181 -0
  355. package/src/monitor/threading.auto-thread.test.ts +327 -0
  356. package/src/monitor/threading.auto-thread.ts +287 -0
  357. package/src/monitor/threading.cache.ts +45 -0
  358. package/src/monitor/threading.parent-info.test.ts +156 -0
  359. package/src/monitor/threading.starter.test.ts +260 -0
  360. package/src/monitor/threading.starter.ts +287 -0
  361. package/src/monitor/threading.ts +20 -0
  362. package/src/monitor/threading.types.ts +102 -0
  363. package/src/monitor/timeouts.ts +84 -0
  364. package/src/monitor/typing.test.ts +42 -0
  365. package/src/monitor/typing.ts +17 -0
  366. package/src/monitor.gateway.test.ts +187 -0
  367. package/src/monitor.gateway.ts +75 -0
  368. package/src/monitor.test.ts +1397 -0
  369. package/src/monitor.ts +28 -0
  370. package/src/normalize.test.ts +56 -0
  371. package/src/normalize.ts +86 -0
  372. package/src/outbound-adapter.interactive-order.test.ts +64 -0
  373. package/src/outbound-adapter.test-harness.ts +207 -0
  374. package/src/outbound-adapter.test.ts +696 -0
  375. package/src/outbound-adapter.ts +291 -0
  376. package/src/outbound-approval.ts +29 -0
  377. package/src/outbound-components.ts +81 -0
  378. package/src/outbound-payload.contract.test.ts +38 -0
  379. package/src/outbound-payload.ts +134 -0
  380. package/src/outbound-send-context.ts +92 -0
  381. package/src/outbound-session-route.test.ts +34 -0
  382. package/src/outbound-session-route.ts +72 -0
  383. package/src/pluralkit.test.ts +67 -0
  384. package/src/pluralkit.ts +58 -0
  385. package/src/preview-streaming.ts +32 -0
  386. package/src/probe.intents.test.ts +94 -0
  387. package/src/probe.parse-token.test.ts +43 -0
  388. package/src/probe.runtime.ts +1 -0
  389. package/src/probe.ts +237 -0
  390. package/src/proxy-fetch.ts +92 -0
  391. package/src/proxy-request-client.test.ts +78 -0
  392. package/src/proxy-request-client.ts +54 -0
  393. package/src/recipient-resolution.ts +39 -0
  394. package/src/resolve-allowlist-common.test.ts +36 -0
  395. package/src/resolve-allowlist-common.ts +39 -0
  396. package/src/resolve-channels.test.ts +340 -0
  397. package/src/resolve-channels.ts +369 -0
  398. package/src/resolve-users.test.ts +222 -0
  399. package/src/resolve-users.ts +184 -0
  400. package/src/retry.test.ts +83 -0
  401. package/src/retry.ts +98 -0
  402. package/src/runtime-api.ts +64 -0
  403. package/src/runtime.ts +22 -5
  404. package/src/secret-config-contract.ts +140 -0
  405. package/src/security-audit.runtime.ts +1 -0
  406. package/src/security-audit.test.ts +246 -0
  407. package/src/security-audit.ts +208 -0
  408. package/src/security-contract.ts +47 -0
  409. package/src/security-doctor.test.ts +25 -0
  410. package/src/security-doctor.ts +20 -0
  411. package/src/security.ts +60 -0
  412. package/src/send-target-parsing.ts +14 -0
  413. package/src/send.channels.ts +139 -0
  414. package/src/send.components.test.ts +275 -0
  415. package/src/send.components.ts +383 -0
  416. package/src/send.creates-thread.test.ts +643 -0
  417. package/src/send.emojis-stickers.ts +57 -0
  418. package/src/send.guild.ts +170 -0
  419. package/src/send.message-request.ts +97 -0
  420. package/src/send.messages.test.ts +53 -0
  421. package/src/send.messages.ts +225 -0
  422. package/src/send.outbound.ts +414 -0
  423. package/src/send.permissions.authz.test.ts +188 -0
  424. package/src/send.permissions.ts +283 -0
  425. package/src/send.reactions.ts +155 -0
  426. package/src/send.sends-basic-channel-messages.test.ts +919 -0
  427. package/src/send.shared.ts +445 -0
  428. package/src/send.test-harness.ts +56 -0
  429. package/src/send.ts +82 -0
  430. package/src/send.types.ts +188 -0
  431. package/src/send.typing.test.ts +41 -0
  432. package/src/send.typing.ts +9 -0
  433. package/src/send.voice.ts +134 -0
  434. package/src/send.webhook-activity.test.ts +105 -0
  435. package/src/send.webhook.proxy.test.ts +191 -0
  436. package/src/send.webhook.ts +133 -0
  437. package/src/session-contract.ts +3 -0
  438. package/src/session-key-normalization.test.ts +44 -0
  439. package/src/session-key-normalization.ts +47 -0
  440. package/src/setup-account-state.test.ts +91 -0
  441. package/src/setup-account-state.ts +144 -0
  442. package/src/setup-adapter.ts +12 -0
  443. package/src/setup-core.ts +180 -0
  444. package/src/setup-runtime-helpers.ts +10 -0
  445. package/src/setup-surface.test.ts +96 -0
  446. package/src/setup-surface.ts +129 -0
  447. package/src/shared-interactive.test.ts +153 -0
  448. package/src/shared-interactive.ts +124 -0
  449. package/src/shared.test.ts +159 -0
  450. package/src/shared.ts +190 -0
  451. package/src/status-issues.test.ts +70 -0
  452. package/src/status-issues.ts +169 -0
  453. package/src/subagent-hooks.test.ts +40 -44
  454. package/src/subagent-hooks.ts +185 -122
  455. package/src/target-parsing.ts +53 -0
  456. package/src/target-resolver.ts +129 -0
  457. package/src/targets.test.ts +367 -0
  458. package/src/targets.ts +12 -0
  459. package/src/test-http-helpers.ts +10 -0
  460. package/src/test-support/component-runtime.ts +190 -0
  461. package/src/test-support/config.ts +7 -0
  462. package/src/test-support/configured-binding-runtime.ts +29 -0
  463. package/src/test-support/partial-channel.ts +26 -0
  464. package/src/test-support/provider.test-support.ts +545 -0
  465. package/src/token.test.ts +107 -0
  466. package/src/token.ts +60 -0
  467. package/src/ui-colors.ts +27 -0
  468. package/src/ui.ts +20 -0
  469. package/src/voice/access.test.ts +217 -0
  470. package/src/voice/access.ts +124 -0
  471. package/src/voice/audio.ts +173 -0
  472. package/src/voice/capture-state.test.ts +48 -0
  473. package/src/voice/capture-state.ts +120 -0
  474. package/src/voice/command.test.ts +164 -0
  475. package/src/voice/command.ts +283 -0
  476. package/src/voice/config.ts +8 -0
  477. package/src/voice/manager.e2e.test.ts +928 -0
  478. package/src/voice/manager.ready-listener.test.ts +37 -0
  479. package/src/voice/manager.runtime.ts +11 -0
  480. package/src/voice/manager.ts +691 -0
  481. package/src/voice/prompt.test.ts +16 -0
  482. package/src/voice/prompt.ts +17 -0
  483. package/src/voice/receive-recovery.test.ts +79 -0
  484. package/src/voice/receive-recovery.ts +159 -0
  485. package/src/voice/sanitize.test.ts +34 -0
  486. package/src/voice/sanitize.ts +32 -0
  487. package/src/voice/sdk-runtime.ts +14 -0
  488. package/src/voice/segment.ts +156 -0
  489. package/src/voice/session.ts +50 -0
  490. package/src/voice/speaker-context.ts +127 -0
  491. package/src/voice/tts.ts +125 -0
  492. package/src/voice-message.test.ts +234 -0
  493. package/src/voice-message.ts +444 -0
  494. package/subagent-hooks-api.ts +27 -0
  495. package/test-api.ts +4 -0
  496. package/thread-binding-api.ts +1 -0
  497. package/timeouts.ts +6 -0
  498. package/tsconfig.json +16 -0
@@ -0,0 +1,745 @@
1
+ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ function createGatewayInfoBody(overrides?: {
4
+ url?: string;
5
+ shards?: number;
6
+ maxConcurrency?: number;
7
+ }): string {
8
+ return JSON.stringify({
9
+ url: overrides?.url ?? "wss://gateway.discord.gg",
10
+ shards: overrides?.shards ?? 1,
11
+ session_start_limit: {
12
+ total: 1000,
13
+ remaining: 999,
14
+ reset_after: 120_000,
15
+ max_concurrency: overrides?.maxConcurrency ?? 1,
16
+ },
17
+ });
18
+ }
19
+
20
+ function resolveGatewayInfoFetch(resolve: ((value: Response) => void) | undefined): void {
21
+ expect(resolve).toBeDefined();
22
+ resolve!({
23
+ ok: true,
24
+ status: 200,
25
+ text: async () => createGatewayInfoBody(),
26
+ } as Response);
27
+ }
28
+
29
+ const {
30
+ GatewayIntents,
31
+ baseRegisterClientSpy,
32
+ captureHttpExchangeSpy,
33
+ captureWsEventSpy,
34
+ GatewayPlugin,
35
+ globalFetchMock,
36
+ HttpsProxyAgent,
37
+ getLastAgent,
38
+ resolveDebugProxySettingsMock,
39
+ resetLastAgent,
40
+ webSocketSpy,
41
+ wsProxyAgentSpy,
42
+ } = vi.hoisted(() => {
43
+ const wsProxyAgentSpy = vi.fn();
44
+ const globalFetchMock = vi.fn();
45
+ const baseRegisterClientSpy = vi.fn();
46
+ const webSocketSpy = vi.fn();
47
+ const captureHttpExchangeSpy = vi.fn();
48
+ const captureWsEventSpy = vi.fn();
49
+ const resolveDebugProxySettingsMock = vi.fn(() => ({ enabled: false }));
50
+
51
+ const GatewayIntents = {
52
+ Guilds: 1 << 0,
53
+ GuildMessages: 1 << 1,
54
+ MessageContent: 1 << 2,
55
+ DirectMessages: 1 << 3,
56
+ GuildMessageReactions: 1 << 4,
57
+ DirectMessageReactions: 1 << 5,
58
+ GuildPresences: 1 << 6,
59
+ GuildMembers: 1 << 7,
60
+ GuildVoiceStates: 1 << 8,
61
+ } as const;
62
+
63
+ class GatewayPlugin {
64
+ options: unknown;
65
+ gatewayInfo: unknown;
66
+ client: unknown;
67
+ ws: unknown;
68
+ isConnecting: boolean;
69
+ constructor(options?: unknown, gatewayInfo?: unknown) {
70
+ this.options = options;
71
+ this.gatewayInfo = gatewayInfo;
72
+ this.client = undefined;
73
+ this.ws = undefined;
74
+ this.isConnecting = false;
75
+ }
76
+ async registerClient(client: unknown) {
77
+ baseRegisterClientSpy(client);
78
+ }
79
+ }
80
+
81
+ class HttpsProxyAgent {
82
+ static lastCreated: HttpsProxyAgent | undefined;
83
+ proxyUrl: string;
84
+ constructor(proxyUrl: string) {
85
+ if (proxyUrl === "bad-proxy") {
86
+ throw new Error("bad proxy");
87
+ }
88
+ this.proxyUrl = proxyUrl;
89
+ HttpsProxyAgent.lastCreated = this;
90
+ wsProxyAgentSpy(proxyUrl);
91
+ }
92
+ }
93
+
94
+ return {
95
+ baseRegisterClientSpy,
96
+ GatewayIntents,
97
+ GatewayPlugin,
98
+ globalFetchMock,
99
+ HttpsProxyAgent,
100
+ getLastAgent: () => HttpsProxyAgent.lastCreated,
101
+ captureHttpExchangeSpy,
102
+ captureWsEventSpy,
103
+ resolveDebugProxySettingsMock,
104
+ resetLastAgent: () => {
105
+ HttpsProxyAgent.lastCreated = undefined;
106
+ },
107
+ webSocketSpy,
108
+ wsProxyAgentSpy,
109
+ };
110
+ });
111
+
112
+ // Unit test: don't import the real gateway just to check the prototype chain.
113
+ vi.mock("../internal/gateway.js", () => ({
114
+ GatewayIntents,
115
+ GatewayPlugin,
116
+ }));
117
+
118
+ vi.mock("../internal/gateway.js", () => ({
119
+ GatewayIntents,
120
+ GatewayPlugin,
121
+ }));
122
+
123
+ vi.mock("https-proxy-agent", () => ({
124
+ HttpsProxyAgent,
125
+ }));
126
+
127
+ vi.mock("ws", () => ({
128
+ default: function MockWebSocket(
129
+ url: string,
130
+ options?: { agent?: unknown; handshakeTimeout?: number },
131
+ ) {
132
+ webSocketSpy(url, options);
133
+ },
134
+ }));
135
+
136
+ vi.mock("openclaw/plugin-sdk/proxy-capture", () => ({
137
+ captureHttpExchange: captureHttpExchangeSpy,
138
+ captureWsEvent: captureWsEventSpy,
139
+ resolveEffectiveDebugProxyUrl: (configuredProxyUrl?: string) =>
140
+ configuredProxyUrl?.trim() || process.env.OPENCLAW_DEBUG_PROXY_URL,
141
+ resolveDebugProxySettings: resolveDebugProxySettingsMock,
142
+ }));
143
+
144
+ vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
145
+ fetchWithSsrFGuard: vi.fn(async (params: { url: string; init?: RequestInit }) => {
146
+ const source = (await globalFetchMock(params.url, params.init)) as Response;
147
+ const body = await source.text();
148
+ return {
149
+ response: new Response(body, {
150
+ status: source.status,
151
+ statusText: source.statusText,
152
+ headers: source.headers,
153
+ }),
154
+ release: vi.fn(),
155
+ };
156
+ }),
157
+ }));
158
+
159
+ describe("createDiscordGatewayPlugin", () => {
160
+ let createDiscordGatewayPlugin: typeof import("./gateway-plugin.js").createDiscordGatewayPlugin;
161
+ let waitForDiscordGatewayPluginRegistration: typeof import("./gateway-plugin.js").waitForDiscordGatewayPluginRegistration;
162
+
163
+ beforeAll(async () => {
164
+ ({ createDiscordGatewayPlugin, waitForDiscordGatewayPluginRegistration } =
165
+ await import("./gateway-plugin.js"));
166
+ });
167
+
168
+ function createRuntime() {
169
+ return {
170
+ log: vi.fn(),
171
+ error: vi.fn(),
172
+ exit: vi.fn(() => {
173
+ throw new Error("exit");
174
+ }),
175
+ };
176
+ }
177
+
178
+ function createProxyTestingOverrides() {
179
+ return {
180
+ HttpsProxyAgentCtor:
181
+ HttpsProxyAgent as unknown as typeof import("https-proxy-agent").HttpsProxyAgent,
182
+ webSocketCtor: function WebSocketCtor(
183
+ url: string,
184
+ options?: { agent?: unknown; handshakeTimeout?: number },
185
+ ) {
186
+ webSocketSpy(url, options);
187
+ } as unknown as new (
188
+ url: string,
189
+ options?: { agent?: unknown; handshakeTimeout?: number },
190
+ ) => import("ws").WebSocket,
191
+ registerClient: async (_plugin: unknown, client: unknown) => {
192
+ baseRegisterClientSpy(client);
193
+ },
194
+ };
195
+ }
196
+
197
+ async function registerGatewayClient(plugin: unknown) {
198
+ await (
199
+ plugin as {
200
+ registerClient: (client: {
201
+ options: { token: string };
202
+ registerListener: typeof baseRegisterClientSpy;
203
+ unregisterListener: ReturnType<typeof vi.fn>;
204
+ }) => Promise<void>;
205
+ }
206
+ ).registerClient({
207
+ options: { token: "token-123" },
208
+ registerListener: baseRegisterClientSpy,
209
+ unregisterListener: vi.fn(),
210
+ });
211
+ }
212
+
213
+ function startIgnoredGatewayRegistration(plugin: unknown) {
214
+ void (
215
+ plugin as {
216
+ registerClient: (client: {
217
+ options: { token: string };
218
+ registerListener: typeof baseRegisterClientSpy;
219
+ unregisterListener: ReturnType<typeof vi.fn>;
220
+ }) => Promise<void>;
221
+ }
222
+ ).registerClient({
223
+ options: { token: "token-123" },
224
+ registerListener: baseRegisterClientSpy,
225
+ unregisterListener: vi.fn(),
226
+ });
227
+ }
228
+
229
+ async function expectGatewayRegisterFetchFailure(response: Response) {
230
+ const runtime = createRuntime();
231
+ globalFetchMock.mockResolvedValue(response);
232
+ const plugin = createDiscordGatewayPlugin({
233
+ discordConfig: {},
234
+ runtime,
235
+ });
236
+
237
+ await expect(registerGatewayClient(plugin)).rejects.toThrow(
238
+ "Failed to get gateway information from Discord",
239
+ );
240
+ expect(baseRegisterClientSpy).not.toHaveBeenCalled();
241
+ }
242
+
243
+ async function expectGatewayRegisterFallback(response: Response) {
244
+ const runtime = createRuntime();
245
+ globalFetchMock.mockResolvedValue(response);
246
+ const plugin = createDiscordGatewayPlugin({
247
+ discordConfig: {},
248
+ runtime,
249
+ });
250
+
251
+ await registerGatewayClient(plugin);
252
+
253
+ expect(baseRegisterClientSpy).toHaveBeenCalledTimes(1);
254
+ expect((plugin as unknown as { gatewayInfo?: { url?: string } }).gatewayInfo?.url).toBe(
255
+ "wss://gateway.discord.gg/",
256
+ );
257
+ expect(runtime.log).toHaveBeenCalledWith(
258
+ expect.stringContaining("discord: gateway metadata lookup failed transiently"),
259
+ );
260
+ }
261
+
262
+ async function registerGatewayClientWithMetadata(params: {
263
+ plugin: unknown;
264
+ fetchMock: typeof globalFetchMock;
265
+ }) {
266
+ params.fetchMock.mockResolvedValue({
267
+ ok: true,
268
+ status: 200,
269
+ text: async () => createGatewayInfoBody(),
270
+ } as Response);
271
+ await registerGatewayClient(params.plugin);
272
+ }
273
+
274
+ beforeEach(() => {
275
+ vi.unstubAllEnvs();
276
+ vi.stubEnv("OPENCLAW_DEBUG_PROXY_ENABLED", "");
277
+ vi.stubEnv("OPENCLAW_DEBUG_PROXY_URL", "");
278
+ vi.stubGlobal("fetch", globalFetchMock);
279
+ vi.useRealTimers();
280
+ baseRegisterClientSpy.mockClear();
281
+ globalFetchMock.mockClear();
282
+ wsProxyAgentSpy.mockClear();
283
+ webSocketSpy.mockClear();
284
+ captureHttpExchangeSpy.mockClear();
285
+ captureWsEventSpy.mockClear();
286
+ resolveDebugProxySettingsMock.mockReset().mockReturnValue({ enabled: false });
287
+ resetLastAgent();
288
+ });
289
+
290
+ afterEach(() => {
291
+ vi.useRealTimers();
292
+ vi.unstubAllEnvs();
293
+ });
294
+
295
+ it("uses safe gateway metadata lookup without proxy", async () => {
296
+ const runtime = createRuntime();
297
+ const plugin = createDiscordGatewayPlugin({
298
+ discordConfig: {},
299
+ runtime,
300
+ });
301
+
302
+ await registerGatewayClientWithMetadata({ plugin, fetchMock: globalFetchMock });
303
+
304
+ expect(globalFetchMock).toHaveBeenCalledWith(
305
+ "https://discord.com/api/v10/gateway/bot",
306
+ expect.objectContaining({
307
+ headers: { Authorization: "Bot token-123" },
308
+ }),
309
+ );
310
+ expect(baseRegisterClientSpy).toHaveBeenCalledTimes(1);
311
+ });
312
+
313
+ it("uses ws for gateway sockets even without proxy", () => {
314
+ const runtime = createRuntime();
315
+ const plugin = createDiscordGatewayPlugin({
316
+ discordConfig: {},
317
+ runtime,
318
+ });
319
+
320
+ const createWebSocket = (plugin as unknown as { createWebSocket: (url: string) => unknown })
321
+ .createWebSocket;
322
+ createWebSocket("wss://gateway.discord.gg");
323
+
324
+ expect(webSocketSpy).toHaveBeenCalledWith("wss://gateway.discord.gg", {
325
+ handshakeTimeout: 30_000,
326
+ });
327
+ expect(wsProxyAgentSpy).not.toHaveBeenCalled();
328
+ });
329
+
330
+ it("allocates a fresh websocket flow id for each gateway socket", () => {
331
+ const runtime = createRuntime();
332
+ const plugin = createDiscordGatewayPlugin({
333
+ discordConfig: {},
334
+ runtime,
335
+ });
336
+
337
+ const createWebSocket = (plugin as unknown as { createWebSocket: (url: string) => unknown })
338
+ .createWebSocket;
339
+ createWebSocket("wss://gateway.discord.gg/?attempt=1");
340
+ createWebSocket("wss://gateway.discord.gg/?attempt=2");
341
+
342
+ const openCalls = captureWsEventSpy.mock.calls.filter(([event]) => event?.kind === "ws-open");
343
+ expect(openCalls).toHaveLength(2);
344
+ expect(openCalls[0]?.[0]?.flowId).not.toBe(openCalls[1]?.[0]?.flowId);
345
+ });
346
+
347
+ it("maps plain-text Discord 503 responses to fetch failed", async () => {
348
+ await expectGatewayRegisterFallback({
349
+ ok: false,
350
+ status: 503,
351
+ text: async () =>
352
+ "upstream connect error or disconnect/reset before headers. reset reason: overflow",
353
+ } as Response);
354
+ });
355
+
356
+ it("keeps fatal Discord metadata failures fatal", async () => {
357
+ await expectGatewayRegisterFetchFailure({
358
+ ok: false,
359
+ status: 401,
360
+ text: async () => "401: Unauthorized",
361
+ } as Response);
362
+ });
363
+
364
+ it("keeps ignored fatal metadata failures handled for supervised startup", async () => {
365
+ const runtime = createRuntime();
366
+ const unhandledReasons: unknown[] = [];
367
+ const onUnhandledRejection = (reason: unknown) => {
368
+ unhandledReasons.push(reason);
369
+ };
370
+ globalFetchMock.mockResolvedValue({
371
+ ok: false,
372
+ status: 401,
373
+ text: async () => "401: Unauthorized",
374
+ } as Response);
375
+ const plugin = createDiscordGatewayPlugin({
376
+ discordConfig: {},
377
+ runtime,
378
+ });
379
+
380
+ process.on("unhandledRejection", onUnhandledRejection);
381
+ try {
382
+ startIgnoredGatewayRegistration(plugin);
383
+ await new Promise((resolve) => setImmediate(resolve));
384
+
385
+ expect(unhandledReasons).toHaveLength(0);
386
+ const registration = waitForDiscordGatewayPluginRegistration(plugin);
387
+ if (!registration) {
388
+ throw new Error("expected Discord gateway registration promise");
389
+ }
390
+ await expect(registration).rejects.toThrow("Failed to get gateway information from Discord");
391
+ expect(baseRegisterClientSpy).not.toHaveBeenCalled();
392
+ } finally {
393
+ process.off("unhandledRejection", onUnhandledRejection);
394
+ }
395
+ });
396
+
397
+ it("exposes ignored successful registrations for startup await", async () => {
398
+ const runtime = createRuntime();
399
+ globalFetchMock.mockResolvedValue({
400
+ ok: true,
401
+ status: 200,
402
+ text: async () => createGatewayInfoBody(),
403
+ } as Response);
404
+ const plugin = createDiscordGatewayPlugin({
405
+ discordConfig: {},
406
+ runtime,
407
+ });
408
+
409
+ startIgnoredGatewayRegistration(plugin);
410
+ const registration = waitForDiscordGatewayPluginRegistration(plugin);
411
+ if (!registration) {
412
+ throw new Error("expected Discord gateway registration promise");
413
+ }
414
+ await registration;
415
+
416
+ expect(baseRegisterClientSpy).toHaveBeenCalledTimes(1);
417
+ expect((plugin as unknown as { gatewayInfo?: { url?: string } }).gatewayInfo?.url).toBe(
418
+ "wss://gateway.discord.gg",
419
+ );
420
+ });
421
+
422
+ it("uses proxy agent for gateway WebSocket when configured", async () => {
423
+ const runtime = createRuntime();
424
+
425
+ const plugin = createDiscordGatewayPlugin({
426
+ discordConfig: { proxy: "http://127.0.0.1:8080" },
427
+ runtime,
428
+ __testing: createProxyTestingOverrides(),
429
+ });
430
+
431
+ expect(Object.getPrototypeOf(plugin)).not.toBe(GatewayPlugin.prototype);
432
+
433
+ const createWebSocket = (plugin as unknown as { createWebSocket: (url: string) => unknown })
434
+ .createWebSocket;
435
+ createWebSocket("wss://gateway.discord.gg");
436
+
437
+ expect(wsProxyAgentSpy).toHaveBeenCalledWith("http://127.0.0.1:8080");
438
+ expect(webSocketSpy).toHaveBeenCalledWith(
439
+ "wss://gateway.discord.gg",
440
+ expect.objectContaining({ agent: getLastAgent(), handshakeTimeout: 30_000 }),
441
+ );
442
+ expect(runtime.log).toHaveBeenCalledWith("discord: gateway proxy enabled");
443
+ expect(runtime.error).not.toHaveBeenCalled();
444
+ });
445
+
446
+ it("falls back to the default gateway plugin when proxy is invalid", async () => {
447
+ const runtime = createRuntime();
448
+
449
+ const plugin = createDiscordGatewayPlugin({
450
+ discordConfig: { proxy: "bad-proxy" },
451
+ runtime,
452
+ });
453
+
454
+ expect(Object.getPrototypeOf(plugin)).not.toBe(GatewayPlugin.prototype);
455
+ expect(runtime.error).toHaveBeenCalled();
456
+ expect(runtime.log).not.toHaveBeenCalled();
457
+ });
458
+
459
+ it("keeps gateway metadata lookup on the guarded direct fetch when proxy is configured", async () => {
460
+ const runtime = createRuntime();
461
+ const plugin = createDiscordGatewayPlugin({
462
+ discordConfig: { proxy: "http://127.0.0.1:8080" },
463
+ runtime,
464
+ __testing: createProxyTestingOverrides(),
465
+ });
466
+
467
+ await registerGatewayClientWithMetadata({ plugin, fetchMock: globalFetchMock });
468
+
469
+ expect(globalFetchMock).toHaveBeenCalledWith(
470
+ "https://discord.com/api/v10/gateway/bot",
471
+ expect.objectContaining({
472
+ headers: { Authorization: "Bot token-123" },
473
+ }),
474
+ );
475
+ expect(baseRegisterClientSpy).toHaveBeenCalledTimes(1);
476
+ });
477
+
478
+ it("does not double-capture gateway metadata fetches when global fetch patching is enabled", async () => {
479
+ resolveDebugProxySettingsMock.mockReturnValue({ enabled: true });
480
+ const runtime = createRuntime();
481
+ const plugin = createDiscordGatewayPlugin({
482
+ discordConfig: {},
483
+ runtime,
484
+ });
485
+
486
+ await registerGatewayClientWithMetadata({ plugin, fetchMock: globalFetchMock });
487
+
488
+ expect(captureHttpExchangeSpy).not.toHaveBeenCalled();
489
+ });
490
+
491
+ it("accepts IPv6 loopback proxy URLs for websocket setup", async () => {
492
+ const runtime = createRuntime();
493
+ const plugin = createDiscordGatewayPlugin({
494
+ discordConfig: { proxy: "http://[::1]:8080" },
495
+ runtime,
496
+ __testing: createProxyTestingOverrides(),
497
+ });
498
+
499
+ const createWebSocket = (plugin as unknown as { createWebSocket: (url: string) => unknown })
500
+ .createWebSocket;
501
+ createWebSocket("wss://gateway.discord.gg");
502
+ await registerGatewayClientWithMetadata({ plugin, fetchMock: globalFetchMock });
503
+
504
+ expect(wsProxyAgentSpy).toHaveBeenCalledWith("http://[::1]:8080");
505
+ expect(runtime.error).not.toHaveBeenCalled();
506
+ });
507
+
508
+ it("falls back to the default gateway plugin when proxy is remote", async () => {
509
+ const runtime = createRuntime();
510
+
511
+ const plugin = createDiscordGatewayPlugin({
512
+ discordConfig: { proxy: "http://proxy.test:8080" },
513
+ runtime,
514
+ });
515
+
516
+ expect(Object.getPrototypeOf(plugin)).not.toBe(GatewayPlugin.prototype);
517
+ expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("loopback host"));
518
+ expect(runtime.log).not.toHaveBeenCalled();
519
+ });
520
+
521
+ it("maps body read failures to fetch failed", async () => {
522
+ await expectGatewayRegisterFallback({
523
+ ok: true,
524
+ status: 200,
525
+ text: async () => {
526
+ throw new Error("body stream closed");
527
+ },
528
+ } as unknown as Response);
529
+ });
530
+
531
+ it("falls back to the default gateway url when metadata lookup times out", async () => {
532
+ vi.useFakeTimers();
533
+ const runtime = createRuntime();
534
+ globalFetchMock.mockImplementation(() => new Promise(() => {}));
535
+ const plugin = createDiscordGatewayPlugin({
536
+ discordConfig: {},
537
+ runtime,
538
+ });
539
+
540
+ const registerPromise = registerGatewayClient(plugin);
541
+ await vi.advanceTimersByTimeAsync(30_000);
542
+ await registerPromise;
543
+
544
+ expect(baseRegisterClientSpy).toHaveBeenCalledTimes(1);
545
+ expect((plugin as unknown as { gatewayInfo?: { url?: string } }).gatewayInfo?.url).toBe(
546
+ "wss://gateway.discord.gg/",
547
+ );
548
+ expect(runtime.log).toHaveBeenCalledWith(
549
+ expect.stringContaining("discord: gateway metadata lookup failed transiently"),
550
+ );
551
+ });
552
+
553
+ it("uses configured gateway metadata timeout before falling back", async () => {
554
+ vi.useFakeTimers();
555
+ const runtime = createRuntime();
556
+ globalFetchMock.mockImplementation(() => new Promise(() => {}));
557
+ const plugin = createDiscordGatewayPlugin({
558
+ discordConfig: { gatewayInfoTimeoutMs: 5_000 },
559
+ runtime,
560
+ });
561
+
562
+ const registerPromise = registerGatewayClient(plugin);
563
+ await vi.advanceTimersByTimeAsync(4_999);
564
+ expect(baseRegisterClientSpy).not.toHaveBeenCalled();
565
+ await vi.advanceTimersByTimeAsync(1);
566
+ await registerPromise;
567
+
568
+ expect((plugin as unknown as { gatewayInfo?: { url?: string } }).gatewayInfo?.url).toBe(
569
+ "wss://gateway.discord.gg/",
570
+ );
571
+ });
572
+
573
+ it("uses env gateway metadata timeout when config is unset", async () => {
574
+ vi.useFakeTimers();
575
+ vi.stubEnv("OPENCLAW_DISCORD_GATEWAY_INFO_TIMEOUT_MS", "6000");
576
+ const runtime = createRuntime();
577
+ globalFetchMock.mockImplementation(() => new Promise(() => {}));
578
+ const plugin = createDiscordGatewayPlugin({
579
+ discordConfig: {},
580
+ runtime,
581
+ });
582
+
583
+ const registerPromise = registerGatewayClient(plugin);
584
+ await vi.advanceTimersByTimeAsync(5_999);
585
+ expect(baseRegisterClientSpy).not.toHaveBeenCalled();
586
+ await vi.advanceTimersByTimeAsync(1);
587
+ await registerPromise;
588
+
589
+ expect((plugin as unknown as { gatewayInfo?: { url?: string } }).gatewayInfo?.url).toBe(
590
+ "wss://gateway.discord.gg/",
591
+ );
592
+ });
593
+
594
+ it("rate-limits repeated gateway metadata fallback logs", async () => {
595
+ vi.useFakeTimers();
596
+ const runtime = createRuntime();
597
+ globalFetchMock.mockResolvedValue({
598
+ ok: false,
599
+ status: 503,
600
+ text: async () => "upstream connect error",
601
+ } as Response);
602
+ const firstPlugin = createDiscordGatewayPlugin({
603
+ discordConfig: {},
604
+ runtime,
605
+ });
606
+ const secondPlugin = createDiscordGatewayPlugin({
607
+ discordConfig: {},
608
+ runtime,
609
+ });
610
+
611
+ await registerGatewayClient(firstPlugin);
612
+ await registerGatewayClient(secondPlugin);
613
+ expect(runtime.log).toHaveBeenCalledTimes(1);
614
+
615
+ await vi.advanceTimersByTimeAsync(60_000);
616
+ await registerGatewayClient(
617
+ createDiscordGatewayPlugin({
618
+ discordConfig: {},
619
+ runtime,
620
+ }),
621
+ );
622
+
623
+ expect(runtime.log).toHaveBeenCalledTimes(2);
624
+ });
625
+
626
+ it("sets client reference before the async gateway-info fetch resolves (regression for #52372)", async () => {
627
+ vi.useFakeTimers();
628
+ const runtime = createRuntime();
629
+ let fetchResolve: ((v: Response) => void) | undefined;
630
+ globalFetchMock.mockImplementation(
631
+ () =>
632
+ new Promise<Response>((resolve) => {
633
+ fetchResolve = resolve;
634
+ }),
635
+ );
636
+ const plugin = createDiscordGatewayPlugin({
637
+ discordConfig: {},
638
+ runtime,
639
+ });
640
+
641
+ const clientArg = {
642
+ options: { token: "token-race" },
643
+ registerListener: baseRegisterClientSpy,
644
+ unregisterListener: vi.fn(),
645
+ };
646
+ const registerPromise = (
647
+ plugin as unknown as {
648
+ registerClient: (c: typeof clientArg) => Promise<void>;
649
+ }
650
+ ).registerClient(clientArg);
651
+
652
+ // Before the metadata fetch resolves, this.client should already be set so
653
+ // that a concurrent identify() cannot observe an undefined client.
654
+ expect((plugin as unknown as { client: unknown }).client).toBe(clientArg);
655
+
656
+ resolveGatewayInfoFetch(fetchResolve);
657
+ await registerPromise;
658
+
659
+ expect(baseRegisterClientSpy).toHaveBeenCalledTimes(1);
660
+ });
661
+
662
+ it("skips super.registerClient when an external connect starts during the metadata fetch (regression for #52372)", async () => {
663
+ const runCase = async (markStarted: (plugin: unknown) => void) => {
664
+ vi.useFakeTimers();
665
+ const runtime = createRuntime();
666
+ let fetchResolve: ((v: Response) => void) | undefined;
667
+ globalFetchMock.mockImplementation(
668
+ () =>
669
+ new Promise<Response>((resolve) => {
670
+ fetchResolve = resolve;
671
+ }),
672
+ );
673
+ const plugin = createDiscordGatewayPlugin({
674
+ discordConfig: {},
675
+ runtime,
676
+ });
677
+
678
+ const clientArg = {
679
+ options: { token: "token-race" },
680
+ registerListener: baseRegisterClientSpy,
681
+ unregisterListener: vi.fn(),
682
+ };
683
+ const registerPromise = (
684
+ plugin as unknown as {
685
+ registerClient: (c: typeof clientArg) => Promise<void>;
686
+ }
687
+ ).registerClient(clientArg);
688
+
689
+ markStarted(plugin);
690
+ resolveGatewayInfoFetch(fetchResolve);
691
+ await registerPromise;
692
+ };
693
+
694
+ await runCase((plugin) => {
695
+ (plugin as { ws: unknown }).ws = { readyState: 1 };
696
+ });
697
+ expect(baseRegisterClientSpy).not.toHaveBeenCalled();
698
+
699
+ baseRegisterClientSpy.mockClear();
700
+ globalFetchMock.mockReset();
701
+ vi.useRealTimers();
702
+
703
+ await runCase((plugin) => {
704
+ (plugin as { isConnecting: boolean }).isConnecting = true;
705
+ });
706
+ expect(baseRegisterClientSpy).not.toHaveBeenCalled();
707
+ });
708
+
709
+ it("refreshes fallback gateway metadata on the next register attempt", async () => {
710
+ const runtime = createRuntime();
711
+ globalFetchMock
712
+ .mockResolvedValueOnce({
713
+ ok: false,
714
+ status: 503,
715
+ text: async () =>
716
+ "upstream connect error or disconnect/reset before headers. reset reason: overflow",
717
+ } as Response)
718
+ .mockResolvedValueOnce({
719
+ ok: true,
720
+ status: 200,
721
+ text: async () =>
722
+ createGatewayInfoBody({
723
+ url: "wss://gateway.discord.gg/?v=10",
724
+ shards: 8,
725
+ maxConcurrency: 16,
726
+ }),
727
+ } as Response);
728
+ const plugin = createDiscordGatewayPlugin({
729
+ discordConfig: {},
730
+ runtime,
731
+ });
732
+
733
+ await registerGatewayClient(plugin);
734
+ await registerGatewayClient(plugin);
735
+
736
+ expect(globalFetchMock).toHaveBeenCalledTimes(2);
737
+ expect(baseRegisterClientSpy).toHaveBeenCalledTimes(2);
738
+ expect(
739
+ (plugin as unknown as { gatewayInfo?: { url?: string; shards?: number } }).gatewayInfo,
740
+ ).toMatchObject({
741
+ url: "wss://gateway.discord.gg/?v=10",
742
+ shards: 8,
743
+ });
744
+ });
745
+ });