@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.
- package/dist/env.d.ts +22 -0
- package/dist/env.js +13 -2
- package/dist/github/index.d.ts +3 -3
- package/dist/github/mcp/auth.d.ts +2 -2
- package/dist/github/mcp/index.d.ts +2 -2
- package/dist/github/mcp/index.js +23 -34
- package/dist/github/mcp/schemas.d.ts +1 -1
- package/dist/github/routes/setup.d.ts +2 -2
- package/dist/github/routes/tokenExchange.d.ts +2 -2
- package/dist/github/routes/webhooks.d.ts +2 -2
- package/dist/slack/i18n/index.d.ts +2 -0
- package/dist/slack/i18n/index.js +3 -0
- package/dist/slack/i18n/strings.d.ts +73 -0
- package/dist/slack/i18n/strings.js +67 -0
- package/dist/slack/index.d.ts +18 -0
- package/dist/slack/index.js +28 -0
- package/dist/slack/middleware/permissions.d.ts +31 -0
- package/dist/slack/middleware/permissions.js +167 -0
- package/dist/slack/routes/events.d.ts +10 -0
- package/dist/slack/routes/events.js +551 -0
- package/dist/slack/routes/index.d.ts +10 -0
- package/dist/slack/routes/index.js +47 -0
- package/dist/slack/routes/oauth.d.ts +20 -0
- package/dist/slack/routes/oauth.js +344 -0
- package/dist/slack/routes/users.d.ts +10 -0
- package/dist/slack/routes/users.js +365 -0
- package/dist/slack/routes/workspaces.d.ts +10 -0
- package/dist/slack/routes/workspaces.js +909 -0
- package/dist/slack/services/agent-resolution.d.ts +41 -0
- package/dist/slack/services/agent-resolution.js +99 -0
- package/dist/slack/services/blocks/index.d.ts +73 -0
- package/dist/slack/services/blocks/index.js +103 -0
- package/dist/slack/services/client.d.ts +108 -0
- package/dist/slack/services/client.js +232 -0
- package/dist/slack/services/commands/index.d.ts +19 -0
- package/dist/slack/services/commands/index.js +553 -0
- package/dist/slack/services/events/app-mention.d.ts +40 -0
- package/dist/slack/services/events/app-mention.js +297 -0
- package/dist/slack/services/events/block-actions.d.ts +40 -0
- package/dist/slack/services/events/block-actions.js +265 -0
- package/dist/slack/services/events/index.d.ts +6 -0
- package/dist/slack/services/events/index.js +7 -0
- package/dist/slack/services/events/modal-submission.d.ts +30 -0
- package/dist/slack/services/events/modal-submission.js +400 -0
- package/dist/slack/services/events/streaming.d.ts +26 -0
- package/dist/slack/services/events/streaming.js +255 -0
- package/dist/slack/services/events/utils.d.ts +146 -0
- package/dist/slack/services/events/utils.js +370 -0
- package/dist/slack/services/index.d.ts +16 -0
- package/dist/slack/services/index.js +16 -0
- package/dist/slack/services/modals.d.ts +86 -0
- package/dist/slack/services/modals.js +355 -0
- package/dist/slack/services/nango.d.ts +85 -0
- package/dist/slack/services/nango.js +476 -0
- package/dist/slack/services/security.d.ts +35 -0
- package/dist/slack/services/security.js +65 -0
- package/dist/slack/services/types.d.ts +26 -0
- package/dist/slack/services/types.js +1 -0
- package/dist/slack/services/workspace-tokens.d.ts +25 -0
- package/dist/slack/services/workspace-tokens.js +27 -0
- package/dist/slack/tracer.d.ts +40 -0
- package/dist/slack/tracer.js +39 -0
- package/dist/slack/types.d.ts +10 -0
- package/dist/slack/types.js +1 -0
- 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 };
|