@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,104 @@
1
+ import { z } from "zod";
2
+ import { registerCommand } from "@open-mercato/shared/lib/commands";
3
+ import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
4
+ import { enforceCommandOptimisticLockWithGuards } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
5
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
6
+ import { CommunicationChannel } from "@open-mercato/core/modules/communication_channels/data/entities";
7
+ import { channelOrgScopeWhereFromFilter } from "@open-mercato/core/modules/communication_channels/lib/access-control";
8
+ import { discordChannelStateSchema } from "../lib/credentials.js";
9
+ const CHANNEL_DISCORD_CHANNEL_RESOURCE_KIND = "communication_channels.channel";
10
+ const CHANNEL_DISCORD_UPDATE_AI_AUTO_REPLY_COMMAND_ID = "channel_discord.channel.update_ai_auto_reply";
11
+ const discordAiAutoReplySettingsSchema = z.object({
12
+ aiAutoReplyEnabled: z.boolean(),
13
+ aiAgentId: z.string().trim().min(1).max(200).optional()
14
+ }).refine((value) => !value.aiAutoReplyEnabled || Boolean(value.aiAgentId), {
15
+ // Arming auto-reply without naming an agent is the state that made the
16
+ // original subscriber dormant; the schema refuses to reproduce it.
17
+ path: ["aiAgentId"],
18
+ message: "channel_discord.aiAutoReply.errors.agentRequired"
19
+ });
20
+ const updateAiAutoReplySchema = z.object({
21
+ channelId: z.string().uuid(),
22
+ settings: discordAiAutoReplySettingsSchema,
23
+ scope: z.object({
24
+ tenantId: z.string().uuid(),
25
+ /** Organization the decryption scope is anchored to. */
26
+ organizationId: z.string().uuid().nullable(),
27
+ /**
28
+ * Organizations the caller may see, as resolved by the route. A Discord bot
29
+ * channel is usually tenant-scoped (`organization_id IS NULL`), so filtering
30
+ * on a single organization would hide the very channels this command
31
+ * configures — the hub's `channelOrgScopeWhereFromFilter` encodes the right
32
+ * rule and is reused here.
33
+ */
34
+ organizationIds: z.array(z.string().uuid()).nullable().optional()
35
+ }),
36
+ expectedUpdatedAt: z.string().optional()
37
+ });
38
+ const updateAiAutoReplyCommand = {
39
+ id: CHANNEL_DISCORD_UPDATE_AI_AUTO_REPLY_COMMAND_ID,
40
+ // Not undoable on purpose: replaying a stale snapshot could re-arm auto-reply
41
+ // on a channel an operator deliberately disarmed — for example after the bot
42
+ // answered something it should not have. Reverting is a deliberate act through
43
+ // the same form, which leaves its own audit entry.
44
+ isUndoable: false,
45
+ async execute(rawInput, ctx) {
46
+ const input = updateAiAutoReplySchema.parse(rawInput);
47
+ const em = ctx.container.resolve("em").fork();
48
+ const dscope = { tenantId: input.scope.tenantId, organizationId: input.scope.organizationId };
49
+ const channel = await findOneWithDecryption(
50
+ em,
51
+ CommunicationChannel,
52
+ {
53
+ id: input.channelId,
54
+ tenantId: input.scope.tenantId,
55
+ providerKey: "discord",
56
+ ...channelOrgScopeWhereFromFilter({ organizationIds: input.scope.organizationIds ?? void 0 }),
57
+ deletedAt: null
58
+ },
59
+ void 0,
60
+ dscope
61
+ );
62
+ if (!channel) return { status: "not_found" };
63
+ await enforceCommandOptimisticLockWithGuards(ctx.container, {
64
+ resourceKind: CHANNEL_DISCORD_CHANNEL_RESOURCE_KIND,
65
+ resourceId: channel.id,
66
+ current: channel.updatedAt,
67
+ expected: input.expectedUpdatedAt ?? null,
68
+ request: ctx.request ?? null
69
+ });
70
+ const current = discordChannelStateSchema.parse(channel.channelState ?? {});
71
+ const aiAgentId = input.settings.aiAutoReplyEnabled ? input.settings.aiAgentId : void 0;
72
+ await withAtomicFlush(
73
+ em,
74
+ [
75
+ () => {
76
+ channel.channelState = {
77
+ ...current,
78
+ aiAutoReplyEnabled: input.settings.aiAutoReplyEnabled,
79
+ // Clearing the agent when auto-reply is switched off keeps the stored
80
+ // state honest: a disabled channel does not keep pointing at an agent.
81
+ aiAgentId
82
+ };
83
+ }
84
+ ],
85
+ { transaction: true, label: "channel_discord.channel.update_ai_auto_reply" }
86
+ );
87
+ return {
88
+ status: "updated",
89
+ channelId: channel.id,
90
+ aiAutoReplyEnabled: input.settings.aiAutoReplyEnabled,
91
+ aiAgentId: aiAgentId ?? null,
92
+ updatedAt: channel.updatedAt ? channel.updatedAt.toISOString() : null
93
+ };
94
+ }
95
+ };
96
+ registerCommand(updateAiAutoReplyCommand);
97
+ var update_ai_auto_reply_default = updateAiAutoReplyCommand;
98
+ export {
99
+ CHANNEL_DISCORD_CHANNEL_RESOURCE_KIND,
100
+ CHANNEL_DISCORD_UPDATE_AI_AUTO_REPLY_COMMAND_ID,
101
+ update_ai_auto_reply_default as default,
102
+ discordAiAutoReplySettingsSchema
103
+ };
104
+ //# sourceMappingURL=update-ai-auto-reply.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/commands/update-ai-auto-reply.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel } from '@open-mercato/core/modules/communication_channels/data/entities'\nimport { channelOrgScopeWhereFromFilter } from '@open-mercato/core/modules/communication_channels/lib/access-control'\nimport { discordChannelStateSchema } from '../lib/credentials'\n\n/**\n * Resource kind the channel's optimistic-lock check is scoped under. It matches\n * the `resourceKind` the route's mutation guard uses, so an operator's stale tab\n * fails the same way here as it would on any other channel write.\n */\nexport const CHANNEL_DISCORD_CHANNEL_RESOURCE_KIND = 'communication_channels.channel'\n\nexport const CHANNEL_DISCORD_UPDATE_AI_AUTO_REPLY_COMMAND_ID =\n 'channel_discord.channel.update_ai_auto_reply'\n\nexport const discordAiAutoReplySettingsSchema = z\n .object({\n aiAutoReplyEnabled: z.boolean(),\n aiAgentId: z.string().trim().min(1).max(200).optional(),\n })\n .refine((value) => !value.aiAutoReplyEnabled || Boolean(value.aiAgentId), {\n // Arming auto-reply without naming an agent is the state that made the\n // original subscriber dormant; the schema refuses to reproduce it.\n path: ['aiAgentId'],\n message: 'channel_discord.aiAutoReply.errors.agentRequired',\n })\n\nexport type DiscordAiAutoReplySettings = z.infer<typeof discordAiAutoReplySettingsSchema>\n\nconst updateAiAutoReplySchema = z.object({\n channelId: z.string().uuid(),\n settings: discordAiAutoReplySettingsSchema,\n scope: z.object({\n tenantId: z.string().uuid(),\n /** Organization the decryption scope is anchored to. */\n organizationId: z.string().uuid().nullable(),\n /**\n * Organizations the caller may see, as resolved by the route. A Discord bot\n * channel is usually tenant-scoped (`organization_id IS NULL`), so filtering\n * on a single organization would hide the very channels this command\n * configures \u2014 the hub's `channelOrgScopeWhereFromFilter` encodes the right\n * rule and is reused here.\n */\n organizationIds: z.array(z.string().uuid()).nullable().optional(),\n }),\n expectedUpdatedAt: z.string().optional(),\n})\n\nexport type UpdateAiAutoReplyInput = z.infer<typeof updateAiAutoReplySchema>\n\nexport type UpdateAiAutoReplyResult =\n | {\n status: 'updated'\n channelId: string\n aiAutoReplyEnabled: boolean\n aiAgentId: string | null\n updatedAt: string | null\n }\n | { status: 'not_found' }\n\n/**\n * Persist a Discord channel's AI auto-reply settings (issue #4778).\n *\n * The write goes through the command bus rather than an entity write in the\n * route so it inherits the platform's audit trail, and so the two keys the\n * subscriber reads have exactly one writer.\n *\n * Two things it is careful about:\n *\n * - **It merges.** `channelState` is shared with the gateway worker, which writes\n * resume state (`sessionId`, `sequence`, `resumeGatewayUrl`, `botUserId`) from\n * a socket callback. Replacing the blob would drop a live session's cursor and\n * force a re-IDENTIFY, so only the two AI keys are touched \u2014 the mirror image\n * of what `lib/channel-state-store.ts` does from the other side.\n * - **It version-checks.** The settings form sends the channel's `updatedAt`, so\n * a save from a tab opened before someone else changed the channel fails with\n * the standard 409 conflict body instead of silently winning.\n */\nconst updateAiAutoReplyCommand: CommandHandler<UpdateAiAutoReplyInput, UpdateAiAutoReplyResult> = {\n id: CHANNEL_DISCORD_UPDATE_AI_AUTO_REPLY_COMMAND_ID,\n // Not undoable on purpose: replaying a stale snapshot could re-arm auto-reply\n // on a channel an operator deliberately disarmed \u2014 for example after the bot\n // answered something it should not have. Reverting is a deliberate act through\n // the same form, which leaves its own audit entry.\n isUndoable: false,\n async execute(rawInput, ctx) {\n const input = updateAiAutoReplySchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const dscope = { tenantId: input.scope.tenantId, organizationId: input.scope.organizationId }\n\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: input.channelId,\n tenantId: input.scope.tenantId,\n providerKey: 'discord',\n ...channelOrgScopeWhereFromFilter({ organizationIds: input.scope.organizationIds ?? undefined }),\n deletedAt: null,\n },\n undefined,\n dscope,\n )\n // Existence masking, consistent with the hub's channel-scoped routes: a\n // channel in another tenant and a channel that never existed answer alike.\n if (!channel) return { status: 'not_found' }\n\n // The DI-aware seam, not the bare synchronous helper: it runs the OSS\n // `updated_at` floor first and then awaits the optional enterprise\n // `record_locks` guard, so a channel someone else holds a lock on is\n // protected on an enterprise build too.\n await enforceCommandOptimisticLockWithGuards(ctx.container, {\n resourceKind: CHANNEL_DISCORD_CHANNEL_RESOURCE_KIND,\n resourceId: channel.id,\n current: channel.updatedAt,\n expected: input.expectedUpdatedAt ?? null,\n request: ctx.request ?? null,\n })\n\n const current = discordChannelStateSchema.parse(channel.channelState ?? {})\n const aiAgentId = input.settings.aiAutoReplyEnabled ? input.settings.aiAgentId : undefined\n\n await withAtomicFlush(\n em,\n [\n () => {\n channel.channelState = {\n ...current,\n aiAutoReplyEnabled: input.settings.aiAutoReplyEnabled,\n // Clearing the agent when auto-reply is switched off keeps the stored\n // state honest: a disabled channel does not keep pointing at an agent.\n aiAgentId,\n }\n },\n ],\n { transaction: true, label: 'channel_discord.channel.update_ai_auto_reply' },\n )\n\n return {\n status: 'updated',\n channelId: channel.id,\n aiAutoReplyEnabled: input.settings.aiAutoReplyEnabled,\n aiAgentId: aiAgentId ?? null,\n updatedAt: channel.updatedAt ? channel.updatedAt.toISOString() : null,\n }\n },\n}\n\nregisterCommand(updateAiAutoReplyCommand as CommandHandler<unknown, unknown>)\n\nexport default updateAiAutoReplyCommand\n"],
5
+ "mappings": "AACA,SAAS,SAAS;AAElB,SAAS,uBAAuB;AAChC,SAAS,uBAAuB;AAChC,SAAS,8CAA8C;AACvD,SAAS,6BAA6B;AACtC,SAAS,4BAA4B;AACrC,SAAS,sCAAsC;AAC/C,SAAS,iCAAiC;AAOnC,MAAM,wCAAwC;AAE9C,MAAM,kDACX;AAEK,MAAM,mCAAmC,EAC7C,OAAO;AAAA,EACN,oBAAoB,EAAE,QAAQ;AAAA,EAC9B,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS;AACxD,CAAC,EACA,OAAO,CAAC,UAAU,CAAC,MAAM,sBAAsB,QAAQ,MAAM,SAAS,GAAG;AAAA;AAAA;AAAA,EAGxE,MAAM,CAAC,WAAW;AAAA,EAClB,SAAS;AACX,CAAC;AAIH,MAAM,0BAA0B,EAAE,OAAO;AAAA,EACvC,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,EAC3B,UAAU;AAAA,EACV,OAAO,EAAE,OAAO;AAAA,IACd,UAAU,EAAE,OAAO,EAAE,KAAK;AAAA;AAAA,IAE1B,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQ3C,iBAAiB,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EAClE,CAAC;AAAA,EACD,mBAAmB,EAAE,OAAO,EAAE,SAAS;AACzC,CAAC;AAgCD,MAAM,2BAA4F;AAAA,EAChG,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA,EAKJ,YAAY;AAAA,EACZ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,QAAQ,wBAAwB,MAAM,QAAQ;AACpD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,SAAS,EAAE,UAAU,MAAM,MAAM,UAAU,gBAAgB,MAAM,MAAM,eAAe;AAE5F,UAAM,UAAU,MAAM;AAAA,MACpB;AAAA,MACA;AAAA,MACA;AAAA,QACE,IAAI,MAAM;AAAA,QACV,UAAU,MAAM,MAAM;AAAA,QACtB,aAAa;AAAA,QACb,GAAG,+BAA+B,EAAE,iBAAiB,MAAM,MAAM,mBAAmB,OAAU,CAAC;AAAA,QAC/F,WAAW;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAGA,QAAI,CAAC,QAAS,QAAO,EAAE,QAAQ,YAAY;AAM3C,UAAM,uCAAuC,IAAI,WAAW;AAAA,MAC1D,cAAc;AAAA,MACd,YAAY,QAAQ;AAAA,MACpB,SAAS,QAAQ;AAAA,MACjB,UAAU,MAAM,qBAAqB;AAAA,MACrC,SAAS,IAAI,WAAW;AAAA,IAC1B,CAAC;AAED,UAAM,UAAU,0BAA0B,MAAM,QAAQ,gBAAgB,CAAC,CAAC;AAC1E,UAAM,YAAY,MAAM,SAAS,qBAAqB,MAAM,SAAS,YAAY;AAEjF,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,QACE,MAAM;AACJ,kBAAQ,eAAe;AAAA,YACrB,GAAG;AAAA,YACH,oBAAoB,MAAM,SAAS;AAAA;AAAA;AAAA,YAGnC;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MACA,EAAE,aAAa,MAAM,OAAO,+CAA+C;AAAA,IAC7E;AAEA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,QAAQ;AAAA,MACnB,oBAAoB,MAAM,SAAS;AAAA,MACnC,WAAW,aAAa;AAAA,MACxB,WAAW,QAAQ,YAAY,QAAQ,UAAU,YAAY,IAAI;AAAA,IACnE;AAAA,EACF;AACF;AAEA,gBAAgB,wBAA4D;AAE5E,IAAO,+BAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,23 @@
1
+ import { asValue } from "awilix";
2
+ import {
3
+ hasChannelAdapter,
4
+ registerChannelAdapter
5
+ } from "@open-mercato/core/modules/communication_channels/lib/adapter-registry-singleton";
6
+ import { getDiscordChannelAdapter } from "./lib/adapter.js";
7
+ import { channelDiscordHealthCheck } from "./lib/health.js";
8
+ function register(container) {
9
+ if (!hasChannelAdapter("discord")) {
10
+ registerChannelAdapter(getDiscordChannelAdapter());
11
+ }
12
+ container.register({
13
+ channelDiscordAdapter: asValue(getDiscordChannelAdapter()),
14
+ // Registered under the exact service name declared in `integration.ts`
15
+ // (`healthCheck.service`). Without this, the hub's `container.resolve(...)`
16
+ // throws and the channel reports permanently 'unhealthy'.
17
+ channelDiscordHealthCheck: asValue(channelDiscordHealthCheck)
18
+ });
19
+ }
20
+ export {
21
+ register
22
+ };
23
+ //# sourceMappingURL=di.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/channel_discord/di.ts"],
4
+ "sourcesContent": ["import { asValue } from 'awilix'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n hasChannelAdapter,\n registerChannelAdapter,\n} from '@open-mercato/core/modules/communication_channels/lib/adapter-registry-singleton'\nimport { getDiscordChannelAdapter } from './lib/adapter'\nimport { channelDiscordHealthCheck } from './lib/health'\n\n/**\n * Re-register the adapter on container creation as a safety net for runtime\n * environments that bypass module setup (worker-only nodes, ad-hoc CLI). The\n * underlying registry is process-wide so the registration is idempotent.\n */\nexport function register(container: AppContainer): void {\n if (!hasChannelAdapter('discord')) {\n registerChannelAdapter(getDiscordChannelAdapter())\n }\n container.register({\n channelDiscordAdapter: asValue(getDiscordChannelAdapter()),\n // Registered under the exact service name declared in `integration.ts`\n // (`healthCheck.service`). Without this, the hub's `container.resolve(...)`\n // throws and the channel reports permanently 'unhealthy'.\n channelDiscordHealthCheck: asValue(channelDiscordHealthCheck),\n })\n}\n"],
5
+ "mappings": "AAAA,SAAS,eAAe;AAExB;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,gCAAgC;AACzC,SAAS,iCAAiC;AAOnC,SAAS,SAAS,WAA+B;AACtD,MAAI,CAAC,kBAAkB,SAAS,GAAG;AACjC,2BAAuB,yBAAyB,CAAC;AAAA,EACnD;AACA,YAAU,SAAS;AAAA,IACjB,uBAAuB,QAAQ,yBAAyB,CAAC;AAAA;AAAA;AAAA;AAAA,IAIzD,2BAA2B,QAAQ,yBAAyB;AAAA,EAC9D,CAAC;AACH;",
6
+ "names": []
7
+ }
@@ -0,0 +1,9 @@
1
+ const metadata = {
2
+ id: "channel_discord",
3
+ title: "Discord Communication Channel",
4
+ description: "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."
5
+ };
6
+ export {
7
+ metadata
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/channel_discord/index.ts"],
4
+ "sourcesContent": ["export const metadata = {\n id: 'channel_discord',\n title: 'Discord Communication Channel',\n description:\n '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.',\n}\n"],
5
+ "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aACE;AACJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,96 @@
1
+ import { buildIntegrationDetailWidgetSpotId } from "@open-mercato/shared/modules/integrations/types";
2
+ const channelDiscordDetailWidgetSpotId = buildIntegrationDetailWidgetSpotId("channel_discord");
3
+ const integration = {
4
+ id: "channel_discord",
5
+ title: "Discord",
6
+ // Kept within the 68–132 character range the six sibling providers occupy: the
7
+ // card grid gives every description the same fixed box, so a long one inflates
8
+ // its whole row. The operational caveats it used to carry (gateway worker,
9
+ // queue strategy) belong on the detail page and in the credential help texts,
10
+ // which is where they now live.
11
+ description: "Two-way Discord bot channel: REST outbound, real-time Gateway inbound, optional AI auto-reply.",
12
+ category: "communication",
13
+ hub: "communication_channels",
14
+ providerKey: "discord",
15
+ icon: "discord",
16
+ docsUrl: "https://discord.com/developers/docs/intro",
17
+ package: "@open-mercato/channel-discord",
18
+ // The provider descriptor's own version, deliberately independent of the
19
+ // monorepo release the package happens to ship in — a copied monorepo version
20
+ // rots on every release bump and reads as a claim it is not making.
21
+ version: "1.0.0",
22
+ author: "Open Mercato Team",
23
+ company: "Open Mercato",
24
+ license: "MIT",
25
+ // `ai` returns here in the same change that makes auto-reply real: an agent the
26
+ // repository ships, a service principal with real features, a settings surface
27
+ // that arms it, and an approval surface for everything it must not send.
28
+ tags: ["discord", "chat", "bot", "gateway", "communication", "ai"],
29
+ detailPage: {
30
+ widgetSpotId: channelDiscordDetailWidgetSpotId
31
+ },
32
+ apiVersions: [
33
+ {
34
+ id: "v10",
35
+ label: "Discord API v10",
36
+ status: "stable",
37
+ default: true,
38
+ changelog: "Discord API v10 (REST) + Gateway v10 real-time inbound + Ed25519-signed Interactions."
39
+ }
40
+ ],
41
+ credentials: {
42
+ fields: [
43
+ {
44
+ key: "botToken",
45
+ label: "Bot token",
46
+ type: "secret",
47
+ required: true,
48
+ helpText: "Developer Portal \u2192 Applications \u2192 your app \u2192 Bot \u2192 Reset Token. Grants full control of the bot user. Stored encrypted at rest; never logged."
49
+ },
50
+ {
51
+ key: "applicationId",
52
+ label: "Application ID",
53
+ type: "text",
54
+ required: true,
55
+ placeholder: "123456789012345678",
56
+ helpText: "Developer Portal \u2192 General Information \u2192 Application ID. Used to register slash commands (mercato channel_discord register-slash-commands) and to answer interactions."
57
+ },
58
+ {
59
+ key: "publicKey",
60
+ label: "Public key",
61
+ type: "text",
62
+ required: true,
63
+ placeholder: "64-character hex Ed25519 public key",
64
+ helpText: "Developer Portal \u2192 General Information \u2192 Public Key. Verifies signed interaction requests; a verified slash command or button press is recorded in the inbox and answered back in Discord."
65
+ },
66
+ {
67
+ key: "guildId",
68
+ label: "Guild (server) ID",
69
+ type: "text",
70
+ required: false,
71
+ placeholder: "123456789012345678",
72
+ helpText: "Recommended: scope the bot to a single server. Enable Developer Mode in Discord, right-click the server, Copy Server ID."
73
+ },
74
+ {
75
+ key: "defaultChannelId",
76
+ label: "Default channel ID",
77
+ type: "text",
78
+ required: false,
79
+ placeholder: "123456789012345678",
80
+ helpText: "Default text channel the adapter posts to when an outbound message names no Discord channel. The hub test-send endpoint accepts a channel snowflake as its recipient, so you can use it to verify this end to end."
81
+ }
82
+ ]
83
+ },
84
+ healthCheck: { service: "channelDiscordHealthCheck" }
85
+ };
86
+ const integrations = [integration];
87
+ const bundles = [];
88
+ const bundle = void 0;
89
+ export {
90
+ bundle,
91
+ bundles,
92
+ channelDiscordDetailWidgetSpotId,
93
+ integration,
94
+ integrations
95
+ };
96
+ //# sourceMappingURL=integration.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/channel_discord/integration.ts"],
4
+ "sourcesContent": ["import { buildIntegrationDetailWidgetSpotId, type IntegrationBundle, type IntegrationDefinition } from '@open-mercato/shared/modules/integrations/types'\n\nexport const channelDiscordDetailWidgetSpotId = buildIntegrationDetailWidgetSpotId('channel_discord')\n\nexport const integration: IntegrationDefinition = {\n id: 'channel_discord',\n title: 'Discord',\n // Kept within the 68\u2013132 character range the six sibling providers occupy: the\n // card grid gives every description the same fixed box, so a long one inflates\n // its whole row. The operational caveats it used to carry (gateway worker,\n // queue strategy) belong on the detail page and in the credential help texts,\n // which is where they now live.\n description: 'Two-way Discord bot channel: REST outbound, real-time Gateway inbound, optional AI auto-reply.',\n category: 'communication',\n hub: 'communication_channels',\n providerKey: 'discord',\n icon: 'discord',\n docsUrl: 'https://discord.com/developers/docs/intro',\n package: '@open-mercato/channel-discord',\n // The provider descriptor's own version, deliberately independent of the\n // monorepo release the package happens to ship in \u2014 a copied monorepo version\n // rots on every release bump and reads as a claim it is not making.\n version: '1.0.0',\n author: 'Open Mercato Team',\n company: 'Open Mercato',\n license: 'MIT',\n // `ai` returns here in the same change that makes auto-reply real: an agent the\n // repository ships, a service principal with real features, a settings surface\n // that arms it, and an approval surface for everything it must not send.\n tags: ['discord', 'chat', 'bot', 'gateway', 'communication', 'ai'],\n detailPage: {\n widgetSpotId: channelDiscordDetailWidgetSpotId,\n },\n apiVersions: [\n {\n id: 'v10',\n label: 'Discord API v10',\n status: 'stable',\n default: true,\n changelog: 'Discord API v10 (REST) + Gateway v10 real-time inbound + Ed25519-signed Interactions.',\n },\n ],\n credentials: {\n fields: [\n {\n key: 'botToken',\n label: 'Bot token',\n type: 'secret',\n required: true,\n helpText:\n 'Developer Portal \u2192 Applications \u2192 your app \u2192 Bot \u2192 Reset Token. Grants full control of the bot user. Stored encrypted at rest; never logged.',\n },\n {\n key: 'applicationId',\n label: 'Application ID',\n type: 'text',\n required: true,\n placeholder: '123456789012345678',\n helpText:\n 'Developer Portal \u2192 General Information \u2192 Application ID. Used to register slash commands'\n + ' (mercato channel_discord register-slash-commands) and to answer interactions.',\n },\n {\n key: 'publicKey',\n label: 'Public key',\n type: 'text',\n required: true,\n placeholder: '64-character hex Ed25519 public key',\n helpText:\n 'Developer Portal \u2192 General Information \u2192 Public Key. Verifies signed interaction requests; a verified'\n + ' slash command or button press is recorded in the inbox and answered back in Discord.',\n },\n {\n key: 'guildId',\n label: 'Guild (server) ID',\n type: 'text',\n required: false,\n placeholder: '123456789012345678',\n helpText: 'Recommended: scope the bot to a single server. Enable Developer Mode in Discord, right-click the server, Copy Server ID.',\n },\n {\n key: 'defaultChannelId',\n label: 'Default channel ID',\n type: 'text',\n required: false,\n placeholder: '123456789012345678',\n helpText:\n 'Default text channel the adapter posts to when an outbound message names no Discord channel. The hub'\n + ' test-send endpoint accepts a channel snowflake as its recipient, so you can use it to verify this end to end.',\n },\n ],\n },\n healthCheck: { service: 'channelDiscordHealthCheck' },\n}\n\nexport const integrations: IntegrationDefinition[] = [integration]\nexport const bundles: IntegrationBundle[] = []\nexport const bundle: IntegrationBundle | undefined = undefined\n"],
5
+ "mappings": "AAAA,SAAS,0CAA8F;AAEhG,MAAM,mCAAmC,mCAAmC,iBAAiB;AAE7F,MAAM,cAAqC;AAAA,EAChD,IAAI;AAAA,EACJ,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMP,aAAa;AAAA,EACb,UAAU;AAAA,EACV,KAAK;AAAA,EACL,aAAa;AAAA,EACb,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA;AAAA;AAAA;AAAA,EAIT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA;AAAA;AAAA;AAAA,EAIT,MAAM,CAAC,WAAW,QAAQ,OAAO,WAAW,iBAAiB,IAAI;AAAA,EACjE,YAAY;AAAA,IACV,cAAc;AAAA,EAChB;AAAA,EACA,aAAa;AAAA,IACX;AAAA,MACE,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,WAAW;AAAA,IACb;AAAA,EACF;AAAA,EACA,aAAa;AAAA,IACX,QAAQ;AAAA,MACN;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,UACE;AAAA,MACJ;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,QACb,UACE;AAAA,MAEJ;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,QACb,UACE;AAAA,MAEJ;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,QACb,UAAU;AAAA,MACZ;AAAA,MACA;AAAA,QACE,KAAK;AAAA,QACL,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,QACV,aAAa;AAAA,QACb,UACE;AAAA,MAEJ;AAAA,IACF;AAAA,EACF;AAAA,EACA,aAAa,EAAE,SAAS,4BAA4B;AACtD;AAEO,MAAM,eAAwC,CAAC,WAAW;AAC1D,MAAM,UAA+B,CAAC;AACtC,MAAM,SAAwC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,248 @@
1
+ import { discordCapabilities } from "./capabilities.js";
2
+ import { parseDiscordCredentialsOrThrow, discordCredentialsSchema } from "./credentials.js";
3
+ import {
4
+ DiscordApiError,
5
+ getDiscordRestClient
6
+ } from "./discord-rest.js";
7
+ import { convertOutboundForDiscord } from "./convert-outbound.js";
8
+ import {
9
+ DISCORD_CONVERSATION_PREFIX,
10
+ normalizeInboundDiscordMessage
11
+ } from "./normalize-inbound.js";
12
+ import {
13
+ parseInteractionBody,
14
+ verifyDiscordSignature
15
+ } from "./interactions-verify.js";
16
+ class DiscordChannelAdapter {
17
+ constructor() {
18
+ this.providerKey = "discord";
19
+ this.channelType = "discord";
20
+ this.capabilities = discordCapabilities;
21
+ }
22
+ async sendMessage(input) {
23
+ const credentials = parseDiscordCredentialsOrThrow(input.credentials);
24
+ let native;
25
+ try {
26
+ native = await convertOutboundForDiscord({
27
+ body: input.content.text ?? input.content.html ?? "",
28
+ bodyFormat: input.content.bodyFormat ?? (input.content.html ? "html" : "text"),
29
+ attachments: input.content.attachments,
30
+ channelMetadata: input.metadata
31
+ });
32
+ } catch (error) {
33
+ const message = error instanceof Error ? error.message : "Outbound conversion failed";
34
+ return { externalMessageId: "", status: "failed", error: message };
35
+ }
36
+ const targetChannelId = resolveTargetChannelId(input, credentials.defaultChannelId);
37
+ if (!targetChannelId) {
38
+ return {
39
+ externalMessageId: "",
40
+ status: "failed",
41
+ error: "[internal] Discord send requires a target channel id (conversationId or defaultChannelId)"
42
+ };
43
+ }
44
+ const meta = native.metadata ?? {};
45
+ try {
46
+ const message = await getDiscordRestClient().createMessage(
47
+ { botToken: credentials.botToken },
48
+ {
49
+ channelId: targetChannelId,
50
+ content: native.content.text ?? "",
51
+ messageReferenceId: typeof meta.messageReferenceId === "string" ? meta.messageReferenceId : void 0,
52
+ allowedMentions: meta.allowedMentions ?? { parse: [] }
53
+ }
54
+ );
55
+ return {
56
+ externalMessageId: message.id,
57
+ conversationId: `${DISCORD_CONVERSATION_PREFIX}${message.channel_id}`,
58
+ status: "sent",
59
+ metadata: { discordChannelId: message.channel_id, discordMessageId: message.id }
60
+ };
61
+ } catch (error) {
62
+ if (error instanceof DiscordApiError && error.status === 401) {
63
+ return { externalMessageId: "", status: "failed", error: "requires_reauth" };
64
+ }
65
+ const message = error instanceof Error ? error.message : "Discord send failed";
66
+ return { externalMessageId: "", status: "failed", error: message };
67
+ }
68
+ }
69
+ async verifyWebhook(input) {
70
+ const rawBody = typeof input.rawBody === "string" ? input.rawBody : input.rawBody.toString("utf-8");
71
+ const interaction = parseInteractionBody(rawBody);
72
+ if (!interaction) {
73
+ return { raw: {}, eventType: "other", metadata: { reason: "discord-uses-gateway-for-messages" } };
74
+ }
75
+ const parsed = discordCredentialsSchema.safeParse(input.credentials);
76
+ const publicKeyHex = parsed.success ? parsed.data.publicKey : "";
77
+ const headers = normalizeHeaders(input.headers);
78
+ const ok = verifyDiscordSignature({
79
+ publicKeyHex,
80
+ signatureHex: headers["x-signature-ed25519"],
81
+ timestamp: headers["x-signature-timestamp"],
82
+ rawBody
83
+ });
84
+ if (!ok) {
85
+ throw new Error("[internal] Discord interaction signature verification failed");
86
+ }
87
+ return { raw: { discordInteraction: interaction }, eventType: "other", metadata: { interactionType: interaction.type } };
88
+ }
89
+ async getStatus(_input) {
90
+ return { status: "sent" };
91
+ }
92
+ async convertOutbound(input) {
93
+ return convertOutboundForDiscord(input);
94
+ }
95
+ async normalizeInbound(raw) {
96
+ const message = pickDiscordMessage(raw);
97
+ return normalizeInboundDiscordMessage(message);
98
+ }
99
+ async normalizeInboundReaction(raw) {
100
+ const payload = raw.raw?.discordReaction ?? raw.raw;
101
+ const emojiName = payload.emoji?.name ?? "";
102
+ const emoji = payload.emoji?.id ? `${emojiName}:${payload.emoji.id}` : emojiName;
103
+ return {
104
+ externalMessageId: payload.message_id ?? "",
105
+ externalConversationId: payload.channel_id ? `${DISCORD_CONVERSATION_PREFIX}${payload.channel_id}` : void 0,
106
+ emoji,
107
+ userIdentifier: payload.user_id ?? "unknown",
108
+ userDisplayName: payload.member?.user?.global_name ?? payload.member?.user?.username,
109
+ action: payload.action === "removed" ? "removed" : "added",
110
+ raw: payload
111
+ };
112
+ }
113
+ async sendReaction(input) {
114
+ const credentials = parseDiscordCredentialsOrThrow(input.credentials);
115
+ const channelId = stripConversationPrefix(input.conversationId) ?? credentials.defaultChannelId;
116
+ if (!channelId) throw new Error("[internal] Discord sendReaction requires a channel id");
117
+ await getDiscordRestClient().addReaction(
118
+ { botToken: credentials.botToken },
119
+ channelId,
120
+ input.externalMessageId,
121
+ input.emoji
122
+ );
123
+ }
124
+ async removeReaction(input) {
125
+ const credentials = parseDiscordCredentialsOrThrow(input.credentials);
126
+ const channelId = stripConversationPrefix(input.conversationId) ?? credentials.defaultChannelId;
127
+ if (!channelId) throw new Error("[internal] Discord removeReaction requires a channel id");
128
+ await getDiscordRestClient().removeReaction(
129
+ { botToken: credentials.botToken },
130
+ channelId,
131
+ input.externalMessageId,
132
+ input.emoji
133
+ );
134
+ }
135
+ async editMessage(input) {
136
+ const credentials = parseDiscordCredentialsOrThrow(input.credentials);
137
+ const channelId = stripConversationPrefix(input.conversationId) ?? credentials.defaultChannelId;
138
+ if (!channelId) throw new Error("[internal] Discord editMessage requires a channel id");
139
+ const content = input.newContent.text ?? input.newContent.html ?? "";
140
+ await getDiscordRestClient().editMessage(
141
+ { botToken: credentials.botToken },
142
+ channelId,
143
+ input.externalMessageId,
144
+ content
145
+ );
146
+ }
147
+ async deleteMessage(input) {
148
+ const credentials = parseDiscordCredentialsOrThrow(input.credentials);
149
+ const channelId = stripConversationPrefix(input.conversationId) ?? credentials.defaultChannelId;
150
+ if (!channelId) throw new Error("[internal] Discord deleteMessage requires a channel id");
151
+ await getDiscordRestClient().deleteMessage(
152
+ { botToken: credentials.botToken },
153
+ channelId,
154
+ input.externalMessageId
155
+ );
156
+ }
157
+ async fetchHistory(input) {
158
+ const credentials = parseDiscordCredentialsOrThrow(input.credentials);
159
+ const channelId = stripConversationPrefix(input.conversationId) ?? credentials.defaultChannelId;
160
+ if (!channelId) {
161
+ return { messages: [], hasMore: false };
162
+ }
163
+ const limit = Math.min(Math.max(input.limit ?? 50, 1), 100);
164
+ const raw = await getDiscordRestClient().listMessages(
165
+ { botToken: credentials.botToken },
166
+ channelId,
167
+ { before: input.cursor, limit }
168
+ );
169
+ const messages = raw.map((message) => normalizeInboundDiscordMessage(message));
170
+ const oldest = raw.length > 0 ? raw[raw.length - 1].id : void 0;
171
+ return {
172
+ messages,
173
+ nextCursor: raw.length >= limit ? oldest : void 0,
174
+ hasMore: raw.length >= limit
175
+ };
176
+ }
177
+ async resolveContact(input) {
178
+ const userId = input.senderIdentifier;
179
+ if (!userId || userId === "unknown") return null;
180
+ const displayName = input.senderDisplayName || (typeof input.channelMetadata?.discordAuthorUsername === "string" ? input.channelMetadata.discordAuthorUsername : void 0);
181
+ return {
182
+ displayName,
183
+ externalProfileUrl: `https://discord.com/users/${encodeURIComponent(userId)}`
184
+ };
185
+ }
186
+ async validateCredentials(input) {
187
+ const parsed = discordCredentialsSchema.safeParse(input.credentials);
188
+ if (!parsed.success) {
189
+ const errors = {};
190
+ for (const issue of parsed.error.issues) {
191
+ const path = issue.path[0];
192
+ if (typeof path !== "string") continue;
193
+ if (!errors[path]) errors[path] = issue.message;
194
+ }
195
+ return { ok: false, errors };
196
+ }
197
+ try {
198
+ await getDiscordRestClient().getCurrentUser({ botToken: parsed.data.botToken });
199
+ return { ok: true };
200
+ } catch (error) {
201
+ const status = error instanceof DiscordApiError ? error.status : 0;
202
+ return {
203
+ ok: false,
204
+ errors: {
205
+ botToken: status === 401 ? "Discord rejected the bot token. Reset it in the Developer Portal and paste the new value." : "Could not reach Discord to validate the bot token. Check the token and try again."
206
+ }
207
+ };
208
+ }
209
+ }
210
+ }
211
+ function normalizeHeaders(headers) {
212
+ const out = {};
213
+ for (const [key, value] of Object.entries(headers)) {
214
+ if (value === void 0) continue;
215
+ out[key.toLowerCase()] = Array.isArray(value) ? value[0] ?? "" : value;
216
+ }
217
+ return out;
218
+ }
219
+ function stripConversationPrefix(conversationId) {
220
+ if (!conversationId) return void 0;
221
+ return conversationId.startsWith(DISCORD_CONVERSATION_PREFIX) ? conversationId.slice(DISCORD_CONVERSATION_PREFIX.length) : conversationId;
222
+ }
223
+ function resolveTargetChannelId(input, defaultChannelId) {
224
+ const fromMeta = input.metadata?.discordChannelId;
225
+ if (typeof fromMeta === "string" && fromMeta.length > 0) return fromMeta;
226
+ const fromConversation = stripConversationPrefix(input.conversationId);
227
+ if (fromConversation && fromConversation.length > 0) return fromConversation;
228
+ const fromRecipient = input.metadata?.to;
229
+ if (typeof fromRecipient === "string" && fromRecipient.length > 0) return fromRecipient;
230
+ return defaultChannelId;
231
+ }
232
+ function pickDiscordMessage(raw) {
233
+ const candidate = raw.raw?.discordMessage ?? raw.raw;
234
+ if (!candidate || typeof candidate !== "object" || typeof candidate.id !== "string") {
235
+ throw new Error("[internal] Discord normalizeInbound requires `raw.discordMessage` to be a Discord message object");
236
+ }
237
+ return candidate;
238
+ }
239
+ let cachedAdapter = null;
240
+ function getDiscordChannelAdapter() {
241
+ if (!cachedAdapter) cachedAdapter = new DiscordChannelAdapter();
242
+ return cachedAdapter;
243
+ }
244
+ export {
245
+ DiscordChannelAdapter,
246
+ getDiscordChannelAdapter
247
+ };
248
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/adapter.ts"],
4
+ "sourcesContent": ["import type {\n ChannelAdapter,\n ChannelNativeContent,\n ContactHint,\n ConvertOutboundInput,\n DeleteChannelMessageInput,\n EditChannelMessageInput,\n FetchHistoryInput,\n GetMessageStatusInput,\n HistoryPage,\n InboundMessage,\n InboundReactionEvent,\n MessageStatus,\n NormalizedInboundMessage,\n RemoveReactionInput,\n ResolveContactInput,\n SendMessageInput,\n SendMessageResult,\n SendReactionInput,\n ValidateCredentialsInput,\n ValidateCredentialsResult,\n VerifyWebhookInput,\n} from '@open-mercato/core/modules/communication_channels/lib/adapter'\nimport { discordCapabilities } from './capabilities'\nimport { parseDiscordCredentialsOrThrow, discordCredentialsSchema } from './credentials'\nimport {\n DiscordApiError,\n getDiscordRestClient,\n type DiscordMessageObject,\n} from './discord-rest'\nimport { convertOutboundForDiscord } from './convert-outbound'\nimport {\n DISCORD_CONVERSATION_PREFIX,\n normalizeInboundDiscordMessage,\n} from './normalize-inbound'\nimport {\n parseInteractionBody,\n verifyDiscordSignature,\n} from './interactions-verify'\n\n/**\n * Discord `ChannelAdapter` (SPEC 2026-06-19).\n *\n * Transport split:\n * - Outbound: Discord REST API (`sendMessage`, edit/delete/reactions).\n * - Inbound messages: a provider-owned **Gateway worker** pushes\n * `MESSAGE_CREATE` events into the hub's ingest command \u2014 NOT this\n * `verifyWebhook` hook (Discord does not POST normal messages to a webhook).\n * - Slash commands / buttons: a signed **Interactions** endpoint\n * (`api/interactions` \u2192 `/api/channel_discord/interactions`) with Ed25519 verification.\n *\n * `verifyWebhook` security contract (fail-closed): the shared\n * `api/post/webhook/[provider]` route treats a non-throwing return as \"verified\".\n * For a Discord interaction body we verify the Ed25519 signature and THROW on any\n * failure; for a non-interaction body (a normal message never arrives here) we\n * return `eventType: 'other'` so the generic route acks without tenant-scoped\n * work. Live PING\u2192PONG handshakes are served by the dedicated interactions route,\n * which can answer synchronously (the generic route only 202-acks).\n */\nclass DiscordChannelAdapter implements ChannelAdapter {\n readonly providerKey = 'discord'\n readonly channelType = 'discord'\n readonly capabilities = discordCapabilities\n\n async sendMessage(input: SendMessageInput): Promise<SendMessageResult> {\n const credentials = parseDiscordCredentialsOrThrow(input.credentials)\n\n let native: ChannelNativeContent\n try {\n native = await convertOutboundForDiscord({\n body: input.content.text ?? input.content.html ?? '',\n bodyFormat: input.content.bodyFormat ?? (input.content.html ? 'html' : 'text'),\n attachments: input.content.attachments,\n channelMetadata: input.metadata,\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Outbound conversion failed'\n return { externalMessageId: '', status: 'failed', error: message }\n }\n\n const targetChannelId = resolveTargetChannelId(input, credentials.defaultChannelId)\n if (!targetChannelId) {\n return {\n externalMessageId: '',\n status: 'failed',\n error: '[internal] Discord send requires a target channel id (conversationId or defaultChannelId)',\n }\n }\n\n const meta = (native.metadata ?? {}) as Record<string, unknown>\n try {\n const message = await getDiscordRestClient().createMessage(\n { botToken: credentials.botToken },\n {\n channelId: targetChannelId,\n content: native.content.text ?? '',\n messageReferenceId: typeof meta.messageReferenceId === 'string' ? meta.messageReferenceId : undefined,\n allowedMentions: (meta.allowedMentions as Record<string, unknown> | undefined) ?? { parse: [] },\n },\n )\n return {\n externalMessageId: message.id,\n conversationId: `${DISCORD_CONVERSATION_PREFIX}${message.channel_id}`,\n status: 'sent',\n metadata: { discordChannelId: message.channel_id, discordMessageId: message.id },\n }\n } catch (error) {\n if (error instanceof DiscordApiError && error.status === 401) {\n // Protocol sentinel the hub keys on (error-classification.isReauthError).\n return { externalMessageId: '', status: 'failed', error: 'requires_reauth' }\n }\n const message = error instanceof Error ? error.message : 'Discord send failed'\n return { externalMessageId: '', status: 'failed', error: message }\n }\n }\n\n async verifyWebhook(input: VerifyWebhookInput): Promise<InboundMessage> {\n const rawBody = typeof input.rawBody === 'string' ? input.rawBody : input.rawBody.toString('utf-8')\n const interaction = parseInteractionBody(rawBody)\n if (!interaction) {\n // Not an interaction payload \u2014 Discord messages arrive via the gateway\n // worker, so ack (202) without tenant-scoped work.\n return { raw: {}, eventType: 'other', metadata: { reason: 'discord-uses-gateway-for-messages' } }\n }\n\n const parsed = discordCredentialsSchema.safeParse(input.credentials)\n const publicKeyHex = parsed.success ? parsed.data.publicKey : ''\n const headers = normalizeHeaders(input.headers)\n const ok = verifyDiscordSignature({\n publicKeyHex,\n signatureHex: headers['x-signature-ed25519'],\n timestamp: headers['x-signature-timestamp'],\n rawBody,\n })\n if (!ok) {\n // FAIL-CLOSED: throwing makes the shared route reject (401) and pins nothing.\n throw new Error('[internal] Discord interaction signature verification failed')\n }\n // Verified interaction. The generic hub route cannot answer the synchronous\n // PING\u2192PONG handshake, so real interactions are served by the dedicated\n // `api/interactions` \u2192 `/api/channel_discord/interactions` route; here we only prove the\n // signature and ack.\n return { raw: { discordInteraction: interaction }, eventType: 'other', metadata: { interactionType: interaction.type } }\n }\n\n async getStatus(_input: GetMessageStatusInput): Promise<MessageStatus> {\n // Discord exposes no per-message delivery-status API; best-effort placeholder.\n return { status: 'sent' }\n }\n\n async convertOutbound(input: ConvertOutboundInput): Promise<ChannelNativeContent> {\n return convertOutboundForDiscord(input)\n }\n\n async normalizeInbound(raw: InboundMessage): Promise<NormalizedInboundMessage> {\n const message = pickDiscordMessage(raw)\n return normalizeInboundDiscordMessage(message)\n }\n\n async normalizeInboundReaction(raw: InboundMessage): Promise<InboundReactionEvent> {\n const payload = (raw.raw?.discordReaction ?? raw.raw) as {\n message_id?: string\n channel_id?: string\n emoji?: { name?: string; id?: string | null }\n user_id?: string\n member?: { user?: { username?: string; global_name?: string | null } }\n action?: 'added' | 'removed'\n }\n const emojiName = payload.emoji?.name ?? ''\n const emoji = payload.emoji?.id ? `${emojiName}:${payload.emoji.id}` : emojiName\n return {\n externalMessageId: payload.message_id ?? '',\n externalConversationId: payload.channel_id\n ? `${DISCORD_CONVERSATION_PREFIX}${payload.channel_id}`\n : undefined,\n emoji,\n userIdentifier: payload.user_id ?? 'unknown',\n userDisplayName: payload.member?.user?.global_name ?? payload.member?.user?.username,\n action: payload.action === 'removed' ? 'removed' : 'added',\n raw: payload as Record<string, unknown>,\n }\n }\n\n async sendReaction(input: SendReactionInput): Promise<void> {\n const credentials = parseDiscordCredentialsOrThrow(input.credentials)\n const channelId = stripConversationPrefix(input.conversationId) ?? credentials.defaultChannelId\n if (!channelId) throw new Error('[internal] Discord sendReaction requires a channel id')\n await getDiscordRestClient().addReaction(\n { botToken: credentials.botToken },\n channelId,\n input.externalMessageId,\n input.emoji,\n )\n }\n\n async removeReaction(input: RemoveReactionInput): Promise<void> {\n const credentials = parseDiscordCredentialsOrThrow(input.credentials)\n const channelId = stripConversationPrefix(input.conversationId) ?? credentials.defaultChannelId\n if (!channelId) throw new Error('[internal] Discord removeReaction requires a channel id')\n await getDiscordRestClient().removeReaction(\n { botToken: credentials.botToken },\n channelId,\n input.externalMessageId,\n input.emoji,\n )\n }\n\n async editMessage(input: EditChannelMessageInput): Promise<void> {\n const credentials = parseDiscordCredentialsOrThrow(input.credentials)\n const channelId = stripConversationPrefix(input.conversationId) ?? credentials.defaultChannelId\n if (!channelId) throw new Error('[internal] Discord editMessage requires a channel id')\n const content = input.newContent.text ?? input.newContent.html ?? ''\n await getDiscordRestClient().editMessage(\n { botToken: credentials.botToken },\n channelId,\n input.externalMessageId,\n content,\n )\n }\n\n async deleteMessage(input: DeleteChannelMessageInput): Promise<void> {\n const credentials = parseDiscordCredentialsOrThrow(input.credentials)\n const channelId = stripConversationPrefix(input.conversationId) ?? credentials.defaultChannelId\n if (!channelId) throw new Error('[internal] Discord deleteMessage requires a channel id')\n await getDiscordRestClient().deleteMessage(\n { botToken: credentials.botToken },\n channelId,\n input.externalMessageId,\n )\n }\n\n async fetchHistory(input: FetchHistoryInput): Promise<HistoryPage> {\n const credentials = parseDiscordCredentialsOrThrow(input.credentials)\n const channelId = stripConversationPrefix(input.conversationId) ?? credentials.defaultChannelId\n if (!channelId) {\n return { messages: [], hasMore: false }\n }\n const limit = Math.min(Math.max(input.limit ?? 50, 1), 100)\n const raw = await getDiscordRestClient().listMessages(\n { botToken: credentials.botToken },\n channelId,\n { before: input.cursor, limit },\n )\n // Discord returns newest-first; keep that order but expose the oldest id as\n // the next `before` cursor for backward pagination.\n const messages = raw.map((message) => normalizeInboundDiscordMessage(message))\n const oldest = raw.length > 0 ? raw[raw.length - 1].id : undefined\n return {\n messages,\n nextCursor: raw.length >= limit ? oldest : undefined,\n hasMore: raw.length >= limit,\n }\n }\n\n async resolveContact(input: ResolveContactInput): Promise<ContactHint | null> {\n const userId = input.senderIdentifier\n if (!userId || userId === 'unknown') return null\n const displayName =\n input.senderDisplayName ||\n (typeof input.channelMetadata?.discordAuthorUsername === 'string'\n ? (input.channelMetadata.discordAuthorUsername as string)\n : undefined)\n return {\n displayName,\n externalProfileUrl: `https://discord.com/users/${encodeURIComponent(userId)}`,\n }\n }\n\n async validateCredentials(input: ValidateCredentialsInput): Promise<ValidateCredentialsResult> {\n const parsed = discordCredentialsSchema.safeParse(input.credentials)\n if (!parsed.success) {\n const errors: Record<string, string> = {}\n for (const issue of parsed.error.issues) {\n const path = issue.path[0]\n if (typeof path !== 'string') continue\n if (!errors[path]) errors[path] = issue.message\n }\n return { ok: false, errors }\n }\n try {\n await getDiscordRestClient().getCurrentUser({ botToken: parsed.data.botToken })\n return { ok: true }\n } catch (error) {\n const status = error instanceof DiscordApiError ? error.status : 0\n return {\n ok: false,\n errors: {\n botToken:\n status === 401\n ? 'Discord rejected the bot token. Reset it in the Developer Portal and paste the new value.'\n : 'Could not reach Discord to validate the bot token. Check the token and try again.',\n },\n }\n }\n }\n}\n\nfunction normalizeHeaders(headers: Record<string, string | string[] | undefined>): Record<string, string> {\n const out: Record<string, string> = {}\n for (const [key, value] of Object.entries(headers)) {\n if (value === undefined) continue\n out[key.toLowerCase()] = Array.isArray(value) ? (value[0] ?? '') : value\n }\n return out\n}\n\nfunction stripConversationPrefix(conversationId: string | undefined): string | undefined {\n if (!conversationId) return undefined\n return conversationId.startsWith(DISCORD_CONVERSATION_PREFIX)\n ? conversationId.slice(DISCORD_CONVERSATION_PREFIX.length)\n : conversationId\n}\n\n/**\n * Where an outbound message is actually posted, in priority order.\n *\n * `metadata.to` is in this list because of what QA found on #4391: the hub's\n * `test-send` route puts the operator's recipient in `metadata.to`, nothing\n * here read it, and every send therefore went to `defaultChannelId` while the\n * endpoint answered `200 sent`. Validating a recipient and then discarding it\n * is worse than rejecting it \u2014 an operator test-sending to one channel got a\n * success for a message that landed in another.\n *\n * `conversationId` deliberately outranks `metadata.to`: on a reply the thread\n * mapping is authoritative, and a stray `to` must never redirect a reply out of\n * its own conversation. `metadata.to` only decides when there is no\n * conversation to belong to, which is exactly the test-send case.\n *\n * With `capabilities.recipientFormat: 'provider-native'` the hub has already\n * applied its transport-safety checks to this value, but it stays untrusted\n * input: it reaches `/channels/{id}/messages` and a wrong-but-well-formed id\n * simply fails at Discord, which is the honest outcome.\n */\nfunction resolveTargetChannelId(input: SendMessageInput, defaultChannelId: string | undefined): string | undefined {\n const fromMeta = input.metadata?.discordChannelId\n if (typeof fromMeta === 'string' && fromMeta.length > 0) return fromMeta\n const fromConversation = stripConversationPrefix(input.conversationId)\n if (fromConversation && fromConversation.length > 0) return fromConversation\n const fromRecipient = input.metadata?.to\n if (typeof fromRecipient === 'string' && fromRecipient.length > 0) return fromRecipient\n return defaultChannelId\n}\n\nfunction pickDiscordMessage(raw: InboundMessage): DiscordMessageObject {\n const candidate = (raw.raw?.discordMessage ?? raw.raw) as DiscordMessageObject | undefined\n if (!candidate || typeof candidate !== 'object' || typeof candidate.id !== 'string') {\n throw new Error('[internal] Discord normalizeInbound requires `raw.discordMessage` to be a Discord message object')\n }\n return candidate\n}\n\nlet cachedAdapter: DiscordChannelAdapter | null = null\n\nexport function getDiscordChannelAdapter(): DiscordChannelAdapter {\n if (!cachedAdapter) cachedAdapter = new DiscordChannelAdapter()\n return cachedAdapter\n}\n\nexport { DiscordChannelAdapter }\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,45 @@
1
+ import { authorizeFeatures } from "@open-mercato/shared/security/featurePolicy";
2
+ import { createLogger } from "@open-mercato/shared/lib/logger";
3
+ const logger = createLogger("channel_discord").child({ component: "ai-agent-directory" });
4
+ async function listDiscordEligibleAgents() {
5
+ let mod;
6
+ try {
7
+ mod = await import("@open-mercato/ai-assistant");
8
+ } catch {
9
+ return { available: false };
10
+ }
11
+ if (typeof mod.loadAgentRegistry !== "function" || typeof mod.listAgents !== "function") {
12
+ return { available: false };
13
+ }
14
+ try {
15
+ await mod.loadAgentRegistry();
16
+ } catch (err) {
17
+ logger.warn("Failed to load the AI agent registry", { err });
18
+ return { available: false };
19
+ }
20
+ const agents = mod.listAgents().filter((agent) => agent.executionMode === "object" || Boolean(agent.output)).map((agent) => ({
21
+ id: agent.id,
22
+ label: agent.label ?? agent.id,
23
+ description: agent.description ?? "",
24
+ requiredFeatures: agent.requiredFeatures ?? []
25
+ }));
26
+ return { available: true, agents };
27
+ }
28
+ async function findDiscordEligibleAgent(agentId) {
29
+ const directory = await listDiscordEligibleAgents();
30
+ if (!directory.available) return null;
31
+ return directory.agents.find((agent) => agent.id === agentId) ?? null;
32
+ }
33
+ function missingAgentFeatures(requiredFeatures, principal) {
34
+ if (!requiredFeatures?.length) return [];
35
+ return requiredFeatures.filter((featureId) => !authorizeFeatures([featureId], {
36
+ grantedFeatures: principal.features,
37
+ unrestricted: principal.isSuperAdmin
38
+ }));
39
+ }
40
+ export {
41
+ findDiscordEligibleAgent,
42
+ listDiscordEligibleAgents,
43
+ missingAgentFeatures
44
+ };
45
+ //# sourceMappingURL=ai-agent-directory.js.map