@open-mercato/channel-discord 0.6.8-develop.7090.1.4dbca7b350

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 (220) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/build.mjs +7 -0
  3. package/dist/index.js +5 -0
  4. package/dist/index.js.map +7 -0
  5. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-001.spec.js +86 -0
  6. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-001.spec.js.map +7 -0
  7. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-002.spec.js +39 -0
  8. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-002.spec.js.map +7 -0
  9. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-003.spec.js +97 -0
  10. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-003.spec.js.map +7 -0
  11. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-004.spec.js +65 -0
  12. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-004.spec.js.map +7 -0
  13. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-005.spec.js +110 -0
  14. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-005.spec.js.map +7 -0
  15. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-006.spec.js +103 -0
  16. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-006.spec.js.map +7 -0
  17. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-007.spec.js +93 -0
  18. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-007.spec.js.map +7 -0
  19. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-008.spec.js +52 -0
  20. package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-008.spec.js.map +7 -0
  21. package/dist/modules/channel_discord/__integration__/helpers/discordSignature.js +46 -0
  22. package/dist/modules/channel_discord/__integration__/helpers/discordSignature.js.map +7 -0
  23. package/dist/modules/channel_discord/__integration__/helpers/eventsQueue.js +16 -0
  24. package/dist/modules/channel_discord/__integration__/helpers/eventsQueue.js.map +7 -0
  25. package/dist/modules/channel_discord/__integration__/meta.js +10 -0
  26. package/dist/modules/channel_discord/__integration__/meta.js.map +7 -0
  27. package/dist/modules/channel_discord/acl.js +15 -0
  28. package/dist/modules/channel_discord/acl.js.map +7 -0
  29. package/dist/modules/channel_discord/ai-agents.js +97 -0
  30. package/dist/modules/channel_discord/ai-agents.js.map +7 -0
  31. package/dist/modules/channel_discord/api/ai-auto-reply/channels/route.js +73 -0
  32. package/dist/modules/channel_discord/api/ai-auto-reply/channels/route.js.map +7 -0
  33. package/dist/modules/channel_discord/api/channels/[id]/ai-auto-reply/route.js +222 -0
  34. package/dist/modules/channel_discord/api/channels/[id]/ai-auto-reply/route.js.map +7 -0
  35. package/dist/modules/channel_discord/api/interactions/route.js +173 -0
  36. package/dist/modules/channel_discord/api/interactions/route.js.map +7 -0
  37. package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.js +196 -0
  38. package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.js.map +7 -0
  39. package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.meta.js +14 -0
  40. package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.meta.js.map +7 -0
  41. package/dist/modules/channel_discord/cli.js +142 -0
  42. package/dist/modules/channel_discord/cli.js.map +7 -0
  43. package/dist/modules/channel_discord/commands/ai-reply-proposal.js +116 -0
  44. package/dist/modules/channel_discord/commands/ai-reply-proposal.js.map +7 -0
  45. package/dist/modules/channel_discord/commands/update-ai-auto-reply.js +104 -0
  46. package/dist/modules/channel_discord/commands/update-ai-auto-reply.js.map +7 -0
  47. package/dist/modules/channel_discord/di.js +23 -0
  48. package/dist/modules/channel_discord/di.js.map +7 -0
  49. package/dist/modules/channel_discord/index.js +9 -0
  50. package/dist/modules/channel_discord/index.js.map +7 -0
  51. package/dist/modules/channel_discord/integration.js +96 -0
  52. package/dist/modules/channel_discord/integration.js.map +7 -0
  53. package/dist/modules/channel_discord/lib/adapter.js +248 -0
  54. package/dist/modules/channel_discord/lib/adapter.js.map +7 -0
  55. package/dist/modules/channel_discord/lib/ai-agent-directory.js +45 -0
  56. package/dist/modules/channel_discord/lib/ai-agent-directory.js.map +7 -0
  57. package/dist/modules/channel_discord/lib/ai-features.js +9 -0
  58. package/dist/modules/channel_discord/lib/ai-features.js.map +7 -0
  59. package/dist/modules/channel_discord/lib/ai-proposal-contract.js +9 -0
  60. package/dist/modules/channel_discord/lib/ai-proposal-contract.js.map +7 -0
  61. package/dist/modules/channel_discord/lib/ai-proposal.js +109 -0
  62. package/dist/modules/channel_discord/lib/ai-proposal.js.map +7 -0
  63. package/dist/modules/channel_discord/lib/ai-reply.js +96 -0
  64. package/dist/modules/channel_discord/lib/ai-reply.js.map +7 -0
  65. package/dist/modules/channel_discord/lib/ai-service-principal.js +70 -0
  66. package/dist/modules/channel_discord/lib/ai-service-principal.js.map +7 -0
  67. package/dist/modules/channel_discord/lib/capabilities.js +42 -0
  68. package/dist/modules/channel_discord/lib/capabilities.js.map +7 -0
  69. package/dist/modules/channel_discord/lib/channel-access.js +62 -0
  70. package/dist/modules/channel_discord/lib/channel-access.js.map +7 -0
  71. package/dist/modules/channel_discord/lib/channel-state-store.js +103 -0
  72. package/dist/modules/channel_discord/lib/channel-state-store.js.map +7 -0
  73. package/dist/modules/channel_discord/lib/convert-outbound.js +38 -0
  74. package/dist/modules/channel_discord/lib/convert-outbound.js.map +7 -0
  75. package/dist/modules/channel_discord/lib/credentials.js +61 -0
  76. package/dist/modules/channel_discord/lib/credentials.js.map +7 -0
  77. package/dist/modules/channel_discord/lib/discord-gateway-client.js +289 -0
  78. package/dist/modules/channel_discord/lib/discord-gateway-client.js.map +7 -0
  79. package/dist/modules/channel_discord/lib/discord-rest.js +203 -0
  80. package/dist/modules/channel_discord/lib/discord-rest.js.map +7 -0
  81. package/dist/modules/channel_discord/lib/failure-reason.js +30 -0
  82. package/dist/modules/channel_discord/lib/failure-reason.js.map +7 -0
  83. package/dist/modules/channel_discord/lib/gateway-bridge.js +42 -0
  84. package/dist/modules/channel_discord/lib/gateway-bridge.js.map +7 -0
  85. package/dist/modules/channel_discord/lib/health.js +37 -0
  86. package/dist/modules/channel_discord/lib/health.js.map +7 -0
  87. package/dist/modules/channel_discord/lib/interactions-dispatch.js +164 -0
  88. package/dist/modules/channel_discord/lib/interactions-dispatch.js.map +7 -0
  89. package/dist/modules/channel_discord/lib/interactions-handler.js +115 -0
  90. package/dist/modules/channel_discord/lib/interactions-handler.js.map +7 -0
  91. package/dist/modules/channel_discord/lib/interactions-queue.js +17 -0
  92. package/dist/modules/channel_discord/lib/interactions-queue.js.map +7 -0
  93. package/dist/modules/channel_discord/lib/interactions-verify.js +84 -0
  94. package/dist/modules/channel_discord/lib/interactions-verify.js.map +7 -0
  95. package/dist/modules/channel_discord/lib/normalize-inbound.js +59 -0
  96. package/dist/modules/channel_discord/lib/normalize-inbound.js.map +7 -0
  97. package/dist/modules/channel_discord/lib/preset.js +63 -0
  98. package/dist/modules/channel_discord/lib/preset.js.map +7 -0
  99. package/dist/modules/channel_discord/lib/queue-strategy.js +12 -0
  100. package/dist/modules/channel_discord/lib/queue-strategy.js.map +7 -0
  101. package/dist/modules/channel_discord/lib/slash-commands.js +42 -0
  102. package/dist/modules/channel_discord/lib/slash-commands.js.map +7 -0
  103. package/dist/modules/channel_discord/message-types.js +64 -0
  104. package/dist/modules/channel_discord/message-types.js.map +7 -0
  105. package/dist/modules/channel_discord/setup.js +62 -0
  106. package/dist/modules/channel_discord/setup.js.map +7 -0
  107. package/dist/modules/channel_discord/subscribers/ai-auto-reply.js +185 -0
  108. package/dist/modules/channel_discord/subscribers/ai-auto-reply.js.map +7 -0
  109. package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.client.js +79 -0
  110. package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.client.js.map +7 -0
  111. package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.js +20 -0
  112. package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.js.map +7 -0
  113. package/dist/modules/channel_discord/widgets/injection/connect/widget.client.js +220 -0
  114. package/dist/modules/channel_discord/widgets/injection/connect/widget.client.js.map +7 -0
  115. package/dist/modules/channel_discord/widgets/injection/connect/widget.js +17 -0
  116. package/dist/modules/channel_discord/widgets/injection/connect/widget.js.map +7 -0
  117. package/dist/modules/channel_discord/widgets/injection-table.js +26 -0
  118. package/dist/modules/channel_discord/widgets/injection-table.js.map +7 -0
  119. package/dist/modules/channel_discord/workers/discord-gateway.js +270 -0
  120. package/dist/modules/channel_discord/workers/discord-gateway.js.map +7 -0
  121. package/dist/modules/channel_discord/workers/discord-interactions.js +113 -0
  122. package/dist/modules/channel_discord/workers/discord-interactions.js.map +7 -0
  123. package/jest.config.cjs +38 -0
  124. package/package.json +104 -0
  125. package/src/index.ts +1 -0
  126. package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-001.spec.ts +109 -0
  127. package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-002.spec.ts +58 -0
  128. package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-003.spec.ts +175 -0
  129. package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-004.spec.ts +83 -0
  130. package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-005.spec.ts +156 -0
  131. package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-006.spec.ts +144 -0
  132. package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-007.spec.ts +119 -0
  133. package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-008.spec.ts +75 -0
  134. package/src/modules/channel_discord/__integration__/helpers/discordSignature.ts +77 -0
  135. package/src/modules/channel_discord/__integration__/helpers/eventsQueue.ts +26 -0
  136. package/src/modules/channel_discord/__integration__/meta.ts +6 -0
  137. package/src/modules/channel_discord/__tests__/ai-auto-reply.policy.integration.test.ts +286 -0
  138. package/src/modules/channel_discord/acl.ts +19 -0
  139. package/src/modules/channel_discord/ai-agents.ts +157 -0
  140. package/src/modules/channel_discord/api/ai-auto-reply/channels/__tests__/route.test.ts +175 -0
  141. package/src/modules/channel_discord/api/ai-auto-reply/channels/route.ts +99 -0
  142. package/src/modules/channel_discord/api/channels/[id]/ai-auto-reply/__tests__/route.test.ts +311 -0
  143. package/src/modules/channel_discord/api/channels/[id]/ai-auto-reply/route.ts +299 -0
  144. package/src/modules/channel_discord/api/interactions/route.ts +242 -0
  145. package/src/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.meta.ts +18 -0
  146. package/src/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.tsx +288 -0
  147. package/src/modules/channel_discord/cli.ts +234 -0
  148. package/src/modules/channel_discord/commands/__tests__/ai-reply-proposal.test.ts +155 -0
  149. package/src/modules/channel_discord/commands/__tests__/update-ai-auto-reply.test.ts +185 -0
  150. package/src/modules/channel_discord/commands/ai-reply-proposal.ts +162 -0
  151. package/src/modules/channel_discord/commands/update-ai-auto-reply.ts +157 -0
  152. package/src/modules/channel_discord/di.ts +26 -0
  153. package/src/modules/channel_discord/i18n/de.json +57 -0
  154. package/src/modules/channel_discord/i18n/en.json +57 -0
  155. package/src/modules/channel_discord/i18n/es.json +57 -0
  156. package/src/modules/channel_discord/i18n/ko.json +57 -0
  157. package/src/modules/channel_discord/i18n/pl.json +57 -0
  158. package/src/modules/channel_discord/index.ts +6 -0
  159. package/src/modules/channel_discord/integration.ts +98 -0
  160. package/src/modules/channel_discord/lib/__tests__/adapter.test.ts +206 -0
  161. package/src/modules/channel_discord/lib/__tests__/ai-agent-directory.test.ts +61 -0
  162. package/src/modules/channel_discord/lib/__tests__/ai-reply.test.ts +115 -0
  163. package/src/modules/channel_discord/lib/__tests__/capabilities.test.ts +170 -0
  164. package/src/modules/channel_discord/lib/__tests__/channel-quarantine.test.ts +89 -0
  165. package/src/modules/channel_discord/lib/__tests__/channel-state-store.auto-reply-marker.test.ts +145 -0
  166. package/src/modules/channel_discord/lib/__tests__/channel-state-store.test.ts +78 -0
  167. package/src/modules/channel_discord/lib/__tests__/convert-outbound.test.ts +39 -0
  168. package/src/modules/channel_discord/lib/__tests__/discord-gateway-client.test.ts +243 -0
  169. package/src/modules/channel_discord/lib/__tests__/failure-reason.test.ts +68 -0
  170. package/src/modules/channel_discord/lib/__tests__/gateway-bridge.test.ts +62 -0
  171. package/src/modules/channel_discord/lib/__tests__/interactions-dispatch.test.ts +223 -0
  172. package/src/modules/channel_discord/lib/__tests__/interactions-handler.test.ts +469 -0
  173. package/src/modules/channel_discord/lib/__tests__/interactions-verify.test.ts +126 -0
  174. package/src/modules/channel_discord/lib/__tests__/normalize-inbound.identity.test.ts +64 -0
  175. package/src/modules/channel_discord/lib/__tests__/normalize-inbound.test.ts +67 -0
  176. package/src/modules/channel_discord/lib/__tests__/preset.test.ts +110 -0
  177. package/src/modules/channel_discord/lib/__tests__/queue-strategy.test.ts +33 -0
  178. package/src/modules/channel_discord/lib/adapter.ts +359 -0
  179. package/src/modules/channel_discord/lib/ai-agent-directory.ts +124 -0
  180. package/src/modules/channel_discord/lib/ai-features.ts +23 -0
  181. package/src/modules/channel_discord/lib/ai-proposal-contract.ts +24 -0
  182. package/src/modules/channel_discord/lib/ai-proposal.ts +179 -0
  183. package/src/modules/channel_discord/lib/ai-reply.ts +179 -0
  184. package/src/modules/channel_discord/lib/ai-service-principal.ts +143 -0
  185. package/src/modules/channel_discord/lib/capabilities.ts +104 -0
  186. package/src/modules/channel_discord/lib/channel-access.ts +110 -0
  187. package/src/modules/channel_discord/lib/channel-state-store.ts +217 -0
  188. package/src/modules/channel_discord/lib/convert-outbound.ts +88 -0
  189. package/src/modules/channel_discord/lib/credentials.ts +88 -0
  190. package/src/modules/channel_discord/lib/discord-gateway-client.ts +432 -0
  191. package/src/modules/channel_discord/lib/discord-rest.ts +393 -0
  192. package/src/modules/channel_discord/lib/failure-reason.ts +49 -0
  193. package/src/modules/channel_discord/lib/gateway-bridge.ts +79 -0
  194. package/src/modules/channel_discord/lib/health.ts +55 -0
  195. package/src/modules/channel_discord/lib/interactions-dispatch.ts +288 -0
  196. package/src/modules/channel_discord/lib/interactions-handler.ts +267 -0
  197. package/src/modules/channel_discord/lib/interactions-queue.ts +52 -0
  198. package/src/modules/channel_discord/lib/interactions-verify.ts +174 -0
  199. package/src/modules/channel_discord/lib/normalize-inbound.ts +89 -0
  200. package/src/modules/channel_discord/lib/preset.ts +109 -0
  201. package/src/modules/channel_discord/lib/queue-strategy.ts +29 -0
  202. package/src/modules/channel_discord/lib/slash-commands.ts +57 -0
  203. package/src/modules/channel_discord/message-types.ts +75 -0
  204. package/src/modules/channel_discord/setup.ts +78 -0
  205. package/src/modules/channel_discord/subscribers/__tests__/ai-auto-reply.armed.test.ts +149 -0
  206. package/src/modules/channel_discord/subscribers/__tests__/ai-auto-reply.failure-marker.test.ts +133 -0
  207. package/src/modules/channel_discord/subscribers/__tests__/ai-auto-reply.test.ts +273 -0
  208. package/src/modules/channel_discord/subscribers/ai-auto-reply.ts +325 -0
  209. package/src/modules/channel_discord/widgets/injection/ai-auto-reply/widget.client.tsx +143 -0
  210. package/src/modules/channel_discord/widgets/injection/ai-auto-reply/widget.ts +19 -0
  211. package/src/modules/channel_discord/widgets/injection/connect/widget.client.tsx +237 -0
  212. package/src/modules/channel_discord/widgets/injection/connect/widget.ts +16 -0
  213. package/src/modules/channel_discord/widgets/injection-table.ts +24 -0
  214. package/src/modules/channel_discord/workers/__tests__/discord-gateway.single-identify.test.ts +88 -0
  215. package/src/modules/channel_discord/workers/__tests__/discord-gateway.test.ts +179 -0
  216. package/src/modules/channel_discord/workers/__tests__/discord-interactions.test.ts +152 -0
  217. package/src/modules/channel_discord/workers/discord-gateway.ts +458 -0
  218. package/src/modules/channel_discord/workers/discord-interactions.ts +162 -0
  219. package/tsconfig.json +9 -0
  220. package/watch.mjs +7 -0
@@ -0,0 +1,57 @@
1
+ {
2
+ "channel_discord.aiAutoReply.errors.agentFeaturesMissing": "El usuario bot del canal no tiene los permisos que necesita este agente: {features}. Concédelos primero o elige otro agente.",
3
+ "channel_discord.aiAutoReply.errors.agentNotEligible": "Ese agente no puede responder en un canal: elige uno que produzca salida estructurada",
4
+ "channel_discord.aiAutoReply.errors.agentRequired": "Elige el agente que debe responder en este canal",
5
+ "channel_discord.aiAutoReply.errors.aiUnavailable": "La respuesta automática necesita el módulo de asistente de IA, que no está instalado en este despliegue",
6
+ "channel_discord.aiAutoReply.errors.load": "No se pudo cargar la configuración de respuesta automática de IA",
7
+ "channel_discord.aiAutoReply.errors.loadChannels": "No se pudieron cargar los canales de Discord",
8
+ "channel_discord.aiAutoReply.errors.save": "No se pudo guardar la configuración de respuesta automática de IA",
9
+ "channel_discord.aiAutoReply.fields.agent": "Agente",
10
+ "channel_discord.aiAutoReply.fields.agentHelp": "Solo los agentes que producen salida estructurada pueden responder en un canal. Concede al usuario bot del inquilino los permisos que exige el agente antes de elegir uno de otro módulo.",
11
+ "channel_discord.aiAutoReply.fields.agentMissingFeatures": "{label}: necesita {features}",
12
+ "channel_discord.aiAutoReply.fields.enabled": "Responder los mensajes entrantes de Discord con IA",
13
+ "channel_discord.aiAutoReply.fields.enabledHelp": "Desactivado por defecto. Al activarlo, los mensajes sencillos se responden automáticamente; todo lo delicado, poco fiable o sospechoso se propone para que lo apruebe una persona.",
14
+ "channel_discord.aiAutoReply.intro": "Decide si este canal de Discord puede responder por su cuenta y qué agente redacta la respuesta.",
15
+ "channel_discord.aiAutoReply.lastError": "El último intento de respuesta automática en este canal no produjo nada: {reason}",
16
+ "channel_discord.aiAutoReply.lastErrorAt": "(visto por primera vez el {at})",
17
+ "channel_discord.aiAutoReply.loading": "Cargando la configuración de respuesta automática de IA...",
18
+ "channel_discord.aiAutoReply.loadingChannels": "Cargando canales de Discord...",
19
+ "channel_discord.aiAutoReply.notFound": "Canal de Discord no encontrado",
20
+ "channel_discord.aiAutoReply.page.title": "Respuesta automática de IA en Discord",
21
+ "channel_discord.aiAutoReply.save": "Guardar",
22
+ "channel_discord.aiAutoReply.saved": "Configuración de respuesta automática de IA guardada.",
23
+ "channel_discord.aiAutoReply.unavailable": "El módulo de asistente de IA no está instalado en este despliegue, así que no se puede activar la respuesta automática. El canal sigue funcionando como una bandeja de entrada normal.",
24
+ "channel_discord.aiAutoReply.widget.configure": "Configurar",
25
+ "channel_discord.aiAutoReply.widget.description": "Deja que un agente de IA responda los mensajes entrantes de Discord. Cada canal está desactivado por defecto, y todo lo delicado o poco fiable se propone para que lo apruebe una persona en lugar de enviarse.",
26
+ "channel_discord.aiAutoReply.widget.failing": "Respuesta automática con fallos",
27
+ "channel_discord.aiAutoReply.widget.lastError": "El último intento falló: {reason}",
28
+ "channel_discord.aiAutoReply.widget.noAgent": "Ningún agente seleccionado",
29
+ "channel_discord.aiAutoReply.widget.noChannels": "Conecta primero un bot de Discord: la respuesta automática se configura por canal.",
30
+ "channel_discord.aiAutoReply.widget.off": "Respuesta automática desactivada",
31
+ "channel_discord.aiAutoReply.widget.on": "Respuesta automática activada",
32
+ "channel_discord.aiAutoReply.widget.title": "Respuesta automática de IA",
33
+ "channel_discord.aiAutoReply.widget.truncated": "Solo se muestran los canales más recientes. Abre un canal desde la página de canales para configurar el resto.",
34
+ "channel_discord.aiProposal.actions.approve": "Aprobar y enviar",
35
+ "channel_discord.aiProposal.actions.dismiss": "Descartar",
36
+ "channel_discord.aiProposal.messageType": "Respuesta de Discord propuesta",
37
+ "channel_discord.connect.blocked": "Conexión bloqueada por validación",
38
+ "channel_discord.connect.button": "Conectar Discord",
39
+ "channel_discord.connect.cancel": "Cancelar",
40
+ "channel_discord.connect.connected": "Canal de Discord conectado.",
41
+ "channel_discord.connect.connecting": "Conectando...",
42
+ "channel_discord.connect.failed": "No se pudo conectar el bot de Discord.",
43
+ "channel_discord.connect.fields.applicationId": "ID de aplicación",
44
+ "channel_discord.connect.fields.botToken": "Token del bot",
45
+ "channel_discord.connect.fields.defaultChannelId": "ID del canal predeterminado",
46
+ "channel_discord.connect.fields.displayName": "Nombre para mostrar",
47
+ "channel_discord.connect.fields.guildId": "ID del servidor (guild)",
48
+ "channel_discord.connect.fields.publicKey": "Clave pública",
49
+ "channel_discord.connect.save": "Conectar",
50
+ "channel_discord.connect.title": "Conectar bot de Discord",
51
+ "channel_discord.interactions.commandReceived": "Recibido: `/{command}` ya está en la bandeja de entrada de Open Mercato y alguien se encargará desde allí.",
52
+ "channel_discord.interactions.componentReceived": "Recibido: tu respuesta ya está en la bandeja de entrada de Open Mercato y alguien se encargará desde allí.",
53
+ "channel_discord.interactions.dispatchFailed": "Open Mercato no ha podido registrar esta interacción ahora mismo. Inténtalo de nuevo en un momento.",
54
+ "channel_discord.interactions.notDispatchable": "A esta interacción le falta el contexto de canal o de usuario que Open Mercato necesita, así que no se registró.",
55
+ "channel_discord.interactions.unsupported": "El canal de Discord de Open Mercato no gestiona este tipo de interacción.",
56
+ "channel_discord.provider.label": "Discord"
57
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "channel_discord.aiAutoReply.errors.agentFeaturesMissing": "채널 봇 사용자에게 이 에이전트가 필요로 하는 권한이 없습니다: {features}. 먼저 권한을 부여하거나 다른 에이전트를 선택하세요.",
3
+ "channel_discord.aiAutoReply.errors.agentNotEligible": "해당 에이전트는 채널에서 답장할 수 없습니다 — 구조화된 출력을 생성하는 에이전트를 선택하세요",
4
+ "channel_discord.aiAutoReply.errors.agentRequired": "이 채널에서 답장할 에이전트를 선택하세요",
5
+ "channel_discord.aiAutoReply.errors.aiUnavailable": "자동 응답에는 AI 어시스턴트 모듈이 필요하지만 이 배포에는 설치되어 있지 않습니다",
6
+ "channel_discord.aiAutoReply.errors.load": "AI 자동 응답 설정을 불러오지 못했습니다",
7
+ "channel_discord.aiAutoReply.errors.loadChannels": "Discord 채널을 불러오지 못했습니다",
8
+ "channel_discord.aiAutoReply.errors.save": "AI 자동 응답 설정을 저장하지 못했습니다",
9
+ "channel_discord.aiAutoReply.fields.agent": "에이전트",
10
+ "channel_discord.aiAutoReply.fields.agentHelp": "구조화된 출력을 생성하는 에이전트만 채널에서 답장할 수 있습니다. 다른 모듈의 에이전트를 선택하기 전에 테넌트의 채널 봇 사용자에게 해당 에이전트가 요구하는 권한을 부여하세요.",
11
+ "channel_discord.aiAutoReply.fields.agentMissingFeatures": "{label} — {features} 필요",
12
+ "channel_discord.aiAutoReply.fields.enabled": "수신된 Discord 메시지에 AI로 답장",
13
+ "channel_discord.aiAutoReply.fields.enabledHelp": "기본값은 꺼짐입니다. 켜면 단순한 메시지는 자동으로 답장되고, 민감하거나 확신이 낮거나 의심스러운 내용은 사람이 승인하도록 제안됩니다.",
14
+ "channel_discord.aiAutoReply.intro": "이 Discord 채널이 스스로 답장할 수 있는지, 그리고 어떤 에이전트가 답장을 작성할지 선택하세요.",
15
+ "channel_discord.aiAutoReply.lastError": "이 채널의 마지막 자동 응답 시도는 아무것도 만들어내지 못했습니다: {reason}",
16
+ "channel_discord.aiAutoReply.lastErrorAt": "(최초 발견 {at})",
17
+ "channel_discord.aiAutoReply.loading": "AI 자동 응답 설정을 불러오는 중...",
18
+ "channel_discord.aiAutoReply.loadingChannels": "Discord 채널을 불러오는 중...",
19
+ "channel_discord.aiAutoReply.notFound": "Discord 채널을 찾을 수 없습니다",
20
+ "channel_discord.aiAutoReply.page.title": "Discord AI 자동 응답",
21
+ "channel_discord.aiAutoReply.save": "저장",
22
+ "channel_discord.aiAutoReply.saved": "AI 자동 응답 설정을 저장했습니다.",
23
+ "channel_discord.aiAutoReply.unavailable": "이 배포에는 AI 어시스턴트 모듈이 설치되어 있지 않아 자동 응답을 켤 수 없습니다. 채널은 일반 수신함으로 계속 동작합니다.",
24
+ "channel_discord.aiAutoReply.widget.configure": "설정",
25
+ "channel_discord.aiAutoReply.widget.description": "AI 에이전트가 수신된 Discord 메시지에 답장하도록 할 수 있습니다. 모든 채널은 기본적으로 꺼져 있으며, 민감하거나 확신이 낮은 내용은 전송되지 않고 사람이 승인하도록 제안됩니다.",
26
+ "channel_discord.aiAutoReply.widget.failing": "자동 응답 실패 중",
27
+ "channel_discord.aiAutoReply.widget.lastError": "마지막 시도 실패: {reason}",
28
+ "channel_discord.aiAutoReply.widget.noAgent": "선택된 에이전트 없음",
29
+ "channel_discord.aiAutoReply.widget.noChannels": "먼저 Discord 봇을 연결하세요 — 자동 응답은 채널별로 설정합니다.",
30
+ "channel_discord.aiAutoReply.widget.off": "자동 응답 꺼짐",
31
+ "channel_discord.aiAutoReply.widget.on": "자동 응답 켜짐",
32
+ "channel_discord.aiAutoReply.widget.title": "AI 자동 응답",
33
+ "channel_discord.aiAutoReply.widget.truncated": "최근 채널만 표시됩니다. 나머지는 채널 목록에서 채널을 열어 설정하세요.",
34
+ "channel_discord.aiProposal.actions.approve": "승인하고 전송",
35
+ "channel_discord.aiProposal.actions.dismiss": "무시",
36
+ "channel_discord.aiProposal.messageType": "제안된 Discord 답장",
37
+ "channel_discord.connect.blocked": "유효성 검사로 인해 연결이 차단되었습니다",
38
+ "channel_discord.connect.button": "Discord 연결",
39
+ "channel_discord.connect.cancel": "취소",
40
+ "channel_discord.connect.connected": "Discord 채널이 연결되었습니다.",
41
+ "channel_discord.connect.connecting": "연결 중...",
42
+ "channel_discord.connect.failed": "Discord 봇을 연결하지 못했습니다.",
43
+ "channel_discord.connect.fields.applicationId": "애플리케이션 ID",
44
+ "channel_discord.connect.fields.botToken": "봇 토큰",
45
+ "channel_discord.connect.fields.defaultChannelId": "기본 채널 ID",
46
+ "channel_discord.connect.fields.displayName": "표시 이름",
47
+ "channel_discord.connect.fields.guildId": "길드(서버) ID",
48
+ "channel_discord.connect.fields.publicKey": "공개 키",
49
+ "channel_discord.connect.save": "연결",
50
+ "channel_discord.connect.title": "Discord 봇 연결",
51
+ "channel_discord.interactions.commandReceived": "접수됨 — `/{command}` 이(가) Open Mercato 받은편지함에 등록되었으며 담당자가 이어서 처리합니다.",
52
+ "channel_discord.interactions.componentReceived": "접수됨 — 응답이 Open Mercato 받은편지함에 등록되었으며 담당자가 이어서 처리합니다.",
53
+ "channel_discord.interactions.dispatchFailed": "Open Mercato가 지금은 이 상호작용을 기록하지 못했습니다. 잠시 후 다시 시도해 주세요.",
54
+ "channel_discord.interactions.notDispatchable": "이 상호작용에는 Open Mercato에 필요한 채널 또는 사용자 정보가 없어 기록되지 않았습니다.",
55
+ "channel_discord.interactions.unsupported": "Open Mercato Discord 채널은 이 상호작용 유형을 처리하지 않습니다.",
56
+ "channel_discord.provider.label": "Discord"
57
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "channel_discord.aiAutoReply.errors.agentFeaturesMissing": "Użytkownik-bot kanału nie ma uprawnień wymaganych przez tego agenta: {features}. Nadaj je najpierw albo wybierz innego agenta.",
3
+ "channel_discord.aiAutoReply.errors.agentNotEligible": "Ten agent nie może odpowiadać na kanale — wybierz takiego, który zwraca ustrukturyzowany wynik",
4
+ "channel_discord.aiAutoReply.errors.agentRequired": "Wybierz agenta, który ma odpowiadać na tym kanale",
5
+ "channel_discord.aiAutoReply.errors.aiUnavailable": "Automatyczne odpowiedzi wymagają modułu asystenta AI, którego nie ma w tym wdrożeniu",
6
+ "channel_discord.aiAutoReply.errors.load": "Nie udało się wczytać ustawień automatycznych odpowiedzi AI",
7
+ "channel_discord.aiAutoReply.errors.loadChannels": "Nie udało się wczytać kanałów Discord",
8
+ "channel_discord.aiAutoReply.errors.save": "Nie udało się zapisać ustawień automatycznych odpowiedzi AI",
9
+ "channel_discord.aiAutoReply.fields.agent": "Agent",
10
+ "channel_discord.aiAutoReply.fields.agentHelp": "Na kanale mogą odpowiadać tylko agenci zwracający ustrukturyzowany wynik. Zanim wybierzesz agenta z innego modułu, nadaj użytkownikowi-botowi tenanta uprawnienia wymagane przez tego agenta.",
11
+ "channel_discord.aiAutoReply.fields.agentMissingFeatures": "{label} — wymaga {features}",
12
+ "channel_discord.aiAutoReply.fields.enabled": "Odpowiadaj na przychodzące wiadomości Discord przy pomocy AI",
13
+ "channel_discord.aiAutoReply.fields.enabledHelp": "Domyślnie wyłączone. Po włączeniu proste wiadomości otrzymują odpowiedź automatycznie; wszystko wrażliwe, niepewne lub podejrzane trafia do zatwierdzenia przez człowieka.",
14
+ "channel_discord.aiAutoReply.intro": "Zdecyduj, czy ten kanał Discord może odpowiadać samodzielnie i który agent ma przygotowywać odpowiedź.",
15
+ "channel_discord.aiAutoReply.lastError": "Ostatnia próba automatycznej odpowiedzi na tym kanale nic nie dała: {reason}",
16
+ "channel_discord.aiAutoReply.lastErrorAt": "(pierwszy raz {at})",
17
+ "channel_discord.aiAutoReply.loading": "Wczytywanie ustawień automatycznych odpowiedzi AI...",
18
+ "channel_discord.aiAutoReply.loadingChannels": "Wczytywanie kanałów Discord...",
19
+ "channel_discord.aiAutoReply.notFound": "Nie znaleziono kanału Discord",
20
+ "channel_discord.aiAutoReply.page.title": "Automatyczne odpowiedzi AI na Discordzie",
21
+ "channel_discord.aiAutoReply.save": "Zapisz",
22
+ "channel_discord.aiAutoReply.saved": "Zapisano ustawienia automatycznych odpowiedzi AI.",
23
+ "channel_discord.aiAutoReply.unavailable": "W tym wdrożeniu nie zainstalowano modułu asystenta AI, więc nie można włączyć automatycznych odpowiedzi. Kanał nadal działa jak zwykła skrzynka odbiorcza.",
24
+ "channel_discord.aiAutoReply.widget.configure": "Konfiguruj",
25
+ "channel_discord.aiAutoReply.widget.description": "Pozwól agentowi AI odpowiadać na przychodzące wiadomości Discord. Każdy kanał jest domyślnie wyłączony, a treści wrażliwe lub niepewne trafiają do zatwierdzenia przez człowieka zamiast zostać wysłane.",
26
+ "channel_discord.aiAutoReply.widget.failing": "Automatyczne odpowiedzi zawodzą",
27
+ "channel_discord.aiAutoReply.widget.lastError": "Ostatnia próba nieudana: {reason}",
28
+ "channel_discord.aiAutoReply.widget.noAgent": "Nie wybrano agenta",
29
+ "channel_discord.aiAutoReply.widget.noChannels": "Najpierw połącz bota Discord — automatyczne odpowiedzi konfiguruje się osobno dla każdego kanału.",
30
+ "channel_discord.aiAutoReply.widget.off": "Odpowiedzi wyłączone",
31
+ "channel_discord.aiAutoReply.widget.on": "Odpowiedzi włączone",
32
+ "channel_discord.aiAutoReply.widget.title": "Automatyczne odpowiedzi AI",
33
+ "channel_discord.aiAutoReply.widget.truncated": "Pokazano tylko najnowsze kanały. Pozostałe skonfigurujesz, otwierając kanał na liście kanałów.",
34
+ "channel_discord.aiProposal.actions.approve": "Zatwierdź i wyślij",
35
+ "channel_discord.aiProposal.actions.dismiss": "Odrzuć",
36
+ "channel_discord.aiProposal.messageType": "Propozycja odpowiedzi na Discordzie",
37
+ "channel_discord.connect.blocked": "Połączenie zablokowane przez walidację",
38
+ "channel_discord.connect.button": "Połącz Discord",
39
+ "channel_discord.connect.cancel": "Anuluj",
40
+ "channel_discord.connect.connected": "Kanał Discord połączony.",
41
+ "channel_discord.connect.connecting": "Łączenie...",
42
+ "channel_discord.connect.failed": "Nie udało się połączyć bota Discord.",
43
+ "channel_discord.connect.fields.applicationId": "ID aplikacji",
44
+ "channel_discord.connect.fields.botToken": "Token bota",
45
+ "channel_discord.connect.fields.defaultChannelId": "Domyślne ID kanału",
46
+ "channel_discord.connect.fields.displayName": "Nazwa wyświetlana",
47
+ "channel_discord.connect.fields.guildId": "ID serwera (guild)",
48
+ "channel_discord.connect.fields.publicKey": "Klucz publiczny",
49
+ "channel_discord.connect.save": "Połącz",
50
+ "channel_discord.connect.title": "Połącz bota Discord",
51
+ "channel_discord.interactions.commandReceived": "Odebrano — `/{command}` trafiło do skrzynki Open Mercato i ktoś się tym zajmie.",
52
+ "channel_discord.interactions.componentReceived": "Odebrano — Twoja odpowiedź trafiła do skrzynki Open Mercato i ktoś się tym zajmie.",
53
+ "channel_discord.interactions.dispatchFailed": "Open Mercato nie mogło teraz zapisać tej interakcji. Spróbuj ponownie za chwilę.",
54
+ "channel_discord.interactions.notDispatchable": "Tej interakcji brakuje kontekstu kanału lub użytkownika, którego potrzebuje Open Mercato, więc nie została zapisana.",
55
+ "channel_discord.interactions.unsupported": "Kanał Discord w Open Mercato nie obsługuje tego typu interakcji.",
56
+ "channel_discord.provider.label": "Discord"
57
+ }
@@ -0,0 +1,6 @@
1
+ export const metadata = {
2
+ id: 'channel_discord',
3
+ title: 'Discord Communication Channel',
4
+ description:
5
+ 'Connect a Discord bot as a two-way communication channel. Outbound via the Discord REST API; inbound in real time via a Gateway worker; signed (Ed25519) Interactions endpoint that acknowledges slash commands / buttons. Optional per-channel AI auto-reply answers straightforward messages and proposes the rest for human approval. Pairs with the Communications Hub.',
6
+ }
@@ -0,0 +1,98 @@
1
+ import { buildIntegrationDetailWidgetSpotId, type IntegrationBundle, type IntegrationDefinition } from '@open-mercato/shared/modules/integrations/types'
2
+
3
+ export const channelDiscordDetailWidgetSpotId = buildIntegrationDetailWidgetSpotId('channel_discord')
4
+
5
+ export const integration: IntegrationDefinition = {
6
+ id: 'channel_discord',
7
+ title: 'Discord',
8
+ // Kept within the 68–132 character range the six sibling providers occupy: the
9
+ // card grid gives every description the same fixed box, so a long one inflates
10
+ // its whole row. The operational caveats it used to carry (gateway worker,
11
+ // queue strategy) belong on the detail page and in the credential help texts,
12
+ // which is where they now live.
13
+ description: 'Two-way Discord bot channel: REST outbound, real-time Gateway inbound, optional AI auto-reply.',
14
+ category: 'communication',
15
+ hub: 'communication_channels',
16
+ providerKey: 'discord',
17
+ icon: 'discord',
18
+ docsUrl: 'https://discord.com/developers/docs/intro',
19
+ package: '@open-mercato/channel-discord',
20
+ // The provider descriptor's own version, deliberately independent of the
21
+ // monorepo release the package happens to ship in — a copied monorepo version
22
+ // rots on every release bump and reads as a claim it is not making.
23
+ version: '1.0.0',
24
+ author: 'Open Mercato Team',
25
+ company: 'Open Mercato',
26
+ license: 'MIT',
27
+ // `ai` returns here in the same change that makes auto-reply real: an agent the
28
+ // repository ships, a service principal with real features, a settings surface
29
+ // that arms it, and an approval surface for everything it must not send.
30
+ tags: ['discord', 'chat', 'bot', 'gateway', 'communication', 'ai'],
31
+ detailPage: {
32
+ widgetSpotId: channelDiscordDetailWidgetSpotId,
33
+ },
34
+ apiVersions: [
35
+ {
36
+ id: 'v10',
37
+ label: 'Discord API v10',
38
+ status: 'stable',
39
+ default: true,
40
+ changelog: 'Discord API v10 (REST) + Gateway v10 real-time inbound + Ed25519-signed Interactions.',
41
+ },
42
+ ],
43
+ credentials: {
44
+ fields: [
45
+ {
46
+ key: 'botToken',
47
+ label: 'Bot token',
48
+ type: 'secret',
49
+ required: true,
50
+ helpText:
51
+ 'Developer Portal → Applications → your app → Bot → Reset Token. Grants full control of the bot user. Stored encrypted at rest; never logged.',
52
+ },
53
+ {
54
+ key: 'applicationId',
55
+ label: 'Application ID',
56
+ type: 'text',
57
+ required: true,
58
+ placeholder: '123456789012345678',
59
+ helpText:
60
+ 'Developer Portal → General Information → Application ID. Used to register slash commands'
61
+ + ' (mercato channel_discord register-slash-commands) and to answer interactions.',
62
+ },
63
+ {
64
+ key: 'publicKey',
65
+ label: 'Public key',
66
+ type: 'text',
67
+ required: true,
68
+ placeholder: '64-character hex Ed25519 public key',
69
+ helpText:
70
+ 'Developer Portal → General Information → Public Key. Verifies signed interaction requests; a verified'
71
+ + ' slash command or button press is recorded in the inbox and answered back in Discord.',
72
+ },
73
+ {
74
+ key: 'guildId',
75
+ label: 'Guild (server) ID',
76
+ type: 'text',
77
+ required: false,
78
+ placeholder: '123456789012345678',
79
+ helpText: 'Recommended: scope the bot to a single server. Enable Developer Mode in Discord, right-click the server, Copy Server ID.',
80
+ },
81
+ {
82
+ key: 'defaultChannelId',
83
+ label: 'Default channel ID',
84
+ type: 'text',
85
+ required: false,
86
+ placeholder: '123456789012345678',
87
+ helpText:
88
+ 'Default text channel the adapter posts to when an outbound message names no Discord channel. The hub'
89
+ + ' test-send endpoint accepts a channel snowflake as its recipient, so you can use it to verify this end to end.',
90
+ },
91
+ ],
92
+ },
93
+ healthCheck: { service: 'channelDiscordHealthCheck' },
94
+ }
95
+
96
+ export const integrations: IntegrationDefinition[] = [integration]
97
+ export const bundles: IntegrationBundle[] = []
98
+ export const bundle: IntegrationBundle | undefined = undefined
@@ -0,0 +1,206 @@
1
+ import { generateKeyPairSync, sign as cryptoSign } from 'node:crypto'
2
+ import { getDiscordChannelAdapter } from '../adapter'
3
+ import {
4
+ setDiscordRestClient,
5
+ type CreateMessageInput,
6
+ type DiscordMessageObject,
7
+ type DiscordRestClient,
8
+ type DiscordUser,
9
+ } from '../discord-rest'
10
+ import { DISCORD_CONVERSATION_PREFIX } from '../normalize-inbound'
11
+
12
+ function makeSigner() {
13
+ const { publicKey, privateKey } = generateKeyPairSync('ed25519')
14
+ const spki = publicKey.export({ type: 'spki', format: 'der' }) as Buffer
15
+ return {
16
+ publicKeyHex: spki.subarray(spki.length - 32).toString('hex'),
17
+ sign: (message: string) => cryptoSign(null, Buffer.from(message, 'utf-8'), privateKey).toString('hex'),
18
+ }
19
+ }
20
+
21
+ function stubRest(overrides: Partial<DiscordRestClient> = {}): {
22
+ captured: { createMessage?: CreateMessageInput }
23
+ } {
24
+ const captured: { createMessage?: CreateMessageInput } = {}
25
+ const base: DiscordRestClient = {
26
+ async createMessage(_auth, input): Promise<DiscordMessageObject> {
27
+ captured.createMessage = input
28
+ return {
29
+ id: 'sent-1',
30
+ channel_id: input.channelId,
31
+ content: input.content,
32
+ author: { id: 'bot', username: 'bot' },
33
+ timestamp: '2026-06-19T10:00:00.000Z',
34
+ }
35
+ },
36
+ async editMessage(_a, channelId, _m, content) {
37
+ return { id: 'm', channel_id: channelId, content, author: { id: 'b', username: 'b' }, timestamp: '' }
38
+ },
39
+ async deleteMessage() {},
40
+ async addReaction() {},
41
+ async removeReaction() {},
42
+ async listMessages() {
43
+ return []
44
+ },
45
+ async getCurrentUser(): Promise<DiscordUser> {
46
+ return { id: 'bot-user', username: 'bot' }
47
+ },
48
+ async getGatewayBot() {
49
+ return { url: 'wss://gateway', shards: 1 }
50
+ },
51
+ async registerGuildCommands() {},
52
+ ...overrides,
53
+ }
54
+ setDiscordRestClient(base)
55
+ return { captured }
56
+ }
57
+
58
+ const credentials = {
59
+ botToken: 'bot-token-abc',
60
+ applicationId: '123',
61
+ publicKey: 'a'.repeat(64),
62
+ }
63
+
64
+ afterEach(() => setDiscordRestClient(null))
65
+
66
+ describe('DiscordChannelAdapter.sendMessage', () => {
67
+ it('builds a REST createMessage request against the resolved channel id', async () => {
68
+ const { captured } = stubRest()
69
+ const adapter = getDiscordChannelAdapter()
70
+ const result = await adapter.sendMessage({
71
+ conversationId: `${DISCORD_CONVERSATION_PREFIX}chan-77`,
72
+ content: { text: 'hi there', bodyFormat: 'markdown' },
73
+ credentials,
74
+ scope: { organizationId: 'o', tenantId: 't' },
75
+ })
76
+ expect(captured.createMessage?.channelId).toBe('chan-77')
77
+ expect(captured.createMessage?.content).toBe('hi there')
78
+ expect(result.status).toBe('sent')
79
+ expect(result.externalMessageId).toBe('sent-1')
80
+ expect(result.conversationId).toBe(`${DISCORD_CONVERSATION_PREFIX}chan-77`)
81
+ })
82
+
83
+ it('falls back to defaultChannelId when no conversation id is given', async () => {
84
+ const { captured } = stubRest()
85
+ const adapter = getDiscordChannelAdapter()
86
+ await adapter.sendMessage({
87
+ content: { text: 'x' },
88
+ credentials: { ...credentials, defaultChannelId: 'default-chan' },
89
+ scope: { organizationId: 'o', tenantId: 't' },
90
+ })
91
+ expect(captured.createMessage?.channelId).toBe('default-chan')
92
+ })
93
+
94
+ it('posts to the recipient the hub resolved, instead of silently using the default', async () => {
95
+ // QA of #4391 against a live bot: the hub's test-send route puts the
96
+ // operator's recipient in `metadata.to`, nothing read it, and every send
97
+ // went to `defaultChannelId` while the endpoint answered `200 sent`. A
98
+ // recipient that is validated and then discarded is worse than one that is
99
+ // rejected — the operator gets a success for a message that went elsewhere.
100
+ const { captured } = stubRest()
101
+ const adapter = getDiscordChannelAdapter()
102
+ await adapter.sendMessage({
103
+ content: { text: 'x' },
104
+ credentials: { ...credentials, defaultChannelId: 'default-chan' },
105
+ metadata: { to: 'requested-chan', testSend: true },
106
+ scope: { organizationId: 'o', tenantId: 't' },
107
+ })
108
+ expect(captured.createMessage?.channelId).toBe('requested-chan')
109
+ })
110
+
111
+ it('never lets a stray recipient redirect a reply out of its own conversation', async () => {
112
+ // `conversationId` outranks `metadata.to` on purpose: on a reply the thread
113
+ // mapping is authoritative.
114
+ const { captured } = stubRest()
115
+ const adapter = getDiscordChannelAdapter()
116
+ await adapter.sendMessage({
117
+ conversationId: `${DISCORD_CONVERSATION_PREFIX}thread-chan`,
118
+ content: { text: 'x' },
119
+ credentials: { ...credentials, defaultChannelId: 'default-chan' },
120
+ metadata: { to: 'somewhere-else' },
121
+ scope: { organizationId: 'o', tenantId: 't' },
122
+ })
123
+ expect(captured.createMessage?.channelId).toBe('thread-chan')
124
+ })
125
+
126
+ it('clamps content to Discord 2000-char limit', async () => {
127
+ const { captured } = stubRest()
128
+ const adapter = getDiscordChannelAdapter()
129
+ await adapter.sendMessage({
130
+ conversationId: 'chan',
131
+ content: { text: 'a'.repeat(5000), bodyFormat: 'markdown' },
132
+ credentials,
133
+ scope: { organizationId: 'o', tenantId: 't' },
134
+ })
135
+ expect(captured.createMessage?.content.length).toBe(2000)
136
+ })
137
+ })
138
+
139
+ describe('DiscordChannelAdapter.verifyWebhook (fail-closed)', () => {
140
+ const signer = makeSigner()
141
+ const creds = { ...credentials, publicKey: signer.publicKeyHex }
142
+ const timestamp = '1700000000'
143
+ const rawBody = JSON.stringify({ type: 1 })
144
+
145
+ it('returns eventType other for a validly signed interaction', async () => {
146
+ const adapter = getDiscordChannelAdapter()
147
+ const result = await adapter.verifyWebhook({
148
+ rawBody,
149
+ headers: {
150
+ 'x-signature-ed25519': signer.sign(timestamp + rawBody),
151
+ 'x-signature-timestamp': timestamp,
152
+ },
153
+ credentials: creds,
154
+ scope: { organizationId: 'o', tenantId: 't' },
155
+ })
156
+ expect(result.eventType).toBe('other')
157
+ })
158
+
159
+ it('throws on a tampered interaction signature', async () => {
160
+ const adapter = getDiscordChannelAdapter()
161
+ await expect(
162
+ adapter.verifyWebhook({
163
+ rawBody,
164
+ headers: {
165
+ 'x-signature-ed25519': signer.sign(timestamp + '{"type":2}'),
166
+ 'x-signature-timestamp': timestamp,
167
+ },
168
+ credentials: creds,
169
+ scope: { organizationId: 'o', tenantId: 't' },
170
+ }),
171
+ ).rejects.toThrow()
172
+ })
173
+
174
+ it('returns eventType other for a non-interaction body (gateway-delivered messages)', async () => {
175
+ const adapter = getDiscordChannelAdapter()
176
+ const result = await adapter.verifyWebhook({
177
+ rawBody: JSON.stringify({ not: 'an interaction' }),
178
+ headers: {},
179
+ credentials: creds,
180
+ scope: { organizationId: 'o', tenantId: 't' },
181
+ })
182
+ expect(result.eventType).toBe('other')
183
+ })
184
+ })
185
+
186
+ describe('DiscordChannelAdapter.validateCredentials', () => {
187
+ it('accepts a good token (users/@me succeeds)', async () => {
188
+ stubRest()
189
+ const adapter = getDiscordChannelAdapter()
190
+ const result = await adapter.validateCredentials({ providerKey: 'discord', credentials, scope: { organizationId: 'o', tenantId: 't' } })
191
+ expect(result.ok).toBe(true)
192
+ })
193
+
194
+ it('rejects a bad token with a field error', async () => {
195
+ stubRest({
196
+ async getCurrentUser() {
197
+ const { DiscordApiError } = await import('../discord-rest')
198
+ throw new DiscordApiError('unauthorized', 401, 'unauthorized')
199
+ },
200
+ })
201
+ const adapter = getDiscordChannelAdapter()
202
+ const result = await adapter.validateCredentials({ providerKey: 'discord', credentials, scope: { organizationId: 'o', tenantId: 't' } })
203
+ expect(result.ok).toBe(false)
204
+ expect(result.errors?.botToken).toBeTruthy()
205
+ })
206
+ })
@@ -0,0 +1,61 @@
1
+ import { missingAgentFeatures } from '../ai-agent-directory'
2
+
3
+ /**
4
+ * The authorization half of "can auto-reply invoke this agent" (issue #4778).
5
+ *
6
+ * `listDiscordEligibleAgents` only answers the shape question — object-mode, so
7
+ * `runAiAgentObject` will take the call. The runtime then runs the agent's
8
+ * `requiredFeatures` against the auto-reply principal, and an agent that clears
9
+ * the first check but fails the second is exactly the setting that arms a channel
10
+ * which then answers nothing, forever, inside a background subscriber.
11
+ *
12
+ * These cases pin the comparison against the platform's real `authorizeFeatures`
13
+ * rather than a stub, because the wildcard rows are the ones a hand-rolled
14
+ * `Set.has` would get wrong — and getting them wrong means refusing a channel-bot
15
+ * user whose role legitimately covers the agent.
16
+ */
17
+ describe('missingAgentFeatures', () => {
18
+ const principal = (features: string[], isSuperAdmin = false) => ({ features, isSuperAdmin })
19
+
20
+ it('treats an agent that requires nothing as invocable by anyone', () => {
21
+ expect(missingAgentFeatures([], principal([]))).toEqual([])
22
+ expect(missingAgentFeatures(undefined, principal([]))).toEqual([])
23
+ })
24
+
25
+ it('accepts an exact grant', () => {
26
+ expect(missingAgentFeatures(['customers.view'], principal(['customers.view']))).toEqual([])
27
+ })
28
+
29
+ it('names every required feature the principal does not hold', () => {
30
+ const missing = missingAgentFeatures(
31
+ ['customers.view', 'customers.manage', 'sales.view'],
32
+ principal(['customers.view']),
33
+ )
34
+ expect(missing).toEqual(['customers.manage', 'sales.view'])
35
+ })
36
+
37
+ it('honours a module wildcard grant the way the runtime does', () => {
38
+ expect(missingAgentFeatures(['customers.view'], principal(['customers.*']))).toEqual([])
39
+ })
40
+
41
+ it('honours a global wildcard grant', () => {
42
+ expect(missingAgentFeatures(['customers.view', 'sales.view'], principal(['*']))).toEqual([])
43
+ })
44
+
45
+ it('does not let a sibling-module grant stand in for the required one', () => {
46
+ expect(missingAgentFeatures(['customers.view'], principal(['sales.*']))).toEqual(['customers.view'])
47
+ })
48
+
49
+ it('reports the provider agent as invocable under the service principal alone', () => {
50
+ // The provider's own agent is what has to work with no operator setup at all.
51
+ const missing = missingAgentFeatures(
52
+ ['channel_discord.ai_auto_reply.run'],
53
+ principal(['channel_discord.ai_auto_reply.run']),
54
+ )
55
+ expect(missing).toEqual([])
56
+ })
57
+
58
+ it('is not satisfied by an empty principal, which is what a features:[] regression looks like', () => {
59
+ expect(missingAgentFeatures(['customers.view'], principal([]))).toEqual(['customers.view'])
60
+ })
61
+ })