@inkeep/agents-work-apps 0.47.4 → 0.48.0

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 (65) hide show
  1. package/dist/env.d.ts +22 -0
  2. package/dist/env.js +13 -2
  3. package/dist/github/index.d.ts +3 -3
  4. package/dist/github/mcp/auth.d.ts +2 -2
  5. package/dist/github/mcp/index.d.ts +2 -2
  6. package/dist/github/mcp/index.js +23 -34
  7. package/dist/github/mcp/schemas.d.ts +1 -1
  8. package/dist/github/routes/setup.d.ts +2 -2
  9. package/dist/github/routes/tokenExchange.d.ts +2 -2
  10. package/dist/github/routes/webhooks.d.ts +2 -2
  11. package/dist/slack/i18n/index.d.ts +2 -0
  12. package/dist/slack/i18n/index.js +3 -0
  13. package/dist/slack/i18n/strings.d.ts +73 -0
  14. package/dist/slack/i18n/strings.js +67 -0
  15. package/dist/slack/index.d.ts +18 -0
  16. package/dist/slack/index.js +28 -0
  17. package/dist/slack/middleware/permissions.d.ts +31 -0
  18. package/dist/slack/middleware/permissions.js +167 -0
  19. package/dist/slack/routes/events.d.ts +10 -0
  20. package/dist/slack/routes/events.js +551 -0
  21. package/dist/slack/routes/index.d.ts +10 -0
  22. package/dist/slack/routes/index.js +47 -0
  23. package/dist/slack/routes/oauth.d.ts +20 -0
  24. package/dist/slack/routes/oauth.js +344 -0
  25. package/dist/slack/routes/users.d.ts +10 -0
  26. package/dist/slack/routes/users.js +365 -0
  27. package/dist/slack/routes/workspaces.d.ts +10 -0
  28. package/dist/slack/routes/workspaces.js +909 -0
  29. package/dist/slack/services/agent-resolution.d.ts +41 -0
  30. package/dist/slack/services/agent-resolution.js +99 -0
  31. package/dist/slack/services/blocks/index.d.ts +73 -0
  32. package/dist/slack/services/blocks/index.js +103 -0
  33. package/dist/slack/services/client.d.ts +108 -0
  34. package/dist/slack/services/client.js +232 -0
  35. package/dist/slack/services/commands/index.d.ts +19 -0
  36. package/dist/slack/services/commands/index.js +553 -0
  37. package/dist/slack/services/events/app-mention.d.ts +40 -0
  38. package/dist/slack/services/events/app-mention.js +297 -0
  39. package/dist/slack/services/events/block-actions.d.ts +40 -0
  40. package/dist/slack/services/events/block-actions.js +265 -0
  41. package/dist/slack/services/events/index.d.ts +6 -0
  42. package/dist/slack/services/events/index.js +7 -0
  43. package/dist/slack/services/events/modal-submission.d.ts +30 -0
  44. package/dist/slack/services/events/modal-submission.js +400 -0
  45. package/dist/slack/services/events/streaming.d.ts +26 -0
  46. package/dist/slack/services/events/streaming.js +255 -0
  47. package/dist/slack/services/events/utils.d.ts +146 -0
  48. package/dist/slack/services/events/utils.js +370 -0
  49. package/dist/slack/services/index.d.ts +16 -0
  50. package/dist/slack/services/index.js +16 -0
  51. package/dist/slack/services/modals.d.ts +86 -0
  52. package/dist/slack/services/modals.js +355 -0
  53. package/dist/slack/services/nango.d.ts +85 -0
  54. package/dist/slack/services/nango.js +476 -0
  55. package/dist/slack/services/security.d.ts +35 -0
  56. package/dist/slack/services/security.js +65 -0
  57. package/dist/slack/services/types.d.ts +26 -0
  58. package/dist/slack/services/types.js +1 -0
  59. package/dist/slack/services/workspace-tokens.d.ts +25 -0
  60. package/dist/slack/services/workspace-tokens.js +27 -0
  61. package/dist/slack/tracer.d.ts +40 -0
  62. package/dist/slack/tracer.js +39 -0
  63. package/dist/slack/types.d.ts +10 -0
  64. package/dist/slack/types.js +1 -0
  65. package/package.json +11 -2
@@ -0,0 +1,551 @@
1
+ import { env } from "../../env.js";
2
+ import { getLogger } from "../../logger.js";
3
+ import runDbClient_default from "../../db/runDbClient.js";
4
+ import { findWorkspaceConnectionByTeamId, getSlackIntegrationId, getSlackNango, updateConnectionMetadata } from "../services/nango.js";
5
+ import { getSlackClient, getSlackUserInfo } from "../services/client.js";
6
+ import { sendResponseUrlMessage } from "../services/events/utils.js";
7
+ import { handleCommand } from "../services/commands/index.js";
8
+ import { SLACK_SPAN_KEYS, SLACK_SPAN_NAMES, tracer } from "../tracer.js";
9
+ import { handleAppMention } from "../services/events/app-mention.js";
10
+ import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "../services/events/block-actions.js";
11
+ import { handleFollowUpSubmission, handleModalSubmission } from "../services/events/modal-submission.js";
12
+ import "../services/events/index.js";
13
+ import { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest } from "../services/security.js";
14
+ import "../services/index.js";
15
+ import { OpenAPIHono } from "@hono/zod-openapi";
16
+ import { deleteAllWorkAppSlackChannelAgentConfigsByTeam, deleteAllWorkAppSlackUserMappingsByTeam, deleteWorkAppSlackWorkspaceByNangoConnectionId } from "@inkeep/agents-core";
17
+ import { SpanStatusCode } from "@opentelemetry/api";
18
+
19
+ //#region src/slack/routes/events.ts
20
+ /**
21
+ * Slack Events Routes
22
+ *
23
+ * Endpoints for handling Slack events, commands, and webhooks:
24
+ * - POST /commands - Handle /inkeep slash commands
25
+ * - POST /events - Handle Slack events & interactivity
26
+ * - POST /nango-webhook - Handle Nango auth webhooks
27
+ */
28
+ const logger = getLogger("slack-events");
29
+ const app = new OpenAPIHono();
30
+ app.post("/commands", async (c) => {
31
+ const body = await c.req.text();
32
+ const timestamp = c.req.header("x-slack-request-timestamp") || "";
33
+ const signature = c.req.header("x-slack-signature") || "";
34
+ if (!env.SLACK_SIGNING_SECRET) {
35
+ logger.error({}, "SLACK_SIGNING_SECRET not configured - rejecting request");
36
+ return c.json({
37
+ response_type: "ephemeral",
38
+ text: "Server configuration error"
39
+ }, 500);
40
+ }
41
+ if (!verifySlackRequest(env.SLACK_SIGNING_SECRET, body, timestamp, signature)) {
42
+ logger.error({}, "Invalid Slack request signature");
43
+ return c.json({
44
+ response_type: "ephemeral",
45
+ text: "Invalid request signature"
46
+ }, 401);
47
+ }
48
+ const params = parseSlackCommandBody(body);
49
+ const response = await handleCommand({
50
+ command: params.command || "",
51
+ text: params.text || "",
52
+ userId: params.user_id || "",
53
+ userName: params.user_name || "",
54
+ teamId: params.team_id || "",
55
+ teamDomain: params.team_domain || "",
56
+ enterpriseId: params.enterprise_id,
57
+ channelId: params.channel_id || "",
58
+ channelName: params.channel_name || "",
59
+ responseUrl: params.response_url || "",
60
+ triggerId: params.trigger_id || ""
61
+ });
62
+ if (Object.keys(response).length === 0) return c.body(null, 200);
63
+ return c.json(response);
64
+ });
65
+ app.post("/events", async (c) => {
66
+ return tracer.startActiveSpan(SLACK_SPAN_NAMES.WEBHOOK, async (span) => {
67
+ let outcome = "ignored_unknown_event";
68
+ try {
69
+ const contentType = c.req.header("content-type") || "";
70
+ const body = await c.req.text();
71
+ const timestamp = c.req.header("x-slack-request-timestamp") || "";
72
+ const signature = c.req.header("x-slack-signature") || "";
73
+ let eventBody;
74
+ try {
75
+ eventBody = parseSlackEventBody(body, contentType);
76
+ } catch (error) {
77
+ outcome = "validation_error";
78
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
79
+ logger.error({
80
+ error,
81
+ contentType,
82
+ bodyPreview: body.slice(0, 200)
83
+ }, "Failed to parse Slack event body");
84
+ span.end();
85
+ return c.json({ error: "Invalid payload" }, 400);
86
+ }
87
+ const eventType = eventBody.type;
88
+ span.setAttribute(SLACK_SPAN_KEYS.EVENT_TYPE, eventType || "unknown");
89
+ span.updateName(`${SLACK_SPAN_NAMES.WEBHOOK} ${eventType || "unknown"}`);
90
+ if (!env.SLACK_SIGNING_SECRET) {
91
+ outcome = "error";
92
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
93
+ logger.error({}, "SLACK_SIGNING_SECRET not configured - rejecting request");
94
+ span.end();
95
+ return c.json({ error: "Server configuration error" }, 500);
96
+ }
97
+ if (!verifySlackRequest(env.SLACK_SIGNING_SECRET, body, timestamp, signature)) {
98
+ outcome = "signature_invalid";
99
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
100
+ logger.error({ eventType }, "Invalid Slack request signature");
101
+ span.end();
102
+ return c.json({ error: "Invalid request signature" }, 401);
103
+ }
104
+ if (eventType === "url_verification") {
105
+ outcome = "url_verification";
106
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
107
+ logger.info({}, "Responding to Slack URL verification challenge");
108
+ span.end();
109
+ return c.text(String(eventBody.challenge));
110
+ }
111
+ if (eventType === "event_callback") {
112
+ const teamId = eventBody.team_id;
113
+ const event = eventBody.event;
114
+ const innerEventType = event?.type || "unknown";
115
+ span.setAttribute(SLACK_SPAN_KEYS.INNER_EVENT_TYPE, innerEventType);
116
+ if (teamId) span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
117
+ if (event?.channel) span.setAttribute(SLACK_SPAN_KEYS.CHANNEL_ID, event.channel);
118
+ if (event?.user) span.setAttribute(SLACK_SPAN_KEYS.USER_ID, event.user);
119
+ span.updateName(`${SLACK_SPAN_NAMES.WEBHOOK} event_callback.${innerEventType}`);
120
+ if (event?.bot_id || event?.subtype === "bot_message") {
121
+ outcome = "ignored_bot_message";
122
+ span.setAttribute(SLACK_SPAN_KEYS.IS_BOT_MESSAGE, true);
123
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
124
+ logger.info({
125
+ botId: event.bot_id,
126
+ subtype: event?.subtype,
127
+ teamId,
128
+ innerEventType
129
+ }, "Ignoring bot message");
130
+ span.end();
131
+ return c.json({ ok: true });
132
+ }
133
+ if (event?.type === "app_mention" && event.channel && event.user && teamId) {
134
+ outcome = "handled";
135
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
136
+ const question = (event.text || "").replace(/<@[A-Z0-9]+>/g, "").trim();
137
+ span.setAttribute(SLACK_SPAN_KEYS.HAS_QUERY, question.length > 0);
138
+ if (event.thread_ts) span.setAttribute(SLACK_SPAN_KEYS.THREAD_TS, event.thread_ts);
139
+ if (event.ts) span.setAttribute(SLACK_SPAN_KEYS.MESSAGE_TS, event.ts);
140
+ logger.info({
141
+ userId: event.user,
142
+ channel: event.channel,
143
+ teamId,
144
+ hasQuery: question.length > 0
145
+ }, "Handling event: app_mention");
146
+ handleAppMention({
147
+ slackUserId: event.user,
148
+ channel: event.channel,
149
+ text: question,
150
+ threadTs: event.thread_ts || event.ts || "",
151
+ messageTs: event.ts || "",
152
+ teamId
153
+ }).catch((err) => {
154
+ const errorMessage = err instanceof Error ? err.message : String(err);
155
+ const errorStack = err instanceof Error ? err.stack : void 0;
156
+ logger.error({
157
+ errorMessage,
158
+ errorStack
159
+ }, "Failed to handle app mention (outer catch)");
160
+ });
161
+ } else {
162
+ outcome = "ignored_unknown_event";
163
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
164
+ logger.info({
165
+ innerEventType,
166
+ teamId
167
+ }, `Ignoring unhandled event_callback: ${innerEventType}`);
168
+ }
169
+ }
170
+ if (eventType === "block_actions" || eventType === "interactive_message") {
171
+ const actions = eventBody.actions;
172
+ const teamId = eventBody.team?.id;
173
+ const responseUrl = eventBody.response_url;
174
+ const triggerId = eventBody.trigger_id;
175
+ const actionIds = actions?.map((a) => a.action_id) || [];
176
+ if (teamId) span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
177
+ span.setAttribute(SLACK_SPAN_KEYS.ACTION_IDS, actionIds.join(","));
178
+ span.updateName(`${SLACK_SPAN_NAMES.WEBHOOK} ${eventType} [${actionIds.join(",")}]`);
179
+ if (actions && teamId) {
180
+ let anyHandled = false;
181
+ for (const action of actions) {
182
+ if (action.action_id === "open_agent_selector_modal" && action.value && triggerId) {
183
+ anyHandled = true;
184
+ logger.info({
185
+ teamId,
186
+ actionId: action.action_id
187
+ }, "Handling block_action: open_agent_selector_modal");
188
+ handleOpenAgentSelectorModal({
189
+ triggerId,
190
+ actionValue: action.value,
191
+ teamId,
192
+ responseUrl: responseUrl || ""
193
+ }).catch(async (err) => {
194
+ const errorMessage = err instanceof Error ? err.message : String(err);
195
+ logger.error({
196
+ errorMessage,
197
+ actionId: action.action_id
198
+ }, "Failed to open agent selector modal");
199
+ if (responseUrl) await sendResponseUrlMessage(responseUrl, {
200
+ text: "Sorry, something went wrong while opening the agent selector. Please try again.",
201
+ response_type: "ephemeral"
202
+ }).catch((e) => logger.warn({ error: e }, "Failed to send error notification via response URL"));
203
+ });
204
+ }
205
+ if (action.action_id === "modal_project_select") {
206
+ anyHandled = true;
207
+ const selectedProjectId = action.selected_option?.value;
208
+ const view = eventBody.view;
209
+ logger.info({
210
+ teamId,
211
+ actionId: action.action_id,
212
+ selectedProjectId
213
+ }, "Handling block_action: modal_project_select");
214
+ if (selectedProjectId && view?.id) (async () => {
215
+ try {
216
+ const metadata = JSON.parse(view.private_metadata || "{}");
217
+ const tenantId = metadata.tenantId;
218
+ if (!tenantId) {
219
+ logger.warn({ teamId }, "No tenantId in modal metadata — skipping project update");
220
+ return;
221
+ }
222
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
223
+ if (!workspace?.botToken) return;
224
+ const slackClient = getSlackClient(workspace.botToken);
225
+ const { fetchProjectsForTenant, fetchAgentsForProject } = await import("../services/events/utils.js");
226
+ const { buildAgentSelectorModal, buildMessageShortcutModal } = await import("../services/modals.js");
227
+ const projectList = await fetchProjectsForTenant(tenantId);
228
+ const agentList = await fetchAgentsForProject(tenantId, selectedProjectId);
229
+ const agentOptions = agentList.map((a) => ({
230
+ id: a.id,
231
+ name: a.name,
232
+ projectId: a.projectId,
233
+ projectName: a.projectName || a.projectId
234
+ }));
235
+ const modal = metadata.messageContext ? buildMessageShortcutModal({
236
+ projects: projectList,
237
+ agents: agentOptions,
238
+ metadata,
239
+ selectedProjectId,
240
+ messageContext: metadata.messageContext
241
+ }) : buildAgentSelectorModal({
242
+ projects: projectList,
243
+ agents: agentOptions,
244
+ metadata,
245
+ selectedProjectId
246
+ });
247
+ await slackClient.views.update({
248
+ view_id: view.id,
249
+ view: modal
250
+ });
251
+ logger.debug({
252
+ selectedProjectId,
253
+ agentCount: agentList.length
254
+ }, "Updated modal with agents for selected project");
255
+ } catch (err) {
256
+ logger.error({
257
+ err,
258
+ selectedProjectId
259
+ }, "Failed to update modal on project change");
260
+ }
261
+ })();
262
+ }
263
+ if (action.action_id === "open_follow_up_modal" && action.value && triggerId) {
264
+ anyHandled = true;
265
+ logger.info({
266
+ teamId,
267
+ actionId: action.action_id
268
+ }, "Handling block_action: open_follow_up_modal");
269
+ handleOpenFollowUpModal({
270
+ triggerId,
271
+ actionValue: action.value,
272
+ teamId,
273
+ responseUrl: responseUrl || void 0
274
+ }).catch((err) => {
275
+ const errorMessage = err instanceof Error ? err.message : String(err);
276
+ logger.error({
277
+ errorMessage,
278
+ actionId: action.action_id
279
+ }, "Failed to open follow-up modal");
280
+ });
281
+ }
282
+ }
283
+ outcome = anyHandled ? "handled" : "ignored_no_action_match";
284
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
285
+ if (!anyHandled) logger.info({
286
+ teamId,
287
+ actionIds,
288
+ eventType
289
+ }, "Ignoring block_actions: no matching action handlers");
290
+ } else {
291
+ outcome = "ignored_no_action_match";
292
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
293
+ logger.info({
294
+ teamId,
295
+ eventType,
296
+ hasActions: Boolean(actions)
297
+ }, "Ignoring block_actions: missing actions or teamId");
298
+ }
299
+ }
300
+ if (eventType === "message_action") {
301
+ const callbackId = eventBody.callback_id;
302
+ span.setAttribute(SLACK_SPAN_KEYS.CALLBACK_ID, callbackId || "unknown");
303
+ span.updateName(`${SLACK_SPAN_NAMES.WEBHOOK} message_action.${callbackId || "unknown"}`);
304
+ if (callbackId === "ask_agent_shortcut") {
305
+ const triggerId = eventBody.trigger_id;
306
+ const teamId = eventBody.team?.id;
307
+ const channelId = eventBody.channel?.id;
308
+ const userId = eventBody.user?.id;
309
+ const message = eventBody.message;
310
+ const responseUrl = eventBody.response_url;
311
+ if (teamId) span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
312
+ if (channelId) span.setAttribute(SLACK_SPAN_KEYS.CHANNEL_ID, channelId);
313
+ if (userId) span.setAttribute(SLACK_SPAN_KEYS.USER_ID, userId);
314
+ if (triggerId && teamId && channelId && userId && message?.ts) {
315
+ outcome = "handled";
316
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
317
+ logger.info({
318
+ teamId,
319
+ channelId,
320
+ userId,
321
+ callbackId
322
+ }, "Handling message_action: ask_agent_shortcut");
323
+ handleMessageShortcut({
324
+ triggerId,
325
+ teamId,
326
+ channelId,
327
+ userId,
328
+ messageTs: message.ts,
329
+ messageText: message.text || "",
330
+ threadTs: message.thread_ts,
331
+ responseUrl
332
+ }).catch((err) => {
333
+ const errorMessage = err instanceof Error ? err.message : String(err);
334
+ logger.error({
335
+ errorMessage,
336
+ callbackId
337
+ }, "Failed to handle message shortcut");
338
+ });
339
+ } else {
340
+ outcome = "ignored_unknown_event";
341
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
342
+ logger.info({
343
+ teamId,
344
+ channelId,
345
+ userId,
346
+ callbackId,
347
+ hasTriggerId: Boolean(triggerId)
348
+ }, "Ignoring message_action: missing required fields");
349
+ }
350
+ } else {
351
+ outcome = "ignored_unknown_event";
352
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
353
+ logger.info({ callbackId }, `Ignoring unhandled message_action: ${callbackId}`);
354
+ }
355
+ }
356
+ if (eventType === "view_submission") {
357
+ const callbackId = eventBody.view?.callback_id;
358
+ span.setAttribute(SLACK_SPAN_KEYS.CALLBACK_ID, callbackId || "unknown");
359
+ span.updateName(`${SLACK_SPAN_NAMES.WEBHOOK} view_submission.${callbackId || "unknown"}`);
360
+ if (callbackId === "agent_selector_modal") {
361
+ const view = eventBody.view;
362
+ const agentSelect = view.state?.values?.agent_select_block?.agent_select;
363
+ if (!agentSelect?.selected_option?.value || agentSelect.selected_option.value === "none") {
364
+ outcome = "validation_error";
365
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
366
+ logger.info({ callbackId }, "Rejecting view_submission: no agent selected");
367
+ span.end();
368
+ return c.json({
369
+ response_action: "errors",
370
+ errors: { agent_select_block: "Please select an agent. If none are available, add agents to this project in the dashboard." }
371
+ });
372
+ }
373
+ outcome = "handled";
374
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
375
+ logger.info({ callbackId }, "Handling view_submission: agent_selector_modal");
376
+ handleModalSubmission(view).catch((err) => {
377
+ const errorMessage = err instanceof Error ? err.message : String(err);
378
+ logger.error({
379
+ errorMessage,
380
+ callbackId
381
+ }, "Failed to handle modal submission");
382
+ });
383
+ span.end();
384
+ return new Response(null, { status: 200 });
385
+ }
386
+ if (callbackId === "follow_up_modal") {
387
+ const view = eventBody.view;
388
+ outcome = "handled";
389
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
390
+ logger.info({ callbackId }, "Handling view_submission: follow_up_modal");
391
+ handleFollowUpSubmission(view).catch((err) => {
392
+ const errorMessage = err instanceof Error ? err.message : String(err);
393
+ logger.error({
394
+ errorMessage,
395
+ callbackId
396
+ }, "Failed to handle follow-up submission");
397
+ });
398
+ span.end();
399
+ return new Response(null, { status: 200 });
400
+ }
401
+ outcome = "ignored_unknown_event";
402
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
403
+ logger.info({ callbackId }, `Ignoring unhandled view_submission: ${callbackId}`);
404
+ }
405
+ if (eventType !== "event_callback" && eventType !== "block_actions" && eventType !== "interactive_message" && eventType !== "message_action" && eventType !== "view_submission") {
406
+ outcome = "ignored_unknown_event";
407
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
408
+ logger.info({ eventType }, `Ignoring unhandled Slack event type: ${eventType}`);
409
+ }
410
+ span.end();
411
+ return c.json({ ok: true });
412
+ } catch (error) {
413
+ outcome = "error";
414
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
415
+ span.setStatus({
416
+ code: SpanStatusCode.ERROR,
417
+ message: String(error)
418
+ });
419
+ if (error instanceof Error) span.recordException(error);
420
+ span.end();
421
+ throw error;
422
+ }
423
+ });
424
+ });
425
+ app.post("/nango-webhook", async (c) => {
426
+ const body = await c.req.text();
427
+ const nangoSecret = env.NANGO_SLACK_SECRET_KEY || env.NANGO_SECRET_KEY;
428
+ if (!nangoSecret) {
429
+ logger.error({}, "No Nango secret key configured — rejecting webhook");
430
+ return c.json({ error: "Server configuration error" }, 503);
431
+ }
432
+ const signature = c.req.header("x-nango-signature");
433
+ if (!signature) {
434
+ logger.warn({}, "Missing Nango webhook signature");
435
+ return c.json({ error: "Missing signature" }, 401);
436
+ }
437
+ const crypto = await import("node:crypto");
438
+ const expectedSignature = crypto.createHmac("sha256", nangoSecret).update(body).digest("hex");
439
+ if (signature.length !== expectedSignature.length || !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
440
+ logger.warn({ signature }, "Invalid Nango webhook signature");
441
+ return c.json({ error: "Invalid signature" }, 401);
442
+ }
443
+ let payload;
444
+ try {
445
+ payload = JSON.parse(body);
446
+ } catch (error) {
447
+ logger.error({
448
+ error,
449
+ bodyPreview: body.slice(0, 200)
450
+ }, "Failed to parse Nango webhook JSON");
451
+ return c.json({ error: "Invalid JSON" }, 400);
452
+ }
453
+ logger.debug({ payload }, "Nango webhook received");
454
+ if (payload.type === "connection_deleted" && payload.connectionId && payload.providerConfigKey) {
455
+ const { connectionId, providerConfigKey } = payload;
456
+ if (providerConfigKey === getSlackIntegrationId()) try {
457
+ const teamMatch = connectionId.match(/T:([A-Z0-9]+)/);
458
+ if (teamMatch) {
459
+ const teamId = teamMatch[1];
460
+ const tenantId = (await findWorkspaceConnectionByTeamId(teamId))?.tenantId;
461
+ if (await deleteWorkAppSlackWorkspaceByNangoConnectionId(runDbClient_default)(connectionId)) logger.info({ connectionId }, "Deleted workspace from database via Nango webhook");
462
+ if (tenantId) {
463
+ const deletedMappings = await deleteAllWorkAppSlackUserMappingsByTeam(runDbClient_default)(tenantId, teamId);
464
+ if (deletedMappings > 0) logger.info({
465
+ teamId,
466
+ deletedMappings
467
+ }, "Deleted user mappings via Nango webhook");
468
+ const deletedChannelConfigs = await deleteAllWorkAppSlackChannelAgentConfigsByTeam(runDbClient_default)(tenantId, teamId);
469
+ if (deletedChannelConfigs > 0) logger.info({
470
+ teamId,
471
+ deletedChannelConfigs
472
+ }, "Deleted channel configs via Nango webhook");
473
+ } else logger.warn({
474
+ connectionId,
475
+ teamId
476
+ }, "No tenantId found, skipping user/channel cleanup");
477
+ logger.info({
478
+ connectionId,
479
+ teamId
480
+ }, "Processed Nango connection deletion webhook");
481
+ }
482
+ } catch (error) {
483
+ logger.error({
484
+ error,
485
+ connectionId
486
+ }, "Failed to process Nango deletion webhook");
487
+ return c.json({ error: "Deletion processing failed" }, 500);
488
+ }
489
+ return c.json({ received: true });
490
+ }
491
+ if (payload.type === "auth" && payload.success && payload.endUser && payload.connectionId) {
492
+ const { endUser, connectionId } = payload;
493
+ const integrationId = getSlackIntegrationId();
494
+ try {
495
+ const rawResponse = (await getSlackNango().getConnection(integrationId, connectionId)).credentials?.raw;
496
+ logger.debug({ teamId: rawResponse?.team?.id }, "Retrieved Nango connection info");
497
+ if (rawResponse?.ok && rawResponse.access_token) {
498
+ const slackUserId = rawResponse.authed_user?.id || "";
499
+ const slackTeamId = rawResponse.team?.id || "";
500
+ const accessToken = rawResponse.access_token;
501
+ let slackUsername = "";
502
+ let slackDisplayName = "";
503
+ let slackEmail = "";
504
+ let isSlackAdmin = false;
505
+ let isSlackOwner = false;
506
+ if (slackUserId && accessToken) {
507
+ const userInfo = await getSlackUserInfo(getSlackClient(accessToken), slackUserId);
508
+ if (userInfo) {
509
+ slackUsername = userInfo.name || "";
510
+ slackDisplayName = userInfo.displayName || userInfo.realName || "";
511
+ slackEmail = userInfo.email || "";
512
+ isSlackAdmin = userInfo.isAdmin || false;
513
+ isSlackOwner = userInfo.isOwner || false;
514
+ }
515
+ }
516
+ const tenantId = payload.organization?.id || "";
517
+ await updateConnectionMetadata(connectionId, {
518
+ linked_at: (/* @__PURE__ */ new Date()).toISOString(),
519
+ app_user_id: endUser.endUserId,
520
+ app_user_email: endUser.endUserEmail || "",
521
+ tenant_id: tenantId,
522
+ slack_user_id: slackUserId,
523
+ slack_team_id: slackTeamId,
524
+ slack_team_name: rawResponse.team?.name || "",
525
+ slack_username: slackUsername,
526
+ slack_display_name: slackDisplayName,
527
+ slack_email: slackEmail,
528
+ is_slack_admin: String(isSlackAdmin),
529
+ is_slack_owner: String(isSlackOwner),
530
+ enterprise_id: rawResponse.enterprise?.id || "",
531
+ enterprise_name: rawResponse.enterprise?.name || ""
532
+ });
533
+ logger.info({
534
+ appUserId: endUser.endUserId,
535
+ slackUserId,
536
+ slackEmail
537
+ }, "User linked to Slack with enriched metadata");
538
+ }
539
+ } catch (error) {
540
+ logger.error({
541
+ error,
542
+ connectionId
543
+ }, "Failed to process Nango webhook");
544
+ }
545
+ }
546
+ return c.json({ received: true });
547
+ });
548
+ var events_default = app;
549
+
550
+ //#endregion
551
+ export { events_default as default };
@@ -0,0 +1,10 @@
1
+ import { WorkAppsVariables } from "../types.js";
2
+ import { OpenAPIHono } from "@hono/zod-openapi";
3
+
4
+ //#region src/slack/routes/index.d.ts
5
+
6
+ declare const app: OpenAPIHono<{
7
+ Variables: WorkAppsVariables;
8
+ }, {}, "/">;
9
+ //#endregion
10
+ export { app as default };
@@ -0,0 +1,47 @@
1
+ import events_default from "./events.js";
2
+ import oauth_default from "./oauth.js";
3
+ import users_default from "./users.js";
4
+ import workspaces_default from "./workspaces.js";
5
+ import { OpenAPIHono } from "@hono/zod-openapi";
6
+
7
+ //#region src/slack/routes/index.ts
8
+ /**
9
+ * Slack Work App Routes - Main Router
10
+ *
11
+ * Modular RESTful API structure:
12
+ *
13
+ * OAuth & Installation (oauth.ts):
14
+ * GET /install - Redirect to Slack OAuth
15
+ * GET /oauth_redirect - OAuth callback
16
+ *
17
+ * Workspaces (workspaces.ts):
18
+ * GET /workspaces - List all workspaces
19
+ * GET /workspaces/:teamId - Get workspace details
20
+ * GET /workspaces/:teamId/settings - Get workspace settings
21
+ * PUT /workspaces/:teamId/settings - Update workspace settings [ADMIN]
22
+ * DELETE /workspaces/:teamId - Uninstall workspace [ADMIN]
23
+ * GET /workspaces/:teamId/channels - List channels
24
+ * GET/PUT/DELETE /workspaces/:teamId/channels/:channelId/settings - Channel config
25
+ * GET /workspaces/:teamId/users - List linked users
26
+ *
27
+ * Users (users.ts):
28
+ * GET /users/link-status - Check link status
29
+ * POST /users/link/verify-token - Verify JWT link token (primary linking method)
30
+ * POST /users/connect - Create Nango session
31
+ * POST /users/disconnect - Disconnect/unlink user
32
+ * GET /users/status - Get user connection status
33
+ *
34
+ * Events & Commands (events.ts):
35
+ * POST /commands - Handle slash commands
36
+ * POST /events - Handle Slack events
37
+ * POST /nango-webhook - Handle Nango webhooks
38
+ */
39
+ const app = new OpenAPIHono();
40
+ app.route("/workspaces", workspaces_default);
41
+ app.route("/users", users_default);
42
+ app.route("/", oauth_default);
43
+ app.route("/", events_default);
44
+ var routes_default = app;
45
+
46
+ //#endregion
47
+ export { routes_default as default };
@@ -0,0 +1,20 @@
1
+ import { WorkAppsVariables } from "../types.js";
2
+ import { getBotTokenForTeam, setBotTokenForTeam } from "../services/workspace-tokens.js";
3
+ import "../services/index.js";
4
+ import { OpenAPIHono } from "@hono/zod-openapi";
5
+
6
+ //#region src/slack/routes/oauth.d.ts
7
+
8
+ interface OAuthState {
9
+ nonce: string;
10
+ tenantId?: string;
11
+ timestamp: number;
12
+ }
13
+ declare function getStateSigningSecret(): string;
14
+ declare function createOAuthState(tenantId?: string): string;
15
+ declare function parseOAuthState(stateStr: string): OAuthState | null;
16
+ declare const app: OpenAPIHono<{
17
+ Variables: WorkAppsVariables;
18
+ }, {}, "/">;
19
+ //#endregion
20
+ export { createOAuthState, app as default, getBotTokenForTeam, getStateSigningSecret, parseOAuthState, setBotTokenForTeam };