@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,115 @@
1
+ import { DISCORD_MESSAGE_FLAG_EPHEMERAL } from "./discord-rest.js";
2
+ import {
3
+ isDispatchableInteractionType,
4
+ readDispatchableInteraction
5
+ } from "./interactions-dispatch.js";
6
+ import {
7
+ DiscordInteractionResponseType,
8
+ DiscordInteractionType,
9
+ hasVerifiableSignatureHeaders,
10
+ isSignatureTimestampFresh,
11
+ parseInteractionBody,
12
+ readInteractionApplicationId,
13
+ verifyDiscordSignature
14
+ } from "./interactions-verify.js";
15
+ const DEFAULT_INTERACTION_MESSAGES = {
16
+ notDispatchable: "This interaction is missing the channel or user context Open Mercato needs, so it was not recorded.",
17
+ unsupported: "The Open Mercato Discord channel does not handle this interaction type."
18
+ };
19
+ function ephemeralReply(content) {
20
+ return {
21
+ type: DiscordInteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
22
+ data: { content, flags: DISCORD_MESSAGE_FLAG_EPHEMERAL }
23
+ };
24
+ }
25
+ function screenInteractionRequest(input) {
26
+ const { signatureHex, timestamp, freshness } = input;
27
+ if (!isSignatureTimestampFresh(timestamp, freshness)) {
28
+ return { status: 401, body: { error: "stale_timestamp" }, matchedChannel: null };
29
+ }
30
+ if (!hasVerifiableSignatureHeaders(signatureHex, timestamp)) {
31
+ return { status: 401, body: { error: "invalid_signature" }, matchedChannel: null };
32
+ }
33
+ return null;
34
+ }
35
+ async function resolveDiscordInteraction(input) {
36
+ const { rawBody, signatureHex, timestamp, loadCandidates, freshness, messages } = input;
37
+ const screened = screenInteractionRequest({ signatureHex, timestamp, freshness });
38
+ if (screened) return screened;
39
+ const candidates = await loadCandidates({ applicationId: readInteractionApplicationId(rawBody) });
40
+ return handleDiscordInteraction({ rawBody, signatureHex, timestamp, candidates, freshness, messages });
41
+ }
42
+ function handleDiscordInteraction(input) {
43
+ const { rawBody, signatureHex, timestamp, candidates, freshness } = input;
44
+ const messages = input.messages ?? DEFAULT_INTERACTION_MESSAGES;
45
+ const screened = screenInteractionRequest({ signatureHex, timestamp, freshness });
46
+ if (screened) return screened;
47
+ let matched = null;
48
+ for (const candidate of candidates) {
49
+ const ok = verifyDiscordSignature({
50
+ publicKeyHex: candidate.publicKey,
51
+ signatureHex,
52
+ timestamp,
53
+ rawBody
54
+ });
55
+ if (ok) {
56
+ matched = candidate;
57
+ break;
58
+ }
59
+ }
60
+ if (!matched) {
61
+ return { status: 401, body: { error: "invalid_signature" }, matchedChannel: null };
62
+ }
63
+ const interaction = parseInteractionBody(rawBody);
64
+ if (!interaction) {
65
+ return { status: 400, body: { error: "invalid_interaction" }, matchedChannel: matched };
66
+ }
67
+ if (interaction.type === DiscordInteractionType.PING) {
68
+ return {
69
+ status: 200,
70
+ body: { type: DiscordInteractionResponseType.PONG },
71
+ matchedChannel: matched
72
+ };
73
+ }
74
+ if (isDispatchableInteractionType(interaction.type)) {
75
+ const dispatch = readDispatchableInteraction(interaction);
76
+ if (!dispatch) {
77
+ return {
78
+ status: 200,
79
+ body: ephemeralReply(messages.notDispatchable),
80
+ matchedChannel: matched,
81
+ dispatch: null
82
+ };
83
+ }
84
+ return {
85
+ status: 200,
86
+ body: { type: DiscordInteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE },
87
+ matchedChannel: matched,
88
+ dispatch
89
+ };
90
+ }
91
+ if (interaction.type === DiscordInteractionType.APPLICATION_COMMAND_AUTOCOMPLETE) {
92
+ return {
93
+ status: 200,
94
+ body: {
95
+ type: DiscordInteractionResponseType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT,
96
+ data: { choices: [] }
97
+ },
98
+ matchedChannel: matched,
99
+ dispatch: null
100
+ };
101
+ }
102
+ return {
103
+ status: 200,
104
+ body: ephemeralReply(messages.unsupported),
105
+ matchedChannel: matched,
106
+ dispatch: null
107
+ };
108
+ }
109
+ export {
110
+ DEFAULT_INTERACTION_MESSAGES,
111
+ handleDiscordInteraction,
112
+ resolveDiscordInteraction,
113
+ screenInteractionRequest
114
+ };
115
+ //# sourceMappingURL=interactions-handler.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/interactions-handler.ts"],
4
+ "sourcesContent": ["import { DISCORD_MESSAGE_FLAG_EPHEMERAL } from './discord-rest'\nimport {\n isDispatchableInteractionType,\n readDispatchableInteraction,\n type DispatchableInteraction,\n} from './interactions-dispatch'\nimport {\n DiscordInteractionResponseType,\n DiscordInteractionType,\n hasVerifiableSignatureHeaders,\n isSignatureTimestampFresh,\n parseInteractionBody,\n readInteractionApplicationId,\n verifyDiscordSignature,\n type TimestampFreshnessOptions,\n} from './interactions-verify'\n\nexport interface InteractionCandidate {\n channelId: string\n /** Hub channel type, carried through so the dispatch job matches the gateway's. */\n channelType: string\n tenantId: string\n organizationId: string | null\n publicKey: string\n /** Discord application this channel's bot belongs to; `null` when unknown. */\n applicationId: string | null\n /**\n * Scope `integrationCredentialsService.resolve` must be called with to obtain\n * this channel's bot token. Carried instead of the token itself so no\n * credential ever reaches a queue payload.\n */\n credentialScope: { tenantId: string; organizationId: string; userId: string | null }\n}\n\n/**\n * The two user-visible strings the synchronous handler can have to emit.\n *\n * They are passed in rather than translated here so the handler stays pure and\n * synchronous \u2014 Discord gives the endpoint three seconds, and the caller already\n * knows the locale.\n */\nexport interface InteractionMessages {\n /** Verified, but the payload carries no channel / user / token to dispatch. */\n notDispatchable: string\n /** Verified and well-formed, but this provider does not handle the type. */\n unsupported: string\n}\n\nexport const DEFAULT_INTERACTION_MESSAGES: InteractionMessages = {\n notDispatchable:\n 'This interaction is missing the channel or user context Open Mercato needs, so it was not recorded.',\n unsupported: 'The Open Mercato Discord channel does not handle this interaction type.',\n}\n\nfunction ephemeralReply(content: string): Record<string, unknown> {\n return {\n type: DiscordInteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,\n data: { content, flags: DISCORD_MESSAGE_FLAG_EPHEMERAL },\n }\n}\n\n/**\n * What the candidate loader is allowed to narrow by. `applicationId` comes from\n * the unverified request body, so it may only ever shrink the candidate set \u2014\n * see `readInteractionApplicationId`.\n */\nexport interface InteractionCandidateFilter {\n applicationId: string | null\n}\n\nexport interface InteractionResult {\n status: number\n /** JSON body to return (Discord expects a response type for verified interactions). */\n body: Record<string, unknown>\n /** The channel the signature verified against (tenant-pinned). Null on reject. */\n matchedChannel: InteractionCandidate | null\n /**\n * Work the caller MUST hand off when the body above is a deferred\n * acknowledgement. Non-null exactly when the response promises a follow-up:\n * ignoring it is what leaves the user staring at \"thinking\u2026\" forever.\n */\n dispatch?: DispatchableInteraction | null\n}\n\n/**\n * Request-only rejection gate, evaluated before anything tenant-scoped is loaded.\n *\n * WHY IT EXISTS: this endpoint is unauthenticated by design, so any caller can\n * POST to it. Everything decidable from the request alone \u2014 a missing, malformed\n * or wrong-length signature header, a missing or stale timestamp \u2014 is decided\n * here, so that traffic never reaches the candidate loader and never costs a\n * database round-trip or a credential decrypt.\n *\n * Returns the rejection to send, or `null` when the request is well-formed\n * enough that candidates must be loaded to answer it. It is deliberately\n * signature-*shape* only: whether the signature actually verifies still requires\n * a public key, and that check stays where it was.\n */\nexport function screenInteractionRequest(input: {\n signatureHex: string | undefined | null\n timestamp: string | undefined | null\n freshness?: TimestampFreshnessOptions\n}): InteractionResult | null {\n const { signatureHex, timestamp, freshness } = input\n\n if (!isSignatureTimestampFresh(timestamp, freshness)) {\n return { status: 401, body: { error: 'stale_timestamp' }, matchedChannel: null }\n }\n if (!hasVerifiableSignatureHeaders(signatureHex, timestamp)) {\n return { status: 401, body: { error: 'invalid_signature' }, matchedChannel: null }\n }\n return null\n}\n\n/**\n * Screen the request, then load only the candidates that could plausibly match\n * it, then dispatch.\n *\n * This is the order the HTTP route must follow, expressed once and injectable so\n * it can be tested without a database: `loadCandidates` is not called at all for\n * a request the screening gate rejects, and when it is called it receives the\n * `application_id` narrowing so the Ed25519 fan-out runs over the channels of\n * one Discord application instead of every Discord channel in the installation.\n *\n * The narrowing is a NARROWING, never an authorization decision \u2014 the signature\n * gate below is unchanged, so a body that claims someone else's `application_id`\n * still fails unless it also carries that application's signature.\n */\nexport async function resolveDiscordInteraction(input: {\n rawBody: string\n signatureHex: string | undefined | null\n timestamp: string | undefined | null\n loadCandidates: (filter: InteractionCandidateFilter) => Promise<InteractionCandidate[]>\n freshness?: TimestampFreshnessOptions\n messages?: InteractionMessages\n}): Promise<InteractionResult> {\n const { rawBody, signatureHex, timestamp, loadCandidates, freshness, messages } = input\n\n const screened = screenInteractionRequest({ signatureHex, timestamp, freshness })\n if (screened) return screened\n\n const candidates = await loadCandidates({ applicationId: readInteractionApplicationId(rawBody) })\n return handleDiscordInteraction({ rawBody, signatureHex, timestamp, candidates, freshness, messages })\n}\n\n/**\n * Core Discord Interactions dispatch \u2014 pure and testable (no HTTP/DB).\n *\n * SECURITY (fail-closed): the request is rejected (401) unless its Ed25519\n * signature verifies against exactly one candidate channel's public key. The\n * matched channel pins the tenant, so one tenant's interaction never lands in\n * another tenant's scope. A tampered / missing signature verifies against no\n * candidate \u2192 401, and no tenant-scoped work is done.\n *\n * REPLAY GUARD: the signed timestamp must be within\n * `DISCORD_SIGNATURE_MAX_SKEW_SECONDS` of the server clock. The check runs\n * BEFORE the per-candidate Ed25519 fan-out, so a replayed capture (still\n * cryptographically valid forever) is rejected without any verify work.\n *\n * COST: this function only decides what to do with candidates it is handed.\n * Callers that have to *load* those candidates first must run\n * `screenInteractionRequest` before doing so \u2014 that is what keeps an unsigned or\n * stale request genuinely constant-cost rather than merely cheap after the\n * database has already been read (`resolveDiscordInteraction` does it for them).\n *\n * On the mandatory PING (type 1) handshake it returns `{ type: 1 }` (PONG) so\n * Discord accepts the endpoint URL.\n *\n * Application commands, message components and modal submissions return a\n * deferred acknowledgement TOGETHER WITH the `dispatch` the caller must hand to\n * the interactions worker: that worker normalizes the interaction into the hub's\n * existing inbound path and replaces the deferred ack with a real message. The\n * two travel in one result on purpose \u2014 a deferred ack nobody follows up is the\n * permanent \"thinking\u2026\" state this endpoint used to produce.\n *\n * Autocomplete is answered synchronously with an empty choice list, the only\n * response Discord accepts for it, and anything else gets a visible, ephemeral\n * \"not handled\" reply rather than a promise this provider cannot keep.\n */\nexport function handleDiscordInteraction(input: {\n rawBody: string\n signatureHex: string | undefined | null\n timestamp: string | undefined | null\n candidates: InteractionCandidate[]\n freshness?: TimestampFreshnessOptions\n messages?: InteractionMessages\n}): InteractionResult {\n const { rawBody, signatureHex, timestamp, candidates, freshness } = input\n const messages = input.messages ?? DEFAULT_INTERACTION_MESSAGES\n\n const screened = screenInteractionRequest({ signatureHex, timestamp, freshness })\n if (screened) return screened\n\n let matched: InteractionCandidate | null = null\n for (const candidate of candidates) {\n const ok = verifyDiscordSignature({\n publicKeyHex: candidate.publicKey,\n signatureHex,\n timestamp,\n rawBody,\n })\n if (ok) {\n matched = candidate\n break\n }\n }\n\n if (!matched) {\n // FAIL-CLOSED \u2014 never acknowledge an unverified interaction.\n return { status: 401, body: { error: 'invalid_signature' }, matchedChannel: null }\n }\n\n const interaction = parseInteractionBody(rawBody)\n if (!interaction) {\n return { status: 400, body: { error: 'invalid_interaction' }, matchedChannel: matched }\n }\n\n if (interaction.type === DiscordInteractionType.PING) {\n return {\n status: 200,\n body: { type: DiscordInteractionResponseType.PONG },\n matchedChannel: matched,\n }\n }\n\n if (isDispatchableInteractionType(interaction.type)) {\n const dispatch = readDispatchableInteraction(interaction)\n if (!dispatch) {\n // Deferring here would be a promise we cannot keep: without the follow-up\n // token or the channel there is nothing to edit the placeholder with.\n return {\n status: 200,\n body: ephemeralReply(messages.notDispatchable),\n matchedChannel: matched,\n dispatch: null,\n }\n }\n return {\n status: 200,\n body: { type: DiscordInteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE },\n matchedChannel: matched,\n dispatch,\n }\n }\n\n if (interaction.type === DiscordInteractionType.APPLICATION_COMMAND_AUTOCOMPLETE) {\n // Discord rejects a deferred ack for autocomplete. No command this provider\n // registers declares an autocompleting option, so an empty list is the\n // honest answer and it closes the request immediately.\n return {\n status: 200,\n body: {\n type: DiscordInteractionResponseType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT,\n data: { choices: [] },\n },\n matchedChannel: matched,\n dispatch: null,\n }\n }\n\n return {\n status: 200,\n body: ephemeralReply(messages.unsupported),\n matchedChannel: matched,\n dispatch: null,\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,sCAAsC;AAC/C;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAiCA,MAAM,+BAAoD;AAAA,EAC/D,iBACE;AAAA,EACF,aAAa;AACf;AAEA,SAAS,eAAe,SAA0C;AAChE,SAAO;AAAA,IACL,MAAM,+BAA+B;AAAA,IACrC,MAAM,EAAE,SAAS,OAAO,+BAA+B;AAAA,EACzD;AACF;AAuCO,SAAS,yBAAyB,OAIZ;AAC3B,QAAM,EAAE,cAAc,WAAW,UAAU,IAAI;AAE/C,MAAI,CAAC,0BAA0B,WAAW,SAAS,GAAG;AACpD,WAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,OAAO,kBAAkB,GAAG,gBAAgB,KAAK;AAAA,EACjF;AACA,MAAI,CAAC,8BAA8B,cAAc,SAAS,GAAG;AAC3D,WAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,OAAO,oBAAoB,GAAG,gBAAgB,KAAK;AAAA,EACnF;AACA,SAAO;AACT;AAgBA,eAAsB,0BAA0B,OAOjB;AAC7B,QAAM,EAAE,SAAS,cAAc,WAAW,gBAAgB,WAAW,SAAS,IAAI;AAElF,QAAM,WAAW,yBAAyB,EAAE,cAAc,WAAW,UAAU,CAAC;AAChF,MAAI,SAAU,QAAO;AAErB,QAAM,aAAa,MAAM,eAAe,EAAE,eAAe,6BAA6B,OAAO,EAAE,CAAC;AAChG,SAAO,yBAAyB,EAAE,SAAS,cAAc,WAAW,YAAY,WAAW,SAAS,CAAC;AACvG;AAoCO,SAAS,yBAAyB,OAOnB;AACpB,QAAM,EAAE,SAAS,cAAc,WAAW,YAAY,UAAU,IAAI;AACpE,QAAM,WAAW,MAAM,YAAY;AAEnC,QAAM,WAAW,yBAAyB,EAAE,cAAc,WAAW,UAAU,CAAC;AAChF,MAAI,SAAU,QAAO;AAErB,MAAI,UAAuC;AAC3C,aAAW,aAAa,YAAY;AAClC,UAAM,KAAK,uBAAuB;AAAA,MAChC,cAAc,UAAU;AAAA,MACxB;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,QAAI,IAAI;AACN,gBAAU;AACV;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,SAAS;AAEZ,WAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,OAAO,oBAAoB,GAAG,gBAAgB,KAAK;AAAA,EACnF;AAEA,QAAM,cAAc,qBAAqB,OAAO;AAChD,MAAI,CAAC,aAAa;AAChB,WAAO,EAAE,QAAQ,KAAK,MAAM,EAAE,OAAO,sBAAsB,GAAG,gBAAgB,QAAQ;AAAA,EACxF;AAEA,MAAI,YAAY,SAAS,uBAAuB,MAAM;AACpD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,EAAE,MAAM,+BAA+B,KAAK;AAAA,MAClD,gBAAgB;AAAA,IAClB;AAAA,EACF;AAEA,MAAI,8BAA8B,YAAY,IAAI,GAAG;AACnD,UAAM,WAAW,4BAA4B,WAAW;AACxD,QAAI,CAAC,UAAU;AAGb,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,MAAM,eAAe,SAAS,eAAe;AAAA,QAC7C,gBAAgB;AAAA,QAChB,UAAU;AAAA,MACZ;AAAA,IACF;AACA,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,EAAE,MAAM,+BAA+B,qCAAqC;AAAA,MAClF,gBAAgB;AAAA,MAChB;AAAA,IACF;AAAA,EACF;AAEA,MAAI,YAAY,SAAS,uBAAuB,kCAAkC;AAIhF,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ,MAAM,+BAA+B;AAAA,QACrC,MAAM,EAAE,SAAS,CAAC,EAAE;AAAA,MACtB;AAAA,MACA,gBAAgB;AAAA,MAChB,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM,eAAe,SAAS,WAAW;AAAA,IACzC,gBAAgB;AAAA,IAChB,UAAU;AAAA,EACZ;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,17 @@
1
+ import { createModuleQueue } from "@open-mercato/queue";
2
+ const CHANNEL_DISCORD_INTERACTIONS_QUEUE = "channel_discord_interactions";
3
+ const INTERACTIONS_QUEUE_CONCURRENCY = 5;
4
+ let cachedQueue = null;
5
+ function getInteractionDispatchQueue() {
6
+ if (!cachedQueue) {
7
+ cachedQueue = createModuleQueue(CHANNEL_DISCORD_INTERACTIONS_QUEUE, {
8
+ concurrency: INTERACTIONS_QUEUE_CONCURRENCY
9
+ });
10
+ }
11
+ return cachedQueue;
12
+ }
13
+ export {
14
+ CHANNEL_DISCORD_INTERACTIONS_QUEUE,
15
+ getInteractionDispatchQueue
16
+ };
17
+ //# sourceMappingURL=interactions-queue.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/interactions-queue.ts"],
4
+ "sourcesContent": ["import { createModuleQueue, type Queue } from '@open-mercato/queue'\nimport type { DispatchableInteraction } from './interactions-dispatch'\n\n/**\n * Queue carrying verified Discord interactions from the HTTP route to the\n * dispatch worker.\n *\n * It lives here rather than in the worker so the route never imports a worker\n * module: `workers/` is an auto-discovery path, and a route that pulled one in\n * would drag the worker's dependency graph into the request bundle.\n *\n * Mirrors the hub's `getCommunicationChannelsQueue` \u2014 one memoized instance per\n * name, so the route and the worker share it instead of constructing a queue per\n * request.\n */\nexport const CHANNEL_DISCORD_INTERACTIONS_QUEUE = 'channel_discord_interactions'\n\nconst INTERACTIONS_QUEUE_CONCURRENCY = 5\n\nexport type InteractionDispatchJobPayload = {\n channelId: string\n channelType: string\n tenantId: string\n organizationId: string | null\n /**\n * Scope the **bot token** is re-resolved with on the worker side. The bot token\n * itself is deliberately absent, because the local queue strategy persists\n * payloads to disk as plain JSON and that token is long-lived.\n *\n * This is not a blanket \"no credential travels on this payload\" guarantee, and\n * reading it as one would be a mistake: `interaction.token` below IS a\n * credential. It is Discord's interaction webhook token, and on its own \u2014 with\n * no bot token at all \u2014 it can post as the application (the `auth === null`\n * calls in `discord-rest.ts`), so it does land on disk under the local\n * strategy. Discord expires it 15 minutes after the interaction, which bounds\n * the exposure rather than removing it. `interactions-dispatch.ts` states the\n * same constraint from the other side.\n */\n credentialScope: { tenantId: string; organizationId: string; userId: string | null }\n interaction: DispatchableInteraction\n}\n\nlet cachedQueue: Queue<Record<string, unknown>> | null = null\n\nexport function getInteractionDispatchQueue(): Queue<Record<string, unknown>> {\n if (!cachedQueue) {\n cachedQueue = createModuleQueue<Record<string, unknown>>(CHANNEL_DISCORD_INTERACTIONS_QUEUE, {\n concurrency: INTERACTIONS_QUEUE_CONCURRENCY,\n })\n }\n return cachedQueue\n}\n"],
5
+ "mappings": "AAAA,SAAS,yBAAqC;AAevC,MAAM,qCAAqC;AAElD,MAAM,iCAAiC;AAyBvC,IAAI,cAAqD;AAElD,SAAS,8BAA8D;AAC5E,MAAI,CAAC,aAAa;AAChB,kBAAc,kBAA2C,oCAAoC;AAAA,MAC3F,aAAa;AAAA,IACf,CAAC;AAAA,EACH;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,84 @@
1
+ import { createPublicKey, verify as cryptoVerify } from "node:crypto";
2
+ const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
3
+ function isHex(value) {
4
+ return value.length > 0 && value.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(value);
5
+ }
6
+ const ED25519_SIGNATURE_HEX_LENGTH = 128;
7
+ function hasVerifiableSignatureHeaders(signatureHex, timestamp) {
8
+ if (!signatureHex || !timestamp) return false;
9
+ return isHex(signatureHex) && signatureHex.length === ED25519_SIGNATURE_HEX_LENGTH;
10
+ }
11
+ function publicKeyFromHex(publicKeyHex) {
12
+ if (!isHex(publicKeyHex) || publicKeyHex.length !== 64) {
13
+ throw new Error("invalid ed25519 public key");
14
+ }
15
+ const raw = Buffer.from(publicKeyHex, "hex");
16
+ const der = Buffer.concat([ED25519_SPKI_PREFIX, raw]);
17
+ return createPublicKey({ key: der, format: "der", type: "spki" });
18
+ }
19
+ function verifyDiscordSignature(input) {
20
+ const { publicKeyHex, signatureHex, timestamp, rawBody } = input;
21
+ if (!hasVerifiableSignatureHeaders(signatureHex, timestamp)) return false;
22
+ try {
23
+ const key = publicKeyFromHex(publicKeyHex);
24
+ const message = Buffer.from(String(timestamp) + rawBody, "utf-8");
25
+ const signature = Buffer.from(signatureHex, "hex");
26
+ return cryptoVerify(null, message, key, signature);
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+ const DISCORD_SIGNATURE_MAX_SKEW_SECONDS = 300;
32
+ function isSignatureTimestampFresh(timestamp, options) {
33
+ if (!timestamp) return false;
34
+ const value = String(timestamp).trim();
35
+ if (!/^\d{1,12}$/.test(value)) return false;
36
+ const parsedSeconds = Number(value);
37
+ if (!Number.isSafeInteger(parsedSeconds)) return false;
38
+ const maxSkewSeconds = options?.maxSkewSeconds ?? DISCORD_SIGNATURE_MAX_SKEW_SECONDS;
39
+ const nowSeconds = options?.nowEpochSeconds ?? Math.floor(Date.now() / 1e3);
40
+ return Math.abs(nowSeconds - parsedSeconds) <= maxSkewSeconds;
41
+ }
42
+ const DiscordInteractionType = {
43
+ PING: 1,
44
+ APPLICATION_COMMAND: 2,
45
+ MESSAGE_COMPONENT: 3,
46
+ APPLICATION_COMMAND_AUTOCOMPLETE: 4,
47
+ MODAL_SUBMIT: 5
48
+ };
49
+ const DiscordInteractionResponseType = {
50
+ PONG: 1,
51
+ CHANNEL_MESSAGE_WITH_SOURCE: 4,
52
+ DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE: 5,
53
+ DEFERRED_UPDATE_MESSAGE: 6,
54
+ // Autocomplete is the one interaction type Discord will not accept a deferred
55
+ // ack for — it must be answered synchronously with this type and a choice list.
56
+ APPLICATION_COMMAND_AUTOCOMPLETE_RESULT: 8
57
+ };
58
+ function parseInteractionBody(rawBody) {
59
+ try {
60
+ const parsed = JSON.parse(rawBody);
61
+ if (!parsed || typeof parsed !== "object") return null;
62
+ const candidate = parsed;
63
+ if (typeof candidate.type !== "number") return null;
64
+ return parsed;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+ function readInteractionApplicationId(rawBody) {
70
+ const interaction = parseInteractionBody(rawBody);
71
+ const applicationId = interaction?.application_id;
72
+ return typeof applicationId === "string" && applicationId.length > 0 ? applicationId : null;
73
+ }
74
+ export {
75
+ DISCORD_SIGNATURE_MAX_SKEW_SECONDS,
76
+ DiscordInteractionResponseType,
77
+ DiscordInteractionType,
78
+ hasVerifiableSignatureHeaders,
79
+ isSignatureTimestampFresh,
80
+ parseInteractionBody,
81
+ readInteractionApplicationId,
82
+ verifyDiscordSignature
83
+ };
84
+ //# sourceMappingURL=interactions-verify.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/interactions-verify.ts"],
4
+ "sourcesContent": ["import { createPublicKey, verify as cryptoVerify } from 'node:crypto'\n\n/**\n * Discord interaction request signing (Ed25519).\n *\n * Every interaction POST carries `X-Signature-Ed25519` (hex) and\n * `X-Signature-Timestamp`. The signed payload is `timestamp + rawBody`, verified\n * against the application's Ed25519 **public key** (hex, from the General\n * Information tab). We verify with Node's built-in `crypto` (`ed25519`) \u2014 no\n * `tweetnacl` dependency.\n *\n * SECURITY CONTRACT: this function is FAIL-CLOSED. It returns `false` on a\n * missing header, a malformed key/signature, or any verification error \u2014 never\n * throws, never returns `true` on doubt. Callers MUST reject (`401`) on `false`.\n */\n\n// Standard SPKI DER prefix for an Ed25519 public key (RFC 8410). Prepending it\n// to the 32 raw key bytes yields a DER document `createPublicKey` accepts,\n// avoiding a third-party crypto dependency.\nconst ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex')\n\nfunction isHex(value: string): boolean {\n return value.length > 0 && value.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(value)\n}\n\n/** An Ed25519 signature is 64 bytes, so 128 hex characters. */\nconst ED25519_SIGNATURE_HEX_LENGTH = 128\n\n/**\n * Whether the request's signature headers are even capable of verifying \u2014\n * present, hex, and the right length. This depends on the REQUEST ALONE, never\n * on a candidate channel, which is why it is exported separately: the route runs\n * it before it loads any channel, so an unsigned or malformed POST is rejected\n * without a single database round-trip.\n *\n * `verifyDiscordSignature` applies the same guard itself, so this is an\n * optimisation of *when* the answer is known, never a replacement for the\n * fail-closed check.\n */\nexport function hasVerifiableSignatureHeaders(\n signatureHex: string | undefined | null,\n timestamp: string | undefined | null,\n): signatureHex is string {\n if (!signatureHex || !timestamp) return false\n return isHex(signatureHex) && signatureHex.length === ED25519_SIGNATURE_HEX_LENGTH\n}\n\nfunction publicKeyFromHex(publicKeyHex: string) {\n if (!isHex(publicKeyHex) || publicKeyHex.length !== 64) {\n throw new Error('invalid ed25519 public key')\n }\n const raw = Buffer.from(publicKeyHex, 'hex')\n const der = Buffer.concat([ED25519_SPKI_PREFIX, raw])\n return createPublicKey({ key: der, format: 'der', type: 'spki' })\n}\n\nexport interface VerifyDiscordSignatureInput {\n publicKeyHex: string\n signatureHex: string | undefined | null\n timestamp: string | undefined | null\n rawBody: string\n}\n\n/**\n * Verify a Discord interaction signature. Returns `true` only when the signature\n * cryptographically matches `timestamp + rawBody` under `publicKeyHex`.\n */\nexport function verifyDiscordSignature(input: VerifyDiscordSignatureInput): boolean {\n const { publicKeyHex, signatureHex, timestamp, rawBody } = input\n if (!hasVerifiableSignatureHeaders(signatureHex, timestamp)) return false\n try {\n const key = publicKeyFromHex(publicKeyHex)\n const message = Buffer.from(String(timestamp) + rawBody, 'utf-8')\n const signature = Buffer.from(signatureHex, 'hex')\n // Ed25519: the algorithm is implied by the key, so the first arg is null.\n return cryptoVerify(null, message, key, signature)\n } catch {\n return false\n }\n}\n\n/**\n * Replay guard. Discord signs `timestamp + body`, but a captured request stays\n * cryptographically valid forever \u2014 freshness has to be enforced separately.\n * A timestamp outside the \u00B1 skew window (or missing / non-numeric) is rejected,\n * fail-closed, so a recorded interaction cannot be replayed later.\n */\nexport const DISCORD_SIGNATURE_MAX_SKEW_SECONDS = 300\n\nexport interface TimestampFreshnessOptions {\n maxSkewSeconds?: number\n /** Injectable clock (epoch seconds) for tests; defaults to the real clock. */\n nowEpochSeconds?: number\n}\n\nexport function isSignatureTimestampFresh(\n timestamp: string | undefined | null,\n options?: TimestampFreshnessOptions,\n): boolean {\n if (!timestamp) return false\n const value = String(timestamp).trim()\n if (!/^\\d{1,12}$/.test(value)) return false\n const parsedSeconds = Number(value)\n if (!Number.isSafeInteger(parsedSeconds)) return false\n const maxSkewSeconds = options?.maxSkewSeconds ?? DISCORD_SIGNATURE_MAX_SKEW_SECONDS\n const nowSeconds = options?.nowEpochSeconds ?? Math.floor(Date.now() / 1000)\n return Math.abs(nowSeconds - parsedSeconds) <= maxSkewSeconds\n}\n\n/** Discord interaction types. */\nexport const DiscordInteractionType = {\n PING: 1,\n APPLICATION_COMMAND: 2,\n MESSAGE_COMPONENT: 3,\n APPLICATION_COMMAND_AUTOCOMPLETE: 4,\n MODAL_SUBMIT: 5,\n} as const\n\n/** Discord interaction response types. */\nexport const DiscordInteractionResponseType = {\n PONG: 1,\n CHANNEL_MESSAGE_WITH_SOURCE: 4,\n DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE: 5,\n DEFERRED_UPDATE_MESSAGE: 6,\n // Autocomplete is the one interaction type Discord will not accept a deferred\n // ack for \u2014 it must be answered synchronously with this type and a choice list.\n APPLICATION_COMMAND_AUTOCOMPLETE_RESULT: 8,\n} as const\n\nexport interface ParsedInteraction {\n type: number\n data?: Record<string, unknown>\n member?: { user?: { id?: string; username?: string; global_name?: string | null } }\n user?: { id?: string; username?: string; global_name?: string | null }\n channel_id?: string\n guild_id?: string\n application_id?: string\n id?: string\n token?: string\n [key: string]: unknown\n}\n\n/**\n * Parse the interaction body. Returns `null` when the body is not a JSON object\n * with a numeric `type` \u2014 the caller treats that as a non-interaction payload.\n */\nexport function parseInteractionBody(rawBody: string): ParsedInteraction | null {\n try {\n const parsed = JSON.parse(rawBody) as unknown\n if (!parsed || typeof parsed !== 'object') return null\n const candidate = parsed as { type?: unknown }\n if (typeof candidate.type !== 'number') return null\n return parsed as ParsedInteraction\n } catch {\n return null\n }\n}\n\n/**\n * The `application_id` an interaction body claims, or `null` when the body does\n * not carry a usable one.\n *\n * UNTRUSTED \u2014 this value is read before any signature has been verified, so it\n * may only ever be used to NARROW the set of candidate channels the signature is\n * then checked against. It is never an authorization decision: a body claiming\n * another tenant's application still has to carry that tenant's Ed25519\n * signature to be accepted, and a body claiming nothing falls back to the full\n * candidate set rather than being let through.\n */\nexport function readInteractionApplicationId(rawBody: string): string | null {\n const interaction = parseInteractionBody(rawBody)\n const applicationId = interaction?.application_id\n return typeof applicationId === 'string' && applicationId.length > 0 ? applicationId : null\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB,UAAU,oBAAoB;AAmBxD,MAAM,sBAAsB,OAAO,KAAK,4BAA4B,KAAK;AAEzE,SAAS,MAAM,OAAwB;AACrC,SAAO,MAAM,SAAS,KAAK,MAAM,SAAS,MAAM,KAAK,iBAAiB,KAAK,KAAK;AAClF;AAGA,MAAM,+BAA+B;AAa9B,SAAS,8BACd,cACA,WACwB;AACxB,MAAI,CAAC,gBAAgB,CAAC,UAAW,QAAO;AACxC,SAAO,MAAM,YAAY,KAAK,aAAa,WAAW;AACxD;AAEA,SAAS,iBAAiB,cAAsB;AAC9C,MAAI,CAAC,MAAM,YAAY,KAAK,aAAa,WAAW,IAAI;AACtD,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AACA,QAAM,MAAM,OAAO,KAAK,cAAc,KAAK;AAC3C,QAAM,MAAM,OAAO,OAAO,CAAC,qBAAqB,GAAG,CAAC;AACpD,SAAO,gBAAgB,EAAE,KAAK,KAAK,QAAQ,OAAO,MAAM,OAAO,CAAC;AAClE;AAaO,SAAS,uBAAuB,OAA6C;AAClF,QAAM,EAAE,cAAc,cAAc,WAAW,QAAQ,IAAI;AAC3D,MAAI,CAAC,8BAA8B,cAAc,SAAS,EAAG,QAAO;AACpE,MAAI;AACF,UAAM,MAAM,iBAAiB,YAAY;AACzC,UAAM,UAAU,OAAO,KAAK,OAAO,SAAS,IAAI,SAAS,OAAO;AAChE,UAAM,YAAY,OAAO,KAAK,cAAc,KAAK;AAEjD,WAAO,aAAa,MAAM,SAAS,KAAK,SAAS;AAAA,EACnD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQO,MAAM,qCAAqC;AAQ3C,SAAS,0BACd,WACA,SACS;AACT,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,QAAQ,OAAO,SAAS,EAAE,KAAK;AACrC,MAAI,CAAC,aAAa,KAAK,KAAK,EAAG,QAAO;AACtC,QAAM,gBAAgB,OAAO,KAAK;AAClC,MAAI,CAAC,OAAO,cAAc,aAAa,EAAG,QAAO;AACjD,QAAM,iBAAiB,SAAS,kBAAkB;AAClD,QAAM,aAAa,SAAS,mBAAmB,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAC3E,SAAO,KAAK,IAAI,aAAa,aAAa,KAAK;AACjD;AAGO,MAAM,yBAAyB;AAAA,EACpC,MAAM;AAAA,EACN,qBAAqB;AAAA,EACrB,mBAAmB;AAAA,EACnB,kCAAkC;AAAA,EAClC,cAAc;AAChB;AAGO,MAAM,iCAAiC;AAAA,EAC5C,MAAM;AAAA,EACN,6BAA6B;AAAA,EAC7B,sCAAsC;AAAA,EACtC,yBAAyB;AAAA;AAAA;AAAA,EAGzB,yCAAyC;AAC3C;AAmBO,SAAS,qBAAqB,SAA2C;AAC9E,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,QAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,UAAM,YAAY;AAClB,QAAI,OAAO,UAAU,SAAS,SAAU,QAAO;AAC/C,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAaO,SAAS,6BAA6B,SAAgC;AAC3E,QAAM,cAAc,qBAAqB,OAAO;AAChD,QAAM,gBAAgB,aAAa;AACnC,SAAO,OAAO,kBAAkB,YAAY,cAAc,SAAS,IAAI,gBAAgB;AACzF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,59 @@
1
+ const DISCORD_CONVERSATION_PREFIX = "discord-channel:";
2
+ const DISCORD_CHANNEL_CONTENT_TYPE = "discord";
3
+ function mapAttachments(message) {
4
+ if (!Array.isArray(message.attachments) || message.attachments.length === 0) return void 0;
5
+ return message.attachments.map((attachment) => ({
6
+ url: attachment.url,
7
+ mimeType: attachment.content_type ?? "application/octet-stream",
8
+ fileName: attachment.filename,
9
+ fileSize: attachment.size,
10
+ inline: false
11
+ }));
12
+ }
13
+ function normalizeInboundDiscordMessage(message) {
14
+ const author = message.author ?? {};
15
+ const senderDisplayName = author?.global_name || author?.username || void 0;
16
+ const timestamp = message.timestamp ? new Date(message.timestamp) : /* @__PURE__ */ new Date();
17
+ return {
18
+ externalMessageId: message.id,
19
+ externalConversationId: `${DISCORD_CONVERSATION_PREFIX}${message.channel_id}`,
20
+ senderIdentifier: author?.id ?? "unknown",
21
+ senderDisplayName,
22
+ body: message.content ?? "",
23
+ bodyFormat: "markdown",
24
+ attachments: mapAttachments(message),
25
+ timestamp,
26
+ replyToExternalId: message.message_reference?.message_id,
27
+ channelPayload: message,
28
+ channelContentType: DISCORD_CHANNEL_CONTENT_TYPE,
29
+ channelMetadata: {
30
+ discordChannelId: message.channel_id,
31
+ discordGuildId: message.guild_id,
32
+ discordAuthorId: author?.id,
33
+ discordAuthorUsername: author?.username,
34
+ discordAuthorGlobalName: author?.global_name ?? void 0,
35
+ discordAuthorIsBot: Boolean(author?.bot),
36
+ // Only present when the record came from a slash command or a component
37
+ // press rather than someone typing. A consumer that treats every inbound
38
+ // message the same can ignore these; one that answers commands
39
+ // specifically needs to tell them apart without re-parsing `channelPayload`.
40
+ ...message.discord_interaction ? {
41
+ discordInteractionId: message.discord_interaction.id,
42
+ discordInteractionType: message.discord_interaction.type,
43
+ discordCommandName: message.discord_interaction.commandName,
44
+ discordComponentCustomId: message.discord_interaction.customId
45
+ } : {}
46
+ }
47
+ };
48
+ }
49
+ function isAuthoredByBot(message, botUserId) {
50
+ if (message.author?.bot) return true;
51
+ return botUserId ? message.author?.id === botUserId : false;
52
+ }
53
+ export {
54
+ DISCORD_CHANNEL_CONTENT_TYPE,
55
+ DISCORD_CONVERSATION_PREFIX,
56
+ isAuthoredByBot,
57
+ normalizeInboundDiscordMessage
58
+ };
59
+ //# sourceMappingURL=normalize-inbound.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/normalize-inbound.ts"],
4
+ "sourcesContent": ["import type {\n NormalizedInboundMessage,\n NormalizedAttachment,\n} from '@open-mercato/core/modules/communication_channels/lib/adapter'\nimport type { DiscordMessageObject } from './discord-rest'\n\n/**\n * Prefix used to build the hub's `externalConversationId` from a Discord channel\n * (or thread) id. A Discord \"channel\" (text channel / thread) is the unit the\n * hub threads on, mirroring `gmail-thread:` / Slack channel conventions.\n */\nexport const DISCORD_CONVERSATION_PREFIX = 'discord-channel:'\n\nexport const DISCORD_CHANNEL_CONTENT_TYPE = 'discord'\n\nfunction mapAttachments(message: DiscordMessageObject): NormalizedAttachment[] | undefined {\n if (!Array.isArray(message.attachments) || message.attachments.length === 0) return undefined\n return message.attachments.map((attachment) => ({\n url: attachment.url,\n mimeType: attachment.content_type ?? 'application/octet-stream',\n fileName: attachment.filename,\n fileSize: attachment.size,\n inline: false,\n }))\n}\n\n/**\n * Map a raw Discord message object (from `MESSAGE_CREATE` or REST history) into\n * the hub's canonical `NormalizedInboundMessage`.\n *\n * Discord senders have no email/phone \u2014 the CRM contact is resolved from the\n * user id / handle later (`resolveContact`). The `externalConversationId` is the\n * channel/thread id (a Discord thread reuses this same shape), so all messages\n * in one channel thread into one hub conversation.\n */\nexport function normalizeInboundDiscordMessage(message: DiscordMessageObject): NormalizedInboundMessage {\n const author = message.author ?? ({} as DiscordMessageObject['author'])\n const senderDisplayName = author?.global_name || author?.username || undefined\n const timestamp = message.timestamp ? new Date(message.timestamp) : new Date()\n\n return {\n externalMessageId: message.id,\n externalConversationId: `${DISCORD_CONVERSATION_PREFIX}${message.channel_id}`,\n senderIdentifier: author?.id ?? 'unknown',\n senderDisplayName,\n body: message.content ?? '',\n bodyFormat: 'markdown',\n attachments: mapAttachments(message),\n timestamp,\n replyToExternalId: message.message_reference?.message_id,\n channelPayload: message as unknown as Record<string, unknown>,\n channelContentType: DISCORD_CHANNEL_CONTENT_TYPE,\n channelMetadata: {\n discordChannelId: message.channel_id,\n discordGuildId: message.guild_id,\n discordAuthorId: author?.id,\n discordAuthorUsername: author?.username,\n discordAuthorGlobalName: author?.global_name ?? undefined,\n discordAuthorIsBot: Boolean(author?.bot),\n // Only present when the record came from a slash command or a component\n // press rather than someone typing. A consumer that treats every inbound\n // message the same can ignore these; one that answers commands\n // specifically needs to tell them apart without re-parsing `channelPayload`.\n ...(message.discord_interaction\n ? {\n discordInteractionId: message.discord_interaction.id,\n discordInteractionType: message.discord_interaction.type,\n discordCommandName: message.discord_interaction.commandName,\n discordComponentCustomId: message.discord_interaction.customId,\n }\n : {}),\n },\n }\n}\n\n/**\n * True when a raw message must be dropped as bot-authored. The gateway worker\n * uses this as the feedback-loop guard.\n *\n * We drop ANY message flagged `author.bot` (not just our own bot's user id):\n * other bots/webhooks in the channel would otherwise be ingested and could\n * trigger the AI auto-reply, creating cross-bot loops and noise. The explicit\n * `botUserId` check is retained as a belt-and-suspenders guard for the rare case\n * where our own bot's `author.bot` flag is absent from a payload.\n */\nexport function isAuthoredByBot(message: DiscordMessageObject, botUserId: string | undefined): boolean {\n if (message.author?.bot) return true\n return botUserId ? message.author?.id === botUserId : false\n}\n"],
5
+ "mappings": "AAWO,MAAM,8BAA8B;AAEpC,MAAM,+BAA+B;AAE5C,SAAS,eAAe,SAAmE;AACzF,MAAI,CAAC,MAAM,QAAQ,QAAQ,WAAW,KAAK,QAAQ,YAAY,WAAW,EAAG,QAAO;AACpF,SAAO,QAAQ,YAAY,IAAI,CAAC,gBAAgB;AAAA,IAC9C,KAAK,WAAW;AAAA,IAChB,UAAU,WAAW,gBAAgB;AAAA,IACrC,UAAU,WAAW;AAAA,IACrB,UAAU,WAAW;AAAA,IACrB,QAAQ;AAAA,EACV,EAAE;AACJ;AAWO,SAAS,+BAA+B,SAAyD;AACtG,QAAM,SAAS,QAAQ,UAAW,CAAC;AACnC,QAAM,oBAAoB,QAAQ,eAAe,QAAQ,YAAY;AACrE,QAAM,YAAY,QAAQ,YAAY,IAAI,KAAK,QAAQ,SAAS,IAAI,oBAAI,KAAK;AAE7E,SAAO;AAAA,IACL,mBAAmB,QAAQ;AAAA,IAC3B,wBAAwB,GAAG,2BAA2B,GAAG,QAAQ,UAAU;AAAA,IAC3E,kBAAkB,QAAQ,MAAM;AAAA,IAChC;AAAA,IACA,MAAM,QAAQ,WAAW;AAAA,IACzB,YAAY;AAAA,IACZ,aAAa,eAAe,OAAO;AAAA,IACnC;AAAA,IACA,mBAAmB,QAAQ,mBAAmB;AAAA,IAC9C,gBAAgB;AAAA,IAChB,oBAAoB;AAAA,IACpB,iBAAiB;AAAA,MACf,kBAAkB,QAAQ;AAAA,MAC1B,gBAAgB,QAAQ;AAAA,MACxB,iBAAiB,QAAQ;AAAA,MACzB,uBAAuB,QAAQ;AAAA,MAC/B,yBAAyB,QAAQ,eAAe;AAAA,MAChD,oBAAoB,QAAQ,QAAQ,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA,MAKvC,GAAI,QAAQ,sBACR;AAAA,QACE,sBAAsB,QAAQ,oBAAoB;AAAA,QAClD,wBAAwB,QAAQ,oBAAoB;AAAA,QACpD,oBAAoB,QAAQ,oBAAoB;AAAA,QAChD,0BAA0B,QAAQ,oBAAoB;AAAA,MACxD,IACA,CAAC;AAAA,IACP;AAAA,EACF;AACF;AAYO,SAAS,gBAAgB,SAA+B,WAAwC;AACrG,MAAI,QAAQ,QAAQ,IAAK,QAAO;AAChC,SAAO,YAAY,QAAQ,QAAQ,OAAO,YAAY;AACxD;",
6
+ "names": []
7
+ }
@@ -0,0 +1,63 @@
1
+ import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
2
+ import { discordCredentialsSchema } from "./credentials.js";
3
+ const CHANNEL_DISCORD_INTEGRATION_ID = "channel_discord";
4
+ function readEnvValue(env, key) {
5
+ const value = env[key]?.trim();
6
+ return value ? value : void 0;
7
+ }
8
+ function readDiscordEnvPreset(env = process.env) {
9
+ const botToken = readEnvValue(env, "OM_CHANNEL_DISCORD_BOT_TOKEN");
10
+ const applicationId = readEnvValue(env, "OM_CHANNEL_DISCORD_APPLICATION_ID");
11
+ const publicKey = readEnvValue(env, "OM_CHANNEL_DISCORD_PUBLIC_KEY");
12
+ const guildId = readEnvValue(env, "OM_CHANNEL_DISCORD_GUILD_ID");
13
+ const defaultChannelId = readEnvValue(env, "OM_CHANNEL_DISCORD_DEFAULT_CHANNEL_ID");
14
+ if (!botToken && !applicationId && !publicKey) return null;
15
+ const parsed = discordCredentialsSchema.safeParse({
16
+ botToken,
17
+ applicationId,
18
+ publicKey,
19
+ ...guildId ? { guildId } : {},
20
+ ...defaultChannelId ? { defaultChannelId } : {}
21
+ });
22
+ if (!parsed.success) {
23
+ const first = parsed.error.issues[0];
24
+ throw new Error(
25
+ `[internal] Incomplete Discord env preset. Set OM_CHANNEL_DISCORD_BOT_TOKEN, OM_CHANNEL_DISCORD_APPLICATION_ID and OM_CHANNEL_DISCORD_PUBLIC_KEY (${first?.path.join(".") ?? "credentials"}: ${first?.message ?? "invalid"}).`
26
+ );
27
+ }
28
+ return {
29
+ credentials: parsed.data,
30
+ force: parseBooleanToken(env.OM_CHANNEL_DISCORD_FORCE_PRECONFIGURE) ?? false
31
+ };
32
+ }
33
+ async function applyDiscordEnvPreset(params) {
34
+ const preset = readDiscordEnvPreset(params.env);
35
+ if (!preset) {
36
+ return { status: "skipped", reason: "No Discord preset env variables were provided." };
37
+ }
38
+ const force = params.force ?? preset.force;
39
+ if (!force) {
40
+ const existing = await params.credentialsService.getRaw(CHANNEL_DISCORD_INTEGRATION_ID, params.scope);
41
+ if (existing) {
42
+ return {
43
+ status: "skipped",
44
+ reason: "Discord credentials already exist. Use OM_CHANNEL_DISCORD_FORCE_PRECONFIGURE=true to overwrite."
45
+ };
46
+ }
47
+ }
48
+ await params.credentialsService.save(CHANNEL_DISCORD_INTEGRATION_ID, preset.credentials, params.scope);
49
+ if (params.integrationLogService) {
50
+ await params.integrationLogService.scoped(CHANNEL_DISCORD_INTEGRATION_ID, params.scope).info("Discord integration was preconfigured from environment variables.", {
51
+ applicationId: preset.credentials.applicationId,
52
+ guildId: preset.credentials.guildId ?? null,
53
+ defaultChannelId: preset.credentials.defaultChannelId ?? null
54
+ });
55
+ }
56
+ return { status: "configured" };
57
+ }
58
+ export {
59
+ CHANNEL_DISCORD_INTEGRATION_ID,
60
+ applyDiscordEnvPreset,
61
+ readDiscordEnvPreset
62
+ };
63
+ //# sourceMappingURL=preset.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/preset.ts"],
4
+ "sourcesContent": ["import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport type { IntegrationScope } from '@open-mercato/shared/modules/integrations/types'\nimport type { CredentialsService } from '@open-mercato/core/modules/integrations/lib/credentials-service'\nimport type { IntegrationLogService } from '@open-mercato/core/modules/integrations/lib/log-service'\nimport { discordCredentialsSchema, type DiscordCredentials } from './credentials'\n\nexport const CHANNEL_DISCORD_INTEGRATION_ID = 'channel_discord'\n\nexport type DiscordEnvPreset = {\n credentials: DiscordCredentials\n force: boolean\n}\n\nexport type ApplyDiscordPresetResult =\n | { status: 'skipped'; reason: string }\n | { status: 'configured' }\n\nfunction readEnvValue(env: NodeJS.ProcessEnv, key: string): string | undefined {\n const value = env[key]?.trim()\n return value ? value : undefined\n}\n\n/**\n * Read the deployment-managed Discord bootstrap credentials from the environment\n * (SPEC 2026-06-19 \u00A7 Connect the channel in Open Mercato).\n *\n * Returns `null` when no `OM_CHANNEL_DISCORD_*` credential var is set at all \u2014\n * the normal case, where the bot is connected interactively through\n * `/backend/integrations`. When any of them IS set the preset must be complete,\n * so a half-filled deployment fails loudly instead of persisting a bot token\n * that cannot authenticate. The bot token is never logged or echoed.\n */\nexport function readDiscordEnvPreset(env: NodeJS.ProcessEnv = process.env): DiscordEnvPreset | null {\n const botToken = readEnvValue(env, 'OM_CHANNEL_DISCORD_BOT_TOKEN')\n const applicationId = readEnvValue(env, 'OM_CHANNEL_DISCORD_APPLICATION_ID')\n const publicKey = readEnvValue(env, 'OM_CHANNEL_DISCORD_PUBLIC_KEY')\n const guildId = readEnvValue(env, 'OM_CHANNEL_DISCORD_GUILD_ID')\n const defaultChannelId = readEnvValue(env, 'OM_CHANNEL_DISCORD_DEFAULT_CHANNEL_ID')\n\n if (!botToken && !applicationId && !publicKey) return null\n\n const parsed = discordCredentialsSchema.safeParse({\n botToken,\n applicationId,\n publicKey,\n ...(guildId ? { guildId } : {}),\n ...(defaultChannelId ? { defaultChannelId } : {}),\n })\n if (!parsed.success) {\n const first = parsed.error.issues[0]\n throw new Error(\n '[internal] Incomplete Discord env preset. Set OM_CHANNEL_DISCORD_BOT_TOKEN, ' +\n 'OM_CHANNEL_DISCORD_APPLICATION_ID and OM_CHANNEL_DISCORD_PUBLIC_KEY ' +\n `(${first?.path.join('.') ?? 'credentials'}: ${first?.message ?? 'invalid'}).`,\n )\n }\n\n return {\n credentials: parsed.data,\n force: parseBooleanToken(env.OM_CHANNEL_DISCORD_FORCE_PRECONFIGURE) ?? false,\n }\n}\n\n/**\n * Apply the env preset to one (tenant, organization) scope through the standard\n * integration credential service \u2014 never by special-casing the provider in core.\n *\n * Existing credentials win unless `OM_CHANNEL_DISCORD_FORCE_PRECONFIGURE` (or an\n * explicit `force`) says otherwise, so re-running tenant setup or the\n * `configure-from-env` CLI command is safe and idempotent.\n */\nexport async function applyDiscordEnvPreset(params: {\n credentialsService: CredentialsService\n integrationLogService?: IntegrationLogService\n scope: IntegrationScope\n force?: boolean\n env?: NodeJS.ProcessEnv\n}): Promise<ApplyDiscordPresetResult> {\n const preset = readDiscordEnvPreset(params.env)\n if (!preset) {\n return { status: 'skipped', reason: 'No Discord preset env variables were provided.' }\n }\n\n const force = params.force ?? preset.force\n if (!force) {\n const existing = await params.credentialsService.getRaw(CHANNEL_DISCORD_INTEGRATION_ID, params.scope)\n if (existing) {\n return {\n status: 'skipped',\n reason:\n 'Discord credentials already exist. Use OM_CHANNEL_DISCORD_FORCE_PRECONFIGURE=true to overwrite.',\n }\n }\n }\n\n await params.credentialsService.save(CHANNEL_DISCORD_INTEGRATION_ID, preset.credentials, params.scope)\n\n if (params.integrationLogService) {\n await params.integrationLogService\n .scoped(CHANNEL_DISCORD_INTEGRATION_ID, params.scope)\n .info('Discord integration was preconfigured from environment variables.', {\n applicationId: preset.credentials.applicationId,\n guildId: preset.credentials.guildId ?? null,\n defaultChannelId: preset.credentials.defaultChannelId ?? null,\n })\n }\n\n return { status: 'configured' }\n}\n"],
5
+ "mappings": "AAAA,SAAS,yBAAyB;AAIlC,SAAS,gCAAyD;AAE3D,MAAM,iCAAiC;AAW9C,SAAS,aAAa,KAAwB,KAAiC;AAC7E,QAAM,QAAQ,IAAI,GAAG,GAAG,KAAK;AAC7B,SAAO,QAAQ,QAAQ;AACzB;AAYO,SAAS,qBAAqB,MAAyB,QAAQ,KAA8B;AAClG,QAAM,WAAW,aAAa,KAAK,8BAA8B;AACjE,QAAM,gBAAgB,aAAa,KAAK,mCAAmC;AAC3E,QAAM,YAAY,aAAa,KAAK,+BAA+B;AACnE,QAAM,UAAU,aAAa,KAAK,6BAA6B;AAC/D,QAAM,mBAAmB,aAAa,KAAK,uCAAuC;AAElF,MAAI,CAAC,YAAY,CAAC,iBAAiB,CAAC,UAAW,QAAO;AAEtD,QAAM,SAAS,yBAAyB,UAAU;AAAA,IAChD;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,mBAAmB,EAAE,iBAAiB,IAAI,CAAC;AAAA,EACjD,CAAC;AACD,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,QAAQ,OAAO,MAAM,OAAO,CAAC;AACnC,UAAM,IAAI;AAAA,MACR,oJAEM,OAAO,KAAK,KAAK,GAAG,KAAK,aAAa,KAAK,OAAO,WAAW,SAAS;AAAA,IAC9E;AAAA,EACF;AAEA,SAAO;AAAA,IACL,aAAa,OAAO;AAAA,IACpB,OAAO,kBAAkB,IAAI,qCAAqC,KAAK;AAAA,EACzE;AACF;AAUA,eAAsB,sBAAsB,QAMN;AACpC,QAAM,SAAS,qBAAqB,OAAO,GAAG;AAC9C,MAAI,CAAC,QAAQ;AACX,WAAO,EAAE,QAAQ,WAAW,QAAQ,iDAAiD;AAAA,EACvF;AAEA,QAAM,QAAQ,OAAO,SAAS,OAAO;AACrC,MAAI,CAAC,OAAO;AACV,UAAM,WAAW,MAAM,OAAO,mBAAmB,OAAO,gCAAgC,OAAO,KAAK;AACpG,QAAI,UAAU;AACZ,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,QACE;AAAA,MACJ;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAAO,mBAAmB,KAAK,gCAAgC,OAAO,aAAa,OAAO,KAAK;AAErG,MAAI,OAAO,uBAAuB;AAChC,UAAM,OAAO,sBACV,OAAO,gCAAgC,OAAO,KAAK,EACnD,KAAK,qEAAqE;AAAA,MACzE,eAAe,OAAO,YAAY;AAAA,MAClC,SAAS,OAAO,YAAY,WAAW;AAAA,MACvC,kBAAkB,OAAO,YAAY,oBAAoB;AAAA,IAC3D,CAAC;AAAA,EACL;AAEA,SAAO,EAAE,QAAQ,aAAa;AAChC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,12 @@
1
+ import { createLogger } from "@open-mercato/shared/lib/logger";
2
+ const logger = createLogger("channel_discord").child({ component: "cli" });
3
+ function assertInboundDeliverable(queueStrategy) {
4
+ if (queueStrategy === "async") return;
5
+ const message = "channel_discord start-gateway requires QUEUE_STRATEGY=async (with Redis configured). Under the default in-process queue strategy the gateway cannot hand inbound messages to the communication-channels workers running in the app server process \u2014 they would be silently dropped. Set QUEUE_STRATEGY=async and point the queue at Redis, then start this command again.";
6
+ logger.error(message, { queueStrategy: queueStrategy ?? "local (default)" });
7
+ throw new Error(`[internal] ${message}`);
8
+ }
9
+ export {
10
+ assertInboundDeliverable
11
+ };
12
+ //# sourceMappingURL=queue-strategy.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/queue-strategy.ts"],
4
+ "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('channel_discord').child({ component: 'cli' })\n\n/**\n * Refuse to start the gateway when inbound messages cannot possibly be delivered.\n *\n * `start-gateway` is a standalone process that runs a worker for the\n * `channel_discord_gateway` queue only, while the bridge enqueues received\n * messages into `communication-channels-inbound` / `-reactions`, whose workers\n * live in the app server process. Under the DEFAULT `QUEUE_STRATEGY=local` the\n * queue is in-process, so those jobs never cross the process boundary: they are\n * dropped with no error, no log, no retry and no `failed` entry, while the\n * channel keeps reporting `Connected`. Failing loudly at startup is the only\n * honest option \u2014 the alternative looks like it works.\n *\n * Lives outside `cli.ts` so it stays unit-testable without pulling the DI\n * container graph into the test.\n */\nexport function assertInboundDeliverable(queueStrategy: string | undefined): void {\n if (queueStrategy === 'async') return\n const message =\n 'channel_discord start-gateway requires QUEUE_STRATEGY=async (with Redis configured). ' +\n 'Under the default in-process queue strategy the gateway cannot hand inbound messages to the ' +\n 'communication-channels workers running in the app server process \u2014 they would be silently dropped. ' +\n 'Set QUEUE_STRATEGY=async and point the queue at Redis, then start this command again.'\n logger.error(message, { queueStrategy: queueStrategy ?? 'local (default)' })\n throw new Error(`[internal] ${message}`)\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,iBAAiB,EAAE,MAAM,EAAE,WAAW,MAAM,CAAC;AAiBlE,SAAS,yBAAyB,eAAyC;AAChF,MAAI,kBAAkB,QAAS;AAC/B,QAAM,UACJ;AAIF,SAAO,MAAM,SAAS,EAAE,eAAe,iBAAiB,kBAAkB,CAAC;AAC3E,QAAM,IAAI,MAAM,cAAc,OAAO,EAAE;AACzC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,42 @@
1
+ const CHANNEL_DISCORD_SLASH_COMMAND_TYPE_CHAT_INPUT = 1;
2
+ const CHANNEL_DISCORD_OPTION_TYPE_STRING = 3;
3
+ const DISCORD_DEFAULT_SLASH_COMMANDS = [
4
+ {
5
+ name: "mercato",
6
+ type: CHANNEL_DISCORD_SLASH_COMMAND_TYPE_CHAT_INPUT,
7
+ description: "Send a message to the team through Open Mercato",
8
+ options: [
9
+ {
10
+ type: CHANNEL_DISCORD_OPTION_TYPE_STRING,
11
+ name: "message",
12
+ description: "What do you need?",
13
+ required: true
14
+ }
15
+ ]
16
+ }
17
+ ];
18
+ function parseSlashCommandDefinitions(value) {
19
+ if (!Array.isArray(value)) {
20
+ throw new Error("[internal] Discord slash command definitions must be a JSON array.");
21
+ }
22
+ return value.map((entry, index) => {
23
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
24
+ throw new Error(`[internal] Discord slash command at index ${index} is not an object.`);
25
+ }
26
+ const command = entry;
27
+ if (typeof command.name !== "string" || command.name.length === 0) {
28
+ throw new Error(`[internal] Discord slash command at index ${index} has no name.`);
29
+ }
30
+ if (typeof command.description !== "string" || command.description.length === 0) {
31
+ throw new Error(`[internal] Discord slash command "${command.name}" has no description.`);
32
+ }
33
+ return command;
34
+ });
35
+ }
36
+ export {
37
+ CHANNEL_DISCORD_OPTION_TYPE_STRING,
38
+ CHANNEL_DISCORD_SLASH_COMMAND_TYPE_CHAT_INPUT,
39
+ DISCORD_DEFAULT_SLASH_COMMANDS,
40
+ parseSlashCommandDefinitions
41
+ };
42
+ //# sourceMappingURL=slash-commands.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/slash-commands.ts"],
4
+ "sourcesContent": ["/**\n * Application (slash) commands this provider registers against a guild.\n *\n * NOT LOCALIZED, deliberately: a Discord application registers ONE canonical\n * command name per guild \u2014 the localized variants Discord supports live in\n * `name_localizations` / `description_localizations`, which are a per-command\n * map rather than an app locale, and no operator surface exists yet to choose\n * which locales a tenant wants. The registration is also an operator action\n * (`mercato channel_discord register-slash-commands`), not a rendered UI string.\n *\n * The set is deliberately minimal. Every command routes to the same place \u2014\n * the hub inbox \u2014 so the provider ships the one command that makes that useful\n * and leaves richer command trees to whoever knows their own workflow. Register\n * a different set by passing `--commands <path-to-json>`.\n */\nexport const CHANNEL_DISCORD_SLASH_COMMAND_TYPE_CHAT_INPUT = 1\nexport const CHANNEL_DISCORD_OPTION_TYPE_STRING = 3\n\nexport const DISCORD_DEFAULT_SLASH_COMMANDS: Array<Record<string, unknown>> = [\n {\n name: 'mercato',\n type: CHANNEL_DISCORD_SLASH_COMMAND_TYPE_CHAT_INPUT,\n description: 'Send a message to the team through Open Mercato',\n options: [\n {\n type: CHANNEL_DISCORD_OPTION_TYPE_STRING,\n name: 'message',\n description: 'What do you need?',\n required: true,\n },\n ],\n },\n]\n\n/**\n * Validate an operator-supplied command list read from JSON. Discord rejects a\n * malformed registration with a 400 whose body names an array index, so failing\n * here with the index and the reason is strictly more useful than forwarding it.\n */\nexport function parseSlashCommandDefinitions(value: unknown): Array<Record<string, unknown>> {\n if (!Array.isArray(value)) {\n throw new Error('[internal] Discord slash command definitions must be a JSON array.')\n }\n return value.map((entry, index) => {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n throw new Error(`[internal] Discord slash command at index ${index} is not an object.`)\n }\n const command = entry as Record<string, unknown>\n if (typeof command.name !== 'string' || command.name.length === 0) {\n throw new Error(`[internal] Discord slash command at index ${index} has no name.`)\n }\n if (typeof command.description !== 'string' || command.description.length === 0) {\n throw new Error(`[internal] Discord slash command \"${command.name}\" has no description.`)\n }\n return command\n })\n}\n"],
5
+ "mappings": "AAeO,MAAM,gDAAgD;AACtD,MAAM,qCAAqC;AAE3C,MAAM,iCAAiE;AAAA,EAC5E;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,MACP;AAAA,QACE,MAAM;AAAA,QACN,MAAM;AAAA,QACN,aAAa;AAAA,QACb,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AACF;AAOO,SAAS,6BAA6B,OAAgD;AAC3F,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,UAAM,IAAI,MAAM,oEAAoE;AAAA,EACtF;AACA,SAAO,MAAM,IAAI,CAAC,OAAO,UAAU;AACjC,QAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AAC/D,YAAM,IAAI,MAAM,6CAA6C,KAAK,oBAAoB;AAAA,IACxF;AACA,UAAM,UAAU;AAChB,QAAI,OAAO,QAAQ,SAAS,YAAY,QAAQ,KAAK,WAAW,GAAG;AACjE,YAAM,IAAI,MAAM,6CAA6C,KAAK,eAAe;AAAA,IACnF;AACA,QAAI,OAAO,QAAQ,gBAAgB,YAAY,QAAQ,YAAY,WAAW,GAAG;AAC/E,YAAM,IAAI,MAAM,qCAAqC,QAAQ,IAAI,uBAAuB;AAAA,IAC1F;AACA,WAAO;AAAA,EACT,CAAC;AACH;",
6
+ "names": []
7
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ CHANNEL_DISCORD_AI_PROPOSAL_APPROVE_COMMAND_ID,
3
+ CHANNEL_DISCORD_AI_PROPOSAL_DISMISS_COMMAND_ID
4
+ } from "./lib/ai-proposal-contract.js";
5
+ const CHANNEL_DISCORD_AI_PROPOSAL_MESSAGE_TYPE = "channel_discord.ai_reply_proposal";
6
+ const CHANNEL_DISCORD_AI_PROPOSAL_APPROVE_ACTION_ID = "approve_send";
7
+ const CHANNEL_DISCORD_AI_PROPOSAL_DISMISS_ACTION_ID = "dismiss";
8
+ const proposalActions = [
9
+ {
10
+ id: CHANNEL_DISCORD_AI_PROPOSAL_APPROVE_ACTION_ID,
11
+ label: "Approve and send",
12
+ labelKey: "channel_discord.aiProposal.actions.approve",
13
+ variant: "default",
14
+ icon: "send",
15
+ commandId: CHANNEL_DISCORD_AI_PROPOSAL_APPROVE_COMMAND_ID,
16
+ isTerminal: true,
17
+ // Sending posts publicly into a Discord server on the tenant's behalf, so the
18
+ // operator confirms once before it leaves the building. No `confirmMessage`:
19
+ // the inbox renders that field verbatim (`message-detail/panels/dialogs.tsx`),
20
+ // so a translation key would leak into the dialog — omitting it falls back to
21
+ // the module's own localized confirmation copy.
22
+ confirmRequired: true
23
+ },
24
+ {
25
+ id: CHANNEL_DISCORD_AI_PROPOSAL_DISMISS_ACTION_ID,
26
+ label: "Dismiss",
27
+ labelKey: "channel_discord.aiProposal.actions.dismiss",
28
+ variant: "outline",
29
+ icon: "x",
30
+ commandId: CHANNEL_DISCORD_AI_PROPOSAL_DISMISS_COMMAND_ID,
31
+ isTerminal: true
32
+ }
33
+ ];
34
+ const messageTypes = [
35
+ {
36
+ type: CHANNEL_DISCORD_AI_PROPOSAL_MESSAGE_TYPE,
37
+ module: "channel_discord",
38
+ labelKey: "channel_discord.aiProposal.messageType",
39
+ icon: "bot",
40
+ color: "violet",
41
+ ui: {
42
+ listItemComponent: "messages.default.listItem",
43
+ contentComponent: "messages.default.content",
44
+ actionsComponent: "messages.default.actions"
45
+ },
46
+ // Replying to the proposal itself would go nowhere — the conversation lives
47
+ // on the Discord thread the proposal points at, not on this internal note.
48
+ allowReply: false,
49
+ allowForward: true,
50
+ defaultActions: proposalActions,
51
+ // A week-old proposal is stale: the person on Discord has moved on and the
52
+ // draft was written against a conversation that has since changed.
53
+ actionsExpireAfterHours: 168
54
+ }
55
+ ];
56
+ var message_types_default = messageTypes;
57
+ export {
58
+ CHANNEL_DISCORD_AI_PROPOSAL_APPROVE_ACTION_ID,
59
+ CHANNEL_DISCORD_AI_PROPOSAL_DISMISS_ACTION_ID,
60
+ CHANNEL_DISCORD_AI_PROPOSAL_MESSAGE_TYPE,
61
+ message_types_default as default,
62
+ messageTypes
63
+ };
64
+ //# sourceMappingURL=message-types.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/channel_discord/message-types.ts"],
4
+ "sourcesContent": ["import type { MessageAction, MessageTypeDefinition } from '@open-mercato/shared/modules/messages/types'\nimport {\n CHANNEL_DISCORD_AI_PROPOSAL_APPROVE_COMMAND_ID,\n CHANNEL_DISCORD_AI_PROPOSAL_DISMISS_COMMAND_ID,\n} from './lib/ai-proposal-contract'\n\n/**\n * The message type the `complex`-tier AI proposal is filed under (issue #4778).\n *\n * Declaring it here is what makes the approval surface work at all: the messages\n * module builds its allowlist of dispatchable action commands from the\n * `defaultActions` of every REGISTERED message type\n * (`messages/lib/actions.ts` \u2192 `getMessageSafeCommandIds`). An action whose\n * `commandId` is not reachable that way is rejected with \"Action command is not\n * allowed\" \u2014 the confused-deputy guard that stops a composer from making a\n * recipient dispatch an arbitrary command with their own auth. So the two\n * commands below are allowlisted precisely because this file declares them, and\n * for no other message type.\n */\nexport const CHANNEL_DISCORD_AI_PROPOSAL_MESSAGE_TYPE = 'channel_discord.ai_reply_proposal'\n\nexport const CHANNEL_DISCORD_AI_PROPOSAL_APPROVE_ACTION_ID = 'approve_send'\nexport const CHANNEL_DISCORD_AI_PROPOSAL_DISMISS_ACTION_ID = 'dismiss'\n\nconst proposalActions: MessageAction[] = [\n {\n id: CHANNEL_DISCORD_AI_PROPOSAL_APPROVE_ACTION_ID,\n label: 'Approve and send',\n labelKey: 'channel_discord.aiProposal.actions.approve',\n variant: 'default',\n icon: 'send',\n commandId: CHANNEL_DISCORD_AI_PROPOSAL_APPROVE_COMMAND_ID,\n isTerminal: true,\n // Sending posts publicly into a Discord server on the tenant's behalf, so the\n // operator confirms once before it leaves the building. No `confirmMessage`:\n // the inbox renders that field verbatim (`message-detail/panels/dialogs.tsx`),\n // so a translation key would leak into the dialog \u2014 omitting it falls back to\n // the module's own localized confirmation copy.\n confirmRequired: true,\n },\n {\n id: CHANNEL_DISCORD_AI_PROPOSAL_DISMISS_ACTION_ID,\n label: 'Dismiss',\n labelKey: 'channel_discord.aiProposal.actions.dismiss',\n variant: 'outline',\n icon: 'x',\n commandId: CHANNEL_DISCORD_AI_PROPOSAL_DISMISS_COMMAND_ID,\n isTerminal: true,\n },\n]\n\nexport const messageTypes: MessageTypeDefinition[] = [\n {\n type: CHANNEL_DISCORD_AI_PROPOSAL_MESSAGE_TYPE,\n module: 'channel_discord',\n labelKey: 'channel_discord.aiProposal.messageType',\n icon: 'bot',\n color: 'violet',\n ui: {\n listItemComponent: 'messages.default.listItem',\n contentComponent: 'messages.default.content',\n actionsComponent: 'messages.default.actions',\n },\n // Replying to the proposal itself would go nowhere \u2014 the conversation lives\n // on the Discord thread the proposal points at, not on this internal note.\n allowReply: false,\n allowForward: true,\n defaultActions: proposalActions,\n // A week-old proposal is stale: the person on Discord has moved on and the\n // draft was written against a conversation that has since changed.\n actionsExpireAfterHours: 168,\n },\n]\n\nexport default messageTypes\n"],
5
+ "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAeA,MAAM,2CAA2C;AAEjD,MAAM,gDAAgD;AACtD,MAAM,gDAAgD;AAE7D,MAAM,kBAAmC;AAAA,EACvC;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,WAAW;AAAA,IACX,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMZ,iBAAiB;AAAA,EACnB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,OAAO;AAAA,IACP,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,WAAW;AAAA,IACX,YAAY;AAAA,EACd;AACF;AAEO,MAAM,eAAwC;AAAA,EACnD;AAAA,IACE,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,MAAM;AAAA,IACN,OAAO;AAAA,IACP,IAAI;AAAA,MACF,mBAAmB;AAAA,MACnB,kBAAkB;AAAA,MAClB,kBAAkB;AAAA,IACpB;AAAA;AAAA;AAAA,IAGA,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,gBAAgB;AAAA;AAAA;AAAA,IAGhB,yBAAyB;AAAA,EAC3B;AACF;AAEA,IAAO,wBAAQ;",
6
+ "names": []
7
+ }