@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,222 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
6
+ import { validateRouteMutationGuard } from "@open-mercato/core/modules/communication_channels/lib/route-mutation-guard";
7
+ import { CHANNEL_DISCORD_AUTO_REPLY_AGENT_ID } from "../../../../ai-agents.js";
8
+ import {
9
+ CHANNEL_DISCORD_CHANNEL_RESOURCE_KIND,
10
+ CHANNEL_DISCORD_UPDATE_AI_AUTO_REPLY_COMMAND_ID,
11
+ discordAiAutoReplySettingsSchema
12
+ } from "../../../../commands/update-ai-auto-reply.js";
13
+ import {
14
+ findDiscordEligibleAgent,
15
+ listDiscordEligibleAgents,
16
+ missingAgentFeatures
17
+ } from "../../../../lib/ai-agent-directory.js";
18
+ import { CHANNEL_DISCORD_CONFIGURE_FEATURE, CHANNEL_DISCORD_VIEW_FEATURE } from "../../../../lib/ai-features.js";
19
+ import { resolveDiscordAiPrincipal } from "../../../../lib/ai-service-principal.js";
20
+ import { loadDiscordChannelForRequest } from "../../../../lib/channel-access.js";
21
+ import { discordChannelStateSchema } from "../../../../lib/credentials.js";
22
+ const metadata = {
23
+ GET: {
24
+ requireAuth: true,
25
+ requireFeatures: [CHANNEL_DISCORD_VIEW_FEATURE]
26
+ },
27
+ PUT: {
28
+ requireAuth: true,
29
+ requireFeatures: [CHANNEL_DISCORD_CONFIGURE_FEATURE]
30
+ }
31
+ };
32
+ function fieldError(field, message, extra) {
33
+ return NextResponse.json(
34
+ { error: message, fieldErrors: { [field]: message }, ...extra },
35
+ { status: 400 }
36
+ );
37
+ }
38
+ async function loadInvokingPrincipal(container, scope) {
39
+ const em = container.resolve("em").fork();
40
+ return resolveDiscordAiPrincipal({
41
+ em,
42
+ resolver: { resolve: (name) => container.resolve(name) },
43
+ scope
44
+ });
45
+ }
46
+ async function GET(req, context) {
47
+ const { id } = await context.params;
48
+ if (!z.string().uuid().safeParse(id).success) {
49
+ return NextResponse.json({ error: "Invalid channel id" }, { status: 400 });
50
+ }
51
+ const auth = await getAuthFromRequest(req);
52
+ if (!auth?.sub || !auth?.tenantId) {
53
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
54
+ }
55
+ const container = await createRequestContainer();
56
+ const access = await loadDiscordChannelForRequest({ container, req, auth, channelId: id, mode: "read" });
57
+ if ("response" in access) return access.response;
58
+ const { channel } = access;
59
+ const state = discordChannelStateSchema.safeParse(channel.channelState ?? {});
60
+ const directory = await listDiscordEligibleAgents();
61
+ const principal = directory.available ? await loadInvokingPrincipal(container, {
62
+ tenantId: auth.tenantId,
63
+ organizationId: access.rbacOrganizationId
64
+ }) : null;
65
+ const agents = directory.available && principal ? directory.agents.map((agent) => {
66
+ const missingFeatures = missingAgentFeatures(agent.requiredFeatures, principal);
67
+ return { ...agent, invocable: missingFeatures.length === 0, missingFeatures };
68
+ }) : [];
69
+ return NextResponse.json({
70
+ id: channel.id,
71
+ channelId: channel.id,
72
+ displayName: channel.displayName,
73
+ updatedAt: channel.updatedAt ? channel.updatedAt.toISOString() : null,
74
+ aiAutoReplyEnabled: state.success ? Boolean(state.data.aiAutoReplyEnabled) : false,
75
+ aiAgentId: (state.success ? state.data.aiAgentId : void 0) ?? null,
76
+ // Why the last attempt produced nothing, so an armed channel that answers
77
+ // nothing says so instead of looking healthy.
78
+ aiAutoReplyLastError: (state.success ? state.data.aiAutoReplyLastError : void 0) ?? null,
79
+ aiAutoReplyLastErrorAt: (state.success ? state.data.aiAutoReplyLastErrorAt : void 0) ?? null,
80
+ defaultAgentId: CHANNEL_DISCORD_AUTO_REPLY_AGENT_ID,
81
+ aiAvailable: directory.available,
82
+ agents
83
+ });
84
+ }
85
+ async function PUT(req, context) {
86
+ const { id } = await context.params;
87
+ if (!z.string().uuid().safeParse(id).success) {
88
+ return NextResponse.json({ error: "Invalid channel id" }, { status: 400 });
89
+ }
90
+ const auth = await getAuthFromRequest(req);
91
+ if (!auth?.sub || !auth?.tenantId) {
92
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
93
+ }
94
+ const organizationId = auth.orgId ?? null;
95
+ const rawBody = await req.json().catch(() => null);
96
+ if (rawBody == null || typeof rawBody !== "object") {
97
+ return NextResponse.json({ error: "Request body must be a JSON object" }, { status: 400 });
98
+ }
99
+ const parsed = discordAiAutoReplySettingsSchema.safeParse(rawBody);
100
+ if (!parsed.success) {
101
+ const issue = parsed.error.issues[0];
102
+ const field = issue?.path.join(".") || "aiAutoReplyEnabled";
103
+ return fieldError(field, issue?.message ?? "Invalid AI auto-reply settings");
104
+ }
105
+ const container = await createRequestContainer();
106
+ const access = await loadDiscordChannelForRequest({ container, req, auth, channelId: id, mode: "manage" });
107
+ if ("response" in access) return access.response;
108
+ if (parsed.data.aiAutoReplyEnabled) {
109
+ const agentId = parsed.data.aiAgentId;
110
+ const agent = await findDiscordEligibleAgent(agentId);
111
+ if (!agent) {
112
+ const directory = await listDiscordEligibleAgents();
113
+ return directory.available ? fieldError("aiAgentId", "channel_discord.aiAutoReply.errors.agentNotEligible") : fieldError("aiAutoReplyEnabled", "channel_discord.aiAutoReply.errors.aiUnavailable");
114
+ }
115
+ const principal = await loadInvokingPrincipal(container, {
116
+ tenantId: auth.tenantId,
117
+ organizationId: access.rbacOrganizationId
118
+ });
119
+ const missingFeatures = missingAgentFeatures(agent.requiredFeatures, principal);
120
+ if (missingFeatures.length > 0) {
121
+ return fieldError(
122
+ "aiAgentId",
123
+ "channel_discord.aiAutoReply.errors.agentFeaturesMissing",
124
+ { missingFeatures }
125
+ );
126
+ }
127
+ }
128
+ const guard = await validateRouteMutationGuard({
129
+ container,
130
+ req,
131
+ auth,
132
+ input: {
133
+ resourceKind: CHANNEL_DISCORD_CHANNEL_RESOURCE_KIND,
134
+ resourceId: id,
135
+ operation: "update",
136
+ mutationPayload: { ...parsed.data }
137
+ }
138
+ });
139
+ if ("response" in guard) return guard.response;
140
+ const commandBus = container.resolve("commandBus");
141
+ const input = {
142
+ channelId: id,
143
+ settings: parsed.data,
144
+ scope: {
145
+ tenantId: auth.tenantId,
146
+ organizationId: access.rbacOrganizationId,
147
+ organizationIds: access.organizationIds
148
+ }
149
+ };
150
+ let result;
151
+ try {
152
+ const executed = await commandBus.execute(
153
+ CHANNEL_DISCORD_UPDATE_AI_AUTO_REPLY_COMMAND_ID,
154
+ {
155
+ input,
156
+ ctx: {
157
+ container,
158
+ auth,
159
+ organizationScope: null,
160
+ selectedOrganizationId: organizationId,
161
+ organizationIds: organizationId ? [organizationId] : null,
162
+ request: req
163
+ }
164
+ }
165
+ );
166
+ result = executed.result;
167
+ } catch (err) {
168
+ if (isCrudHttpError(err)) {
169
+ return NextResponse.json(err.body, { status: err.status });
170
+ }
171
+ throw err;
172
+ }
173
+ if (result.status === "not_found") {
174
+ return NextResponse.json({ error: "Channel not found" }, { status: 404 });
175
+ }
176
+ await guard.afterSuccess();
177
+ return NextResponse.json(
178
+ {
179
+ channelId: result.channelId,
180
+ aiAutoReplyEnabled: result.aiAutoReplyEnabled,
181
+ aiAgentId: result.aiAgentId,
182
+ updatedAt: result.updatedAt
183
+ },
184
+ { status: 200 }
185
+ );
186
+ }
187
+ const openApi = {
188
+ tags: ["CommunicationChannels"],
189
+ methods: {
190
+ GET: {
191
+ summary: "Read a Discord channel's AI auto-reply configuration",
192
+ tags: ["CommunicationChannels"],
193
+ responses: [
194
+ { status: 200, description: "Current settings plus the agents this channel may be pointed at" },
195
+ { status: 400, description: "Invalid channel id" },
196
+ { status: 401, description: "Unauthorized" },
197
+ { status: 404, description: "Channel not found, not a Discord channel in scope, or not the caller\u2019s to see" }
198
+ ]
199
+ },
200
+ PUT: {
201
+ summary: "Update a Discord channel's AI auto-reply configuration",
202
+ tags: ["CommunicationChannels"],
203
+ responses: [
204
+ { status: 200, description: "Settings stored" },
205
+ {
206
+ status: 400,
207
+ description: "Invalid payload, AI module absent, agent not eligible, or the auto-reply principal lacks the agent\u2019s required features"
208
+ },
209
+ { status: 401, description: "Unauthorized" },
210
+ { status: 404, description: "Channel not found, or not a Discord channel in this scope" },
211
+ { status: 409, description: "The channel changed since the form was loaded" }
212
+ ]
213
+ }
214
+ }
215
+ };
216
+ export {
217
+ GET,
218
+ PUT,
219
+ metadata,
220
+ openApi
221
+ };
222
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/channel_discord/api/channels/%5Bid%5D/ai-auto-reply/route.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { validateRouteMutationGuard } from '@open-mercato/core/modules/communication_channels/lib/route-mutation-guard'\nimport { CHANNEL_DISCORD_AUTO_REPLY_AGENT_ID } from '../../../../ai-agents'\nimport {\n CHANNEL_DISCORD_CHANNEL_RESOURCE_KIND,\n CHANNEL_DISCORD_UPDATE_AI_AUTO_REPLY_COMMAND_ID,\n discordAiAutoReplySettingsSchema,\n type UpdateAiAutoReplyInput,\n type UpdateAiAutoReplyResult,\n} from '../../../../commands/update-ai-auto-reply'\nimport {\n findDiscordEligibleAgent,\n listDiscordEligibleAgents,\n missingAgentFeatures,\n type AgentInvokingPrincipal,\n} from '../../../../lib/ai-agent-directory'\nimport { CHANNEL_DISCORD_CONFIGURE_FEATURE, CHANNEL_DISCORD_VIEW_FEATURE } from '../../../../lib/ai-features'\nimport { resolveDiscordAiPrincipal } from '../../../../lib/ai-service-principal'\nimport { loadDiscordChannelForRequest } from '../../../../lib/channel-access'\nimport { discordChannelStateSchema } from '../../../../lib/credentials'\n\nexport const metadata = {\n GET: {\n requireAuth: true,\n requireFeatures: [CHANNEL_DISCORD_VIEW_FEATURE],\n },\n PUT: {\n requireAuth: true,\n requireFeatures: [CHANNEL_DISCORD_CONFIGURE_FEATURE],\n },\n}\n\ntype RouteContext = {\n params: Promise<{ id: string }> | { id: string }\n}\n\nfunction fieldError(field: string, message: string, extra?: Record<string, unknown>): Response {\n return NextResponse.json(\n { error: message, fieldErrors: { [field]: message }, ...extra },\n { status: 400 },\n )\n}\n\n/**\n * The identity the auto-reply subscriber will run the chosen agent under, so both\n * methods can answer \"would the runtime accept this?\" with the same grants the\n * subscriber will present \u2014 the tenant's channel-bot user when it exists, the\n * provider service principal otherwise.\n */\nasync function loadInvokingPrincipal(\n container: AwilixContainer,\n scope: { tenantId: string; organizationId: string | null },\n): Promise<AgentInvokingPrincipal> {\n const em = (container.resolve('em') as EntityManager).fork()\n return resolveDiscordAiPrincipal({\n em,\n resolver: { resolve: <T,>(name: string): T => container.resolve(name) as T },\n scope,\n })\n}\n\n/**\n * Read a Discord channel's AI auto-reply settings plus everything the settings\n * form needs to render itself: the eligible agents, whether the optional AI peer\n * is installed at all, and the channel's `updatedAt` so the form can attach the\n * optimistic-lock header on save.\n */\nexport async function GET(req: Request, context: RouteContext): Promise<Response> {\n const { id } = await context.params\n if (!z.string().uuid().safeParse(id).success) {\n return NextResponse.json({ error: 'Invalid channel id' }, { status: 400 })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const access = await loadDiscordChannelForRequest({ container, req, auth, channelId: id, mode: 'read' })\n if ('response' in access) return access.response\n const { channel } = access\n\n const state = discordChannelStateSchema.safeParse(channel.channelState ?? {})\n const directory = await listDiscordEligibleAgents()\n\n // Every offered agent carries whether the auto-reply principal could actually\n // invoke it. Without this the picker offers agents from other modules that the\n // runtime will refuse, and the refusal only ever happens later in a background\n // subscriber \u2014 so the operator sees \"Auto-reply on\" and silence.\n const principal = directory.available\n ? await loadInvokingPrincipal(container, {\n tenantId: auth.tenantId as string,\n organizationId: access.rbacOrganizationId,\n })\n : null\n const agents = directory.available && principal\n ? directory.agents.map((agent) => {\n const missingFeatures = missingAgentFeatures(agent.requiredFeatures, principal)\n return { ...agent, invocable: missingFeatures.length === 0, missingFeatures }\n })\n : []\n\n return NextResponse.json({\n id: channel.id,\n channelId: channel.id,\n displayName: channel.displayName,\n updatedAt: channel.updatedAt ? channel.updatedAt.toISOString() : null,\n aiAutoReplyEnabled: state.success ? Boolean(state.data.aiAutoReplyEnabled) : false,\n aiAgentId: (state.success ? state.data.aiAgentId : undefined) ?? null,\n // Why the last attempt produced nothing, so an armed channel that answers\n // nothing says so instead of looking healthy.\n aiAutoReplyLastError: (state.success ? state.data.aiAutoReplyLastError : undefined) ?? null,\n aiAutoReplyLastErrorAt: (state.success ? state.data.aiAutoReplyLastErrorAt : undefined) ?? null,\n defaultAgentId: CHANNEL_DISCORD_AUTO_REPLY_AGENT_ID,\n aiAvailable: directory.available,\n agents,\n })\n}\n\n/**\n * Write a Discord channel's AI auto-reply settings \u2014 the configuration path the\n * subscriber was missing (issue #4778).\n *\n * The route is deliberately strict about *enabling*: it refuses to arm a channel\n * whose AI peer is absent, one pointed at an agent the runtime would reject on\n * shape, and one pointed at an agent the auto-reply principal is not authorized\n * to invoke. All three would store a setting that can only fail later, inside a\n * background subscriber where nobody sees the error \u2014 which is precisely how the\n * feature ended up dormant in the first place. The three checks mirror, in order,\n * what `runAiAgentObject` \u2192 `checkAgentPolicy` will ask at run time.\n */\nexport async function PUT(req: Request, context: RouteContext): Promise<Response> {\n const { id } = await context.params\n if (!z.string().uuid().safeParse(id).success) {\n return NextResponse.json({ error: 'Invalid channel id' }, { status: 400 })\n }\n\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub || !auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = (auth as { orgId?: string | null }).orgId ?? null\n\n const rawBody = await req.json().catch(() => null)\n if (rawBody == null || typeof rawBody !== 'object') {\n return NextResponse.json({ error: 'Request body must be a JSON object' }, { status: 400 })\n }\n\n const parsed = discordAiAutoReplySettingsSchema.safeParse(rawBody)\n if (!parsed.success) {\n const issue = parsed.error.issues[0]\n const field = issue?.path.join('.') || 'aiAutoReplyEnabled'\n return fieldError(field, issue?.message ?? 'Invalid AI auto-reply settings')\n }\n\n const container = await createRequestContainer()\n // Authorize before the guard so a caller who may not touch this channel gets a\n // masked 404 rather than a guard verdict about a record they cannot see. It\n // also comes before the arming checks below, which need the channel's resolved\n // organization to build the principal they judge the agent against.\n const access = await loadDiscordChannelForRequest({ container, req, auth, channelId: id, mode: 'manage' })\n if ('response' in access) return access.response\n\n if (parsed.data.aiAutoReplyEnabled) {\n const agentId = parsed.data.aiAgentId as string\n const agent = await findDiscordEligibleAgent(agentId)\n if (!agent) {\n // No eligible agent under this id: either the optional AI peer is absent, or\n // the agent is not object-mode and `runAiAgentObject` would reject it.\n const directory = await listDiscordEligibleAgents()\n return directory.available\n ? fieldError('aiAgentId', 'channel_discord.aiAutoReply.errors.agentNotEligible')\n : fieldError('aiAutoReplyEnabled', 'channel_discord.aiAutoReply.errors.aiUnavailable')\n }\n\n // Shape is not authorization. `checkAgentPolicy` enforces the agent's\n // `requiredFeatures` against the auto-reply principal at run time, so arming a\n // channel against an agent that principal cannot invoke stores a setting that\n // can only fail later \u2014 inside a background subscriber, where the operator\n // never sees it. Refuse here, naming the grants the tenant's channel-bot user\n // is missing, rather than accepting a channel that would answer nothing.\n const principal = await loadInvokingPrincipal(container, {\n tenantId: auth.tenantId as string,\n organizationId: access.rbacOrganizationId,\n })\n const missingFeatures = missingAgentFeatures(agent.requiredFeatures, principal)\n if (missingFeatures.length > 0) {\n return fieldError(\n 'aiAgentId',\n 'channel_discord.aiAutoReply.errors.agentFeaturesMissing',\n { missingFeatures },\n )\n }\n }\n\n const guard = await validateRouteMutationGuard({\n container,\n req,\n auth,\n input: {\n resourceKind: CHANNEL_DISCORD_CHANNEL_RESOURCE_KIND,\n resourceId: id,\n operation: 'update',\n mutationPayload: { ...parsed.data },\n },\n })\n if ('response' in guard) return guard.response\n\n const commandBus = container.resolve('commandBus') as CommandBus\n const input: UpdateAiAutoReplyInput = {\n channelId: id,\n settings: parsed.data,\n scope: {\n tenantId: auth.tenantId as string,\n organizationId: access.rbacOrganizationId,\n organizationIds: access.organizationIds,\n },\n }\n\n let result: UpdateAiAutoReplyResult\n try {\n const executed = await commandBus.execute<UpdateAiAutoReplyInput, UpdateAiAutoReplyResult>(\n CHANNEL_DISCORD_UPDATE_AI_AUTO_REPLY_COMMAND_ID,\n {\n input,\n ctx: {\n container,\n auth: auth as never,\n organizationScope: null,\n selectedOrganizationId: organizationId,\n organizationIds: organizationId ? [organizationId] : null,\n request: req,\n },\n },\n )\n result = executed.result\n } catch (err) {\n // The optimistic-lock guard throws the shared structured 409 so the form's\n // conflict bar renders the same way it does anywhere else in the product.\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n throw err\n }\n\n if (result.status === 'not_found') {\n return NextResponse.json({ error: 'Channel not found' }, { status: 404 })\n }\n\n await guard.afterSuccess()\n return NextResponse.json(\n {\n channelId: result.channelId,\n aiAutoReplyEnabled: result.aiAutoReplyEnabled,\n aiAgentId: result.aiAgentId,\n updatedAt: result.updatedAt,\n },\n { status: 200 },\n )\n}\n\nexport const openApi = {\n tags: ['CommunicationChannels'],\n methods: {\n GET: {\n summary: \"Read a Discord channel's AI auto-reply configuration\",\n tags: ['CommunicationChannels'],\n responses: [\n { status: 200, description: 'Current settings plus the agents this channel may be pointed at' },\n { status: 400, description: 'Invalid channel id' },\n { status: 401, description: 'Unauthorized' },\n { status: 404, description: 'Channel not found, not a Discord channel in scope, or not the caller\u2019s to see' },\n ],\n },\n PUT: {\n summary: \"Update a Discord channel's AI auto-reply configuration\",\n tags: ['CommunicationChannels'],\n responses: [\n { status: 200, description: 'Settings stored' },\n {\n status: 400,\n description:\n 'Invalid payload, AI module absent, agent not eligible, or the auto-reply principal lacks the agent\u2019s required features',\n },\n { status: 401, description: 'Unauthorized' },\n { status: 404, description: 'Channel not found, or not a Discord channel in this scope' },\n { status: 409, description: 'The channel changed since the form was loaded' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,173 @@
1
+ import { NextResponse } from "next/server";
2
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
3
+ import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
4
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
+ import { createLogger } from "@open-mercato/shared/lib/logger";
6
+ import { CommunicationChannel } from "@open-mercato/core/modules/communication_channels/data/entities";
7
+ import { discordCredentialsSchema } from "../../lib/credentials.js";
8
+ import { DISCORD_MESSAGE_FLAG_EPHEMERAL } from "../../lib/discord-rest.js";
9
+ import {
10
+ DEFAULT_INTERACTION_MESSAGES,
11
+ resolveDiscordInteraction,
12
+ screenInteractionRequest
13
+ } from "../../lib/interactions-handler.js";
14
+ import {
15
+ getInteractionDispatchQueue
16
+ } from "../../lib/interactions-queue.js";
17
+ import { DiscordInteractionResponseType } from "../../lib/interactions-verify.js";
18
+ const logger = createLogger("channel_discord").child({ component: "interactions-route" });
19
+ const metadata = {
20
+ // Pinned explicitly: this URL is operator-facing — it goes into the Discord
21
+ // application's Interactions Endpoint field — so it is part of this route's
22
+ // contract rather than a by-product of where the file happens to sit. It is
23
+ // the same path the folder derives, so the pin documents the contract instead
24
+ // of overriding it.
25
+ path: "/channel_discord/interactions",
26
+ POST: {
27
+ requireAuth: false,
28
+ // Unauthenticated by design. Unsigned traffic is now rejected before any
29
+ // candidate is loaded, so this bounds the residual case: a caller who does
30
+ // present well-formed, fresh headers and drives the narrowed candidate load
31
+ // repeatedly before the signature gate rejects them.
32
+ rateLimit: { points: 120, duration: 60, keyPrefix: "discord_interactions" }
33
+ }
34
+ };
35
+ async function POST(req) {
36
+ const rawBody = await req.text();
37
+ const signatureHex = req.headers.get("x-signature-ed25519");
38
+ const timestamp = req.headers.get("x-signature-timestamp");
39
+ const screened = screenInteractionRequest({ signatureHex, timestamp });
40
+ if (screened) return NextResponse.json(screened.body, { status: screened.status });
41
+ const { t } = await resolveTranslations();
42
+ const result = await resolveDiscordInteraction({
43
+ rawBody,
44
+ signatureHex,
45
+ timestamp,
46
+ loadCandidates: loadInteractionCandidates,
47
+ messages: {
48
+ notDispatchable: t(
49
+ "channel_discord.interactions.notDispatchable",
50
+ DEFAULT_INTERACTION_MESSAGES.notDispatchable
51
+ ),
52
+ unsupported: t("channel_discord.interactions.unsupported", DEFAULT_INTERACTION_MESSAGES.unsupported)
53
+ }
54
+ });
55
+ if (result.dispatch && result.matchedChannel) {
56
+ const enqueued = await enqueueInteractionDispatch(result.dispatch, result.matchedChannel);
57
+ if (!enqueued) {
58
+ return NextResponse.json(
59
+ {
60
+ type: DiscordInteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
61
+ data: {
62
+ content: t(
63
+ "channel_discord.interactions.dispatchFailed",
64
+ "Open Mercato could not record this interaction right now. Please try again in a moment."
65
+ ),
66
+ flags: DISCORD_MESSAGE_FLAG_EPHEMERAL
67
+ }
68
+ },
69
+ { status: 200 }
70
+ );
71
+ }
72
+ }
73
+ return NextResponse.json(result.body, { status: result.status });
74
+ }
75
+ async function enqueueInteractionDispatch(dispatch, channel) {
76
+ const payload = {
77
+ channelId: channel.channelId,
78
+ channelType: channel.channelType,
79
+ tenantId: channel.tenantId,
80
+ organizationId: channel.organizationId,
81
+ credentialScope: channel.credentialScope,
82
+ interaction: dispatch
83
+ };
84
+ try {
85
+ await getInteractionDispatchQueue().enqueue(payload);
86
+ return true;
87
+ } catch (err) {
88
+ logger.error("failed to enqueue discord interaction dispatch", { err, channelId: channel.channelId });
89
+ return false;
90
+ }
91
+ }
92
+ async function loadInteractionCandidates(filter) {
93
+ const container = await createRequestContainer();
94
+ const em = container.resolve("em").fork();
95
+ let credentialsService = null;
96
+ try {
97
+ credentialsService = container.resolve("integrationCredentialsService");
98
+ } catch {
99
+ credentialsService = null;
100
+ }
101
+ if (!credentialsService) return [];
102
+ const candidates = [];
103
+ try {
104
+ const rows = await findWithDecryption(em, CommunicationChannel, {
105
+ providerKey: "discord",
106
+ isActive: true,
107
+ deletedAt: null
108
+ });
109
+ const resolvedByScope = /* @__PURE__ */ new Map();
110
+ for (const channel of rows) {
111
+ if (!channel.credentialsRef) continue;
112
+ const organizationId = channel.organizationId ?? channel.tenantId;
113
+ const userId = channel.userId ?? null;
114
+ const scopeKey = `${channel.tenantId}|${organizationId}|${userId ?? ""}`;
115
+ if (!resolvedByScope.has(scopeKey)) {
116
+ try {
117
+ resolvedByScope.set(
118
+ scopeKey,
119
+ await credentialsService.resolve("channel_discord", {
120
+ tenantId: channel.tenantId,
121
+ organizationId,
122
+ userId
123
+ })
124
+ );
125
+ } catch {
126
+ resolvedByScope.set(scopeKey, null);
127
+ }
128
+ }
129
+ const parsed = discordCredentialsSchema.safeParse(resolvedByScope.get(scopeKey) ?? {});
130
+ if (!parsed.success) continue;
131
+ if (filter.applicationId && parsed.data.applicationId !== filter.applicationId) continue;
132
+ candidates.push({
133
+ channelId: channel.id,
134
+ channelType: channel.channelType,
135
+ tenantId: channel.tenantId,
136
+ organizationId: channel.organizationId ?? null,
137
+ publicKey: parsed.data.publicKey,
138
+ applicationId: parsed.data.applicationId,
139
+ // The exact scope this row's credentials resolved under, so the worker
140
+ // re-resolves the same bag instead of guessing — and no token travels.
141
+ credentialScope: { tenantId: channel.tenantId, organizationId, userId }
142
+ });
143
+ }
144
+ } catch (err) {
145
+ logger.warn("failed to load discord interaction candidates", { err });
146
+ return [];
147
+ }
148
+ return candidates;
149
+ }
150
+ const openApi = {
151
+ tags: ["ChannelDiscord"],
152
+ summary: "Discord Interactions endpoint (slash commands, buttons, PING handshake)",
153
+ methods: {
154
+ POST: {
155
+ summary: "Verify (Ed25519, fail-closed) and dispatch a Discord interaction",
156
+ tags: ["ChannelDiscord"],
157
+ responses: [
158
+ {
159
+ status: 200,
160
+ description: "Verified interaction \u2014 PONG for the handshake, a deferred ack for a dispatched slash command / component / modal submission, an empty autocomplete result, or an ephemeral message when the interaction cannot be dispatched"
161
+ },
162
+ { status: 400, description: "Verified but malformed interaction body" },
163
+ { status: 401, description: "Signature verification failed against every candidate channel, or the signed timestamp is outside the replay window" }
164
+ ]
165
+ }
166
+ }
167
+ };
168
+ export {
169
+ POST,
170
+ metadata,
171
+ openApi
172
+ };
173
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/channel_discord/api/interactions/route.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { CommunicationChannel } from '@open-mercato/core/modules/communication_channels/data/entities'\nimport { discordCredentialsSchema } from '../../lib/credentials'\nimport { DISCORD_MESSAGE_FLAG_EPHEMERAL } from '../../lib/discord-rest'\nimport type { DispatchableInteraction } from '../../lib/interactions-dispatch'\nimport {\n DEFAULT_INTERACTION_MESSAGES,\n resolveDiscordInteraction,\n screenInteractionRequest,\n type InteractionCandidate,\n type InteractionCandidateFilter,\n} from '../../lib/interactions-handler'\nimport {\n getInteractionDispatchQueue,\n type InteractionDispatchJobPayload,\n} from '../../lib/interactions-queue'\nimport { DiscordInteractionResponseType } from '../../lib/interactions-verify'\n\nconst logger = createLogger('channel_discord').child({ component: 'interactions-route' })\n\n/**\n * Discord Interactions endpoint (slash commands, buttons, PING handshake).\n *\n * This is a **provider-owned** signed route \u2014 the resolution to the spec's one\n * \"under negotiation\" hub touch-point. Discord requires a *synchronous* PONG\n * (`{ type: 1 }`) on the initial PING, which the hub's generic\n * `api/post/webhook/[provider]` route cannot return (it 202-acks + enqueues). By\n * shipping this route from the provider package we serve the handshake without\n * changing the hub contract. Operators set the Interactions Endpoint URL to\n * `/api/channel_discord/interactions`.\n *\n * Auth model: unauthenticated at the platform layer \u2014 Ed25519 signature\n * verification IS the auth, and it is fail-closed (a tampered/missing signature\n * verifies against no candidate channel \u2192 401).\n *\n * Timing model: Discord closes an interaction that is not answered within three\n * seconds, so a verified slash command / component press is answered with a\n * DEFERRED acknowledgement and handed to `workers/discord-interactions.ts`,\n * which does the two slow halves \u2014 writing the interaction into the hub's\n * inbound queue and replacing the \"thinking\u2026\" placeholder with a real message.\n * The hand-off happens BEFORE the ack is returned, so a queue that cannot accept\n * the job downgrades the response to a visible error instead of promising a\n * follow-up nothing will send.\n */\nexport const metadata = {\n // Pinned explicitly: this URL is operator-facing \u2014 it goes into the Discord\n // application's Interactions Endpoint field \u2014 so it is part of this route's\n // contract rather than a by-product of where the file happens to sit. It is\n // the same path the folder derives, so the pin documents the contract instead\n // of overriding it.\n path: '/channel_discord/interactions',\n POST: {\n requireAuth: false,\n // Unauthenticated by design. Unsigned traffic is now rejected before any\n // candidate is loaded, so this bounds the residual case: a caller who does\n // present well-formed, fresh headers and drives the narrowed candidate load\n // repeatedly before the signature gate rejects them.\n rateLimit: { points: 120, duration: 60, keyPrefix: 'discord_interactions' },\n },\n}\n\nexport async function POST(req: Request): Promise<Response> {\n const rawBody = await req.text()\n const signatureHex = req.headers.get('x-signature-ed25519')\n const timestamp = req.headers.get('x-signature-timestamp')\n\n // Screened here as well as inside `resolveDiscordInteraction` so that unsigned,\n // malformed or stale traffic returns before the dictionary load below \u2014 this\n // route is unauthenticated, so everything decidable from the request alone\n // stays ahead of every other cost.\n const screened = screenInteractionRequest({ signatureHex, timestamp })\n if (screened) return NextResponse.json(screened.body, { status: screened.status })\n\n const { t } = await resolveTranslations()\n\n const result = await resolveDiscordInteraction({\n rawBody,\n signatureHex,\n timestamp,\n loadCandidates: loadInteractionCandidates,\n messages: {\n notDispatchable: t(\n 'channel_discord.interactions.notDispatchable',\n DEFAULT_INTERACTION_MESSAGES.notDispatchable,\n ),\n unsupported: t('channel_discord.interactions.unsupported', DEFAULT_INTERACTION_MESSAGES.unsupported),\n },\n })\n\n // A deferred acknowledgement is a promise of a follow-up. Enqueue it BEFORE\n // answering: if the hand-off fails the user gets a visible error instead of a\n // \"thinking\u2026\" state nothing will ever replace.\n if (result.dispatch && result.matchedChannel) {\n const enqueued = await enqueueInteractionDispatch(result.dispatch, result.matchedChannel)\n if (!enqueued) {\n return NextResponse.json(\n {\n type: DiscordInteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,\n data: {\n content: t(\n 'channel_discord.interactions.dispatchFailed',\n 'Open Mercato could not record this interaction right now. Please try again in a moment.',\n ),\n flags: DISCORD_MESSAGE_FLAG_EPHEMERAL,\n },\n },\n { status: 200 },\n )\n }\n }\n\n return NextResponse.json(result.body, { status: result.status })\n}\n\n/**\n * Hand the verified interaction to the dispatch worker. Returns `false` when the\n * job could not be queued at all, which is the one case where the deferred ack\n * must be downgraded to a visible reply.\n */\nasync function enqueueInteractionDispatch(\n dispatch: DispatchableInteraction,\n channel: InteractionCandidate,\n): Promise<boolean> {\n const payload: InteractionDispatchJobPayload = {\n channelId: channel.channelId,\n channelType: channel.channelType,\n tenantId: channel.tenantId,\n organizationId: channel.organizationId,\n credentialScope: channel.credentialScope,\n interaction: dispatch,\n }\n try {\n await getInteractionDispatchQueue().enqueue(payload as unknown as Record<string, unknown>)\n return true\n } catch (err) {\n logger.error('failed to enqueue discord interaction dispatch', { err, channelId: channel.channelId })\n return false\n }\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n}\n\nasync function loadInteractionCandidates(\n filter: InteractionCandidateFilter,\n): Promise<InteractionCandidate[]> {\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n let credentialsService: CredentialsServiceLike | null = null\n try {\n credentialsService = container.resolve<CredentialsServiceLike>('integrationCredentialsService')\n } catch {\n credentialsService = null\n }\n if (!credentialsService) return []\n\n const candidates: InteractionCandidate[] = []\n try {\n const rows = (await findWithDecryption(em, CommunicationChannel, {\n providerKey: 'discord',\n isActive: true,\n deletedAt: null,\n })) as CommunicationChannel[]\n\n // Credentials are resolved per (tenant, organization, user) scope, and a\n // tenant's Discord channels usually share one \u2014 cache within this request so\n // N channel rows do not become N decrypts of the same credential bag.\n const resolvedByScope = new Map<string, Record<string, unknown> | null>()\n\n for (const channel of rows) {\n if (!channel.credentialsRef) continue\n const organizationId = channel.organizationId ?? channel.tenantId\n const userId = channel.userId ?? null\n const scopeKey = `${channel.tenantId}|${organizationId}|${userId ?? ''}`\n if (!resolvedByScope.has(scopeKey)) {\n try {\n resolvedByScope.set(\n scopeKey,\n await credentialsService.resolve('channel_discord', {\n tenantId: channel.tenantId,\n organizationId,\n userId,\n }),\n )\n } catch {\n resolvedByScope.set(scopeKey, null)\n }\n }\n const parsed = discordCredentialsSchema.safeParse(resolvedByScope.get(scopeKey) ?? {})\n if (!parsed.success) continue\n // Narrowing only \u2014 the signature still decides. A body claiming an\n // application nobody here owns simply verifies against nothing.\n if (filter.applicationId && parsed.data.applicationId !== filter.applicationId) continue\n candidates.push({\n channelId: channel.id,\n channelType: channel.channelType,\n tenantId: channel.tenantId,\n organizationId: channel.organizationId ?? null,\n publicKey: parsed.data.publicKey,\n applicationId: parsed.data.applicationId,\n // The exact scope this row's credentials resolved under, so the worker\n // re-resolves the same bag instead of guessing \u2014 and no token travels.\n credentialScope: { tenantId: channel.tenantId, organizationId, userId },\n })\n }\n } catch (err) {\n logger.warn('failed to load discord interaction candidates', { err })\n return []\n }\n\n return candidates\n}\n\nexport const openApi = {\n tags: ['ChannelDiscord'],\n summary: 'Discord Interactions endpoint (slash commands, buttons, PING handshake)',\n methods: {\n POST: {\n summary: 'Verify (Ed25519, fail-closed) and dispatch a Discord interaction',\n tags: ['ChannelDiscord'],\n responses: [\n {\n status: 200,\n description:\n 'Verified interaction \u2014 PONG for the handshake, a deferred ack for a dispatched slash command / component / modal submission, an empty autocomplete result, or an ephemeral message when the interaction cannot be dispatched',\n },\n { status: 400, description: 'Verified but malformed interaction body' },\n { status: 401, description: 'Signature verification failed against every candidate channel, or the signed timestamp is outside the replay window' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }