@inkeep/agents-work-apps 0.50.1 → 0.50.3
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/_virtual/rolldown_runtime.js +32 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.js +1 -0
- package/dist/github/mcp/auth.d.ts +2 -2
- package/dist/github/mcp/index.d.ts +2 -2
- 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/node_modules/.pnpm/@slack_logger@4.0.0/node_modules/@slack/logger/dist/index.js +89 -0
- package/dist/node_modules/.pnpm/@slack_socket-mode@2.0.5/node_modules/@slack/socket-mode/dist/package.js +85 -0
- package/dist/node_modules/.pnpm/@slack_socket-mode@2.0.5/node_modules/@slack/socket-mode/dist/src/SlackWebSocket.js +223 -0
- package/dist/node_modules/.pnpm/@slack_socket-mode@2.0.5/node_modules/@slack/socket-mode/dist/src/SocketModeClient.js +367 -0
- package/dist/node_modules/.pnpm/@slack_socket-mode@2.0.5/node_modules/@slack/socket-mode/dist/src/UnrecoverableSocketModeStartError.js +20 -0
- package/dist/node_modules/.pnpm/@slack_socket-mode@2.0.5/node_modules/@slack/socket-mode/dist/src/errors.js +71 -0
- package/dist/node_modules/.pnpm/@slack_socket-mode@2.0.5/node_modules/@slack/socket-mode/dist/src/index.js +44 -0
- package/dist/node_modules/.pnpm/@slack_socket-mode@2.0.5/node_modules/@slack/socket-mode/dist/src/logger.js +32 -0
- package/dist/node_modules/.pnpm/eventemitter3@5.0.1/node_modules/eventemitter3/index.js +241 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/index.js +23 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/buffer-util.js +107 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/constants.js +29 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/event-target.js +226 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/extension.js +150 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/limiter.js +57 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/permessage-deflate.js +342 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/receiver.js +457 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/sender.js +505 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/stream.js +123 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/subprotocol.js +46 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/validation.js +203 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/websocket-server.js +385 -0
- package/dist/node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/websocket.js +985 -0
- package/dist/slack/dispatcher.d.ts +16 -0
- package/dist/slack/dispatcher.js +335 -0
- package/dist/slack/i18n/strings.d.ts +5 -5
- package/dist/slack/i18n/strings.js +9 -9
- package/dist/slack/index.d.ts +3 -1
- package/dist/slack/index.js +4 -2
- package/dist/slack/middleware/permissions.js +120 -107
- package/dist/slack/routes/events.js +10 -328
- package/dist/slack/routes/oauth.js +6 -3
- package/dist/slack/routes/users.js +12 -6
- package/dist/slack/routes/workspaces.js +31 -36
- package/dist/slack/services/blocks/index.js +7 -11
- package/dist/slack/services/commands/index.js +2 -2
- package/dist/slack/services/dev-config.d.ts +23 -0
- package/dist/slack/services/dev-config.js +91 -0
- package/dist/slack/services/events/app-mention.js +6 -17
- package/dist/slack/services/events/modal-submission.js +5 -5
- package/dist/slack/services/events/streaming.js +4 -3
- package/dist/slack/services/events/utils.js +8 -8
- package/dist/slack/services/index.js +1 -1
- package/dist/slack/services/modals.js +4 -4
- package/dist/slack/services/nango.d.ts +2 -0
- package/dist/slack/services/nango.js +84 -2
- package/dist/slack/socket-mode.d.ts +4 -0
- package/dist/slack/socket-mode.js +130 -0
- package/package.json +3 -2
|
@@ -4,6 +4,7 @@ import { findWorkspaceConnectionByTeamId } from "../services/nango.js";
|
|
|
4
4
|
import { checkUserIsChannelMember, getSlackClient } from "../services/client.js";
|
|
5
5
|
import { OrgRoles, createApiError, findWorkAppSlackUserMappingByInkeepUserId, getUserOrganizationsFromDb } from "@inkeep/agents-core";
|
|
6
6
|
import { createMiddleware } from "hono/factory";
|
|
7
|
+
import { registerAuthzMeta } from "@inkeep/agents-core/middleware";
|
|
7
8
|
|
|
8
9
|
//#region src/slack/middleware/permissions.ts
|
|
9
10
|
const logger = getLogger("slack-permissions");
|
|
@@ -41,49 +42,57 @@ async function resolveWorkAppTenantContext(c, teamId, userId) {
|
|
|
41
42
|
* Middleware that requires Inkeep org admin/owner role.
|
|
42
43
|
* Use for workspace-level settings that only Inkeep organization admins can modify.
|
|
43
44
|
*/
|
|
44
|
-
const requireWorkspaceAdmin = () =>
|
|
45
|
-
|
|
45
|
+
const requireWorkspaceAdmin = () => {
|
|
46
|
+
const mw = createMiddleware(async (c, next) => {
|
|
47
|
+
if (process.env.ENVIRONMENT === "test" && c.req.header("x-test-bypass-auth") === "true") {
|
|
48
|
+
await next();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const userId = c.get("userId");
|
|
52
|
+
if (!userId) throw createApiError({
|
|
53
|
+
code: "unauthorized",
|
|
54
|
+
message: "User context not found",
|
|
55
|
+
instance: c.req.path
|
|
56
|
+
});
|
|
57
|
+
if (userId === "system" || userId.startsWith("apikey:")) {
|
|
58
|
+
await next();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const teamId = c.req.param("teamId") || c.req.param("workspaceId");
|
|
62
|
+
if (teamId && !c.get("tenantRole")) await resolveWorkAppTenantContext(c, teamId, userId);
|
|
63
|
+
const tenantId = c.get("tenantId");
|
|
64
|
+
const tenantRole = c.get("tenantRole");
|
|
65
|
+
if (!tenantId) throw createApiError({
|
|
66
|
+
code: "unauthorized",
|
|
67
|
+
message: "Organization context not found",
|
|
68
|
+
instance: c.req.path
|
|
69
|
+
});
|
|
70
|
+
if (!isOrgAdmin(tenantRole)) {
|
|
71
|
+
logger.warn({
|
|
72
|
+
userId,
|
|
73
|
+
tenantId,
|
|
74
|
+
tenantRole,
|
|
75
|
+
path: c.req.path
|
|
76
|
+
}, "User does not have admin role for workspace operation");
|
|
77
|
+
throw createApiError({
|
|
78
|
+
code: "forbidden",
|
|
79
|
+
message: "Only organization administrators can modify workspace settings",
|
|
80
|
+
instance: c.req.path,
|
|
81
|
+
extensions: {
|
|
82
|
+
requiredRole: "admin or owner",
|
|
83
|
+
currentRole: tenantRole
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
46
87
|
await next();
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const userId = c.get("userId");
|
|
50
|
-
if (!userId) throw createApiError({
|
|
51
|
-
code: "unauthorized",
|
|
52
|
-
message: "User context not found",
|
|
53
|
-
instance: c.req.path
|
|
54
88
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const teamId = c.req.param("teamId") || c.req.param("workspaceId");
|
|
60
|
-
if (teamId && !c.get("tenantRole")) await resolveWorkAppTenantContext(c, teamId, userId);
|
|
61
|
-
const tenantId = c.get("tenantId");
|
|
62
|
-
const tenantRole = c.get("tenantRole");
|
|
63
|
-
if (!tenantId) throw createApiError({
|
|
64
|
-
code: "unauthorized",
|
|
65
|
-
message: "Organization context not found",
|
|
66
|
-
instance: c.req.path
|
|
89
|
+
registerAuthzMeta(mw, {
|
|
90
|
+
resource: "organization",
|
|
91
|
+
permission: "admin",
|
|
92
|
+
description: "Requires Inkeep org admin/owner role to modify workspace settings"
|
|
67
93
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
userId,
|
|
71
|
-
tenantId,
|
|
72
|
-
tenantRole,
|
|
73
|
-
path: c.req.path
|
|
74
|
-
}, "User does not have admin role for workspace operation");
|
|
75
|
-
throw createApiError({
|
|
76
|
-
code: "forbidden",
|
|
77
|
-
message: "Only organization administrators can modify workspace settings",
|
|
78
|
-
instance: c.req.path,
|
|
79
|
-
extensions: {
|
|
80
|
-
requiredRole: "admin or owner",
|
|
81
|
-
currentRole: tenantRole
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
await next();
|
|
86
|
-
});
|
|
94
|
+
return mw;
|
|
95
|
+
};
|
|
87
96
|
/**
|
|
88
97
|
* Middleware that requires either:
|
|
89
98
|
* 1. Org admin/owner role (can modify any channel), OR
|
|
@@ -91,77 +100,81 @@ const requireWorkspaceAdmin = () => createMiddleware(async (c, next) => {
|
|
|
91
100
|
*
|
|
92
101
|
* Use for channel-level settings where members can configure their own channels.
|
|
93
102
|
*/
|
|
94
|
-
const requireChannelMemberOrAdmin = () =>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
103
|
+
const requireChannelMemberOrAdmin = () => {
|
|
104
|
+
const mw = createMiddleware(async (c, next) => {
|
|
105
|
+
if (process.env.ENVIRONMENT === "test" && c.req.header("x-test-bypass-auth") === "true") {
|
|
106
|
+
await next();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const userId = c.get("userId");
|
|
110
|
+
if (!userId) throw createApiError({
|
|
111
|
+
code: "unauthorized",
|
|
112
|
+
message: "User context not found",
|
|
113
|
+
instance: c.req.path
|
|
114
|
+
});
|
|
115
|
+
if (userId === "system" || userId.startsWith("apikey:")) {
|
|
116
|
+
await next();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const teamId = c.req.param("teamId");
|
|
120
|
+
if (teamId && !c.get("tenantRole")) await resolveWorkAppTenantContext(c, teamId, userId);
|
|
121
|
+
const tenantId = c.get("tenantId");
|
|
122
|
+
const tenantRole = c.get("tenantRole");
|
|
123
|
+
if (!tenantId) throw createApiError({
|
|
124
|
+
code: "unauthorized",
|
|
125
|
+
message: "Organization context not found",
|
|
126
|
+
instance: c.req.path
|
|
127
|
+
});
|
|
128
|
+
if (isOrgAdmin(tenantRole)) {
|
|
129
|
+
await next();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const channelId = c.req.param("channelId");
|
|
133
|
+
if (!teamId || !channelId) throw createApiError({
|
|
134
|
+
code: "bad_request",
|
|
135
|
+
message: "Team ID and Channel ID are required",
|
|
136
|
+
instance: c.req.path
|
|
137
|
+
});
|
|
138
|
+
const userMapping = (await findWorkAppSlackUserMappingByInkeepUserId(runDbClient_default)(userId)).find((m) => m.tenantId === tenantId && m.slackTeamId === teamId);
|
|
139
|
+
if (!userMapping) throw createApiError({
|
|
140
|
+
code: "forbidden",
|
|
141
|
+
message: "You must link your Slack account to modify channel settings. Use /inkeep link in Slack.",
|
|
142
|
+
instance: c.req.path
|
|
143
|
+
});
|
|
144
|
+
const workspace = await findWorkspaceConnectionByTeamId(teamId);
|
|
145
|
+
if (!workspace?.botToken) throw createApiError({
|
|
146
|
+
code: "not_found",
|
|
147
|
+
message: "Slack workspace not found or not properly configured",
|
|
148
|
+
instance: c.req.path
|
|
149
|
+
});
|
|
150
|
+
if (!await checkUserIsChannelMember(getSlackClient(workspace.botToken), channelId, userMapping.slackUserId)) {
|
|
151
|
+
logger.info({
|
|
152
|
+
userId,
|
|
153
|
+
slackUserId: userMapping.slackUserId,
|
|
154
|
+
channelId,
|
|
155
|
+
teamId
|
|
156
|
+
}, "User is not a member of the channel");
|
|
157
|
+
throw createApiError({
|
|
158
|
+
code: "forbidden",
|
|
159
|
+
message: "You can only configure channels you are a member of",
|
|
160
|
+
instance: c.req.path,
|
|
161
|
+
extensions: {
|
|
162
|
+
channelId,
|
|
163
|
+
reason: "not_channel_member"
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
logger.debug({
|
|
142
168
|
userId,
|
|
143
169
|
slackUserId: userMapping.slackUserId,
|
|
144
170
|
channelId,
|
|
145
171
|
teamId
|
|
146
|
-
}, "User
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
channelId,
|
|
153
|
-
reason: "not_channel_member"
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
logger.debug({
|
|
158
|
-
userId,
|
|
159
|
-
slackUserId: userMapping.slackUserId,
|
|
160
|
-
channelId,
|
|
161
|
-
teamId
|
|
162
|
-
}, "User verified as channel member");
|
|
163
|
-
await next();
|
|
164
|
-
});
|
|
172
|
+
}, "User verified as channel member");
|
|
173
|
+
await next();
|
|
174
|
+
});
|
|
175
|
+
registerAuthzMeta(mw, { description: "Requires Inkeep organization admin role, or Slack channel membership to modify channel settings" });
|
|
176
|
+
return mw;
|
|
177
|
+
};
|
|
165
178
|
|
|
166
179
|
//#endregion
|
|
167
180
|
export { isOrgAdmin, requireChannelMemberOrAdmin, requireWorkspaceAdmin };
|
|
@@ -3,17 +3,13 @@ import { getLogger } from "../../logger.js";
|
|
|
3
3
|
import runDbClient_default from "../../db/runDbClient.js";
|
|
4
4
|
import { findWorkspaceConnectionByTeamId, getSlackIntegrationId, getSlackNango, updateConnectionMetadata } from "../services/nango.js";
|
|
5
5
|
import { getSlackClient, getSlackUserInfo } from "../services/client.js";
|
|
6
|
-
import { sendResponseUrlMessage } from "../services/events/utils.js";
|
|
7
6
|
import { handleCommand } from "../services/commands/index.js";
|
|
8
7
|
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
8
|
import { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest } from "../services/security.js";
|
|
14
9
|
import "../services/index.js";
|
|
10
|
+
import { dispatchSlackEvent } from "../dispatcher.js";
|
|
15
11
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
16
|
-
import { deleteAllWorkAppSlackChannelAgentConfigsByTeam, deleteAllWorkAppSlackUserMappingsByTeam, deleteWorkAppSlackWorkspaceByNangoConnectionId,
|
|
12
|
+
import { deleteAllWorkAppSlackChannelAgentConfigsByTeam, deleteAllWorkAppSlackUserMappingsByTeam, deleteWorkAppSlackWorkspaceByNangoConnectionId, getWaitUntil } from "@inkeep/agents-core";
|
|
17
13
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
18
14
|
|
|
19
15
|
//#region src/slack/routes/events.ts
|
|
@@ -122,328 +118,14 @@ app.post("/events", async (c) => {
|
|
|
122
118
|
span.end();
|
|
123
119
|
return c.text(String(eventBody.challenge));
|
|
124
120
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
span.updateName(`${SLACK_SPAN_NAMES.WEBHOOK} event_callback.${innerEventType}`);
|
|
134
|
-
if (event?.bot_id || event?.subtype === "bot_message") {
|
|
135
|
-
outcome = "ignored_bot_message";
|
|
136
|
-
span.setAttribute(SLACK_SPAN_KEYS.IS_BOT_MESSAGE, true);
|
|
137
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
138
|
-
logger.info({
|
|
139
|
-
botId: event.bot_id,
|
|
140
|
-
subtype: event?.subtype,
|
|
141
|
-
teamId,
|
|
142
|
-
innerEventType
|
|
143
|
-
}, "Ignoring bot message");
|
|
144
|
-
span.end();
|
|
145
|
-
return c.json({ ok: true });
|
|
146
|
-
}
|
|
147
|
-
if (event?.type === "app_mention" && event.channel && event.user && teamId) {
|
|
148
|
-
outcome = "handled";
|
|
149
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
150
|
-
const question = (event.text || "").replace(/<@[A-Z0-9]+>/g, "").trim();
|
|
151
|
-
span.setAttribute(SLACK_SPAN_KEYS.HAS_QUERY, question.length > 0);
|
|
152
|
-
if (event.thread_ts) span.setAttribute(SLACK_SPAN_KEYS.THREAD_TS, event.thread_ts);
|
|
153
|
-
if (event.ts) span.setAttribute(SLACK_SPAN_KEYS.MESSAGE_TS, event.ts);
|
|
154
|
-
logger.info({
|
|
155
|
-
userId: event.user,
|
|
156
|
-
channel: event.channel,
|
|
157
|
-
teamId,
|
|
158
|
-
hasQuery: question.length > 0
|
|
159
|
-
}, "Handling event: app_mention");
|
|
160
|
-
const dispatchedAt = Date.now();
|
|
161
|
-
const mentionWork = handleAppMention({
|
|
162
|
-
slackUserId: event.user,
|
|
163
|
-
channel: event.channel,
|
|
164
|
-
text: question,
|
|
165
|
-
threadTs: event.thread_ts || event.ts || "",
|
|
166
|
-
messageTs: event.ts || "",
|
|
167
|
-
teamId,
|
|
168
|
-
dispatchedAt
|
|
169
|
-
}).catch((err) => {
|
|
170
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
171
|
-
const errorStack = err instanceof Error ? err.stack : void 0;
|
|
172
|
-
logger.error({
|
|
173
|
-
errorMessage,
|
|
174
|
-
errorStack
|
|
175
|
-
}, "Failed to handle app mention (outer catch)");
|
|
176
|
-
}).finally(() => flushTraces());
|
|
177
|
-
if (waitUntil) {
|
|
178
|
-
waitUntil(mentionWork);
|
|
179
|
-
logger.info({
|
|
180
|
-
teamId,
|
|
181
|
-
channel: event.channel,
|
|
182
|
-
dispatchedAt
|
|
183
|
-
}, "app_mention work registered with waitUntil");
|
|
184
|
-
} else logger.warn({
|
|
185
|
-
teamId,
|
|
186
|
-
channel: event.channel,
|
|
187
|
-
dispatchedAt
|
|
188
|
-
}, "waitUntil unavailable — app_mention background work is untracked fire-and-forget");
|
|
189
|
-
} else {
|
|
190
|
-
outcome = "ignored_unknown_event";
|
|
191
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
192
|
-
logger.info({
|
|
193
|
-
innerEventType,
|
|
194
|
-
teamId
|
|
195
|
-
}, `Ignoring unhandled event_callback: ${innerEventType}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if (eventType === "block_actions" || eventType === "interactive_message") {
|
|
199
|
-
const actions = eventBody.actions;
|
|
200
|
-
const teamId = eventBody.team?.id;
|
|
201
|
-
const responseUrl = eventBody.response_url;
|
|
202
|
-
const triggerId = eventBody.trigger_id;
|
|
203
|
-
const actionIds = actions?.map((a) => a.action_id) || [];
|
|
204
|
-
if (teamId) span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
|
|
205
|
-
span.setAttribute(SLACK_SPAN_KEYS.ACTION_IDS, actionIds.join(","));
|
|
206
|
-
span.updateName(`${SLACK_SPAN_NAMES.WEBHOOK} ${eventType} [${actionIds.join(",")}]`);
|
|
207
|
-
if (actions && teamId) {
|
|
208
|
-
let anyHandled = false;
|
|
209
|
-
for (const action of actions) {
|
|
210
|
-
if (action.action_id === "open_agent_selector_modal" && action.value && triggerId) {
|
|
211
|
-
anyHandled = true;
|
|
212
|
-
logger.info({
|
|
213
|
-
teamId,
|
|
214
|
-
actionId: action.action_id
|
|
215
|
-
}, "Handling block_action: open_agent_selector_modal");
|
|
216
|
-
const selectorWork = handleOpenAgentSelectorModal({
|
|
217
|
-
triggerId,
|
|
218
|
-
actionValue: action.value,
|
|
219
|
-
teamId,
|
|
220
|
-
responseUrl: responseUrl || ""
|
|
221
|
-
}).catch(async (err) => {
|
|
222
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
223
|
-
logger.error({
|
|
224
|
-
errorMessage,
|
|
225
|
-
actionId: action.action_id
|
|
226
|
-
}, "Failed to open agent selector modal");
|
|
227
|
-
if (responseUrl) await sendResponseUrlMessage(responseUrl, {
|
|
228
|
-
text: "Sorry, something went wrong while opening the agent selector. Please try again.",
|
|
229
|
-
response_type: "ephemeral"
|
|
230
|
-
}).catch((e) => logger.warn({ error: e }, "Failed to send error notification via response URL"));
|
|
231
|
-
}).finally(() => flushTraces());
|
|
232
|
-
if (waitUntil) waitUntil(selectorWork);
|
|
233
|
-
}
|
|
234
|
-
if (action.action_id === "modal_project_select") {
|
|
235
|
-
anyHandled = true;
|
|
236
|
-
const selectedProjectId = action.selected_option?.value;
|
|
237
|
-
const view = eventBody.view;
|
|
238
|
-
logger.info({
|
|
239
|
-
teamId,
|
|
240
|
-
actionId: action.action_id,
|
|
241
|
-
selectedProjectId
|
|
242
|
-
}, "Handling block_action: modal_project_select");
|
|
243
|
-
if (selectedProjectId && view?.id) {
|
|
244
|
-
const projectSelectWork = (async () => {
|
|
245
|
-
try {
|
|
246
|
-
const metadata = JSON.parse(view.private_metadata || "{}");
|
|
247
|
-
const tenantId = metadata.tenantId;
|
|
248
|
-
if (!tenantId) {
|
|
249
|
-
logger.warn({ teamId }, "No tenantId in modal metadata — skipping project update");
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
const workspace = await findWorkspaceConnectionByTeamId(teamId);
|
|
253
|
-
if (!workspace?.botToken) return;
|
|
254
|
-
const slackClient = getSlackClient(workspace.botToken);
|
|
255
|
-
const { fetchProjectsForTenant, fetchAgentsForProject } = await import("../services/events/utils.js");
|
|
256
|
-
const { buildAgentSelectorModal, buildMessageShortcutModal } = await import("../services/modals.js");
|
|
257
|
-
const projectList = await fetchProjectsForTenant(tenantId);
|
|
258
|
-
const agentList = await fetchAgentsForProject(tenantId, selectedProjectId);
|
|
259
|
-
const agentOptions = agentList.map((a) => ({
|
|
260
|
-
id: a.id,
|
|
261
|
-
name: a.name,
|
|
262
|
-
projectId: a.projectId,
|
|
263
|
-
projectName: a.projectName || a.projectId
|
|
264
|
-
}));
|
|
265
|
-
const modal = metadata.messageContext ? buildMessageShortcutModal({
|
|
266
|
-
projects: projectList,
|
|
267
|
-
agents: agentOptions,
|
|
268
|
-
metadata,
|
|
269
|
-
selectedProjectId,
|
|
270
|
-
messageContext: metadata.messageContext
|
|
271
|
-
}) : buildAgentSelectorModal({
|
|
272
|
-
projects: projectList,
|
|
273
|
-
agents: agentOptions,
|
|
274
|
-
metadata,
|
|
275
|
-
selectedProjectId
|
|
276
|
-
});
|
|
277
|
-
await slackClient.views.update({
|
|
278
|
-
view_id: view.id,
|
|
279
|
-
view: modal
|
|
280
|
-
});
|
|
281
|
-
logger.debug({
|
|
282
|
-
selectedProjectId,
|
|
283
|
-
agentCount: agentList.length
|
|
284
|
-
}, "Updated modal with agents for selected project");
|
|
285
|
-
} catch (err) {
|
|
286
|
-
logger.error({
|
|
287
|
-
err,
|
|
288
|
-
selectedProjectId
|
|
289
|
-
}, "Failed to update modal on project change");
|
|
290
|
-
} finally {
|
|
291
|
-
await flushTraces();
|
|
292
|
-
}
|
|
293
|
-
})();
|
|
294
|
-
if (waitUntil) waitUntil(projectSelectWork);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (action.action_id === "open_follow_up_modal" && action.value && triggerId) {
|
|
298
|
-
anyHandled = true;
|
|
299
|
-
logger.info({
|
|
300
|
-
teamId,
|
|
301
|
-
actionId: action.action_id
|
|
302
|
-
}, "Handling block_action: open_follow_up_modal");
|
|
303
|
-
const followUpModalWork = handleOpenFollowUpModal({
|
|
304
|
-
triggerId,
|
|
305
|
-
actionValue: action.value,
|
|
306
|
-
teamId,
|
|
307
|
-
responseUrl: responseUrl || void 0
|
|
308
|
-
}).catch((err) => {
|
|
309
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
310
|
-
logger.error({
|
|
311
|
-
errorMessage,
|
|
312
|
-
actionId: action.action_id
|
|
313
|
-
}, "Failed to open follow-up modal");
|
|
314
|
-
}).finally(() => flushTraces());
|
|
315
|
-
if (waitUntil) waitUntil(followUpModalWork);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
outcome = anyHandled ? "handled" : "ignored_no_action_match";
|
|
319
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
320
|
-
if (!anyHandled) logger.info({
|
|
321
|
-
teamId,
|
|
322
|
-
actionIds,
|
|
323
|
-
eventType
|
|
324
|
-
}, "Ignoring block_actions: no matching action handlers");
|
|
325
|
-
} else {
|
|
326
|
-
outcome = "ignored_no_action_match";
|
|
327
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
328
|
-
logger.info({
|
|
329
|
-
teamId,
|
|
330
|
-
eventType,
|
|
331
|
-
hasActions: Boolean(actions)
|
|
332
|
-
}, "Ignoring block_actions: missing actions or teamId");
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
if (eventType === "message_action") {
|
|
336
|
-
const callbackId = eventBody.callback_id;
|
|
337
|
-
span.setAttribute(SLACK_SPAN_KEYS.CALLBACK_ID, callbackId || "unknown");
|
|
338
|
-
span.updateName(`${SLACK_SPAN_NAMES.WEBHOOK} message_action.${callbackId || "unknown"}`);
|
|
339
|
-
if (callbackId === "ask_agent_shortcut") {
|
|
340
|
-
const triggerId = eventBody.trigger_id;
|
|
341
|
-
const teamId = eventBody.team?.id;
|
|
342
|
-
const channelId = eventBody.channel?.id;
|
|
343
|
-
const userId = eventBody.user?.id;
|
|
344
|
-
const message = eventBody.message;
|
|
345
|
-
const responseUrl = eventBody.response_url;
|
|
346
|
-
if (teamId) span.setAttribute(SLACK_SPAN_KEYS.TEAM_ID, teamId);
|
|
347
|
-
if (channelId) span.setAttribute(SLACK_SPAN_KEYS.CHANNEL_ID, channelId);
|
|
348
|
-
if (userId) span.setAttribute(SLACK_SPAN_KEYS.USER_ID, userId);
|
|
349
|
-
if (triggerId && teamId && channelId && userId && message?.ts) {
|
|
350
|
-
outcome = "handled";
|
|
351
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
352
|
-
logger.info({
|
|
353
|
-
teamId,
|
|
354
|
-
channelId,
|
|
355
|
-
userId,
|
|
356
|
-
callbackId
|
|
357
|
-
}, "Handling message_action: ask_agent_shortcut");
|
|
358
|
-
const shortcutWork = handleMessageShortcut({
|
|
359
|
-
triggerId,
|
|
360
|
-
teamId,
|
|
361
|
-
channelId,
|
|
362
|
-
userId,
|
|
363
|
-
messageTs: message.ts,
|
|
364
|
-
messageText: message.text || "",
|
|
365
|
-
threadTs: message.thread_ts,
|
|
366
|
-
responseUrl
|
|
367
|
-
}).catch((err) => {
|
|
368
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
369
|
-
logger.error({
|
|
370
|
-
errorMessage,
|
|
371
|
-
callbackId
|
|
372
|
-
}, "Failed to handle message shortcut");
|
|
373
|
-
}).finally(() => flushTraces());
|
|
374
|
-
if (waitUntil) waitUntil(shortcutWork);
|
|
375
|
-
} else {
|
|
376
|
-
outcome = "ignored_unknown_event";
|
|
377
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
378
|
-
logger.info({
|
|
379
|
-
teamId,
|
|
380
|
-
channelId,
|
|
381
|
-
userId,
|
|
382
|
-
callbackId,
|
|
383
|
-
hasTriggerId: Boolean(triggerId)
|
|
384
|
-
}, "Ignoring message_action: missing required fields");
|
|
385
|
-
}
|
|
386
|
-
} else {
|
|
387
|
-
outcome = "ignored_unknown_event";
|
|
388
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
389
|
-
logger.info({ callbackId }, `Ignoring unhandled message_action: ${callbackId}`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if (eventType === "view_submission") {
|
|
393
|
-
const callbackId = eventBody.view?.callback_id;
|
|
394
|
-
span.setAttribute(SLACK_SPAN_KEYS.CALLBACK_ID, callbackId || "unknown");
|
|
395
|
-
span.updateName(`${SLACK_SPAN_NAMES.WEBHOOK} view_submission.${callbackId || "unknown"}`);
|
|
396
|
-
if (callbackId === "agent_selector_modal") {
|
|
397
|
-
const view = eventBody.view;
|
|
398
|
-
const agentSelect = view.state?.values?.agent_select_block?.agent_select;
|
|
399
|
-
if (!agentSelect?.selected_option?.value || agentSelect.selected_option.value === "none") {
|
|
400
|
-
outcome = "validation_error";
|
|
401
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
402
|
-
logger.info({ callbackId }, "Rejecting view_submission: no agent selected");
|
|
403
|
-
span.end();
|
|
404
|
-
return c.json({
|
|
405
|
-
response_action: "errors",
|
|
406
|
-
errors: { agent_select_block: "Please select an agent. If none are available, add agents to this project in the dashboard." }
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
outcome = "handled";
|
|
410
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
411
|
-
logger.info({ callbackId }, "Handling view_submission: agent_selector_modal");
|
|
412
|
-
const modalWork = handleModalSubmission(view).catch((err) => {
|
|
413
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
414
|
-
logger.error({
|
|
415
|
-
errorMessage,
|
|
416
|
-
callbackId
|
|
417
|
-
}, "Failed to handle modal submission");
|
|
418
|
-
}).finally(() => flushTraces());
|
|
419
|
-
if (waitUntil) waitUntil(modalWork);
|
|
420
|
-
span.end();
|
|
421
|
-
return new Response(null, { status: 200 });
|
|
422
|
-
}
|
|
423
|
-
if (callbackId === "follow_up_modal") {
|
|
424
|
-
const view = eventBody.view;
|
|
425
|
-
outcome = "handled";
|
|
426
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
427
|
-
logger.info({ callbackId }, "Handling view_submission: follow_up_modal");
|
|
428
|
-
const followUpWork = handleFollowUpSubmission(view).catch((err) => {
|
|
429
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
430
|
-
logger.error({
|
|
431
|
-
errorMessage,
|
|
432
|
-
callbackId
|
|
433
|
-
}, "Failed to handle follow-up submission");
|
|
434
|
-
}).finally(() => flushTraces());
|
|
435
|
-
if (waitUntil) waitUntil(followUpWork);
|
|
436
|
-
span.end();
|
|
437
|
-
return new Response(null, { status: 200 });
|
|
438
|
-
}
|
|
439
|
-
outcome = "ignored_unknown_event";
|
|
440
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
441
|
-
logger.info({ callbackId }, `Ignoring unhandled view_submission: ${callbackId}`);
|
|
442
|
-
}
|
|
443
|
-
if (eventType !== "event_callback" && eventType !== "block_actions" && eventType !== "interactive_message" && eventType !== "message_action" && eventType !== "view_submission") {
|
|
444
|
-
outcome = "ignored_unknown_event";
|
|
445
|
-
span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
|
|
446
|
-
logger.info({ eventType }, `Ignoring unhandled Slack event type: ${eventType}`);
|
|
121
|
+
const registerBackgroundWork = (work) => {
|
|
122
|
+
if (waitUntil) waitUntil(work);
|
|
123
|
+
};
|
|
124
|
+
const result = await dispatchSlackEvent(eventType || "", eventBody, { registerBackgroundWork }, span);
|
|
125
|
+
outcome = result.outcome;
|
|
126
|
+
if (result.response) {
|
|
127
|
+
span.end();
|
|
128
|
+
return c.json(result.response);
|
|
447
129
|
}
|
|
448
130
|
span.end();
|
|
449
131
|
return c.json({ ok: true });
|
|
@@ -5,9 +5,10 @@ import { clearWorkspaceConnectionCache, computeWorkspaceConnectionId, deleteWork
|
|
|
5
5
|
import { getSlackClient, getSlackTeamInfo, getSlackUserInfo } from "../services/client.js";
|
|
6
6
|
import { getBotTokenForTeam, setBotTokenForTeam } from "../services/workspace-tokens.js";
|
|
7
7
|
import "../services/index.js";
|
|
8
|
-
import { OpenAPIHono,
|
|
8
|
+
import { OpenAPIHono, z } from "@hono/zod-openapi";
|
|
9
9
|
import { createWorkAppSlackWorkspace } from "@inkeep/agents-core";
|
|
10
10
|
import * as crypto$1 from "node:crypto";
|
|
11
|
+
import { createProtectedRoute, noAuth } from "@inkeep/agents-core/middleware";
|
|
11
12
|
|
|
12
13
|
//#region src/slack/routes/oauth.ts
|
|
13
14
|
/**
|
|
@@ -70,7 +71,7 @@ function parseOAuthState(stateStr) {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
const app = new OpenAPIHono();
|
|
73
|
-
app.openapi(
|
|
74
|
+
app.openapi(createProtectedRoute({
|
|
74
75
|
method: "get",
|
|
75
76
|
path: "/install",
|
|
76
77
|
summary: "Install Slack App",
|
|
@@ -81,6 +82,7 @@ app.openapi(createRoute({
|
|
|
81
82
|
"Slack",
|
|
82
83
|
"OAuth"
|
|
83
84
|
],
|
|
85
|
+
permission: noAuth(),
|
|
84
86
|
request: { query: z.object({ tenant_id: z.string().optional() }) },
|
|
85
87
|
responses: { 302: { description: "Redirect to Slack OAuth" } }
|
|
86
88
|
}), (c) => {
|
|
@@ -115,7 +117,7 @@ app.openapi(createRoute({
|
|
|
115
117
|
}, "Redirecting to Slack OAuth");
|
|
116
118
|
return c.redirect(slackAuthUrl.toString());
|
|
117
119
|
});
|
|
118
|
-
app.openapi(
|
|
120
|
+
app.openapi(createProtectedRoute({
|
|
119
121
|
method: "get",
|
|
120
122
|
path: "/oauth_redirect",
|
|
121
123
|
summary: "Slack OAuth Callback",
|
|
@@ -126,6 +128,7 @@ app.openapi(createRoute({
|
|
|
126
128
|
"Slack",
|
|
127
129
|
"OAuth"
|
|
128
130
|
],
|
|
131
|
+
permission: noAuth(),
|
|
129
132
|
request: { query: z.object({
|
|
130
133
|
code: z.string().optional(),
|
|
131
134
|
error: z.string().optional(),
|