@inkeep/agents-work-apps 0.47.5 → 0.48.1

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 (61) hide show
  1. package/dist/env.d.ts +24 -2
  2. package/dist/env.js +13 -2
  3. package/dist/github/mcp/index.d.ts +2 -2
  4. package/dist/github/mcp/index.js +23 -34
  5. package/dist/github/routes/setup.d.ts +2 -2
  6. package/dist/github/routes/webhooks.d.ts +2 -2
  7. package/dist/slack/i18n/index.d.ts +2 -0
  8. package/dist/slack/i18n/index.js +3 -0
  9. package/dist/slack/i18n/strings.d.ts +73 -0
  10. package/dist/slack/i18n/strings.js +67 -0
  11. package/dist/slack/index.d.ts +18 -0
  12. package/dist/slack/index.js +28 -0
  13. package/dist/slack/middleware/permissions.d.ts +31 -0
  14. package/dist/slack/middleware/permissions.js +167 -0
  15. package/dist/slack/routes/events.d.ts +10 -0
  16. package/dist/slack/routes/events.js +551 -0
  17. package/dist/slack/routes/index.d.ts +10 -0
  18. package/dist/slack/routes/index.js +47 -0
  19. package/dist/slack/routes/oauth.d.ts +20 -0
  20. package/dist/slack/routes/oauth.js +344 -0
  21. package/dist/slack/routes/users.d.ts +10 -0
  22. package/dist/slack/routes/users.js +365 -0
  23. package/dist/slack/routes/workspaces.d.ts +10 -0
  24. package/dist/slack/routes/workspaces.js +909 -0
  25. package/dist/slack/services/agent-resolution.d.ts +41 -0
  26. package/dist/slack/services/agent-resolution.js +99 -0
  27. package/dist/slack/services/blocks/index.d.ts +73 -0
  28. package/dist/slack/services/blocks/index.js +103 -0
  29. package/dist/slack/services/client.d.ts +108 -0
  30. package/dist/slack/services/client.js +232 -0
  31. package/dist/slack/services/commands/index.d.ts +19 -0
  32. package/dist/slack/services/commands/index.js +553 -0
  33. package/dist/slack/services/events/app-mention.d.ts +40 -0
  34. package/dist/slack/services/events/app-mention.js +304 -0
  35. package/dist/slack/services/events/block-actions.d.ts +40 -0
  36. package/dist/slack/services/events/block-actions.js +265 -0
  37. package/dist/slack/services/events/index.d.ts +6 -0
  38. package/dist/slack/services/events/index.js +7 -0
  39. package/dist/slack/services/events/modal-submission.d.ts +30 -0
  40. package/dist/slack/services/events/modal-submission.js +400 -0
  41. package/dist/slack/services/events/streaming.d.ts +26 -0
  42. package/dist/slack/services/events/streaming.js +272 -0
  43. package/dist/slack/services/events/utils.d.ts +146 -0
  44. package/dist/slack/services/events/utils.js +370 -0
  45. package/dist/slack/services/index.d.ts +16 -0
  46. package/dist/slack/services/index.js +16 -0
  47. package/dist/slack/services/modals.d.ts +86 -0
  48. package/dist/slack/services/modals.js +355 -0
  49. package/dist/slack/services/nango.d.ts +85 -0
  50. package/dist/slack/services/nango.js +476 -0
  51. package/dist/slack/services/security.d.ts +35 -0
  52. package/dist/slack/services/security.js +65 -0
  53. package/dist/slack/services/types.d.ts +26 -0
  54. package/dist/slack/services/types.js +1 -0
  55. package/dist/slack/services/workspace-tokens.d.ts +25 -0
  56. package/dist/slack/services/workspace-tokens.js +27 -0
  57. package/dist/slack/tracer.d.ts +40 -0
  58. package/dist/slack/tracer.js +39 -0
  59. package/dist/slack/types.d.ts +10 -0
  60. package/dist/slack/types.js +1 -0
  61. package/package.json +11 -2
@@ -0,0 +1,909 @@
1
+ import { getLogger } from "../../logger.js";
2
+ import runDbClient_default from "../../db/runDbClient.js";
3
+ import { clearWorkspaceConnectionCache, computeWorkspaceConnectionId, deleteWorkspaceInstallation, findWorkspaceConnectionByTeamId, getWorkspaceDefaultAgentFromNango, listWorkspaceInstallations, setWorkspaceDefaultAgent } from "../services/nango.js";
4
+ import { getSlackChannels, getSlackClient, revokeSlackToken } from "../services/client.js";
5
+ import "../services/index.js";
6
+ import { requireChannelMemberOrAdmin, requireWorkspaceAdmin } from "../middleware/permissions.js";
7
+ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
8
+ import { deleteAllWorkAppSlackChannelAgentConfigsByTeam, deleteAllWorkAppSlackUserMappingsByTeam, deleteWorkAppSlackChannelAgentConfig, deleteWorkAppSlackWorkspaceByNangoConnectionId, findWorkAppSlackChannelAgentConfig, listWorkAppSlackChannelAgentConfigsByTeam, listWorkAppSlackUserMappingsByTeam, upsertWorkAppSlackChannelAgentConfig } from "@inkeep/agents-core";
9
+
10
+ //#region src/slack/routes/workspaces.ts
11
+ /**
12
+ * Slack Workspace Routes
13
+ *
14
+ * RESTful endpoints for managing Slack workspaces and their configurations:
15
+ * - GET /workspaces - List all installed workspaces
16
+ * - GET /workspaces/:teamId - Get workspace details
17
+ * - PUT /workspaces/:teamId/settings - Update workspace settings (default agent) [ADMIN ONLY]
18
+ * - DELETE /workspaces/:teamId - Uninstall workspace [ADMIN ONLY]
19
+ * - GET /workspaces/:teamId/channels - List channels
20
+ * - GET /workspaces/:teamId/channels/:channelId/settings - Get channel config
21
+ * - PUT /workspaces/:teamId/channels/:channelId/settings - Set channel default agent [ADMIN or CHANNEL MEMBER]
22
+ * - DELETE /workspaces/:teamId/channels/:channelId/settings - Remove channel config [ADMIN or CHANNEL MEMBER]
23
+ * - GET /workspaces/:teamId/users - List linked users
24
+ *
25
+ * Permission Model:
26
+ * - Read operations (GET): Authenticated users only (tenant isolation via verifyTenantOwnership in handler)
27
+ * - Workspace settings (PUT): Inkeep org admin/owner only (requireWorkspaceAdmin middleware)
28
+ * - Channel settings (PUT/DELETE): Inkeep org admin/owner OR channel member (requireChannelMemberOrAdmin middleware)
29
+ * - Workspace uninstall (DELETE): Inkeep org admin/owner only (requireWorkspaceAdmin middleware)
30
+ */
31
+ const logger = getLogger("slack-workspaces");
32
+ /**
33
+ * Verify workspace belongs to the authenticated user's tenant.
34
+ * Returns true if access is allowed, false if denied.
35
+ */
36
+ function verifyTenantOwnership(c, workspaceTenantId) {
37
+ const sessionTenantId = c.get("tenantId");
38
+ if (!sessionTenantId) return false;
39
+ return sessionTenantId === workspaceTenantId;
40
+ }
41
+ const app = new OpenAPIHono();
42
+ app.use("/:teamId/settings", async (c, next) => {
43
+ if (c.req.method === "PUT") return requireWorkspaceAdmin()(c, next);
44
+ return next();
45
+ });
46
+ app.use("/:teamId", async (c, next) => {
47
+ if (c.req.method === "DELETE") return requireWorkspaceAdmin()(c, next);
48
+ return next();
49
+ });
50
+ app.use("/:teamId/channels/:channelId/settings", async (c, next) => {
51
+ if (c.req.method === "PUT" || c.req.method === "DELETE") return requireChannelMemberOrAdmin()(c, next);
52
+ return next();
53
+ });
54
+ app.use("/:teamId/test-message", async (c, next) => {
55
+ if (c.req.method === "POST") return requireWorkspaceAdmin()(c, next);
56
+ return next();
57
+ });
58
+ const ChannelAgentConfigSchema = z.object({
59
+ projectId: z.string(),
60
+ agentId: z.string(),
61
+ agentName: z.string().optional(),
62
+ projectName: z.string().optional()
63
+ });
64
+ const WorkspaceSettingsSchema = z.object({ defaultAgent: ChannelAgentConfigSchema.optional() });
65
+ app.openapi(createRoute({
66
+ method: "get",
67
+ path: "/",
68
+ summary: "List Workspaces",
69
+ description: "List all installed Slack workspaces for the tenant",
70
+ operationId: "slack-list-workspaces",
71
+ tags: [
72
+ "Work Apps",
73
+ "Slack",
74
+ "Workspaces"
75
+ ],
76
+ responses: { 200: {
77
+ description: "List of workspaces",
78
+ content: { "application/json": { schema: z.object({ workspaces: z.array(z.object({
79
+ connectionId: z.string(),
80
+ teamId: z.string(),
81
+ teamName: z.string().optional(),
82
+ tenantId: z.string(),
83
+ hasDefaultAgent: z.boolean(),
84
+ defaultAgentName: z.string().optional()
85
+ })) }) } }
86
+ } }
87
+ }), async (c) => {
88
+ try {
89
+ const allWorkspaces = await listWorkspaceInstallations();
90
+ const sessionTenantId = c.get("tenantId");
91
+ if (!sessionTenantId) {
92
+ logger.warn({}, "No tenantId in session context — cannot list workspaces");
93
+ return c.json({ workspaces: [] });
94
+ }
95
+ const workspaces = allWorkspaces.filter((w) => w.tenantId === sessionTenantId);
96
+ logger.info({
97
+ count: workspaces.length,
98
+ totalCount: allWorkspaces.length,
99
+ tenantId: sessionTenantId
100
+ }, "Listed workspace installations");
101
+ return c.json({ workspaces: workspaces.map((w) => ({
102
+ connectionId: w.connectionId,
103
+ teamId: w.teamId,
104
+ teamName: w.teamName,
105
+ tenantId: w.tenantId,
106
+ hasDefaultAgent: !!w.defaultAgent,
107
+ defaultAgentName: w.defaultAgent?.agentName
108
+ })) });
109
+ } catch (error) {
110
+ logger.error({ error }, "Failed to list workspaces");
111
+ return c.json({ workspaces: [] });
112
+ }
113
+ });
114
+ app.openapi(createRoute({
115
+ method: "get",
116
+ path: "/:teamId",
117
+ summary: "Get Workspace",
118
+ description: "Get details of a specific Slack workspace",
119
+ operationId: "slack-get-workspace",
120
+ tags: [
121
+ "Work Apps",
122
+ "Slack",
123
+ "Workspaces"
124
+ ],
125
+ request: { params: z.object({ teamId: z.string() }) },
126
+ responses: {
127
+ 200: {
128
+ description: "Workspace details",
129
+ content: { "application/json": { schema: z.object({
130
+ teamId: z.string(),
131
+ teamName: z.string().optional(),
132
+ tenantId: z.string(),
133
+ connectionId: z.string(),
134
+ defaultAgent: ChannelAgentConfigSchema.optional()
135
+ }) } }
136
+ },
137
+ 404: { description: "Workspace not found" }
138
+ }
139
+ }), async (c) => {
140
+ const { teamId } = c.req.valid("param");
141
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
142
+ if (!workspace || !verifyTenantOwnership(c, workspace.tenantId)) return c.json({ error: "Workspace not found" }, 404);
143
+ let defaultAgent;
144
+ const nangoDefault = await getWorkspaceDefaultAgentFromNango(teamId);
145
+ if (nangoDefault) defaultAgent = {
146
+ projectId: nangoDefault.projectId,
147
+ agentId: nangoDefault.agentId,
148
+ agentName: nangoDefault.agentName
149
+ };
150
+ return c.json({
151
+ teamId: workspace.teamId,
152
+ teamName: workspace.teamName,
153
+ tenantId: workspace.tenantId,
154
+ connectionId: workspace.connectionId,
155
+ defaultAgent
156
+ });
157
+ });
158
+ app.openapi(createRoute({
159
+ method: "get",
160
+ path: "/:teamId/settings",
161
+ summary: "Get Workspace Settings",
162
+ description: "Get settings for a Slack workspace including default agent",
163
+ operationId: "slack-get-workspace-settings",
164
+ tags: [
165
+ "Work Apps",
166
+ "Slack",
167
+ "Workspaces"
168
+ ],
169
+ request: { params: z.object({ teamId: z.string() }) },
170
+ responses: { 200: {
171
+ description: "Workspace settings",
172
+ content: { "application/json": { schema: WorkspaceSettingsSchema } }
173
+ } }
174
+ }), async (c) => {
175
+ const { teamId } = c.req.valid("param");
176
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
177
+ if (!workspace || !verifyTenantOwnership(c, workspace.tenantId)) {
178
+ logger.warn({ teamId }, "Workspace not found or tenant mismatch");
179
+ return c.json({ defaultAgent: void 0 });
180
+ }
181
+ let defaultAgent;
182
+ const nangoDefault = await getWorkspaceDefaultAgentFromNango(teamId);
183
+ if (nangoDefault) defaultAgent = {
184
+ projectId: nangoDefault.projectId,
185
+ agentId: nangoDefault.agentId,
186
+ agentName: nangoDefault.agentName,
187
+ projectName: nangoDefault.projectName
188
+ };
189
+ return c.json({ defaultAgent });
190
+ });
191
+ app.openapi(createRoute({
192
+ method: "put",
193
+ path: "/:teamId/settings",
194
+ summary: "Update Workspace Settings",
195
+ description: "Update workspace settings including default agent",
196
+ operationId: "slack-update-workspace-settings",
197
+ tags: [
198
+ "Work Apps",
199
+ "Slack",
200
+ "Workspaces"
201
+ ],
202
+ request: {
203
+ params: z.object({ teamId: z.string() }),
204
+ body: { content: { "application/json": { schema: WorkspaceSettingsSchema } } }
205
+ },
206
+ responses: {
207
+ 200: {
208
+ description: "Settings updated",
209
+ content: { "application/json": { schema: z.object({ success: z.boolean() }) } }
210
+ },
211
+ 500: {
212
+ description: "Failed to update settings",
213
+ content: { "application/json": { schema: z.object({ success: z.boolean() }) } }
214
+ }
215
+ }
216
+ }), async (c) => {
217
+ const { teamId } = c.req.valid("param");
218
+ const body = c.req.valid("json");
219
+ if (body.defaultAgent) {
220
+ if (!await setWorkspaceDefaultAgent(teamId, body.defaultAgent)) {
221
+ logger.warn({ teamId }, "Failed to persist workspace settings to Nango");
222
+ return c.json({ success: false }, 500);
223
+ }
224
+ logger.info({
225
+ teamId,
226
+ agentId: body.defaultAgent.agentId,
227
+ agentName: body.defaultAgent.agentName
228
+ }, "Saved workspace default agent to Nango");
229
+ } else {
230
+ await setWorkspaceDefaultAgent(teamId, null);
231
+ logger.info({ teamId }, "Cleared workspace default agent");
232
+ }
233
+ return c.json({ success: true });
234
+ });
235
+ app.openapi(createRoute({
236
+ method: "delete",
237
+ path: "/:teamId",
238
+ summary: "Uninstall Workspace",
239
+ description: "Uninstall Slack app from workspace. Accepts either teamId or connectionId.",
240
+ operationId: "slack-delete-workspace",
241
+ tags: [
242
+ "Work Apps",
243
+ "Slack",
244
+ "Workspaces"
245
+ ],
246
+ request: { params: z.object({ teamId: z.string() }) },
247
+ responses: {
248
+ 200: {
249
+ description: "Workspace uninstalled",
250
+ content: { "application/json": { schema: z.object({ success: z.boolean() }) } }
251
+ },
252
+ 400: { description: "Invalid connectionId format" },
253
+ 404: { description: "Workspace not found" },
254
+ 500: { description: "Failed to uninstall workspace" }
255
+ }
256
+ }), async (c) => {
257
+ const { teamId: workspaceIdentifier } = c.req.valid("param");
258
+ let teamId;
259
+ let connectionId;
260
+ try {
261
+ if (workspaceIdentifier.includes(":")) {
262
+ connectionId = workspaceIdentifier;
263
+ const teamMatch = workspaceIdentifier.match(/T:([A-Z0-9]+)/);
264
+ if (!teamMatch) return c.json({ error: "Invalid connectionId format" }, 400);
265
+ teamId = teamMatch[1];
266
+ } else {
267
+ teamId = workspaceIdentifier;
268
+ connectionId = computeWorkspaceConnectionId({
269
+ teamId,
270
+ enterpriseId: void 0
271
+ });
272
+ }
273
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
274
+ if (!workspace) return c.json({ error: "Workspace not found" }, 404);
275
+ if (workspace.botToken) if (await revokeSlackToken(workspace.botToken)) logger.info({ teamId }, "Revoked Slack bot token");
276
+ else logger.warn({ teamId }, "Failed to revoke Slack bot token, continuing with uninstall");
277
+ const tenantId = workspace.tenantId;
278
+ const deletedChannelConfigs = await deleteAllWorkAppSlackChannelAgentConfigsByTeam(runDbClient_default)(tenantId, teamId);
279
+ if (deletedChannelConfigs > 0) logger.info({
280
+ teamId,
281
+ deletedChannelConfigs
282
+ }, "Deleted channel configs for uninstalled workspace");
283
+ const deletedMappings = await deleteAllWorkAppSlackUserMappingsByTeam(runDbClient_default)(tenantId, teamId);
284
+ if (deletedMappings > 0) logger.info({
285
+ teamId,
286
+ deletedMappings
287
+ }, "Deleted user mappings for uninstalled workspace");
288
+ if (await deleteWorkAppSlackWorkspaceByNangoConnectionId(runDbClient_default)(connectionId)) logger.info({ connectionId }, "Deleted workspace from database");
289
+ if (!await deleteWorkspaceInstallation(connectionId)) logger.error({ connectionId }, "deleteWorkspaceInstallation returned false (DB already cleaned up)");
290
+ clearWorkspaceConnectionCache(teamId);
291
+ logger.info({
292
+ connectionId,
293
+ teamId
294
+ }, "Deleted workspace installation and cleared cache");
295
+ return c.json({ success: true });
296
+ } catch (error) {
297
+ logger.error({
298
+ error,
299
+ workspaceIdentifier
300
+ }, "Failed to uninstall workspace");
301
+ return c.json({ error: "Failed to uninstall workspace" }, 500);
302
+ }
303
+ });
304
+ app.openapi(createRoute({
305
+ method: "get",
306
+ path: "/:teamId/channels",
307
+ summary: "List Channels",
308
+ description: "List Slack channels in the workspace that the bot can see",
309
+ operationId: "slack-list-channels",
310
+ tags: [
311
+ "Work Apps",
312
+ "Slack",
313
+ "Channels"
314
+ ],
315
+ request: {
316
+ params: z.object({ teamId: z.string() }),
317
+ query: z.object({
318
+ limit: z.coerce.number().optional().default(100),
319
+ cursor: z.string().optional(),
320
+ types: z.string().optional()
321
+ })
322
+ },
323
+ responses: {
324
+ 200: {
325
+ description: "List of channels",
326
+ content: { "application/json": { schema: z.object({
327
+ channels: z.array(z.object({
328
+ id: z.string(),
329
+ name: z.string(),
330
+ isPrivate: z.boolean(),
331
+ isShared: z.boolean(),
332
+ memberCount: z.number().optional(),
333
+ hasAgentConfig: z.boolean(),
334
+ agentConfig: ChannelAgentConfigSchema.optional()
335
+ })),
336
+ nextCursor: z.string().optional()
337
+ }) } }
338
+ },
339
+ 404: { description: "Workspace not found" }
340
+ }
341
+ }), async (c) => {
342
+ const { teamId } = c.req.valid("param");
343
+ const { limit } = c.req.valid("query");
344
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
345
+ if (!workspace?.botToken || !verifyTenantOwnership(c, workspace.tenantId)) return c.json({ error: "Workspace not found or no bot token" }, 404);
346
+ const tenantId = workspace.tenantId;
347
+ const slackClient = getSlackClient(workspace.botToken);
348
+ try {
349
+ const channels = await getSlackChannels(slackClient, limit);
350
+ let channelConfigs = [];
351
+ try {
352
+ channelConfigs = await listWorkAppSlackChannelAgentConfigsByTeam(runDbClient_default)(tenantId, teamId);
353
+ } catch (configError) {
354
+ logger.warn({
355
+ error: configError,
356
+ teamId
357
+ }, "Failed to fetch channel configs, table may not exist yet");
358
+ }
359
+ const configMap = new Map(channelConfigs.map((c$1) => [c$1.slackChannelId, c$1]));
360
+ const channelsWithConfig = channels.map((channel) => {
361
+ const config = channel.id ? configMap.get(channel.id) : void 0;
362
+ return {
363
+ id: channel.id || "",
364
+ name: channel.name || "",
365
+ isPrivate: channel.isPrivate ?? false,
366
+ isShared: channel.isShared ?? false,
367
+ memberCount: channel.memberCount,
368
+ hasAgentConfig: !!config,
369
+ agentConfig: config ? {
370
+ projectId: config.projectId,
371
+ agentId: config.agentId,
372
+ agentName: config.agentName || void 0
373
+ } : void 0
374
+ };
375
+ });
376
+ return c.json({
377
+ channels: channelsWithConfig,
378
+ nextCursor: void 0
379
+ });
380
+ } catch (error) {
381
+ logger.error({
382
+ error,
383
+ teamId
384
+ }, "Failed to list channels");
385
+ return c.json({ error: "Failed to list channels" }, 500);
386
+ }
387
+ });
388
+ app.openapi(createRoute({
389
+ method: "get",
390
+ path: "/:teamId/channels/:channelId/settings",
391
+ summary: "Get Channel Settings",
392
+ description: "Get default agent configuration for a specific channel",
393
+ operationId: "slack-get-channel-settings",
394
+ tags: [
395
+ "Work Apps",
396
+ "Slack",
397
+ "Channels"
398
+ ],
399
+ request: { params: z.object({
400
+ teamId: z.string(),
401
+ channelId: z.string()
402
+ }) },
403
+ responses: { 200: {
404
+ description: "Channel settings",
405
+ content: { "application/json": { schema: z.object({
406
+ channelId: z.string(),
407
+ agentConfig: ChannelAgentConfigSchema.optional()
408
+ }) } }
409
+ } }
410
+ }), async (c) => {
411
+ const { teamId, channelId } = c.req.valid("param");
412
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
413
+ if (!workspace || !verifyTenantOwnership(c, workspace.tenantId)) {
414
+ logger.warn({ teamId }, "Workspace not found or tenant mismatch");
415
+ return c.json({
416
+ channelId,
417
+ agentConfig: void 0
418
+ });
419
+ }
420
+ const tenantId = workspace.tenantId;
421
+ const config = await findWorkAppSlackChannelAgentConfig(runDbClient_default)(tenantId, teamId, channelId);
422
+ return c.json({
423
+ channelId,
424
+ agentConfig: config ? {
425
+ projectId: config.projectId,
426
+ agentId: config.agentId,
427
+ agentName: config.agentName || void 0
428
+ } : void 0
429
+ });
430
+ });
431
+ app.openapi(createRoute({
432
+ method: "put",
433
+ path: "/:teamId/channels/:channelId/settings",
434
+ summary: "Set Channel Default Agent",
435
+ description: "Set or update the default agent for a specific channel",
436
+ operationId: "slack-set-channel-settings",
437
+ tags: [
438
+ "Work Apps",
439
+ "Slack",
440
+ "Channels"
441
+ ],
442
+ request: {
443
+ params: z.object({
444
+ teamId: z.string(),
445
+ channelId: z.string()
446
+ }),
447
+ body: { content: { "application/json": { schema: z.object({
448
+ agentConfig: ChannelAgentConfigSchema,
449
+ channelName: z.string().optional(),
450
+ channelType: z.string().optional()
451
+ }) } } }
452
+ },
453
+ responses: { 200: {
454
+ description: "Channel settings updated",
455
+ content: { "application/json": { schema: z.object({
456
+ success: z.boolean(),
457
+ configId: z.string()
458
+ }) } }
459
+ } }
460
+ }), async (c) => {
461
+ const { teamId, channelId } = c.req.valid("param");
462
+ const body = c.req.valid("json");
463
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
464
+ if (!workspace || !verifyTenantOwnership(c, workspace.tenantId)) {
465
+ logger.warn({ teamId }, "Workspace not found or tenant mismatch");
466
+ return c.json({
467
+ success: false,
468
+ configId: ""
469
+ });
470
+ }
471
+ const tenantId = workspace.tenantId;
472
+ const config = await upsertWorkAppSlackChannelAgentConfig(runDbClient_default)({
473
+ tenantId,
474
+ slackTeamId: teamId,
475
+ slackChannelId: channelId,
476
+ slackChannelName: body.channelName,
477
+ slackChannelType: body.channelType,
478
+ projectId: body.agentConfig.projectId,
479
+ agentId: body.agentConfig.agentId,
480
+ agentName: body.agentConfig.agentName,
481
+ enabled: true
482
+ });
483
+ logger.info({
484
+ teamId,
485
+ channelId,
486
+ agentId: body.agentConfig.agentId
487
+ }, "Set channel default agent");
488
+ return c.json({
489
+ success: true,
490
+ configId: config.id
491
+ });
492
+ });
493
+ app.use("/:teamId/channels/bulk", async (c, next) => {
494
+ if (c.req.method === "PUT" || c.req.method === "DELETE") return requireWorkspaceAdmin()(c, next);
495
+ return next();
496
+ });
497
+ app.openapi(createRoute({
498
+ method: "put",
499
+ path: "/:teamId/channels/bulk",
500
+ summary: "Bulk Set Channel Agents",
501
+ description: "Apply the same agent configuration to multiple channels at once",
502
+ operationId: "slack-bulk-set-channel-agents",
503
+ tags: [
504
+ "Work Apps",
505
+ "Slack",
506
+ "Channels"
507
+ ],
508
+ request: {
509
+ params: z.object({ teamId: z.string() }),
510
+ body: { content: { "application/json": { schema: z.object({
511
+ channelIds: z.array(z.string()).min(1),
512
+ agentConfig: ChannelAgentConfigSchema
513
+ }) } } }
514
+ },
515
+ responses: {
516
+ 200: {
517
+ description: "Channels updated",
518
+ content: { "application/json": { schema: z.object({
519
+ success: z.boolean(),
520
+ updated: z.number(),
521
+ failed: z.number(),
522
+ errors: z.array(z.object({
523
+ channelId: z.string(),
524
+ error: z.string()
525
+ })).optional()
526
+ }) } }
527
+ },
528
+ 400: { description: "Invalid request" },
529
+ 404: { description: "Workspace not found" }
530
+ }
531
+ }), async (c) => {
532
+ const { teamId } = c.req.valid("param");
533
+ const body = c.req.valid("json");
534
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
535
+ if (!workspace?.botToken || !verifyTenantOwnership(c, workspace.tenantId)) return c.json({ error: "Workspace not found or no bot token" }, 404);
536
+ const tenantId = workspace.tenantId;
537
+ const slackClient = getSlackClient(workspace.botToken);
538
+ let channels = [];
539
+ try {
540
+ channels = await getSlackChannels(slackClient, 500);
541
+ } catch (error) {
542
+ logger.error({
543
+ error,
544
+ teamId
545
+ }, "Failed to fetch channels for bulk operation");
546
+ return c.json({ error: "Failed to fetch channels" }, 500);
547
+ }
548
+ const channelMap = new Map(channels.map((ch) => [ch.id, ch]));
549
+ let updated = 0;
550
+ const errors = [];
551
+ await Promise.all(body.channelIds.map(async (channelId) => {
552
+ try {
553
+ const channel = channelMap.get(channelId);
554
+ if (!channel) {
555
+ errors.push({
556
+ channelId,
557
+ error: "Channel not found"
558
+ });
559
+ return;
560
+ }
561
+ await upsertWorkAppSlackChannelAgentConfig(runDbClient_default)({
562
+ tenantId,
563
+ slackTeamId: teamId,
564
+ slackChannelId: channelId,
565
+ slackChannelName: channel.name || channelId,
566
+ slackChannelType: "public",
567
+ projectId: body.agentConfig.projectId,
568
+ agentId: body.agentConfig.agentId,
569
+ agentName: body.agentConfig.agentName,
570
+ enabled: true
571
+ });
572
+ updated++;
573
+ } catch (error) {
574
+ const errorMessage = error instanceof Error ? error.message : String(error);
575
+ errors.push({
576
+ channelId,
577
+ error: errorMessage
578
+ });
579
+ }
580
+ }));
581
+ logger.info({
582
+ teamId,
583
+ agentId: body.agentConfig.agentId,
584
+ updated,
585
+ failed: errors.length
586
+ }, "Bulk set channel agents");
587
+ return c.json({
588
+ success: errors.length === 0,
589
+ updated,
590
+ failed: errors.length,
591
+ errors: errors.length > 0 ? errors : void 0
592
+ });
593
+ });
594
+ app.openapi(createRoute({
595
+ method: "delete",
596
+ path: "/:teamId/channels/bulk",
597
+ summary: "Bulk Remove Channel Configs",
598
+ description: "Remove agent configuration from multiple channels at once",
599
+ operationId: "slack-bulk-delete-channel-agents",
600
+ tags: [
601
+ "Work Apps",
602
+ "Slack",
603
+ "Channels"
604
+ ],
605
+ request: {
606
+ params: z.object({ teamId: z.string() }),
607
+ body: { content: { "application/json": { schema: z.object({ channelIds: z.array(z.string()).min(1) }) } } }
608
+ },
609
+ responses: { 200: {
610
+ description: "Configs removed",
611
+ content: { "application/json": { schema: z.object({
612
+ success: z.boolean(),
613
+ removed: z.number()
614
+ }) } }
615
+ } }
616
+ }), async (c) => {
617
+ const { teamId } = c.req.valid("param");
618
+ const body = c.req.valid("json");
619
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
620
+ if (!workspace || !verifyTenantOwnership(c, workspace.tenantId)) {
621
+ logger.warn({ teamId }, "Workspace not found or tenant mismatch");
622
+ return c.json({
623
+ success: false,
624
+ removed: 0
625
+ });
626
+ }
627
+ const tenantId = workspace.tenantId;
628
+ let removed = 0;
629
+ await Promise.all(body.channelIds.map(async (channelId) => {
630
+ if (await deleteWorkAppSlackChannelAgentConfig(runDbClient_default)(tenantId, teamId, channelId)) removed++;
631
+ }));
632
+ logger.info({
633
+ teamId,
634
+ removed
635
+ }, "Bulk removed channel agent configs");
636
+ return c.json({
637
+ success: true,
638
+ removed
639
+ });
640
+ });
641
+ app.openapi(createRoute({
642
+ method: "delete",
643
+ path: "/:teamId/channels/:channelId/settings",
644
+ summary: "Remove Channel Config",
645
+ description: "Remove the default agent configuration for a channel",
646
+ operationId: "slack-delete-channel-settings",
647
+ tags: [
648
+ "Work Apps",
649
+ "Slack",
650
+ "Channels"
651
+ ],
652
+ request: { params: z.object({
653
+ teamId: z.string(),
654
+ channelId: z.string()
655
+ }) },
656
+ responses: { 200: {
657
+ description: "Channel config removed",
658
+ content: { "application/json": { schema: z.object({ success: z.boolean() }) } }
659
+ } }
660
+ }), async (c) => {
661
+ const { teamId, channelId } = c.req.valid("param");
662
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
663
+ if (!workspace || !verifyTenantOwnership(c, workspace.tenantId)) {
664
+ logger.warn({ teamId }, "Workspace not found or tenant mismatch");
665
+ return c.json({ success: false });
666
+ }
667
+ const tenantId = workspace.tenantId;
668
+ const deleted = await deleteWorkAppSlackChannelAgentConfig(runDbClient_default)(tenantId, teamId, channelId);
669
+ logger.info({
670
+ teamId,
671
+ channelId,
672
+ deleted
673
+ }, "Removed channel agent config");
674
+ return c.json({ success: deleted });
675
+ });
676
+ app.openapi(createRoute({
677
+ method: "get",
678
+ path: "/:teamId/users",
679
+ summary: "List Linked Users",
680
+ description: "List all users linked to Inkeep in this workspace",
681
+ operationId: "slack-list-linked-users",
682
+ tags: [
683
+ "Work Apps",
684
+ "Slack",
685
+ "Users"
686
+ ],
687
+ request: { params: z.object({ teamId: z.string() }) },
688
+ responses: { 200: {
689
+ description: "List of linked users",
690
+ content: { "application/json": { schema: z.object({ linkedUsers: z.array(z.object({
691
+ id: z.string(),
692
+ slackUserId: z.string(),
693
+ slackTeamId: z.string(),
694
+ slackUsername: z.string().optional(),
695
+ slackEmail: z.string().optional(),
696
+ userId: z.string(),
697
+ linkedAt: z.string(),
698
+ lastUsedAt: z.string().optional()
699
+ })) }) } }
700
+ } }
701
+ }), async (c) => {
702
+ const { teamId } = c.req.valid("param");
703
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
704
+ if (!workspace || !verifyTenantOwnership(c, workspace.tenantId)) {
705
+ logger.warn({ teamId }, "Workspace not found or tenant mismatch");
706
+ return c.json({ linkedUsers: [] });
707
+ }
708
+ const tenantId = workspace.tenantId;
709
+ const linkedUsers = await listWorkAppSlackUserMappingsByTeam(runDbClient_default)(tenantId, teamId);
710
+ logger.info({
711
+ teamId,
712
+ tenantId,
713
+ count: linkedUsers.length
714
+ }, "Fetched linked users");
715
+ return c.json({ linkedUsers: linkedUsers.map((link) => ({
716
+ id: link.id,
717
+ slackUserId: link.slackUserId,
718
+ slackTeamId: link.slackTeamId,
719
+ slackUsername: link.slackUsername || void 0,
720
+ slackEmail: link.slackEmail || void 0,
721
+ userId: link.inkeepUserId,
722
+ linkedAt: link.linkedAt,
723
+ lastUsedAt: link.lastUsedAt || void 0
724
+ })) });
725
+ });
726
+ app.openapi(createRoute({
727
+ method: "get",
728
+ path: "/:teamId/health",
729
+ summary: "Check Workspace Health",
730
+ description: "Verify the bot token is valid and check permissions. Returns bot info and permission status.",
731
+ operationId: "slack-workspace-health",
732
+ tags: [
733
+ "Work Apps",
734
+ "Slack",
735
+ "Workspaces"
736
+ ],
737
+ request: { params: z.object({ teamId: z.string() }) },
738
+ responses: {
739
+ 200: {
740
+ description: "Health check result",
741
+ content: { "application/json": { schema: z.object({
742
+ healthy: z.boolean(),
743
+ botId: z.string().optional(),
744
+ botName: z.string().optional(),
745
+ teamId: z.string().optional(),
746
+ teamName: z.string().optional(),
747
+ permissions: z.object({
748
+ canPostMessages: z.boolean(),
749
+ canReadChannels: z.boolean(),
750
+ canReadHistory: z.boolean()
751
+ }),
752
+ error: z.string().optional()
753
+ }) } }
754
+ },
755
+ 404: { description: "Workspace not found" }
756
+ }
757
+ }), async (c) => {
758
+ const { teamId } = c.req.valid("param");
759
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
760
+ if (!workspace?.botToken || !verifyTenantOwnership(c, workspace.tenantId)) return c.json({
761
+ healthy: false,
762
+ permissions: {
763
+ canPostMessages: false,
764
+ canReadChannels: false,
765
+ canReadHistory: false
766
+ },
767
+ error: "Workspace not found or no bot token available"
768
+ });
769
+ try {
770
+ const slackClient = getSlackClient(workspace.botToken);
771
+ const authResult = await slackClient.auth.test();
772
+ if (!authResult.ok) return c.json({
773
+ healthy: false,
774
+ permissions: {
775
+ canPostMessages: false,
776
+ canReadChannels: false,
777
+ canReadHistory: false
778
+ },
779
+ error: "Bot token is invalid or revoked"
780
+ });
781
+ const permissions = {
782
+ canPostMessages: true,
783
+ canReadChannels: true,
784
+ canReadHistory: true
785
+ };
786
+ try {
787
+ await slackClient.conversations.list({ limit: 1 });
788
+ } catch (e) {
789
+ permissions.canReadChannels = false;
790
+ logger.debug({ error: e }, "Channel read permission check failed");
791
+ }
792
+ logger.info({
793
+ teamId,
794
+ botId: authResult.user_id,
795
+ permissions
796
+ }, "Workspace health check completed");
797
+ return c.json({
798
+ healthy: true,
799
+ botId: authResult.user_id,
800
+ botName: authResult.user,
801
+ teamId: authResult.team_id,
802
+ teamName: authResult.team,
803
+ permissions
804
+ });
805
+ } catch (error) {
806
+ const errorMessage = error instanceof Error ? error.message : String(error);
807
+ logger.error({
808
+ error: errorMessage,
809
+ teamId
810
+ }, "Health check failed");
811
+ return c.json({
812
+ healthy: false,
813
+ permissions: {
814
+ canPostMessages: false,
815
+ canReadChannels: false,
816
+ canReadHistory: false
817
+ },
818
+ error: errorMessage
819
+ });
820
+ }
821
+ });
822
+ app.openapi(createRoute({
823
+ method: "post",
824
+ path: "/:teamId/test-message",
825
+ summary: "Send Test Message",
826
+ description: "Send a test message to verify the bot is working correctly.",
827
+ operationId: "slack-test-message",
828
+ tags: [
829
+ "Work Apps",
830
+ "Slack",
831
+ "Workspaces"
832
+ ],
833
+ request: {
834
+ params: z.object({ teamId: z.string() }),
835
+ body: { content: { "application/json": { schema: z.object({
836
+ channelId: z.string(),
837
+ message: z.string().optional()
838
+ }) } } }
839
+ },
840
+ responses: {
841
+ 200: {
842
+ description: "Test message sent",
843
+ content: { "application/json": { schema: z.object({
844
+ success: z.boolean(),
845
+ messageTs: z.string().optional(),
846
+ error: z.string().optional()
847
+ }) } }
848
+ },
849
+ 400: { description: "Invalid request" },
850
+ 404: { description: "Workspace not found" }
851
+ }
852
+ }), async (c) => {
853
+ const { teamId } = c.req.valid("param");
854
+ const { channelId, message } = c.req.valid("json");
855
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
856
+ if (!workspace?.botToken || !verifyTenantOwnership(c, workspace.tenantId)) return c.json({
857
+ success: false,
858
+ error: "Workspace not found or no bot token available"
859
+ }, 404);
860
+ try {
861
+ const slackClient = getSlackClient(workspace.botToken);
862
+ const testMessage = message || "✅ *Test message from Inkeep*\n\nYour Slack integration is working correctly!";
863
+ const result = await slackClient.chat.postMessage({
864
+ channel: channelId,
865
+ text: testMessage,
866
+ blocks: [{
867
+ type: "section",
868
+ text: {
869
+ type: "mrkdwn",
870
+ text: testMessage
871
+ }
872
+ }, {
873
+ type: "context",
874
+ elements: [{
875
+ type: "mrkdwn",
876
+ text: "_This is a test message from the Inkeep dashboard_"
877
+ }]
878
+ }]
879
+ });
880
+ if (!result.ok) return c.json({
881
+ success: false,
882
+ error: result.error || "Failed to send message"
883
+ });
884
+ logger.info({
885
+ teamId,
886
+ channelId,
887
+ messageTs: result.ts
888
+ }, "Test message sent");
889
+ return c.json({
890
+ success: true,
891
+ messageTs: result.ts
892
+ });
893
+ } catch (error) {
894
+ const errorMessage = error instanceof Error ? error.message : String(error);
895
+ logger.error({
896
+ error: errorMessage,
897
+ teamId,
898
+ channelId
899
+ }, "Failed to send test message");
900
+ return c.json({
901
+ success: false,
902
+ error: errorMessage
903
+ });
904
+ }
905
+ });
906
+ var workspaces_default = app;
907
+
908
+ //#endregion
909
+ export { workspaces_default as default };