@openclaw/discord 2026.3.13 → 2026.5.2-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 (502) 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 +3326 -1
  14. package/package.json +68 -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 +306 -0
  34. package/src/actions/handle-action.ts +372 -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 +248 -0
  40. package/src/actions/runtime.messaging.shared.ts +97 -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 +1087 -0
  49. package/src/actions/runtime.ts +87 -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 +275 -0
  65. package/src/channel-actions.ts +203 -0
  66. package/src/channel-api.ts +29 -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 +547 -12
  72. package/src/channel.ts +597 -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 +132 -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 +408 -0
  123. package/src/internal/client.ts +308 -0
  124. package/src/internal/command-deploy.ts +237 -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 +603 -0
  140. package/src/internal/gateway.ts +476 -0
  141. package/src/internal/interaction-dispatch.test.ts +148 -0
  142. package/src/internal/interaction-dispatch.ts +162 -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 +325 -0
  146. package/src/internal/interactions.ts +378 -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 +557 -0
  155. package/src/internal/rest.test.ts +673 -0
  156. package/src/internal/rest.ts +322 -0
  157. package/src/internal/schemas.ts +36 -0
  158. package/src/internal/structures.test.ts +43 -0
  159. package/src/internal/structures.ts +280 -0
  160. package/src/internal/test-builders.test-support.ts +163 -0
  161. package/src/internal/voice.ts +49 -0
  162. package/src/media-detection.ts +28 -0
  163. package/src/mentions.test.ts +111 -0
  164. package/src/mentions.ts +147 -0
  165. package/src/monitor/access-groups.ts +55 -0
  166. package/src/monitor/ack-reactions.ts +70 -0
  167. package/src/monitor/acp-bind-here.integration.test.ts +211 -0
  168. package/src/monitor/agent-components-auth.ts +7 -0
  169. package/src/monitor/agent-components-context.ts +154 -0
  170. package/src/monitor/agent-components-data.ts +224 -0
  171. package/src/monitor/agent-components-dm-auth.ts +221 -0
  172. package/src/monitor/agent-components-guild-auth.ts +322 -0
  173. package/src/monitor/agent-components-helpers.runtime.ts +5 -0
  174. package/src/monitor/agent-components-helpers.ts +34 -0
  175. package/src/monitor/agent-components-reply.ts +10 -0
  176. package/src/monitor/agent-components.deps.runtime.ts +2 -0
  177. package/src/monitor/agent-components.dispatch.ts +366 -0
  178. package/src/monitor/agent-components.handlers.ts +303 -0
  179. package/src/monitor/agent-components.modal.ts +160 -0
  180. package/src/monitor/agent-components.plugin-interactive.ts +187 -0
  181. package/src/monitor/agent-components.runtime.ts +14 -0
  182. package/src/monitor/agent-components.system-controls.ts +211 -0
  183. package/src/monitor/agent-components.ts +70 -0
  184. package/src/monitor/agent-components.types.ts +58 -0
  185. package/src/monitor/agent-components.wildcard-controls.ts +168 -0
  186. package/src/monitor/agent-components.wildcard.test.ts +71 -0
  187. package/src/monitor/allow-list.test.ts +14 -0
  188. package/src/monitor/allow-list.ts +633 -0
  189. package/src/monitor/auto-presence.test.ts +156 -0
  190. package/src/monitor/auto-presence.ts +356 -0
  191. package/src/monitor/channel-access.test.ts +99 -0
  192. package/src/monitor/channel-access.ts +102 -0
  193. package/src/monitor/commands.test.ts +24 -0
  194. package/src/monitor/commands.ts +9 -0
  195. package/src/monitor/dm-command-auth.test.ts +197 -0
  196. package/src/monitor/dm-command-auth.ts +158 -0
  197. package/src/monitor/dm-command-decision.test.ts +113 -0
  198. package/src/monitor/dm-command-decision.ts +49 -0
  199. package/src/monitor/exec-approvals.test.ts +226 -0
  200. package/src/monitor/exec-approvals.ts +158 -0
  201. package/src/monitor/format.ts +45 -0
  202. package/src/monitor/gateway-handle.ts +34 -0
  203. package/src/monitor/gateway-metadata.test.ts +29 -0
  204. package/src/monitor/gateway-metadata.ts +298 -0
  205. package/src/monitor/gateway-plugin.test.ts +297 -0
  206. package/src/monitor/gateway-plugin.ts +294 -0
  207. package/src/monitor/gateway-registry.ts +37 -0
  208. package/src/monitor/gateway-supervisor.test.ts +150 -0
  209. package/src/monitor/gateway-supervisor.ts +206 -0
  210. package/src/monitor/inbound-context.test-helpers.ts +37 -0
  211. package/src/monitor/inbound-context.test.ts +106 -0
  212. package/src/monitor/inbound-context.ts +103 -0
  213. package/src/monitor/inbound-dedupe.ts +79 -0
  214. package/src/monitor/inbound-job.test.ts +203 -0
  215. package/src/monitor/inbound-job.ts +118 -0
  216. package/src/monitor/listeners.queue.ts +91 -0
  217. package/src/monitor/listeners.reactions.ts +610 -0
  218. package/src/monitor/listeners.test.ts +200 -0
  219. package/src/monitor/listeners.ts +150 -0
  220. package/src/monitor/message-channel-info.ts +96 -0
  221. package/src/monitor/message-forwarded.ts +107 -0
  222. package/src/monitor/message-handler.batch-gate.test.ts +22 -0
  223. package/src/monitor/message-handler.batch-gate.ts +19 -0
  224. package/src/monitor/message-handler.bot-self-filter.test.ts +68 -0
  225. package/src/monitor/message-handler.context.ts +406 -0
  226. package/src/monitor/message-handler.dm-preflight.ts +123 -0
  227. package/src/monitor/message-handler.draft-preview.ts +246 -0
  228. package/src/monitor/message-handler.hydration.test.ts +80 -0
  229. package/src/monitor/message-handler.hydration.ts +198 -0
  230. package/src/monitor/message-handler.inbound-context.test.ts +59 -0
  231. package/src/monitor/message-handler.module-test-helpers.ts +31 -0
  232. package/src/monitor/message-handler.preflight-channel-access.ts +86 -0
  233. package/src/monitor/message-handler.preflight-channel-context.test.ts +18 -0
  234. package/src/monitor/message-handler.preflight-channel-context.ts +58 -0
  235. package/src/monitor/message-handler.preflight-context.ts +54 -0
  236. package/src/monitor/message-handler.preflight-helpers.ts +164 -0
  237. package/src/monitor/message-handler.preflight-history.ts +23 -0
  238. package/src/monitor/message-handler.preflight-logging.ts +36 -0
  239. package/src/monitor/message-handler.preflight-pluralkit.ts +26 -0
  240. package/src/monitor/message-handler.preflight-runtime.ts +28 -0
  241. package/src/monitor/message-handler.preflight-thread.ts +49 -0
  242. package/src/monitor/message-handler.preflight.acp-bindings.test.ts +369 -0
  243. package/src/monitor/message-handler.preflight.test-helpers.ts +111 -0
  244. package/src/monitor/message-handler.preflight.test.ts +1623 -0
  245. package/src/monitor/message-handler.preflight.ts +679 -0
  246. package/src/monitor/message-handler.preflight.types.ts +110 -0
  247. package/src/monitor/message-handler.process.test.ts +1369 -0
  248. package/src/monitor/message-handler.process.ts +686 -0
  249. package/src/monitor/message-handler.queue.test.ts +496 -0
  250. package/src/monitor/message-handler.routing-preflight.ts +112 -0
  251. package/src/monitor/message-handler.test-harness.ts +99 -0
  252. package/src/monitor/message-handler.test-helpers.ts +75 -0
  253. package/src/monitor/message-handler.ts +274 -0
  254. package/src/monitor/message-media.ts +509 -0
  255. package/src/monitor/message-run-queue.ts +101 -0
  256. package/src/monitor/message-text.ts +171 -0
  257. package/src/monitor/message-utils.test.ts +1157 -0
  258. package/src/monitor/message-utils.ts +32 -0
  259. package/src/monitor/model-picker-preferences.test.ts +67 -0
  260. package/src/monitor/model-picker-preferences.ts +184 -0
  261. package/src/monitor/model-picker.state.ts +364 -0
  262. package/src/monitor/model-picker.test-utils.ts +26 -0
  263. package/src/monitor/model-picker.test.ts +794 -0
  264. package/src/monitor/model-picker.ts +38 -0
  265. package/src/monitor/model-picker.view.ts +695 -0
  266. package/src/monitor/monitor.agent-components.test.ts +375 -0
  267. package/src/monitor/monitor.test.ts +849 -0
  268. package/src/monitor/monitor.threading-utils.test.ts +598 -0
  269. package/src/monitor/native-command-agent-reply.ts +125 -0
  270. package/src/monitor/native-command-arg-ui.ts +233 -0
  271. package/src/monitor/native-command-auth.ts +308 -0
  272. package/src/monitor/native-command-bypass.ts +13 -0
  273. package/src/monitor/native-command-context.test.ts +98 -0
  274. package/src/monitor/native-command-context.ts +103 -0
  275. package/src/monitor/native-command-dispatch.ts +35 -0
  276. package/src/monitor/native-command-model-picker-apply.ts +177 -0
  277. package/src/monitor/native-command-model-picker-interaction.ts +461 -0
  278. package/src/monitor/native-command-model-picker-ui.ts +368 -0
  279. package/src/monitor/native-command-reply.test.ts +68 -0
  280. package/src/monitor/native-command-reply.ts +185 -0
  281. package/src/monitor/native-command-route.ts +91 -0
  282. package/src/monitor/native-command-status.ts +76 -0
  283. package/src/monitor/native-command-ui.ts +26 -0
  284. package/src/monitor/native-command-ui.types.ts +20 -0
  285. package/src/monitor/native-command.args.ts +45 -0
  286. package/src/monitor/native-command.command-arg.test.ts +99 -0
  287. package/src/monitor/native-command.commands-allowfrom.test.ts +490 -0
  288. package/src/monitor/native-command.model-picker.test.ts +767 -0
  289. package/src/monitor/native-command.options.test.ts +369 -0
  290. package/src/monitor/native-command.options.ts +153 -0
  291. package/src/monitor/native-command.plugin-dispatch.test.ts +961 -0
  292. package/src/monitor/native-command.runtime.ts +50 -0
  293. package/src/monitor/native-command.status-direct.test.ts +272 -0
  294. package/src/monitor/native-command.test-helpers.ts +64 -0
  295. package/src/monitor/native-command.think-autocomplete.test.ts +416 -0
  296. package/src/monitor/native-command.ts +700 -0
  297. package/src/monitor/native-command.types.ts +9 -0
  298. package/src/monitor/native-interaction-channel-context.ts +50 -0
  299. package/src/monitor/preflight-audio.runtime.ts +9 -0
  300. package/src/monitor/preflight-audio.test.ts +157 -0
  301. package/src/monitor/preflight-audio.ts +130 -0
  302. package/src/monitor/presence-cache.ts +61 -0
  303. package/src/monitor/presence.test.ts +44 -0
  304. package/src/monitor/presence.ts +50 -0
  305. package/src/monitor/provider-session.runtime.ts +12 -0
  306. package/src/monitor/provider.acp.ts +89 -0
  307. package/src/monitor/provider.allowlist.test.ts +149 -0
  308. package/src/monitor/provider.allowlist.ts +394 -0
  309. package/src/monitor/provider.cleanup.ts +41 -0
  310. package/src/monitor/provider.commands.ts +129 -0
  311. package/src/monitor/provider.config-log.ts +45 -0
  312. package/src/monitor/provider.deploy-errors.ts +362 -0
  313. package/src/monitor/provider.deploy.ts +221 -0
  314. package/src/monitor/provider.interactions.ts +160 -0
  315. package/src/monitor/provider.lifecycle.test.ts +713 -0
  316. package/src/monitor/provider.lifecycle.ts +552 -0
  317. package/src/monitor/provider.proxy.test.ts +745 -0
  318. package/src/monitor/provider.rest-proxy.test.ts +121 -0
  319. package/src/monitor/provider.runtime.ts +1 -0
  320. package/src/monitor/provider.skill-dedupe.test.ts +42 -0
  321. package/src/monitor/provider.startup-log.ts +32 -0
  322. package/src/monitor/provider.startup.test.ts +426 -0
  323. package/src/monitor/provider.startup.ts +323 -0
  324. package/src/monitor/provider.test.ts +1111 -0
  325. package/src/monitor/provider.ts +713 -0
  326. package/src/monitor/reply-context.ts +64 -0
  327. package/src/monitor/reply-delivery.test.ts +244 -0
  328. package/src/monitor/reply-delivery.ts +203 -0
  329. package/src/monitor/rest-fetch.ts +43 -0
  330. package/src/monitor/route-resolution.test.ts +204 -0
  331. package/src/monitor/route-resolution.ts +140 -0
  332. package/src/monitor/sender-identity.ts +81 -0
  333. package/src/monitor/startup-status.test.ts +30 -0
  334. package/src/monitor/startup-status.ts +10 -0
  335. package/src/monitor/status.ts +22 -0
  336. package/src/monitor/system-events.ts +55 -0
  337. package/src/monitor/thread-bindings.config.ts +35 -0
  338. package/src/monitor/thread-bindings.discord-api.test.ts +229 -0
  339. package/src/monitor/thread-bindings.discord-api.ts +310 -0
  340. package/src/monitor/thread-bindings.lifecycle.test.ts +1871 -0
  341. package/src/monitor/thread-bindings.lifecycle.ts +354 -0
  342. package/src/monitor/thread-bindings.manager.ts +553 -0
  343. package/src/monitor/thread-bindings.messages.ts +6 -0
  344. package/src/monitor/thread-bindings.persona.test.ts +34 -0
  345. package/src/monitor/thread-bindings.persona.ts +25 -0
  346. package/src/monitor/thread-bindings.session-adapter.ts +229 -0
  347. package/src/monitor/thread-bindings.session-shared.ts +59 -0
  348. package/src/monitor/thread-bindings.session-updates.ts +35 -0
  349. package/src/monitor/thread-bindings.shared-state.test.ts +36 -0
  350. package/src/monitor/thread-bindings.state.ts +540 -0
  351. package/src/monitor/thread-bindings.ts +48 -0
  352. package/src/monitor/thread-bindings.types.ts +83 -0
  353. package/src/monitor/thread-channel-context.ts +112 -0
  354. package/src/monitor/thread-session-close.test.ts +180 -0
  355. package/src/monitor/thread-session-close.ts +63 -0
  356. package/src/monitor/thread-title.generate.test.ts +197 -0
  357. package/src/monitor/thread-title.test.ts +31 -0
  358. package/src/monitor/thread-title.ts +181 -0
  359. package/src/monitor/threading.auto-thread.test.ts +327 -0
  360. package/src/monitor/threading.auto-thread.ts +287 -0
  361. package/src/monitor/threading.cache.ts +45 -0
  362. package/src/monitor/threading.parent-info.test.ts +156 -0
  363. package/src/monitor/threading.starter.test.ts +260 -0
  364. package/src/monitor/threading.starter.ts +287 -0
  365. package/src/monitor/threading.ts +20 -0
  366. package/src/monitor/threading.types.ts +102 -0
  367. package/src/monitor/timeouts.ts +84 -0
  368. package/src/monitor/typing.test.ts +42 -0
  369. package/src/monitor/typing.ts +17 -0
  370. package/src/monitor.gateway.test.ts +187 -0
  371. package/src/monitor.gateway.ts +75 -0
  372. package/src/monitor.test.ts +1397 -0
  373. package/src/monitor.ts +28 -0
  374. package/src/normalize.test.ts +56 -0
  375. package/src/normalize.ts +86 -0
  376. package/src/outbound-adapter.interactive-order.test.ts +64 -0
  377. package/src/outbound-adapter.test-harness.ts +207 -0
  378. package/src/outbound-adapter.test.ts +696 -0
  379. package/src/outbound-adapter.ts +291 -0
  380. package/src/outbound-approval.ts +29 -0
  381. package/src/outbound-components.ts +81 -0
  382. package/src/outbound-payload.contract.test.ts +38 -0
  383. package/src/outbound-payload.ts +134 -0
  384. package/src/outbound-send-context.ts +92 -0
  385. package/src/outbound-session-route.test.ts +34 -0
  386. package/src/outbound-session-route.ts +72 -0
  387. package/src/pluralkit.test.ts +67 -0
  388. package/src/pluralkit.ts +58 -0
  389. package/src/preview-streaming.ts +32 -0
  390. package/src/probe.intents.test.ts +94 -0
  391. package/src/probe.parse-token.test.ts +43 -0
  392. package/src/probe.runtime.ts +1 -0
  393. package/src/probe.ts +237 -0
  394. package/src/proxy-fetch.ts +92 -0
  395. package/src/proxy-request-client.test.ts +78 -0
  396. package/src/proxy-request-client.ts +21 -0
  397. package/src/recipient-resolution.ts +39 -0
  398. package/src/resolve-allowlist-common.test.ts +36 -0
  399. package/src/resolve-allowlist-common.ts +39 -0
  400. package/src/resolve-channels.test.ts +340 -0
  401. package/src/resolve-channels.ts +369 -0
  402. package/src/resolve-users.test.ts +222 -0
  403. package/src/resolve-users.ts +184 -0
  404. package/src/retry.test.ts +83 -0
  405. package/src/retry.ts +98 -0
  406. package/src/runtime-api.ts +64 -0
  407. package/src/runtime.ts +22 -5
  408. package/src/secret-config-contract.ts +140 -0
  409. package/src/security-audit.runtime.ts +1 -0
  410. package/src/security-audit.test.ts +246 -0
  411. package/src/security-audit.ts +208 -0
  412. package/src/security-contract.ts +47 -0
  413. package/src/security-doctor.test.ts +25 -0
  414. package/src/security-doctor.ts +20 -0
  415. package/src/security.ts +60 -0
  416. package/src/send-target-parsing.ts +14 -0
  417. package/src/send.channels.ts +139 -0
  418. package/src/send.components.test.ts +275 -0
  419. package/src/send.components.ts +381 -0
  420. package/src/send.creates-thread.test.ts +643 -0
  421. package/src/send.emojis-stickers.ts +57 -0
  422. package/src/send.guild.ts +170 -0
  423. package/src/send.message-request.ts +97 -0
  424. package/src/send.messages.test.ts +53 -0
  425. package/src/send.messages.ts +225 -0
  426. package/src/send.outbound.ts +413 -0
  427. package/src/send.permissions.authz.test.ts +188 -0
  428. package/src/send.permissions.ts +283 -0
  429. package/src/send.reactions.ts +155 -0
  430. package/src/send.sends-basic-channel-messages.test.ts +941 -0
  431. package/src/send.shared.ts +447 -0
  432. package/src/send.test-harness.ts +56 -0
  433. package/src/send.ts +82 -0
  434. package/src/send.types.ts +188 -0
  435. package/src/send.typing.test.ts +41 -0
  436. package/src/send.typing.ts +9 -0
  437. package/src/send.voice.ts +134 -0
  438. package/src/send.webhook-activity.test.ts +105 -0
  439. package/src/send.webhook.proxy.test.ts +191 -0
  440. package/src/send.webhook.ts +133 -0
  441. package/src/session-contract.ts +3 -0
  442. package/src/session-key-normalization.test.ts +44 -0
  443. package/src/session-key-normalization.ts +47 -0
  444. package/src/setup-account-state.test.ts +91 -0
  445. package/src/setup-account-state.ts +144 -0
  446. package/src/setup-adapter.ts +12 -0
  447. package/src/setup-core.ts +212 -0
  448. package/src/setup-runtime-helpers.ts +10 -0
  449. package/src/setup-surface.test.ts +137 -0
  450. package/src/setup-surface.ts +129 -0
  451. package/src/shared-interactive.test.ts +153 -0
  452. package/src/shared-interactive.ts +124 -0
  453. package/src/shared.test.ts +165 -0
  454. package/src/shared.ts +190 -0
  455. package/src/status-issues.test.ts +70 -0
  456. package/src/status-issues.ts +169 -0
  457. package/src/subagent-hooks.test.ts +130 -81
  458. package/src/subagent-hooks.ts +184 -122
  459. package/src/target-parsing.ts +53 -0
  460. package/src/target-resolver.ts +129 -0
  461. package/src/targets.test.ts +367 -0
  462. package/src/targets.ts +12 -0
  463. package/src/test-http-helpers.ts +10 -0
  464. package/src/test-support/component-runtime.ts +190 -0
  465. package/src/test-support/config.ts +7 -0
  466. package/src/test-support/configured-binding-runtime.ts +29 -0
  467. package/src/test-support/partial-channel.ts +26 -0
  468. package/src/test-support/provider.test-support.ts +545 -0
  469. package/src/token.test.ts +107 -0
  470. package/src/token.ts +60 -0
  471. package/src/ui-colors.ts +27 -0
  472. package/src/ui.ts +20 -0
  473. package/src/voice/access.test.ts +217 -0
  474. package/src/voice/access.ts +124 -0
  475. package/src/voice/audio.ts +173 -0
  476. package/src/voice/capture-state.test.ts +48 -0
  477. package/src/voice/capture-state.ts +120 -0
  478. package/src/voice/command.test.ts +164 -0
  479. package/src/voice/command.ts +283 -0
  480. package/src/voice/config.ts +8 -0
  481. package/src/voice/manager.e2e.test.ts +928 -0
  482. package/src/voice/manager.ready-listener.test.ts +37 -0
  483. package/src/voice/manager.runtime.ts +11 -0
  484. package/src/voice/manager.ts +691 -0
  485. package/src/voice/prompt.test.ts +16 -0
  486. package/src/voice/prompt.ts +17 -0
  487. package/src/voice/receive-recovery.test.ts +79 -0
  488. package/src/voice/receive-recovery.ts +159 -0
  489. package/src/voice/sanitize.test.ts +34 -0
  490. package/src/voice/sanitize.ts +32 -0
  491. package/src/voice/sdk-runtime.ts +14 -0
  492. package/src/voice/segment.ts +156 -0
  493. package/src/voice/session.ts +50 -0
  494. package/src/voice/speaker-context.ts +127 -0
  495. package/src/voice/tts.ts +125 -0
  496. package/src/voice-message.test.ts +234 -0
  497. package/src/voice-message.ts +444 -0
  498. package/subagent-hooks-api.ts +27 -0
  499. package/test-api.ts +4 -0
  500. package/thread-binding-api.ts +1 -0
  501. package/timeouts.ts +6 -0
  502. package/tsconfig.json +16 -0
@@ -0,0 +1,246 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { ResolvedDiscordAccount } from "./accounts.js";
3
+ import type { OpenClawConfig } from "./runtime-api.js";
4
+ import { collectDiscordSecurityAuditFindings } from "./security-audit.js";
5
+
6
+ type DiscordAccountConfig = ResolvedDiscordAccount["config"];
7
+
8
+ const { readChannelAllowFromStoreMock } = vi.hoisted(() => ({
9
+ readChannelAllowFromStoreMock: vi.fn(async () => [] as string[]),
10
+ }));
11
+
12
+ vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({
13
+ readChannelAllowFromStore: readChannelAllowFromStoreMock,
14
+ }));
15
+
16
+ function createAccount(
17
+ config: DiscordAccountConfig,
18
+ accountId = "default",
19
+ ): ResolvedDiscordAccount {
20
+ return {
21
+ accountId,
22
+ enabled: true,
23
+ token: "t",
24
+ tokenSource: "config",
25
+ config,
26
+ };
27
+ }
28
+
29
+ async function collectFindings(params: {
30
+ cfg: OpenClawConfig;
31
+ config: DiscordAccountConfig;
32
+ accountId?: string;
33
+ orderedAccountIds?: string[];
34
+ hasExplicitAccountPath?: boolean;
35
+ storeAllowFrom?: string[];
36
+ }) {
37
+ readChannelAllowFromStoreMock.mockResolvedValue(params.storeAllowFrom ?? []);
38
+ return await collectDiscordSecurityAuditFindings({
39
+ cfg: params.cfg,
40
+ account: createAccount(params.config, params.accountId),
41
+ accountId: params.accountId ?? "default",
42
+ orderedAccountIds: params.orderedAccountIds ?? ["default"],
43
+ hasExplicitAccountPath: params.hasExplicitAccountPath ?? false,
44
+ });
45
+ }
46
+
47
+ describe("Discord security audit findings", () => {
48
+ it("flags slash commands when access-group enforcement is disabled and no users allowlist exists", async () => {
49
+ const cfg: OpenClawConfig = {
50
+ commands: { native: true, useAccessGroups: false },
51
+ channels: {
52
+ discord: {
53
+ enabled: true,
54
+ token: "t",
55
+ groupPolicy: "allowlist",
56
+ guilds: {
57
+ "123": {
58
+ channels: {
59
+ general: { enabled: true },
60
+ },
61
+ },
62
+ },
63
+ },
64
+ },
65
+ };
66
+
67
+ const discordConfig = cfg.channels?.discord;
68
+ if (!discordConfig) {
69
+ throw new Error("discord config required");
70
+ }
71
+ const findings = await collectFindings({
72
+ cfg,
73
+ config: discordConfig,
74
+ });
75
+
76
+ expect(findings).toEqual(
77
+ expect.arrayContaining([
78
+ expect.objectContaining({
79
+ checkId: "channels.discord.commands.native.unrestricted",
80
+ severity: "critical",
81
+ }),
82
+ ]),
83
+ );
84
+ });
85
+
86
+ it.each([
87
+ {
88
+ name: "flags missing guild user allowlists",
89
+ cfg: {
90
+ commands: { native: true },
91
+ channels: {
92
+ discord: {
93
+ enabled: true,
94
+ token: "t",
95
+ groupPolicy: "allowlist",
96
+ guilds: {
97
+ "123": {
98
+ channels: {
99
+ general: { enabled: true },
100
+ },
101
+ },
102
+ },
103
+ },
104
+ },
105
+ } satisfies OpenClawConfig,
106
+ expectFinding: true,
107
+ },
108
+ {
109
+ name: "does not flag when dm.allowFrom includes a Discord snowflake id",
110
+ cfg: {
111
+ commands: { native: true },
112
+ channels: {
113
+ discord: {
114
+ enabled: true,
115
+ token: "t",
116
+ dm: { allowFrom: ["387380367612706819"] },
117
+ groupPolicy: "allowlist",
118
+ guilds: {
119
+ "123": {
120
+ channels: {
121
+ general: { enabled: true },
122
+ },
123
+ },
124
+ },
125
+ },
126
+ },
127
+ } satisfies OpenClawConfig,
128
+ expectFinding: false,
129
+ },
130
+ ])("$name", async (testCase) => {
131
+ const findings = await collectFindings({
132
+ cfg: testCase.cfg,
133
+ config: testCase.cfg.channels.discord,
134
+ });
135
+
136
+ expect(
137
+ findings.some(
138
+ (finding) => finding.checkId === "channels.discord.commands.native.no_allowlists",
139
+ ),
140
+ ).toBe(testCase.expectFinding);
141
+ });
142
+
143
+ it.each([
144
+ {
145
+ name: "warns when Discord allowlists contain name-based entries",
146
+ config: {
147
+ enabled: true,
148
+ token: "t",
149
+ allowFrom: ["Alice#1234", "<@123456789012345678>"],
150
+ guilds: {
151
+ "123": {
152
+ users: ["trusted.operator"],
153
+ channels: {
154
+ general: {
155
+ users: ["987654321098765432", "security-team"],
156
+ },
157
+ },
158
+ },
159
+ },
160
+ } satisfies DiscordAccountConfig,
161
+ storeAllowFrom: ["team.owner"],
162
+ expectNameBasedSeverity: "warn",
163
+ detailIncludes: [
164
+ "channels.discord.allowFrom:Alice#1234",
165
+ "channels.discord.guilds.123.users:trusted.operator",
166
+ "channels.discord.guilds.123.channels.general.users:security-team",
167
+ "~/.openclaw/credentials/discord-allowFrom.json:team.owner",
168
+ ],
169
+ detailExcludes: ["<@123456789012345678>"],
170
+ },
171
+ {
172
+ name: "marks Discord name-based allowlists as break-glass when dangerous matching is enabled",
173
+ config: {
174
+ enabled: true,
175
+ token: "t",
176
+ dangerouslyAllowNameMatching: true,
177
+ allowFrom: ["Alice#1234"],
178
+ } satisfies DiscordAccountConfig,
179
+ expectNameBasedSeverity: "info",
180
+ detailIncludes: ["out-of-scope"],
181
+ },
182
+ {
183
+ name: "audits name-based allowlists on non-default Discord accounts",
184
+ accountId: "beta",
185
+ orderedAccountIds: ["alpha", "beta"],
186
+ hasExplicitAccountPath: true,
187
+ config: {
188
+ enabled: true,
189
+ token: "b",
190
+ allowFrom: ["Alice#1234"],
191
+ } satisfies DiscordAccountConfig,
192
+ expectNameBasedSeverity: "warn",
193
+ detailIncludes: ["channels.discord.accounts.beta.allowFrom:Alice#1234"],
194
+ },
195
+ {
196
+ name: "does not warn when Discord allowlists use ID-style entries only",
197
+ config: {
198
+ enabled: true,
199
+ token: "t",
200
+ allowFrom: [
201
+ "123456789012345678",
202
+ "<@223456789012345678>",
203
+ "user:323456789012345678",
204
+ "discord:423456789012345678",
205
+ "pk:member-123",
206
+ ],
207
+ guilds: {
208
+ "123": {
209
+ users: ["523456789012345678", "<@623456789012345678>", "pk:member-456"],
210
+ channels: {
211
+ general: {
212
+ users: ["723456789012345678", "user:823456789012345678"],
213
+ },
214
+ },
215
+ },
216
+ },
217
+ } satisfies DiscordAccountConfig,
218
+ expectNoNameBasedFinding: true,
219
+ },
220
+ ])("$name", async (testCase) => {
221
+ const findings = await collectFindings({
222
+ cfg: { channels: { discord: testCase.config } },
223
+ config: testCase.config,
224
+ accountId: testCase.accountId,
225
+ orderedAccountIds: testCase.orderedAccountIds,
226
+ hasExplicitAccountPath: testCase.hasExplicitAccountPath,
227
+ storeAllowFrom: testCase.storeAllowFrom,
228
+ });
229
+ const nameBasedFinding = findings.find(
230
+ (entry) => entry.checkId === "channels.discord.allowFrom.name_based_entries",
231
+ );
232
+
233
+ if (testCase.expectNoNameBasedFinding) {
234
+ expect(nameBasedFinding).toBeUndefined();
235
+ } else {
236
+ expect(nameBasedFinding).toBeDefined();
237
+ expect(nameBasedFinding?.severity).toBe(testCase.expectNameBasedSeverity);
238
+ for (const snippet of testCase.detailIncludes ?? []) {
239
+ expect(nameBasedFinding?.detail).toContain(snippet);
240
+ }
241
+ for (const snippet of testCase.detailExcludes ?? []) {
242
+ expect(nameBasedFinding?.detail).not.toContain(snippet);
243
+ }
244
+ }
245
+ });
246
+ });
@@ -0,0 +1,208 @@
1
+ import { coerceNativeSetting, normalizeAllowFromList } from "openclaw/plugin-sdk/channel-policy";
2
+ import { readChannelAllowFromStore } from "openclaw/plugin-sdk/conversation-runtime";
3
+ import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
4
+ import {
5
+ resolveNativeCommandsEnabled,
6
+ resolveNativeSkillsEnabled,
7
+ } from "openclaw/plugin-sdk/native-command-config-runtime";
8
+ import type { ResolvedDiscordAccount } from "./accounts.js";
9
+ import type { OpenClawConfig } from "./runtime-api.js";
10
+ import { isDiscordMutableAllowEntry } from "./security-doctor.js";
11
+
12
+ function normalizeOptionalString(value: string | null | undefined): string | undefined {
13
+ const normalized = value?.trim();
14
+ return normalized ? normalized : undefined;
15
+ }
16
+
17
+ function addDiscordNameBasedEntries(params: {
18
+ target: Set<string>;
19
+ values: unknown;
20
+ source: string;
21
+ }) {
22
+ if (!Array.isArray(params.values)) {
23
+ return;
24
+ }
25
+ for (const value of params.values) {
26
+ if (!isDiscordMutableAllowEntry(String(value))) {
27
+ continue;
28
+ }
29
+ const text = normalizeOptionalString(String(value)) ?? "";
30
+ if (!text) {
31
+ continue;
32
+ }
33
+ params.target.add(`${params.source}:${text}`);
34
+ }
35
+ }
36
+
37
+ export async function collectDiscordSecurityAuditFindings(params: {
38
+ cfg: OpenClawConfig;
39
+ accountId?: string | null;
40
+ account: ResolvedDiscordAccount;
41
+ orderedAccountIds: string[];
42
+ hasExplicitAccountPath: boolean;
43
+ }) {
44
+ const findings: Array<{
45
+ checkId: string;
46
+ severity: "info" | "warn" | "critical";
47
+ title: string;
48
+ detail: string;
49
+ remediation?: string;
50
+ }> = [];
51
+ const discordCfg = params.account.config ?? {};
52
+ const accountId =
53
+ normalizeOptionalString(params.accountId) ?? params.account.accountId ?? "default";
54
+ const dangerousNameMatchingEnabled = isDangerousNameMatchingEnabled(discordCfg);
55
+ const storeAllowFrom = await readChannelAllowFromStore("discord", process.env, accountId).catch(
56
+ () => [],
57
+ );
58
+ const discordNameBasedAllowEntries = new Set<string>();
59
+ const discordPathPrefix =
60
+ params.orderedAccountIds.length > 1 || params.hasExplicitAccountPath
61
+ ? `channels.discord.accounts.${accountId}`
62
+ : "channels.discord";
63
+
64
+ addDiscordNameBasedEntries({
65
+ target: discordNameBasedAllowEntries,
66
+ values: discordCfg.allowFrom,
67
+ source: `${discordPathPrefix}.allowFrom`,
68
+ });
69
+ addDiscordNameBasedEntries({
70
+ target: discordNameBasedAllowEntries,
71
+ values: (discordCfg.dm as { allowFrom?: unknown } | undefined)?.allowFrom,
72
+ source: `${discordPathPrefix}.dm.allowFrom`,
73
+ });
74
+ addDiscordNameBasedEntries({
75
+ target: discordNameBasedAllowEntries,
76
+ values: storeAllowFrom,
77
+ source: "~/.openclaw/credentials/discord-allowFrom.json",
78
+ });
79
+
80
+ const guildEntries = (discordCfg.guilds as Record<string, unknown> | undefined) ?? {};
81
+ for (const [guildKey, guildValue] of Object.entries(guildEntries)) {
82
+ if (!guildValue || typeof guildValue !== "object") {
83
+ continue;
84
+ }
85
+ const guild = guildValue as Record<string, unknown>;
86
+ addDiscordNameBasedEntries({
87
+ target: discordNameBasedAllowEntries,
88
+ values: guild.users,
89
+ source: `${discordPathPrefix}.guilds.${guildKey}.users`,
90
+ });
91
+ const channels = guild.channels;
92
+ if (!channels || typeof channels !== "object") {
93
+ continue;
94
+ }
95
+ for (const [channelKey, channelValue] of Object.entries(channels as Record<string, unknown>)) {
96
+ if (!channelValue || typeof channelValue !== "object") {
97
+ continue;
98
+ }
99
+ const channel = channelValue as Record<string, unknown>;
100
+ addDiscordNameBasedEntries({
101
+ target: discordNameBasedAllowEntries,
102
+ values: channel.users,
103
+ source: `${discordPathPrefix}.guilds.${guildKey}.channels.${channelKey}.users`,
104
+ });
105
+ }
106
+ }
107
+
108
+ if (discordNameBasedAllowEntries.size > 0) {
109
+ const examples = Array.from(discordNameBasedAllowEntries).slice(0, 5);
110
+ const more =
111
+ discordNameBasedAllowEntries.size > examples.length
112
+ ? ` (+${discordNameBasedAllowEntries.size - examples.length} more)`
113
+ : "";
114
+ findings.push({
115
+ checkId: "channels.discord.allowFrom.name_based_entries",
116
+ severity: dangerousNameMatchingEnabled ? "info" : "warn",
117
+ title: dangerousNameMatchingEnabled
118
+ ? "Discord allowlist uses break-glass name/tag matching"
119
+ : "Discord allowlist contains name or tag entries",
120
+ detail: dangerousNameMatchingEnabled
121
+ ? "Discord name/tag allowlist matching is explicitly enabled via dangerouslyAllowNameMatching. This mutable-identity mode is operator-selected break-glass behavior and out-of-scope for vulnerability reports by itself. " +
122
+ `Found: ${examples.join(", ")}${more}.`
123
+ : "Discord name/tag allowlist matching uses normalized slugs and can collide across users. " +
124
+ `Found: ${examples.join(", ")}${more}.`,
125
+ remediation: dangerousNameMatchingEnabled
126
+ ? "Prefer stable Discord IDs (or <@id>/user:<id>/pk:<id>), then disable dangerouslyAllowNameMatching."
127
+ : "Prefer stable Discord IDs (or <@id>/user:<id>/pk:<id>) in channels.discord.allowFrom and channels.discord.guilds.*.users, or explicitly opt in with dangerouslyAllowNameMatching=true if you accept the risk.",
128
+ });
129
+ }
130
+
131
+ const nativeEnabled = resolveNativeCommandsEnabled({
132
+ providerId: "discord",
133
+ providerSetting: coerceNativeSetting(
134
+ (discordCfg.commands as { native?: unknown } | undefined)?.native,
135
+ ),
136
+ globalSetting: params.cfg.commands?.native,
137
+ });
138
+ const nativeSkillsEnabled = resolveNativeSkillsEnabled({
139
+ providerId: "discord",
140
+ providerSetting: coerceNativeSetting(
141
+ (discordCfg.commands as { nativeSkills?: unknown } | undefined)?.nativeSkills,
142
+ ),
143
+ globalSetting: params.cfg.commands?.nativeSkills,
144
+ });
145
+ if (!nativeEnabled && !nativeSkillsEnabled) {
146
+ return findings;
147
+ }
148
+
149
+ const defaultGroupPolicy = params.cfg.channels?.defaults?.groupPolicy;
150
+ const groupPolicy =
151
+ (discordCfg.groupPolicy as string | undefined) ?? defaultGroupPolicy ?? "allowlist";
152
+ const guildsConfigured = Object.keys(guildEntries).length > 0;
153
+ const hasAnyUserAllowlist = Object.values(guildEntries).some((guild) => {
154
+ if (!guild || typeof guild !== "object") {
155
+ return false;
156
+ }
157
+ const record = guild as Record<string, unknown>;
158
+ if (Array.isArray(record.users) && record.users.length > 0) {
159
+ return true;
160
+ }
161
+ const channels = record.channels;
162
+ if (!channels || typeof channels !== "object") {
163
+ return false;
164
+ }
165
+ return Object.values(channels as Record<string, unknown>).some((channel) => {
166
+ if (!channel || typeof channel !== "object") {
167
+ return false;
168
+ }
169
+ const channelRecord = channel as Record<string, unknown>;
170
+ return Array.isArray(channelRecord.users) && channelRecord.users.length > 0;
171
+ });
172
+ });
173
+ const dmAllowFromRaw = (discordCfg.dm as { allowFrom?: unknown } | undefined)?.allowFrom;
174
+ const dmAllowFrom = Array.isArray(dmAllowFromRaw) ? dmAllowFromRaw : [];
175
+ const ownerAllowFromConfigured =
176
+ normalizeAllowFromList([...dmAllowFrom, ...storeAllowFrom]).length > 0;
177
+ const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
178
+
179
+ if (!useAccessGroups && groupPolicy !== "disabled" && guildsConfigured && !hasAnyUserAllowlist) {
180
+ findings.push({
181
+ checkId: "channels.discord.commands.native.unrestricted",
182
+ severity: "critical",
183
+ title: "Discord slash commands are unrestricted",
184
+ detail:
185
+ "commands.useAccessGroups=false disables sender allowlists for Discord slash commands unless a per-guild/channel users allowlist is configured; with no users allowlist, any user in allowed guild channels can invoke /… commands.",
186
+ remediation:
187
+ "Set commands.useAccessGroups=true (recommended), or configure channels.discord.guilds.<id>.users (or channels.discord.guilds.<id>.channels.<channel>.users).",
188
+ });
189
+ } else if (
190
+ useAccessGroups &&
191
+ groupPolicy !== "disabled" &&
192
+ guildsConfigured &&
193
+ !ownerAllowFromConfigured &&
194
+ !hasAnyUserAllowlist
195
+ ) {
196
+ findings.push({
197
+ checkId: "channels.discord.commands.native.no_allowlists",
198
+ severity: "warn",
199
+ title: "Discord slash commands have no allowlists",
200
+ detail:
201
+ "Discord slash commands are enabled, but neither an owner allowFrom list nor any per-guild/channel users allowlist is configured; /… commands will be rejected for everyone.",
202
+ remediation:
203
+ "Add your user id to channels.discord.allowFrom (or approve yourself via pairing), or configure channels.discord.guilds.<id>.users.",
204
+ });
205
+ }
206
+
207
+ return findings;
208
+ }
@@ -0,0 +1,47 @@
1
+ import { isRecord } from "openclaw/plugin-sdk/text-runtime";
2
+
3
+ type UnsupportedSecretRefConfigCandidate = {
4
+ path: string;
5
+ value: unknown;
6
+ };
7
+
8
+ export const unsupportedSecretRefSurfacePatterns = [
9
+ "channels.discord.threadBindings.webhookToken",
10
+ "channels.discord.accounts.*.threadBindings.webhookToken",
11
+ ] as const;
12
+
13
+ export function collectUnsupportedSecretRefConfigCandidates(
14
+ raw: unknown,
15
+ ): UnsupportedSecretRefConfigCandidate[] {
16
+ if (!isRecord(raw)) {
17
+ return [];
18
+ }
19
+ if (!isRecord(raw.channels) || !isRecord(raw.channels.discord)) {
20
+ return [];
21
+ }
22
+
23
+ const candidates: UnsupportedSecretRefConfigCandidate[] = [];
24
+ const discord = raw.channels.discord;
25
+ const threadBindings = isRecord(discord.threadBindings) ? discord.threadBindings : null;
26
+ if (threadBindings) {
27
+ candidates.push({
28
+ path: "channels.discord.threadBindings.webhookToken",
29
+ value: threadBindings.webhookToken,
30
+ });
31
+ }
32
+
33
+ const accounts = isRecord(discord.accounts) ? discord.accounts : null;
34
+ if (!accounts) {
35
+ return candidates;
36
+ }
37
+ for (const [accountId, account] of Object.entries(accounts)) {
38
+ if (!isRecord(account) || !isRecord(account.threadBindings)) {
39
+ continue;
40
+ }
41
+ candidates.push({
42
+ path: `channels.discord.accounts.${accountId}.threadBindings.webhookToken`,
43
+ value: account.threadBindings.webhookToken,
44
+ });
45
+ }
46
+ return candidates;
47
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isDiscordMutableAllowEntry } from "./security-doctor.js";
3
+
4
+ describe("discord security doctor helpers", () => {
5
+ it("rejects stable ids and wildcard forms", () => {
6
+ expect(isDiscordMutableAllowEntry("*")).toBe(false);
7
+ expect(isDiscordMutableAllowEntry("123456789")).toBe(false);
8
+ expect(isDiscordMutableAllowEntry("<@123456789>")).toBe(false);
9
+ expect(isDiscordMutableAllowEntry("user:123456789")).toBe(false);
10
+ expect(isDiscordMutableAllowEntry("pk:123456789")).toBe(false);
11
+ });
12
+
13
+ it("flags freeform names but not prefixed stable-id namespaces", () => {
14
+ expect(isDiscordMutableAllowEntry("alice")).toBe(true);
15
+ expect(isDiscordMutableAllowEntry("discord:alice")).toBe(false);
16
+ expect(isDiscordMutableAllowEntry("user:alice")).toBe(false);
17
+ expect(isDiscordMutableAllowEntry("pk:alice")).toBe(false);
18
+ });
19
+
20
+ it("treats empty prefixed entries as mutable placeholders", () => {
21
+ expect(isDiscordMutableAllowEntry("discord:")).toBe(true);
22
+ expect(isDiscordMutableAllowEntry("user:")).toBe(true);
23
+ expect(isDiscordMutableAllowEntry("pk:")).toBe(true);
24
+ });
25
+ });
@@ -0,0 +1,20 @@
1
+ export function isDiscordMutableAllowEntry(raw: string): boolean {
2
+ const text = raw.trim();
3
+ if (!text || text === "*") {
4
+ return false;
5
+ }
6
+
7
+ const maybeMentionId = text.replace(/^<@!?/, "").replace(/>$/, "");
8
+ if (/^\d+$/.test(maybeMentionId)) {
9
+ return false;
10
+ }
11
+
12
+ for (const prefix of ["discord:", "user:", "pk:"]) {
13
+ if (!text.startsWith(prefix)) {
14
+ continue;
15
+ }
16
+ return text.slice(prefix.length).trim().length === 0;
17
+ }
18
+
19
+ return true;
20
+ }
@@ -0,0 +1,60 @@
1
+ import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
2
+ import { createOpenProviderConfiguredRouteWarningCollector } from "openclaw/plugin-sdk/channel-policy";
3
+ import {
4
+ resolveDiscordAccountAllowFrom,
5
+ resolveDiscordAccountDmPolicy,
6
+ type ResolvedDiscordAccount,
7
+ } from "./accounts.js";
8
+ import type { ChannelPlugin } from "./channel-api.js";
9
+
10
+ const resolveDiscordDmPolicy = createScopedDmSecurityResolver<ResolvedDiscordAccount>({
11
+ channelKey: "discord",
12
+ resolvePolicy: (account) => account.config.dmPolicy,
13
+ resolveAllowFrom: (account) => account.config.allowFrom,
14
+ resolveAccess: ({ cfg, account }) => ({
15
+ dmPolicy: resolveDiscordAccountDmPolicy({ cfg, accountId: account.accountId }),
16
+ allowFrom: resolveDiscordAccountAllowFrom({ cfg, accountId: account.accountId }),
17
+ }),
18
+ policyPathSuffix: "dmPolicy",
19
+ normalizeEntry: (raw) =>
20
+ raw
21
+ .trim()
22
+ .replace(/^(discord|user):/i, "")
23
+ .replace(/^<@!?(\d+)>$/, "$1"),
24
+ });
25
+
26
+ const collectDiscordSecurityWarnings =
27
+ createOpenProviderConfiguredRouteWarningCollector<ResolvedDiscordAccount>({
28
+ providerConfigPresent: (cfg) => cfg.channels?.discord !== undefined,
29
+ resolveGroupPolicy: (account) => account.config.groupPolicy,
30
+ resolveRouteAllowlistConfigured: (account) =>
31
+ Object.keys(account.config.guilds ?? {}).length > 0,
32
+ configureRouteAllowlist: {
33
+ surface: "Discord guilds",
34
+ openScope: "any channel not explicitly denied",
35
+ groupPolicyPath: "channels.discord.groupPolicy",
36
+ routeAllowlistPath: "channels.discord.guilds.<id>.channels",
37
+ },
38
+ missingRouteAllowlist: {
39
+ surface: "Discord guilds",
40
+ openBehavior: "with no guild/channel allowlist; any channel can trigger (mention-gated)",
41
+ remediation:
42
+ 'Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels',
43
+ },
44
+ });
45
+
46
+ let discordSecurityAuditModulePromise:
47
+ | Promise<typeof import("./security-audit.runtime.js")>
48
+ | undefined;
49
+
50
+ async function loadDiscordSecurityAuditModule() {
51
+ discordSecurityAuditModulePromise ??= import("./security-audit.runtime.js");
52
+ return await discordSecurityAuditModulePromise;
53
+ }
54
+
55
+ export const discordSecurityAdapter = {
56
+ resolveDmPolicy: resolveDiscordDmPolicy,
57
+ collectWarnings: collectDiscordSecurityWarnings,
58
+ collectAuditFindings: async (params) =>
59
+ (await loadDiscordSecurityAuditModule()).collectDiscordSecurityAuditFindings(params),
60
+ } satisfies NonNullable<ChannelPlugin<ResolvedDiscordAccount>["security"]>;
@@ -0,0 +1,14 @@
1
+ import {
2
+ parseDiscordTarget,
3
+ type DiscordTarget,
4
+ type DiscordTargetParseOptions,
5
+ } from "./target-parsing.js";
6
+
7
+ export type SendDiscordTarget = DiscordTarget;
8
+
9
+ type SendDiscordTargetParseOptions = DiscordTargetParseOptions;
10
+
11
+ export const parseDiscordSendTarget = (
12
+ raw: string,
13
+ options: SendDiscordTargetParseOptions = {},
14
+ ): SendDiscordTarget | undefined => parseDiscordTarget(raw, options);