@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,203 @@
1
+ import { fetchWithTimeout, FetchTimeoutError } from "@open-mercato/shared/lib/http/fetchWithTimeout";
2
+ const DISCORD_API_BASE = "https://discord.com/api/v10";
3
+ const DISCORD_MESSAGE_FLAG_EPHEMERAL = 64;
4
+ class DiscordApiError extends Error {
5
+ constructor(message, status, detail) {
6
+ super(message);
7
+ this.name = "DiscordApiError";
8
+ this.status = status;
9
+ this.detail = detail;
10
+ }
11
+ }
12
+ const DISCORD_MAX_RETRIES = 3;
13
+ const DISCORD_BACKOFF_BASE_MS = 500;
14
+ const DISCORD_BACKOFF_CAP_MS = 8e3;
15
+ const DISCORD_DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
16
+ function resolveRequestTimeoutMs() {
17
+ const fromEnv = Number.parseInt(process.env.OM_CHANNEL_DISCORD_REQUEST_TIMEOUT_MS ?? "", 10);
18
+ return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DISCORD_DEFAULT_REQUEST_TIMEOUT_MS;
19
+ }
20
+ function encodeEmoji(emoji) {
21
+ return encodeURIComponent(emoji);
22
+ }
23
+ class FetchDiscordRestClient {
24
+ async createMessage(auth, input) {
25
+ const body = {
26
+ content: input.content,
27
+ allowed_mentions: input.allowedMentions ?? { parse: [] }
28
+ };
29
+ if (input.messageReferenceId) {
30
+ body.message_reference = { message_id: input.messageReferenceId };
31
+ }
32
+ return this.request(
33
+ auth,
34
+ "POST",
35
+ `/channels/${encodeURIComponent(input.channelId)}/messages`,
36
+ body
37
+ );
38
+ }
39
+ async editMessage(auth, channelId, messageId, content) {
40
+ return this.request(
41
+ auth,
42
+ "PATCH",
43
+ `/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}`,
44
+ { content }
45
+ );
46
+ }
47
+ async deleteMessage(auth, channelId, messageId) {
48
+ await this.request(
49
+ auth,
50
+ "DELETE",
51
+ `/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}`
52
+ );
53
+ }
54
+ async addReaction(auth, channelId, messageId, emoji) {
55
+ await this.request(
56
+ auth,
57
+ "PUT",
58
+ `/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/reactions/${encodeEmoji(emoji)}/@me`
59
+ );
60
+ }
61
+ async removeReaction(auth, channelId, messageId, emoji) {
62
+ await this.request(
63
+ auth,
64
+ "DELETE",
65
+ `/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/reactions/${encodeEmoji(emoji)}/@me`
66
+ );
67
+ }
68
+ async listMessages(auth, channelId, input) {
69
+ const search = new URLSearchParams();
70
+ if (input.before) search.set("before", input.before);
71
+ search.set("limit", String(Math.min(Math.max(input.limit ?? 50, 1), 100)));
72
+ const query = search.toString();
73
+ return this.request(
74
+ auth,
75
+ "GET",
76
+ `/channels/${encodeURIComponent(channelId)}/messages?${query}`
77
+ );
78
+ }
79
+ async getCurrentUser(auth) {
80
+ return this.request(auth, "GET", "/users/@me");
81
+ }
82
+ async getGatewayBot(auth) {
83
+ return this.request(auth, "GET", "/gateway/bot");
84
+ }
85
+ async registerGuildCommands(auth, applicationId, guildId, commands) {
86
+ await this.request(
87
+ auth,
88
+ "PUT",
89
+ `/applications/${encodeURIComponent(applicationId)}/guilds/${encodeURIComponent(guildId)}/commands`,
90
+ commands
91
+ );
92
+ }
93
+ async editOriginalInteractionResponse(applicationId, interactionToken, input) {
94
+ await this.request(
95
+ null,
96
+ "PATCH",
97
+ `/webhooks/${encodeURIComponent(applicationId)}/${encodeURIComponent(interactionToken)}/messages/@original`,
98
+ interactionResponseBody(input)
99
+ );
100
+ }
101
+ async createInteractionFollowUp(applicationId, interactionToken, input) {
102
+ await this.request(
103
+ null,
104
+ "POST",
105
+ `/webhooks/${encodeURIComponent(applicationId)}/${encodeURIComponent(interactionToken)}`,
106
+ interactionResponseBody(input)
107
+ );
108
+ }
109
+ async request(auth, method, path, body) {
110
+ const url = `${DISCORD_API_BASE}${path}`;
111
+ const headers = auth ? { Authorization: `Bot ${auth.botToken}` } : {};
112
+ let payload;
113
+ if (body !== void 0) {
114
+ headers["Content-Type"] = "application/json";
115
+ payload = JSON.stringify(body);
116
+ }
117
+ let attempt = 0;
118
+ let lastError = null;
119
+ while (attempt <= DISCORD_MAX_RETRIES) {
120
+ let res;
121
+ try {
122
+ res = await fetchWithTimeout(url, { method, headers, body: payload, timeoutMs: resolveRequestTimeoutMs() });
123
+ } catch (err) {
124
+ const errName = err?.name;
125
+ const aborted = err instanceof FetchTimeoutError || errName === "TimeoutError" || errName === "AbortError";
126
+ if (!aborted) throw err;
127
+ const timeoutError = new DiscordApiError(`Discord API ${method} ${path} timed out`, 599, "request timed out");
128
+ if (attempt === DISCORD_MAX_RETRIES) throw timeoutError;
129
+ lastError = timeoutError;
130
+ await sleep(computeBackoff(attempt));
131
+ attempt += 1;
132
+ continue;
133
+ }
134
+ const text = await res.text();
135
+ if (res.ok) {
136
+ if (!text) return void 0;
137
+ return JSON.parse(text);
138
+ }
139
+ const detail = parseErrorMessage(text) ?? `${res.status} ${res.statusText}`;
140
+ const apiError = new DiscordApiError(`Discord API ${method} ${path} failed: ${detail}`, res.status, detail);
141
+ const transient = res.status === 429 || res.status >= 500 && res.status < 600;
142
+ if (!transient || attempt === DISCORD_MAX_RETRIES) {
143
+ throw apiError;
144
+ }
145
+ lastError = apiError;
146
+ const waitMs = parseRetryAfter(res.headers.get("retry-after")) ?? computeBackoff(attempt);
147
+ await sleep(waitMs);
148
+ attempt += 1;
149
+ }
150
+ throw lastError ?? new DiscordApiError(`Discord API ${method} ${path} exhausted retries`, 599, "retries exhausted");
151
+ }
152
+ }
153
+ function interactionResponseBody(input) {
154
+ return {
155
+ content: input.content,
156
+ // Interaction replies quote whatever the user typed, so mentions inside it
157
+ // must not become live pings — same guard `createMessage` applies.
158
+ allowed_mentions: { parse: [] },
159
+ ...input.ephemeral ? { flags: DISCORD_MESSAGE_FLAG_EPHEMERAL } : {}
160
+ };
161
+ }
162
+ function parseRetryAfter(value) {
163
+ if (!value) return null;
164
+ const asNumber = Number(value);
165
+ if (Number.isFinite(asNumber) && asNumber >= 0) {
166
+ return Math.min(asNumber * 1e3, DISCORD_BACKOFF_CAP_MS);
167
+ }
168
+ return null;
169
+ }
170
+ function computeBackoff(attempt) {
171
+ const raw = DISCORD_BACKOFF_BASE_MS * Math.pow(2, attempt);
172
+ const jitter = Math.floor(Math.random() * 100);
173
+ return Math.min(raw + jitter, DISCORD_BACKOFF_CAP_MS);
174
+ }
175
+ function sleep(ms) {
176
+ return new Promise((resolve) => setTimeout(resolve, ms));
177
+ }
178
+ function parseErrorMessage(text) {
179
+ if (!text) return null;
180
+ try {
181
+ const parsed = JSON.parse(text);
182
+ if (typeof parsed?.message === "string") return parsed.message;
183
+ if (typeof parsed?.error === "string") return parsed.error;
184
+ } catch {
185
+ }
186
+ return text.length > 200 ? text.slice(0, 200) : text;
187
+ }
188
+ let cachedClient = null;
189
+ function getDiscordRestClient() {
190
+ if (!cachedClient) cachedClient = new FetchDiscordRestClient();
191
+ return cachedClient;
192
+ }
193
+ function setDiscordRestClient(client) {
194
+ cachedClient = client;
195
+ }
196
+ export {
197
+ DISCORD_API_BASE,
198
+ DISCORD_MESSAGE_FLAG_EPHEMERAL,
199
+ DiscordApiError,
200
+ getDiscordRestClient,
201
+ setDiscordRestClient
202
+ };
203
+ //# sourceMappingURL=discord-rest.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/discord-rest.ts"],
4
+ "sourcesContent": ["/**\n * Thin Discord REST API client. Same trade-off as the Gmail provider's\n * `gmail-client.ts`: we call `fetch` directly (no `discord.js`) so the adapter\n * ships zero heavyweight runtime dependencies and stays swappable in tests via\n * `setDiscordRestClient(...)`.\n *\n * Only the endpoints the adapter actually uses are exposed:\n * - createMessage \u2192 POST /channels/{id}/messages\n * - editMessage \u2192 PATCH /channels/{id}/messages/{mid}\n * - deleteMessage \u2192 DELETE /channels/{id}/messages/{mid}\n * - addReaction \u2192 PUT /channels/{id}/messages/{mid}/reactions/{emoji}/@me\n * - removeReaction \u2192 DELETE /channels/{id}/messages/{mid}/reactions/{emoji}/@me\n * - listMessages \u2192 GET /channels/{id}/messages?before=\n * - getCurrentUser \u2192 GET /users/@me\n * - getGatewayBot \u2192 GET /gateway/bot\n * - registerGuildCommands \u2192 PUT /applications/{app}/guilds/{guild}/commands\n * - editOriginalInteractionResponse \u2192 PATCH /webhooks/{app}/{token}/messages/@original\n * - createInteractionFollowUp \u2192 POST /webhooks/{app}/{token}\n */\nimport { fetchWithTimeout, FetchTimeoutError } from '@open-mercato/shared/lib/http/fetchWithTimeout'\n\nexport const DISCORD_API_BASE = 'https://discord.com/api/v10'\n\nexport interface DiscordAuth {\n botToken: string\n}\n\nexport interface DiscordUser {\n id: string\n username: string\n global_name?: string | null\n bot?: boolean\n discriminator?: string\n avatar?: string | null\n}\n\n/**\n * Open Mercato marker carried by a message object that was SYNTHESIZED from a\n * Discord interaction rather than received from Discord. Discord never returns\n * this key; it exists so `normalizeInboundDiscordMessage` can tell a hub\n * consumer that the record came from a slash command or a component press\n * instead of someone typing in the channel.\n */\nexport interface DiscordInteractionOrigin {\n id: string\n type: number\n applicationId: string\n commandName?: string\n customId?: string\n}\n\nexport interface DiscordMessageObject {\n id: string\n channel_id: string\n guild_id?: string\n content: string\n author: DiscordUser\n timestamp: string\n attachments?: Array<{ id: string; url: string; filename: string; content_type?: string; size?: number }>\n message_reference?: { message_id?: string; channel_id?: string; guild_id?: string }\n /** Present only on interaction-derived messages \u2014 see `DiscordInteractionOrigin`. */\n discord_interaction?: DiscordInteractionOrigin\n [key: string]: unknown\n}\n\nexport interface CreateMessageInput {\n channelId: string\n content: string\n messageReferenceId?: string\n allowedMentions?: Record<string, unknown>\n}\n\n/**\n * Body of a message that replaces (or follows) a deferred interaction response.\n *\n * `ephemeral` maps to Discord's `flags: 64` \u2014 only the invoking user sees the\n * message. It is the right default for an operational acknowledgement that would\n * otherwise be noise for everyone else in the channel.\n */\nexport interface InteractionResponseInput {\n content: string\n ephemeral?: boolean\n}\n\n/** Discord message flag: visible only to the user who triggered the interaction. */\nexport const DISCORD_MESSAGE_FLAG_EPHEMERAL = 64\n\nexport interface GatewayBotResponse {\n url: string\n shards: number\n session_start_limit?: { total: number; remaining: number; reset_after: number; max_concurrency: number }\n}\n\nexport interface DiscordRestClient {\n createMessage(auth: DiscordAuth, input: CreateMessageInput): Promise<DiscordMessageObject>\n editMessage(auth: DiscordAuth, channelId: string, messageId: string, content: string): Promise<DiscordMessageObject>\n deleteMessage(auth: DiscordAuth, channelId: string, messageId: string): Promise<void>\n addReaction(auth: DiscordAuth, channelId: string, messageId: string, emoji: string): Promise<void>\n removeReaction(auth: DiscordAuth, channelId: string, messageId: string, emoji: string): Promise<void>\n listMessages(\n auth: DiscordAuth,\n channelId: string,\n input: { before?: string; limit?: number },\n ): Promise<DiscordMessageObject[]>\n getCurrentUser(auth: DiscordAuth): Promise<DiscordUser>\n getGatewayBot(auth: DiscordAuth): Promise<GatewayBotResponse>\n registerGuildCommands(\n auth: DiscordAuth,\n applicationId: string,\n guildId: string,\n commands: Array<Record<string, unknown>>,\n ): Promise<void>\n /**\n * Replace the \"thinking\u2026\" placeholder a deferred interaction response left\n * behind. This is the call that ends the pending state, so it is the one the\n * dispatch worker makes first.\n *\n * Takes NO bot token: the interaction token in the URL is the credential, and\n * Discord documents these webhook endpoints as requiring no authorization. The\n * token is single-interaction and expires after 15 minutes.\n */\n editOriginalInteractionResponse(\n applicationId: string,\n interactionToken: string,\n input: InteractionResponseInput,\n ): Promise<void>\n /**\n * Post an ADDITIONAL message against the same interaction token. Used as the\n * fallback when the original response can no longer be edited.\n */\n createInteractionFollowUp(\n applicationId: string,\n interactionToken: string,\n input: InteractionResponseInput,\n ): Promise<void>\n}\n\nexport class DiscordApiError extends Error {\n readonly status: number\n readonly detail: string\n constructor(message: string, status: number, detail: string) {\n super(message)\n this.name = 'DiscordApiError'\n this.status = status\n this.detail = detail\n }\n}\n\nconst DISCORD_MAX_RETRIES = 3\nconst DISCORD_BACKOFF_BASE_MS = 500\nconst DISCORD_BACKOFF_CAP_MS = 8_000\nconst DISCORD_DEFAULT_REQUEST_TIMEOUT_MS = 15_000\n\nfunction resolveRequestTimeoutMs(): number {\n const fromEnv = Number.parseInt(process.env.OM_CHANNEL_DISCORD_REQUEST_TIMEOUT_MS ?? '', 10)\n return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DISCORD_DEFAULT_REQUEST_TIMEOUT_MS\n}\n\n/**\n * Discord emoji path encoding: unicode emoji is URL-encoded as-is; a custom\n * emoji uses the `name:id` form. We pass through the caller's value and only\n * `encodeURIComponent` it so both forms survive.\n */\nfunction encodeEmoji(emoji: string): string {\n return encodeURIComponent(emoji)\n}\n\nclass FetchDiscordRestClient implements DiscordRestClient {\n async createMessage(auth: DiscordAuth, input: CreateMessageInput): Promise<DiscordMessageObject> {\n const body: Record<string, unknown> = {\n content: input.content,\n allowed_mentions: input.allowedMentions ?? { parse: [] },\n }\n if (input.messageReferenceId) {\n body.message_reference = { message_id: input.messageReferenceId }\n }\n return this.request<DiscordMessageObject>(\n auth,\n 'POST',\n `/channels/${encodeURIComponent(input.channelId)}/messages`,\n body,\n )\n }\n\n async editMessage(\n auth: DiscordAuth,\n channelId: string,\n messageId: string,\n content: string,\n ): Promise<DiscordMessageObject> {\n return this.request<DiscordMessageObject>(\n auth,\n 'PATCH',\n `/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}`,\n { content },\n )\n }\n\n async deleteMessage(auth: DiscordAuth, channelId: string, messageId: string): Promise<void> {\n await this.request<void>(\n auth,\n 'DELETE',\n `/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}`,\n )\n }\n\n async addReaction(auth: DiscordAuth, channelId: string, messageId: string, emoji: string): Promise<void> {\n await this.request<void>(\n auth,\n 'PUT',\n `/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/reactions/${encodeEmoji(emoji)}/@me`,\n )\n }\n\n async removeReaction(auth: DiscordAuth, channelId: string, messageId: string, emoji: string): Promise<void> {\n await this.request<void>(\n auth,\n 'DELETE',\n `/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/reactions/${encodeEmoji(emoji)}/@me`,\n )\n }\n\n async listMessages(\n auth: DiscordAuth,\n channelId: string,\n input: { before?: string; limit?: number },\n ): Promise<DiscordMessageObject[]> {\n const search = new URLSearchParams()\n if (input.before) search.set('before', input.before)\n search.set('limit', String(Math.min(Math.max(input.limit ?? 50, 1), 100)))\n const query = search.toString()\n return this.request<DiscordMessageObject[]>(\n auth,\n 'GET',\n `/channels/${encodeURIComponent(channelId)}/messages?${query}`,\n )\n }\n\n async getCurrentUser(auth: DiscordAuth): Promise<DiscordUser> {\n return this.request<DiscordUser>(auth, 'GET', '/users/@me')\n }\n\n async getGatewayBot(auth: DiscordAuth): Promise<GatewayBotResponse> {\n return this.request<GatewayBotResponse>(auth, 'GET', '/gateway/bot')\n }\n\n async registerGuildCommands(\n auth: DiscordAuth,\n applicationId: string,\n guildId: string,\n commands: Array<Record<string, unknown>>,\n ): Promise<void> {\n await this.request<unknown>(\n auth,\n 'PUT',\n `/applications/${encodeURIComponent(applicationId)}/guilds/${encodeURIComponent(guildId)}/commands`,\n commands,\n )\n }\n\n async editOriginalInteractionResponse(\n applicationId: string,\n interactionToken: string,\n input: InteractionResponseInput,\n ): Promise<void> {\n await this.request<unknown>(\n null,\n 'PATCH',\n `/webhooks/${encodeURIComponent(applicationId)}/${encodeURIComponent(interactionToken)}/messages/@original`,\n interactionResponseBody(input),\n )\n }\n\n async createInteractionFollowUp(\n applicationId: string,\n interactionToken: string,\n input: InteractionResponseInput,\n ): Promise<void> {\n await this.request<unknown>(\n null,\n 'POST',\n `/webhooks/${encodeURIComponent(applicationId)}/${encodeURIComponent(interactionToken)}`,\n interactionResponseBody(input),\n )\n }\n\n private async request<T>(\n auth: DiscordAuth | null,\n method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',\n path: string,\n body?: unknown,\n ): Promise<T> {\n const url = `${DISCORD_API_BASE}${path}`\n // `auth === null` is the interaction-webhook family: the token in the path is\n // the credential and Discord rejects nothing for the missing header, so\n // sending the bot token there would leak it for no benefit.\n const headers: Record<string, string> = auth ? { Authorization: `Bot ${auth.botToken}` } : {}\n let payload: BodyInit | undefined\n if (body !== undefined) {\n headers['Content-Type'] = 'application/json'\n payload = JSON.stringify(body)\n }\n\n let attempt = 0\n let lastError: DiscordApiError | null = null\n while (attempt <= DISCORD_MAX_RETRIES) {\n let res: Response\n try {\n res = await fetchWithTimeout(url, { method, headers, body: payload, timeoutMs: resolveRequestTimeoutMs() })\n } catch (err) {\n const errName = (err as { name?: unknown } | null)?.name\n const aborted = err instanceof FetchTimeoutError || errName === 'TimeoutError' || errName === 'AbortError'\n if (!aborted) throw err\n const timeoutError = new DiscordApiError(`Discord API ${method} ${path} timed out`, 599, 'request timed out')\n if (attempt === DISCORD_MAX_RETRIES) throw timeoutError\n lastError = timeoutError\n await sleep(computeBackoff(attempt))\n attempt += 1\n continue\n }\n\n const text = await res.text()\n if (res.ok) {\n if (!text) return undefined as unknown as T\n return JSON.parse(text) as T\n }\n\n const detail = parseErrorMessage(text) ?? `${res.status} ${res.statusText}`\n const apiError = new DiscordApiError(`Discord API ${method} ${path} failed: ${detail}`, res.status, detail)\n const transient = res.status === 429 || (res.status >= 500 && res.status < 600)\n if (!transient || attempt === DISCORD_MAX_RETRIES) {\n throw apiError\n }\n lastError = apiError\n const waitMs = parseRetryAfter(res.headers.get('retry-after')) ?? computeBackoff(attempt)\n await sleep(waitMs)\n attempt += 1\n }\n throw lastError ?? new DiscordApiError(`Discord API ${method} ${path} exhausted retries`, 599, 'retries exhausted')\n }\n}\n\nfunction interactionResponseBody(input: InteractionResponseInput): Record<string, unknown> {\n return {\n content: input.content,\n // Interaction replies quote whatever the user typed, so mentions inside it\n // must not become live pings \u2014 same guard `createMessage` applies.\n allowed_mentions: { parse: [] },\n ...(input.ephemeral ? { flags: DISCORD_MESSAGE_FLAG_EPHEMERAL } : {}),\n }\n}\n\nfunction parseRetryAfter(value: string | null): number | null {\n if (!value) return null\n const asNumber = Number(value)\n if (Number.isFinite(asNumber) && asNumber >= 0) {\n return Math.min(asNumber * 1000, DISCORD_BACKOFF_CAP_MS)\n }\n return null\n}\n\nfunction computeBackoff(attempt: number): number {\n const raw = DISCORD_BACKOFF_BASE_MS * Math.pow(2, attempt)\n const jitter = Math.floor(Math.random() * 100)\n return Math.min(raw + jitter, DISCORD_BACKOFF_CAP_MS)\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nfunction parseErrorMessage(text: string): string | null {\n if (!text) return null\n try {\n const parsed = JSON.parse(text) as { message?: string; error?: string }\n if (typeof parsed?.message === 'string') return parsed.message\n if (typeof parsed?.error === 'string') return parsed.error\n } catch {\n /* fall through */\n }\n return text.length > 200 ? text.slice(0, 200) : text\n}\n\nlet cachedClient: DiscordRestClient | null = null\n\nexport function getDiscordRestClient(): DiscordRestClient {\n if (!cachedClient) cachedClient = new FetchDiscordRestClient()\n return cachedClient\n}\n\nexport function setDiscordRestClient(client: DiscordRestClient | null): void {\n cachedClient = client\n}\n"],
5
+ "mappings": "AAmBA,SAAS,kBAAkB,yBAAyB;AAE7C,MAAM,mBAAmB;AAgEzB,MAAM,iCAAiC;AAoDvC,MAAM,wBAAwB,MAAM;AAAA,EAGzC,YAAY,SAAiB,QAAgB,QAAgB;AAC3D,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,SAAS;AAAA,EAChB;AACF;AAEA,MAAM,sBAAsB;AAC5B,MAAM,0BAA0B;AAChC,MAAM,yBAAyB;AAC/B,MAAM,qCAAqC;AAE3C,SAAS,0BAAkC;AACzC,QAAM,UAAU,OAAO,SAAS,QAAQ,IAAI,yCAAyC,IAAI,EAAE;AAC3F,SAAO,OAAO,SAAS,OAAO,KAAK,UAAU,IAAI,UAAU;AAC7D;AAOA,SAAS,YAAY,OAAuB;AAC1C,SAAO,mBAAmB,KAAK;AACjC;AAEA,MAAM,uBAAoD;AAAA,EACxD,MAAM,cAAc,MAAmB,OAA0D;AAC/F,UAAM,OAAgC;AAAA,MACpC,SAAS,MAAM;AAAA,MACf,kBAAkB,MAAM,mBAAmB,EAAE,OAAO,CAAC,EAAE;AAAA,IACzD;AACA,QAAI,MAAM,oBAAoB;AAC5B,WAAK,oBAAoB,EAAE,YAAY,MAAM,mBAAmB;AAAA,IAClE;AACA,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA,aAAa,mBAAmB,MAAM,SAAS,CAAC;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,YACJ,MACA,WACA,WACA,SAC+B;AAC/B,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA,aAAa,mBAAmB,SAAS,CAAC,aAAa,mBAAmB,SAAS,CAAC;AAAA,MACpF,EAAE,QAAQ;AAAA,IACZ;AAAA,EACF;AAAA,EAEA,MAAM,cAAc,MAAmB,WAAmB,WAAkC;AAC1F,UAAM,KAAK;AAAA,MACT;AAAA,MACA;AAAA,MACA,aAAa,mBAAmB,SAAS,CAAC,aAAa,mBAAmB,SAAS,CAAC;AAAA,IACtF;AAAA,EACF;AAAA,EAEA,MAAM,YAAY,MAAmB,WAAmB,WAAmB,OAA8B;AACvG,UAAM,KAAK;AAAA,MACT;AAAA,MACA;AAAA,MACA,aAAa,mBAAmB,SAAS,CAAC,aAAa,mBAAmB,SAAS,CAAC,cAAc,YAAY,KAAK,CAAC;AAAA,IACtH;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,MAAmB,WAAmB,WAAmB,OAA8B;AAC1G,UAAM,KAAK;AAAA,MACT;AAAA,MACA;AAAA,MACA,aAAa,mBAAmB,SAAS,CAAC,aAAa,mBAAmB,SAAS,CAAC,cAAc,YAAY,KAAK,CAAC;AAAA,IACtH;AAAA,EACF;AAAA,EAEA,MAAM,aACJ,MACA,WACA,OACiC;AACjC,UAAM,SAAS,IAAI,gBAAgB;AACnC,QAAI,MAAM,OAAQ,QAAO,IAAI,UAAU,MAAM,MAAM;AACnD,WAAO,IAAI,SAAS,OAAO,KAAK,IAAI,KAAK,IAAI,MAAM,SAAS,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;AACzE,UAAM,QAAQ,OAAO,SAAS;AAC9B,WAAO,KAAK;AAAA,MACV;AAAA,MACA;AAAA,MACA,aAAa,mBAAmB,SAAS,CAAC,aAAa,KAAK;AAAA,IAC9D;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,MAAyC;AAC5D,WAAO,KAAK,QAAqB,MAAM,OAAO,YAAY;AAAA,EAC5D;AAAA,EAEA,MAAM,cAAc,MAAgD;AAClE,WAAO,KAAK,QAA4B,MAAM,OAAO,cAAc;AAAA,EACrE;AAAA,EAEA,MAAM,sBACJ,MACA,eACA,SACA,UACe;AACf,UAAM,KAAK;AAAA,MACT;AAAA,MACA;AAAA,MACA,iBAAiB,mBAAmB,aAAa,CAAC,WAAW,mBAAmB,OAAO,CAAC;AAAA,MACxF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,gCACJ,eACA,kBACA,OACe;AACf,UAAM,KAAK;AAAA,MACT;AAAA,MACA;AAAA,MACA,aAAa,mBAAmB,aAAa,CAAC,IAAI,mBAAmB,gBAAgB,CAAC;AAAA,MACtF,wBAAwB,KAAK;AAAA,IAC/B;AAAA,EACF;AAAA,EAEA,MAAM,0BACJ,eACA,kBACA,OACe;AACf,UAAM,KAAK;AAAA,MACT;AAAA,MACA;AAAA,MACA,aAAa,mBAAmB,aAAa,CAAC,IAAI,mBAAmB,gBAAgB,CAAC;AAAA,MACtF,wBAAwB,KAAK;AAAA,IAC/B;AAAA,EACF;AAAA,EAEA,MAAc,QACZ,MACA,QACA,MACA,MACY;AACZ,UAAM,MAAM,GAAG,gBAAgB,GAAG,IAAI;AAItC,UAAM,UAAkC,OAAO,EAAE,eAAe,OAAO,KAAK,QAAQ,GAAG,IAAI,CAAC;AAC5F,QAAI;AACJ,QAAI,SAAS,QAAW;AACtB,cAAQ,cAAc,IAAI;AAC1B,gBAAU,KAAK,UAAU,IAAI;AAAA,IAC/B;AAEA,QAAI,UAAU;AACd,QAAI,YAAoC;AACxC,WAAO,WAAW,qBAAqB;AACrC,UAAI;AACJ,UAAI;AACF,cAAM,MAAM,iBAAiB,KAAK,EAAE,QAAQ,SAAS,MAAM,SAAS,WAAW,wBAAwB,EAAE,CAAC;AAAA,MAC5G,SAAS,KAAK;AACZ,cAAM,UAAW,KAAmC;AACpD,cAAM,UAAU,eAAe,qBAAqB,YAAY,kBAAkB,YAAY;AAC9F,YAAI,CAAC,QAAS,OAAM;AACpB,cAAM,eAAe,IAAI,gBAAgB,eAAe,MAAM,IAAI,IAAI,cAAc,KAAK,mBAAmB;AAC5G,YAAI,YAAY,oBAAqB,OAAM;AAC3C,oBAAY;AACZ,cAAM,MAAM,eAAe,OAAO,CAAC;AACnC,mBAAW;AACX;AAAA,MACF;AAEA,YAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,UAAI,IAAI,IAAI;AACV,YAAI,CAAC,KAAM,QAAO;AAClB,eAAO,KAAK,MAAM,IAAI;AAAA,MACxB;AAEA,YAAM,SAAS,kBAAkB,IAAI,KAAK,GAAG,IAAI,MAAM,IAAI,IAAI,UAAU;AACzE,YAAM,WAAW,IAAI,gBAAgB,eAAe,MAAM,IAAI,IAAI,YAAY,MAAM,IAAI,IAAI,QAAQ,MAAM;AAC1G,YAAM,YAAY,IAAI,WAAW,OAAQ,IAAI,UAAU,OAAO,IAAI,SAAS;AAC3E,UAAI,CAAC,aAAa,YAAY,qBAAqB;AACjD,cAAM;AAAA,MACR;AACA,kBAAY;AACZ,YAAM,SAAS,gBAAgB,IAAI,QAAQ,IAAI,aAAa,CAAC,KAAK,eAAe,OAAO;AACxF,YAAM,MAAM,MAAM;AAClB,iBAAW;AAAA,IACb;AACA,UAAM,aAAa,IAAI,gBAAgB,eAAe,MAAM,IAAI,IAAI,sBAAsB,KAAK,mBAAmB;AAAA,EACpH;AACF;AAEA,SAAS,wBAAwB,OAA0D;AACzF,SAAO;AAAA,IACL,SAAS,MAAM;AAAA;AAAA;AAAA,IAGf,kBAAkB,EAAE,OAAO,CAAC,EAAE;AAAA,IAC9B,GAAI,MAAM,YAAY,EAAE,OAAO,+BAA+B,IAAI,CAAC;AAAA,EACrE;AACF;AAEA,SAAS,gBAAgB,OAAqC;AAC5D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,WAAW,OAAO,KAAK;AAC7B,MAAI,OAAO,SAAS,QAAQ,KAAK,YAAY,GAAG;AAC9C,WAAO,KAAK,IAAI,WAAW,KAAM,sBAAsB;AAAA,EACzD;AACA,SAAO;AACT;AAEA,SAAS,eAAe,SAAyB;AAC/C,QAAM,MAAM,0BAA0B,KAAK,IAAI,GAAG,OAAO;AACzD,QAAM,SAAS,KAAK,MAAM,KAAK,OAAO,IAAI,GAAG;AAC7C,SAAO,KAAK,IAAI,MAAM,QAAQ,sBAAsB;AACtD;AAEA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAEA,SAAS,kBAAkB,MAA6B;AACtD,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,QAAI,OAAO,QAAQ,YAAY,SAAU,QAAO,OAAO;AACvD,QAAI,OAAO,QAAQ,UAAU,SAAU,QAAO,OAAO;AAAA,EACvD,QAAQ;AAAA,EAER;AACA,SAAO,KAAK,SAAS,MAAM,KAAK,MAAM,GAAG,GAAG,IAAI;AAClD;AAEA,IAAI,eAAyC;AAEtC,SAAS,uBAA0C;AACxD,MAAI,CAAC,aAAc,gBAAe,IAAI,uBAAuB;AAC7D,SAAO;AACT;AAEO,SAAS,qBAAqB,QAAwC;AAC3E,iBAAe;AACjB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,30 @@
1
+ const SECRET_PATTERNS = [
2
+ // `Authorization: Bot <token>` / `Bearer <token>`, however it is spelled.
3
+ /\b(?:Bot|Bearer|Basic|token|api[_-]?key|secret|password)\b\s*[:=]?\s*['"]?[A-Za-z0-9._~+/=-]{8,}['"]?/gi,
4
+ // Discord bot tokens and JWT-shaped values: three dot-separated base64url runs.
5
+ /\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{20,}\b/g,
6
+ // Provider API keys that announce themselves with a prefix.
7
+ /\b(?:sk|pk|rk)-[A-Za-z0-9_-]{16,}\b/g,
8
+ // Anything carried in a query string, which is where URLs hide credentials.
9
+ /([?&](?:access_token|token|key|api[_-]?key|secret|signature|sig)=)[^&\s]+/gi
10
+ ];
11
+ const REDACTED = "[redacted]";
12
+ function redactSecrets(text) {
13
+ return SECRET_PATTERNS.reduce(
14
+ // Only the query-string pattern captures a group. `String.replace` passes the
15
+ // match offset in that slot for the patterns that do not, so the type check is
16
+ // load-bearing — without it a redacted value comes back stamped with a number.
17
+ (current, pattern) => current.replace(pattern, (_match, group) => typeof group === "string" ? `${group}${REDACTED}` : REDACTED),
18
+ text
19
+ );
20
+ }
21
+ function describeAgentFailure(agentId, err) {
22
+ const raw = err instanceof Error ? err.message : String(err);
23
+ const firstLine = raw.split("\n", 1)[0]?.trim() || "unknown error";
24
+ return `agent ${agentId}: ${redactSecrets(firstLine)}`;
25
+ }
26
+ export {
27
+ describeAgentFailure,
28
+ redactSecrets
29
+ };
30
+ //# sourceMappingURL=failure-reason.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/failure-reason.ts"],
4
+ "sourcesContent": ["/**\n * Turn an arbitrary thrown value into a one-line reason that is safe to persist\n * and render.\n *\n * The AI auto-reply marker stores this on `channelState` and the settings page\n * renders it, so the string leaves the log and enters the product. An upstream\n * provider error is not a curated message: an HTTP client that echoes the failing\n * URL, or an SDK that quotes the Authorization header, would put a live\n * credential in a JSONB column and on an operator's screen. Redacting is cheap;\n * discovering later that a bot token is sitting in `channel_state` is not.\n *\n * The patterns are deliberately broad-but-anchored \u2014 a false positive costs one\n * unreadable fragment of a diagnostic, a false negative costs a leaked secret.\n */\nconst SECRET_PATTERNS: readonly RegExp[] = [\n // `Authorization: Bot <token>` / `Bearer <token>`, however it is spelled.\n /\\b(?:Bot|Bearer|Basic|token|api[_-]?key|secret|password)\\b\\s*[:=]?\\s*['\"]?[A-Za-z0-9._~+/=-]{8,}['\"]?/gi,\n // Discord bot tokens and JWT-shaped values: three dot-separated base64url runs.\n /\\b[A-Za-z0-9_-]{16,}\\.[A-Za-z0-9_-]{5,}\\.[A-Za-z0-9_-]{20,}\\b/g,\n // Provider API keys that announce themselves with a prefix.\n /\\b(?:sk|pk|rk)-[A-Za-z0-9_-]{16,}\\b/g,\n // Anything carried in a query string, which is where URLs hide credentials.\n /([?&](?:access_token|token|key|api[_-]?key|secret|signature|sig)=)[^&\\s]+/gi,\n]\n\nconst REDACTED = '[redacted]'\n\nexport function redactSecrets(text: string): string {\n return SECRET_PATTERNS.reduce(\n // Only the query-string pattern captures a group. `String.replace` passes the\n // match offset in that slot for the patterns that do not, so the type check is\n // load-bearing \u2014 without it a redacted value comes back stamped with a number.\n (current, pattern) => current.replace(pattern, (_match, group: unknown) => (\n typeof group === 'string' ? `${group}${REDACTED}` : REDACTED\n )),\n text,\n )\n}\n\n/**\n * A one-line, operator-readable reason naming the agent that failed \u2014 which is\n * the first thing someone staring at a silent channel needs to know \u2014 with the\n * upstream message redacted and its stack left behind.\n */\nexport function describeAgentFailure(agentId: string, err: unknown): string {\n const raw = err instanceof Error ? err.message : String(err)\n const firstLine = raw.split('\\n', 1)[0]?.trim() || 'unknown error'\n return `agent ${agentId}: ${redactSecrets(firstLine)}`\n}\n"],
5
+ "mappings": "AAcA,MAAM,kBAAqC;AAAA;AAAA,EAEzC;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AACF;AAEA,MAAM,WAAW;AAEV,SAAS,cAAc,MAAsB;AAClD,SAAO,gBAAgB;AAAA;AAAA;AAAA;AAAA,IAIrB,CAAC,SAAS,YAAY,QAAQ,QAAQ,SAAS,CAAC,QAAQ,UACtD,OAAO,UAAU,WAAW,GAAG,KAAK,GAAG,QAAQ,KAAK,QACrD;AAAA,IACD;AAAA,EACF;AACF;AAOO,SAAS,qBAAqB,SAAiB,KAAsB;AAC1E,QAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,QAAM,YAAY,IAAI,MAAM,MAAM,CAAC,EAAE,CAAC,GAAG,KAAK,KAAK;AACnD,SAAO,SAAS,OAAO,KAAK,cAAc,SAAS,CAAC;AACtD;",
6
+ "names": []
7
+ }
@@ -0,0 +1,42 @@
1
+ import { getDiscordChannelAdapter } from "./adapter.js";
2
+ import { isAuthoredByBot } from "./normalize-inbound.js";
3
+ import { DISCORD_CONVERSATION_PREFIX } from "./normalize-inbound.js";
4
+ function buildInboundMessageJob(input) {
5
+ const { message, channel, botUserId } = input;
6
+ if (isAuthoredByBot(message, botUserId)) return null;
7
+ const raw = {
8
+ raw: { discordMessage: message },
9
+ eventType: "message"
10
+ };
11
+ return {
12
+ providerKey: "discord",
13
+ channelId: channel.channelId,
14
+ channelType: channel.channelType,
15
+ raw,
16
+ scope: { tenantId: channel.tenantId, organizationId: channel.organizationId }
17
+ };
18
+ }
19
+ async function buildReactionJob(input) {
20
+ const { reaction, action, channel, botUserId } = input;
21
+ const reactionUserId = typeof reaction.user_id === "string" ? reaction.user_id : void 0;
22
+ if (botUserId && reactionUserId === botUserId) return null;
23
+ const event = await getDiscordChannelAdapter().normalizeInboundReaction({
24
+ raw: { discordReaction: { ...reaction, action } },
25
+ eventType: "reaction"
26
+ });
27
+ return {
28
+ kind: "inbound",
29
+ providerKey: "discord",
30
+ channelId: channel.channelId,
31
+ channelType: channel.channelType,
32
+ event,
33
+ scope: { tenantId: channel.tenantId, organizationId: channel.organizationId },
34
+ attempt: 1
35
+ };
36
+ }
37
+ export {
38
+ DISCORD_CONVERSATION_PREFIX,
39
+ buildInboundMessageJob,
40
+ buildReactionJob
41
+ };
42
+ //# sourceMappingURL=gateway-bridge.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/gateway-bridge.ts"],
4
+ "sourcesContent": ["import type { InboundMessage } from '@open-mercato/core/modules/communication_channels/lib/adapter'\nimport type { InboundProcessorPayload } from '@open-mercato/core/modules/communication_channels/workers/inbound-processor'\nimport type { ReactionInboundJob } from '@open-mercato/core/modules/communication_channels/lib/reaction-processor-types'\nimport type { DiscordMessageObject } from './discord-rest'\nimport { getDiscordChannelAdapter } from './adapter'\nimport { isAuthoredByBot } from './normalize-inbound'\nimport { DISCORD_CONVERSATION_PREFIX } from './normalize-inbound'\n\nexport interface GatewayChannelScope {\n channelId: string\n channelType: string\n tenantId: string\n organizationId: string | null\n}\n\n/**\n * Build the hub inbound-processor job for a `MESSAGE_CREATE` gateway event.\n *\n * Returns `null` when the message was authored by the bot itself \u2014 this is the\n * feedback-loop guard the spec requires (the bot must never ingest + answer its\n * own outbound messages). The hub then dedups by `(channel_id, external_message_id)`.\n *\n * The raw Discord object is wrapped as an `InboundMessage` so the existing\n * `inbound-processor` worker calls `adapter.normalizeInbound` exactly as it does\n * for the webhook path \u2014 no hub change.\n */\nexport function buildInboundMessageJob(input: {\n message: DiscordMessageObject\n channel: GatewayChannelScope\n botUserId: string | undefined\n}): InboundProcessorPayload | null {\n const { message, channel, botUserId } = input\n if (isAuthoredByBot(message, botUserId)) return null\n\n const raw: InboundMessage = {\n raw: { discordMessage: message as unknown as Record<string, unknown> },\n eventType: 'message',\n }\n return {\n providerKey: 'discord',\n channelId: channel.channelId,\n channelType: channel.channelType,\n raw,\n scope: { tenantId: channel.tenantId, organizationId: channel.organizationId },\n }\n}\n\n/**\n * Build the hub reaction-processor job for a `MESSAGE_REACTION_ADD/REMOVE`\n * gateway event. Returns `null` for reactions the bot itself added. The reaction\n * is normalized through the adapter so it matches the webhook reaction path.\n */\nexport async function buildReactionJob(input: {\n reaction: Record<string, unknown>\n action: 'added' | 'removed'\n channel: GatewayChannelScope\n botUserId: string | undefined\n}): Promise<ReactionInboundJob | null> {\n const { reaction, action, channel, botUserId } = input\n const reactionUserId = typeof reaction.user_id === 'string' ? reaction.user_id : undefined\n if (botUserId && reactionUserId === botUserId) return null\n\n const event = await getDiscordChannelAdapter().normalizeInboundReaction({\n raw: { discordReaction: { ...reaction, action } },\n eventType: 'reaction',\n })\n\n return {\n kind: 'inbound',\n providerKey: 'discord',\n channelId: channel.channelId,\n channelType: channel.channelType,\n event,\n scope: { tenantId: channel.tenantId, organizationId: channel.organizationId },\n attempt: 1,\n }\n}\n\nexport { DISCORD_CONVERSATION_PREFIX }\n"],
5
+ "mappings": "AAIA,SAAS,gCAAgC;AACzC,SAAS,uBAAuB;AAChC,SAAS,mCAAmC;AAoBrC,SAAS,uBAAuB,OAIJ;AACjC,QAAM,EAAE,SAAS,SAAS,UAAU,IAAI;AACxC,MAAI,gBAAgB,SAAS,SAAS,EAAG,QAAO;AAEhD,QAAM,MAAsB;AAAA,IAC1B,KAAK,EAAE,gBAAgB,QAA8C;AAAA,IACrE,WAAW;AAAA,EACb;AACA,SAAO;AAAA,IACL,aAAa;AAAA,IACb,WAAW,QAAQ;AAAA,IACnB,aAAa,QAAQ;AAAA,IACrB;AAAA,IACA,OAAO,EAAE,UAAU,QAAQ,UAAU,gBAAgB,QAAQ,eAAe;AAAA,EAC9E;AACF;AAOA,eAAsB,iBAAiB,OAKA;AACrC,QAAM,EAAE,UAAU,QAAQ,SAAS,UAAU,IAAI;AACjD,QAAM,iBAAiB,OAAO,SAAS,YAAY,WAAW,SAAS,UAAU;AACjF,MAAI,aAAa,mBAAmB,UAAW,QAAO;AAEtD,QAAM,QAAQ,MAAM,yBAAyB,EAAE,yBAAyB;AAAA,IACtE,KAAK,EAAE,iBAAiB,EAAE,GAAG,UAAU,OAAO,EAAE;AAAA,IAChD,WAAW;AAAA,EACb,CAAC;AAED,SAAO;AAAA,IACL,MAAM;AAAA,IACN,aAAa;AAAA,IACb,WAAW,QAAQ;AAAA,IACnB,aAAa,QAAQ;AAAA,IACrB;AAAA,IACA,OAAO,EAAE,UAAU,QAAQ,UAAU,gBAAgB,QAAQ,eAAe;AAAA,IAC5E,SAAS;AAAA,EACX;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,37 @@
1
+ import { discordCredentialsSchema } from "./credentials.js";
2
+ import { DiscordApiError, getDiscordRestClient } from "./discord-rest.js";
3
+ const channelDiscordHealthCheck = {
4
+ async check(credentials, _scope) {
5
+ const parsed = discordCredentialsSchema.safeParse(credentials ?? {});
6
+ if (!parsed.success) {
7
+ const first = parsed.error.issues[0];
8
+ return {
9
+ status: "unhealthy",
10
+ message: `Discord credentials invalid: ${first?.message ?? "unknown validation error"}`,
11
+ details: { reason: "invalid_credentials" }
12
+ };
13
+ }
14
+ try {
15
+ const user = await getDiscordRestClient().getCurrentUser({ botToken: parsed.data.botToken });
16
+ return {
17
+ status: "healthy",
18
+ message: "Discord bot token valid",
19
+ details: { botUserId: user.id, botUsername: user.username }
20
+ };
21
+ } catch (error) {
22
+ const status = error instanceof DiscordApiError ? error.status : 0;
23
+ const reason = status === 401 ? "invalid_token" : "discord_probe_failed";
24
+ const raw = error instanceof Error ? error.message : "Discord probe failed";
25
+ const message = raw.replace(/^\[internal\]\s*/, "");
26
+ return {
27
+ status: "unhealthy",
28
+ message: status === 401 ? "Discord bot token rejected (401)" : `Discord probe failed: ${message}`,
29
+ details: { reason, status }
30
+ };
31
+ }
32
+ }
33
+ };
34
+ export {
35
+ channelDiscordHealthCheck
36
+ };
37
+ //# sourceMappingURL=health.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/health.ts"],
4
+ "sourcesContent": ["import type { IntegrationScope } from '@open-mercato/shared/modules/integrations/types'\nimport { discordCredentialsSchema } from './credentials'\nimport { DiscordApiError, getDiscordRestClient } from './discord-rest'\n\nexport type HealthCheckStatus = 'healthy' | 'degraded' | 'unhealthy'\n\nexport interface HealthCheckResult {\n status: HealthCheckStatus\n message?: string\n details?: Record<string, unknown>\n}\n\n/**\n * Liveness probe for the Discord integration. The hub resolves it by the service\n * name declared in `integration.ts` (`channelDiscordHealthCheck`) and passes the\n * tenant-scoped `IntegrationCredentials` blob (the bot token + ids).\n *\n * The bot token is enough for a real probe: we call `GET /users/@me`. A clean\n * response is `healthy`; a `401` (revoked/invalid token) is `unhealthy` with an\n * actionable reason; any other failure is `unhealthy`.\n */\nexport const channelDiscordHealthCheck = {\n async check(\n credentials: Record<string, unknown> | null,\n _scope: IntegrationScope,\n ): Promise<HealthCheckResult> {\n const parsed = discordCredentialsSchema.safeParse(credentials ?? {})\n if (!parsed.success) {\n const first = parsed.error.issues[0]\n return {\n status: 'unhealthy',\n message: `Discord credentials invalid: ${first?.message ?? 'unknown validation error'}`,\n details: { reason: 'invalid_credentials' },\n }\n }\n try {\n const user = await getDiscordRestClient().getCurrentUser({ botToken: parsed.data.botToken })\n return {\n status: 'healthy',\n message: 'Discord bot token valid',\n details: { botUserId: user.id, botUsername: user.username },\n }\n } catch (error) {\n const status = error instanceof DiscordApiError ? error.status : 0\n const reason = status === 401 ? 'invalid_token' : 'discord_probe_failed'\n const raw = error instanceof Error ? error.message : 'Discord probe failed'\n const message = raw.replace(/^\\[internal\\]\\s*/, '')\n return {\n status: 'unhealthy',\n message: status === 401 ? 'Discord bot token rejected (401)' : `Discord probe failed: ${message}`,\n details: { reason, status },\n }\n }\n },\n}\n"],
5
+ "mappings": "AACA,SAAS,gCAAgC;AACzC,SAAS,iBAAiB,4BAA4B;AAmB/C,MAAM,4BAA4B;AAAA,EACvC,MAAM,MACJ,aACA,QAC4B;AAC5B,UAAM,SAAS,yBAAyB,UAAU,eAAe,CAAC,CAAC;AACnE,QAAI,CAAC,OAAO,SAAS;AACnB,YAAM,QAAQ,OAAO,MAAM,OAAO,CAAC;AACnC,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,gCAAgC,OAAO,WAAW,0BAA0B;AAAA,QACrF,SAAS,EAAE,QAAQ,sBAAsB;AAAA,MAC3C;AAAA,IACF;AACA,QAAI;AACF,YAAM,OAAO,MAAM,qBAAqB,EAAE,eAAe,EAAE,UAAU,OAAO,KAAK,SAAS,CAAC;AAC3F,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,SAAS,EAAE,WAAW,KAAK,IAAI,aAAa,KAAK,SAAS;AAAA,MAC5D;AAAA,IACF,SAAS,OAAO;AACd,YAAM,SAAS,iBAAiB,kBAAkB,MAAM,SAAS;AACjE,YAAM,SAAS,WAAW,MAAM,kBAAkB;AAClD,YAAM,MAAM,iBAAiB,QAAQ,MAAM,UAAU;AACrD,YAAM,UAAU,IAAI,QAAQ,oBAAoB,EAAE;AAClD,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,WAAW,MAAM,qCAAqC,yBAAyB,OAAO;AAAA,QAC/F,SAAS,EAAE,QAAQ,OAAO;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,164 @@
1
+ import { DISCORD_MAX_BODY_LENGTH } from "./capabilities.js";
2
+ import { buildInboundMessageJob } from "./gateway-bridge.js";
3
+ import { DiscordInteractionType } from "./interactions-verify.js";
4
+ const DISPATCHABLE_INTERACTION_TYPES = [
5
+ DiscordInteractionType.APPLICATION_COMMAND,
6
+ DiscordInteractionType.MESSAGE_COMPONENT,
7
+ DiscordInteractionType.MODAL_SUBMIT
8
+ ];
9
+ function isDispatchableInteractionType(type) {
10
+ return DISPATCHABLE_INTERACTION_TYPES.includes(type);
11
+ }
12
+ function readString(source, key) {
13
+ const value = source?.[key];
14
+ return typeof value === "string" && value.length > 0 ? value : void 0;
15
+ }
16
+ function readOptionValue(option) {
17
+ const value = option.value;
18
+ if (value === void 0 || value === null) return "";
19
+ if (typeof value === "object") return JSON.stringify(value);
20
+ return String(value);
21
+ }
22
+ function renderCommandOptions(options) {
23
+ if (!Array.isArray(options)) return [];
24
+ const rendered = [];
25
+ for (const entry of options) {
26
+ if (!entry || typeof entry !== "object") continue;
27
+ const option = entry;
28
+ const name = readString(option, "name");
29
+ if (!name) continue;
30
+ const nested = renderCommandOptions(option.options);
31
+ if (nested.length > 0) {
32
+ rendered.push(name, ...nested);
33
+ continue;
34
+ }
35
+ rendered.push(`${name}:${readOptionValue(option)}`);
36
+ }
37
+ return rendered;
38
+ }
39
+ function renderComponentValues(data) {
40
+ if (!data) return [];
41
+ const values = data.values;
42
+ if (Array.isArray(values)) return values.map((value) => String(value));
43
+ const rows = data.components;
44
+ if (!Array.isArray(rows)) return [];
45
+ const rendered = [];
46
+ for (const row of rows) {
47
+ const children = row?.components;
48
+ if (!Array.isArray(children)) continue;
49
+ for (const child of children) {
50
+ if (!child || typeof child !== "object") continue;
51
+ const field = child;
52
+ const customId = readString(field, "custom_id");
53
+ const value = readString(field, "value");
54
+ if (!customId) continue;
55
+ rendered.push(`${customId}:${value ?? ""}`);
56
+ }
57
+ }
58
+ return rendered;
59
+ }
60
+ function renderInteractionContent(interaction) {
61
+ const data = interaction.data;
62
+ const commandName = readString(data, "name");
63
+ const customId = readString(data, "custom_id");
64
+ let rendered;
65
+ if (interaction.type === DiscordInteractionType.APPLICATION_COMMAND) {
66
+ rendered = [`/${commandName ?? "command"}`, ...renderCommandOptions(data?.options)].join(" ");
67
+ } else if (interaction.type === DiscordInteractionType.MESSAGE_COMPONENT) {
68
+ rendered = [`[component] ${customId ?? "unknown"}`, ...renderComponentValues(data)].join(" ");
69
+ } else if (interaction.type === DiscordInteractionType.MODAL_SUBMIT) {
70
+ rendered = [`[modal] ${customId ?? "unknown"}`, ...renderComponentValues(data)].join(" ");
71
+ } else {
72
+ rendered = `[interaction ${interaction.type}]`;
73
+ }
74
+ return rendered.length > DISCORD_MAX_BODY_LENGTH ? rendered.slice(0, DISCORD_MAX_BODY_LENGTH) : rendered;
75
+ }
76
+ function readDispatchableInteraction(interaction, options) {
77
+ if (!isDispatchableInteractionType(interaction.type)) return null;
78
+ const id = typeof interaction.id === "string" && interaction.id.length > 0 ? interaction.id : null;
79
+ const token = typeof interaction.token === "string" && interaction.token.length > 0 ? interaction.token : null;
80
+ const applicationId = typeof interaction.application_id === "string" && interaction.application_id.length > 0 ? interaction.application_id : null;
81
+ const discordChannelId = typeof interaction.channel_id === "string" && interaction.channel_id.length > 0 ? interaction.channel_id : null;
82
+ const rawUser = interaction.member?.user ?? interaction.user;
83
+ const userId = typeof rawUser?.id === "string" && rawUser.id.length > 0 ? rawUser.id : null;
84
+ if (!id || !token || !applicationId || !discordChannelId || !userId) return null;
85
+ return {
86
+ id,
87
+ token,
88
+ type: interaction.type,
89
+ applicationId,
90
+ discordChannelId,
91
+ guildId: typeof interaction.guild_id === "string" ? interaction.guild_id : void 0,
92
+ user: {
93
+ id: userId,
94
+ username: rawUser?.username ?? userId,
95
+ global_name: rawUser?.global_name ?? null
96
+ },
97
+ commandName: readString(interaction.data, "name"),
98
+ customId: readString(interaction.data, "custom_id"),
99
+ content: renderInteractionContent(interaction),
100
+ timestamp: (options?.now ?? /* @__PURE__ */ new Date()).toISOString()
101
+ };
102
+ }
103
+ function buildInteractionMessage(dispatch) {
104
+ return {
105
+ id: dispatch.id,
106
+ channel_id: dispatch.discordChannelId,
107
+ guild_id: dispatch.guildId,
108
+ content: dispatch.content,
109
+ author: dispatch.user,
110
+ timestamp: dispatch.timestamp,
111
+ discord_interaction: {
112
+ id: dispatch.id,
113
+ type: dispatch.type,
114
+ applicationId: dispatch.applicationId,
115
+ commandName: dispatch.commandName,
116
+ customId: dispatch.customId
117
+ }
118
+ };
119
+ }
120
+ function buildInteractionInboundJob(input) {
121
+ return buildInboundMessageJob({
122
+ message: buildInteractionMessage(input.dispatch),
123
+ channel: input.channel,
124
+ botUserId: input.botUserId
125
+ });
126
+ }
127
+ function buildInteractionFollowUpContent(dispatch, t) {
128
+ if (dispatch.type === DiscordInteractionType.APPLICATION_COMMAND) {
129
+ return t(
130
+ "channel_discord.interactions.commandReceived",
131
+ "Received \u2014 `/{command}` is now in the Open Mercato inbox and someone will pick it up from there.",
132
+ { command: dispatch.commandName ?? "" }
133
+ );
134
+ }
135
+ return t(
136
+ "channel_discord.interactions.componentReceived",
137
+ "Received \u2014 your response is now in the Open Mercato inbox and someone will pick it up from there."
138
+ );
139
+ }
140
+ async function sendInteractionFollowUp(client, input) {
141
+ const body = { content: input.content, ephemeral: input.ephemeral };
142
+ try {
143
+ await client.editOriginalInteractionResponse(input.applicationId, input.interactionToken, body);
144
+ return "edited-original";
145
+ } catch (editError) {
146
+ try {
147
+ await client.createInteractionFollowUp(input.applicationId, input.interactionToken, body);
148
+ return "follow-up";
149
+ } catch {
150
+ throw editError;
151
+ }
152
+ }
153
+ }
154
+ export {
155
+ DISPATCHABLE_INTERACTION_TYPES,
156
+ buildInteractionFollowUpContent,
157
+ buildInteractionInboundJob,
158
+ buildInteractionMessage,
159
+ isDispatchableInteractionType,
160
+ readDispatchableInteraction,
161
+ renderInteractionContent,
162
+ sendInteractionFollowUp
163
+ };
164
+ //# sourceMappingURL=interactions-dispatch.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/channel_discord/lib/interactions-dispatch.ts"],
4
+ "sourcesContent": ["import type { InboundProcessorPayload } from '@open-mercato/core/modules/communication_channels/workers/inbound-processor'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { DISCORD_MAX_BODY_LENGTH } from './capabilities'\nimport type { DiscordMessageObject, DiscordRestClient, DiscordUser } from './discord-rest'\nimport { buildInboundMessageJob, type GatewayChannelScope } from './gateway-bridge'\nimport { DiscordInteractionType, type ParsedInteraction } from './interactions-verify'\n\n/**\n * Everything the dispatch path needs from a verified interaction, extracted once\n * so nothing downstream has to re-read the untrusted body.\n *\n * This shape crosses a queue boundary (the HTTP route must answer Discord within\n * three seconds, so the hub write and the follow-up REST call happen in a\n * worker), which is why it carries no credential: `token` is the interaction's\n * own short-lived webhook credential and the bot token is re-resolved from the\n * encrypted credential store on the other side.\n */\nexport interface DispatchableInteraction {\n /** Interaction snowflake \u2014 also the hub's `externalMessageId`, so redelivery dedups. */\n id: string\n /** Interaction webhook token. Valid for 15 minutes, single interaction. */\n token: string\n type: number\n applicationId: string\n /** Discord text channel the interaction happened in. */\n discordChannelId: string\n guildId?: string\n user: DiscordUser\n /** Slash-command name, for `APPLICATION_COMMAND` / autocomplete. */\n commandName?: string\n /** Component / modal `custom_id`, for `MESSAGE_COMPONENT` / `MODAL_SUBMIT`. */\n customId?: string\n /** Human-readable rendering of what the user did \u2014 the hub message body. */\n content: string\n /** ISO timestamp the hub message is stamped with. */\n timestamp: string\n}\n\n/** The interaction types this provider turns into hub messages. */\nexport const DISPATCHABLE_INTERACTION_TYPES: readonly number[] = [\n DiscordInteractionType.APPLICATION_COMMAND,\n DiscordInteractionType.MESSAGE_COMPONENT,\n DiscordInteractionType.MODAL_SUBMIT,\n]\n\nexport function isDispatchableInteractionType(type: number): boolean {\n return DISPATCHABLE_INTERACTION_TYPES.includes(type)\n}\n\nfunction readString(source: Record<string, unknown> | undefined, key: string): string | undefined {\n const value = source?.[key]\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nfunction readOptionValue(option: Record<string, unknown>): string {\n const value = option.value\n if (value === undefined || value === null) return ''\n if (typeof value === 'object') return JSON.stringify(value)\n return String(value)\n}\n\n/**\n * Flatten a slash command's options into `name:value` pairs.\n *\n * Discord nests subcommands and subcommand groups as options whose own `options`\n * array holds the real arguments, so a naive one-level read renders\n * `/support ticket:` and loses everything the user typed. Recursion is bounded by\n * Discord's own two-level nesting limit.\n */\nfunction renderCommandOptions(options: unknown): string[] {\n if (!Array.isArray(options)) return []\n const rendered: string[] = []\n for (const entry of options) {\n if (!entry || typeof entry !== 'object') continue\n const option = entry as Record<string, unknown>\n const name = readString(option, 'name')\n if (!name) continue\n const nested = renderCommandOptions(option.options)\n if (nested.length > 0) {\n rendered.push(name, ...nested)\n continue\n }\n rendered.push(`${name}:${readOptionValue(option)}`)\n }\n return rendered\n}\n\n/** Values a select menu returned, or the fields a modal submitted. */\nfunction renderComponentValues(data: Record<string, unknown> | undefined): string[] {\n if (!data) return []\n const values = data.values\n if (Array.isArray(values)) return values.map((value) => String(value))\n\n // Modal submissions nest one input per action row.\n const rows = data.components\n if (!Array.isArray(rows)) return []\n const rendered: string[] = []\n for (const row of rows) {\n const children = (row as { components?: unknown } | null)?.components\n if (!Array.isArray(children)) continue\n for (const child of children) {\n if (!child || typeof child !== 'object') continue\n const field = child as Record<string, unknown>\n const customId = readString(field, 'custom_id')\n const value = readString(field, 'value')\n if (!customId) continue\n rendered.push(`${customId}:${value ?? ''}`)\n }\n }\n return rendered\n}\n\n/**\n * Render an interaction as the text a human reading the hub inbox needs.\n *\n * A slash command becomes `/support message:the printer is jammed`; a button or\n * select becomes `[component] escalate` plus any selected values. The hub stores\n * the full interaction object in `channelPayload`, so this is a readable summary\n * rather than the record of truth.\n */\nexport function renderInteractionContent(interaction: ParsedInteraction): string {\n const data = interaction.data\n const commandName = readString(data, 'name')\n const customId = readString(data, 'custom_id')\n\n let rendered: string\n if (interaction.type === DiscordInteractionType.APPLICATION_COMMAND) {\n rendered = [`/${commandName ?? 'command'}`, ...renderCommandOptions(data?.options)].join(' ')\n } else if (interaction.type === DiscordInteractionType.MESSAGE_COMPONENT) {\n rendered = [`[component] ${customId ?? 'unknown'}`, ...renderComponentValues(data)].join(' ')\n } else if (interaction.type === DiscordInteractionType.MODAL_SUBMIT) {\n rendered = [`[modal] ${customId ?? 'unknown'}`, ...renderComponentValues(data)].join(' ')\n } else {\n rendered = `[interaction ${interaction.type}]`\n }\n\n return rendered.length > DISCORD_MAX_BODY_LENGTH ? rendered.slice(0, DISCORD_MAX_BODY_LENGTH) : rendered\n}\n\n/**\n * Narrow a verified interaction to the fields the dispatch needs, or `null` when\n * the payload cannot be dispatched at all.\n *\n * Returning `null` is not a rejection of the *caller* \u2014 the signature already\n * verified \u2014 it means Discord sent a shape this provider cannot turn into a hub\n * message (no channel, no invoking user, no follow-up token). The route answers\n * those with a visible message rather than a deferred ack it could never\n * replace.\n */\nexport function readDispatchableInteraction(\n interaction: ParsedInteraction,\n options?: { now?: Date },\n): DispatchableInteraction | null {\n if (!isDispatchableInteractionType(interaction.type)) return null\n\n const id = typeof interaction.id === 'string' && interaction.id.length > 0 ? interaction.id : null\n const token = typeof interaction.token === 'string' && interaction.token.length > 0 ? interaction.token : null\n const applicationId =\n typeof interaction.application_id === 'string' && interaction.application_id.length > 0\n ? interaction.application_id\n : null\n const discordChannelId =\n typeof interaction.channel_id === 'string' && interaction.channel_id.length > 0 ? interaction.channel_id : null\n // A guild interaction carries the invoker under `member.user`; a DM carries it\n // directly under `user`.\n const rawUser = interaction.member?.user ?? interaction.user\n const userId = typeof rawUser?.id === 'string' && rawUser.id.length > 0 ? rawUser.id : null\n\n if (!id || !token || !applicationId || !discordChannelId || !userId) return null\n\n return {\n id,\n token,\n type: interaction.type,\n applicationId,\n discordChannelId,\n guildId: typeof interaction.guild_id === 'string' ? interaction.guild_id : undefined,\n user: {\n id: userId,\n username: rawUser?.username ?? userId,\n global_name: rawUser?.global_name ?? null,\n },\n commandName: readString(interaction.data, 'name'),\n customId: readString(interaction.data, 'custom_id'),\n content: renderInteractionContent(interaction),\n timestamp: (options?.now ?? new Date()).toISOString(),\n }\n}\n\n/**\n * Present the interaction as the raw Discord message object the hub's inbound\n * path already understands.\n *\n * WHY A SYNTHESIZED MESSAGE: the hub contract stays untouched. The inbound\n * processor calls `adapter.normalizeInbound` on whatever `raw` it is handed, so\n * shaping the interaction like a message means a slash command lands in the same\n * conversation, under the same tenant scope, with the same dedup key discipline\n * as anything typed in the channel \u2014 no hub-side special case for interactions.\n */\nexport function buildInteractionMessage(dispatch: DispatchableInteraction): DiscordMessageObject {\n return {\n id: dispatch.id,\n channel_id: dispatch.discordChannelId,\n guild_id: dispatch.guildId,\n content: dispatch.content,\n author: dispatch.user,\n timestamp: dispatch.timestamp,\n discord_interaction: {\n id: dispatch.id,\n type: dispatch.type,\n applicationId: dispatch.applicationId,\n commandName: dispatch.commandName,\n customId: dispatch.customId,\n },\n }\n}\n\n/**\n * Build the hub inbound-processor job for a dispatchable interaction \u2014 the same\n * job shape, on the same queue, that the gateway worker produces for a typed\n * message. Returns `null` when the interaction was invoked by a bot (the\n * feedback-loop guard `buildInboundMessageJob` owns).\n */\nexport function buildInteractionInboundJob(input: {\n dispatch: DispatchableInteraction\n channel: GatewayChannelScope\n botUserId?: string\n}): InboundProcessorPayload | null {\n return buildInboundMessageJob({\n message: buildInteractionMessage(input.dispatch),\n channel: input.channel,\n botUserId: input.botUserId,\n })\n}\n\n/**\n * Copy shown to the user in Discord once the interaction has been recorded.\n *\n * Takes the translator rather than resolving one, so the pure builder stays\n * usable from the worker, from the route and from a test with no module\n * registry. Each key carries its English fallback: a worker process that never\n * loaded the app dictionaries must still say something a human can read, not\n * echo a translation key back into a Discord channel.\n */\nexport function buildInteractionFollowUpContent(dispatch: DispatchableInteraction, t: TranslateFn): string {\n if (dispatch.type === DiscordInteractionType.APPLICATION_COMMAND) {\n return t(\n 'channel_discord.interactions.commandReceived',\n 'Received \u2014 `/{command}` is now in the Open Mercato inbox and someone will pick it up from there.',\n { command: dispatch.commandName ?? '' },\n )\n }\n return t(\n 'channel_discord.interactions.componentReceived',\n 'Received \u2014 your response is now in the Open Mercato inbox and someone will pick it up from there.',\n )\n}\n\n/**\n * Replace the deferred acknowledgement with a real message.\n *\n * ORDER MATTERS: editing the original response is what actually ends Discord's\n * \"thinking\u2026\" state, so it is tried first. A follow-up POST is the fallback for\n * the case where the original response is already gone (Discord answers 404),\n * which still gets the user a visible answer instead of a spinner that never\n * resolves.\n *\n * Returns which call delivered the message so the worker can log it; throws only\n * when neither did, letting the queue's retry policy have another go inside the\n * interaction token's 15-minute lifetime.\n */\nexport async function sendInteractionFollowUp(\n client: DiscordRestClient,\n input: { applicationId: string; interactionToken: string; content: string; ephemeral?: boolean },\n): Promise<'edited-original' | 'follow-up'> {\n const body = { content: input.content, ephemeral: input.ephemeral }\n try {\n await client.editOriginalInteractionResponse(input.applicationId, input.interactionToken, body)\n return 'edited-original'\n } catch (editError) {\n try {\n await client.createInteractionFollowUp(input.applicationId, input.interactionToken, body)\n return 'follow-up'\n } catch {\n throw editError\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }