@inkeep/agents-work-apps 0.0.0-dev-20260211191741 → 0.0.0-dev-20260211220939

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 (60) hide show
  1. package/dist/env.d.ts +24 -2
  2. package/dist/env.js +13 -2
  3. package/dist/github/index.d.ts +3 -3
  4. package/dist/github/mcp/index.d.ts +2 -2
  5. package/dist/github/routes/setup.d.ts +2 -2
  6. package/dist/github/routes/tokenExchange.d.ts +2 -2
  7. package/dist/github/routes/webhooks.d.ts +2 -2
  8. package/dist/slack/i18n/index.d.ts +2 -0
  9. package/dist/slack/i18n/index.js +3 -0
  10. package/dist/slack/i18n/strings.d.ts +73 -0
  11. package/dist/slack/i18n/strings.js +67 -0
  12. package/dist/slack/index.d.ts +18 -0
  13. package/dist/slack/index.js +28 -0
  14. package/dist/slack/middleware/permissions.d.ts +31 -0
  15. package/dist/slack/middleware/permissions.js +159 -0
  16. package/dist/slack/routes/events.d.ts +10 -0
  17. package/dist/slack/routes/events.js +390 -0
  18. package/dist/slack/routes/index.d.ts +10 -0
  19. package/dist/slack/routes/index.js +47 -0
  20. package/dist/slack/routes/oauth.d.ts +20 -0
  21. package/dist/slack/routes/oauth.js +325 -0
  22. package/dist/slack/routes/users.d.ts +10 -0
  23. package/dist/slack/routes/users.js +358 -0
  24. package/dist/slack/routes/workspaces.d.ts +10 -0
  25. package/dist/slack/routes/workspaces.js +875 -0
  26. package/dist/slack/services/agent-resolution.d.ts +41 -0
  27. package/dist/slack/services/agent-resolution.js +99 -0
  28. package/dist/slack/services/blocks/index.d.ts +73 -0
  29. package/dist/slack/services/blocks/index.js +103 -0
  30. package/dist/slack/services/client.d.ts +105 -0
  31. package/dist/slack/services/client.js +220 -0
  32. package/dist/slack/services/commands/index.d.ts +19 -0
  33. package/dist/slack/services/commands/index.js +538 -0
  34. package/dist/slack/services/events/app-mention.d.ts +40 -0
  35. package/dist/slack/services/events/app-mention.js +234 -0
  36. package/dist/slack/services/events/block-actions.d.ts +40 -0
  37. package/dist/slack/services/events/block-actions.js +221 -0
  38. package/dist/slack/services/events/index.d.ts +6 -0
  39. package/dist/slack/services/events/index.js +7 -0
  40. package/dist/slack/services/events/modal-submission.d.ts +30 -0
  41. package/dist/slack/services/events/modal-submission.js +346 -0
  42. package/dist/slack/services/events/streaming.d.ts +26 -0
  43. package/dist/slack/services/events/streaming.js +228 -0
  44. package/dist/slack/services/events/utils.d.ts +146 -0
  45. package/dist/slack/services/events/utils.js +369 -0
  46. package/dist/slack/services/index.d.ts +16 -0
  47. package/dist/slack/services/index.js +16 -0
  48. package/dist/slack/services/modals.d.ts +86 -0
  49. package/dist/slack/services/modals.js +355 -0
  50. package/dist/slack/services/nango.d.ts +85 -0
  51. package/dist/slack/services/nango.js +462 -0
  52. package/dist/slack/services/security.d.ts +35 -0
  53. package/dist/slack/services/security.js +65 -0
  54. package/dist/slack/services/types.d.ts +26 -0
  55. package/dist/slack/services/types.js +1 -0
  56. package/dist/slack/services/workspace-tokens.d.ts +25 -0
  57. package/dist/slack/services/workspace-tokens.js +27 -0
  58. package/dist/slack/types.d.ts +10 -0
  59. package/dist/slack/types.js +1 -0
  60. package/package.json +10 -2
@@ -0,0 +1,462 @@
1
+ import { env } from "../../env.js";
2
+ import { getLogger } from "../../logger.js";
3
+ import runDbClient_default from "../../db/runDbClient.js";
4
+ import { findWorkAppSlackWorkspaceBySlackTeamId } from "@inkeep/agents-core";
5
+ import { Nango } from "@nangohq/node";
6
+
7
+ //#region src/slack/services/nango.ts
8
+ /**
9
+ * Nango Service for Slack OAuth Token Management
10
+ *
11
+ * ARCHITECTURE NOTE: PostgreSQL is the authoritative source of truth for:
12
+ * - User linking data (work_app_slack_user_mappings table)
13
+ * - Workspace metadata (work_app_slack_workspaces table)
14
+ * - Channel agent configs (work_app_slack_channel_agent_configs table)
15
+ *
16
+ * Nango is used ONLY for:
17
+ * - OAuth token storage and refresh (bot tokens for workspaces)
18
+ * - OAuth flow management (createConnectSession)
19
+ * - Workspace default agent config (stored in connection metadata)
20
+ *
21
+ * PERFORMANCE: Workspace lookups use PostgreSQL first (O(1)), with Nango
22
+ * fallback only when needed for bot token retrieval.
23
+ *
24
+ * For user data, use the PostgreSQL data access layer:
25
+ * @see packages/agents-core/src/data-access/runtime/workAppSlack.ts
26
+ */
27
+ const MAX_WORKSPACE_CACHE_SIZE = 1e3;
28
+ const workspaceConnectionCache = /* @__PURE__ */ new Map();
29
+ const CACHE_TTL_MS = 6e4;
30
+ const logger = getLogger("slack-nango");
31
+ /**
32
+ * Retry a function with exponential backoff for transient failures.
33
+ * Retries on AbortError (timeout) and 5xx HTTP errors.
34
+ */
35
+ async function retryWithBackoff(fn, maxAttempts = 3) {
36
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) try {
37
+ return await fn();
38
+ } catch (error) {
39
+ const isTimeout = error.name === "AbortError";
40
+ const isServerError = typeof error.status === "number" && error.status >= 500;
41
+ if (!(isTimeout || isServerError) || attempt === maxAttempts) throw error;
42
+ const delay = Math.min(500 * 2 ** (attempt - 1), 2e3) + Math.random() * 100;
43
+ logger.warn({
44
+ attempt,
45
+ maxAttempts,
46
+ isTimeout,
47
+ delay: Math.round(delay)
48
+ }, "Retrying Nango API call after transient failure");
49
+ await new Promise((resolve) => setTimeout(resolve, delay));
50
+ }
51
+ throw new Error("Unreachable");
52
+ }
53
+ /**
54
+ * Evict expired entries from workspace cache to bound memory.
55
+ */
56
+ function evictWorkspaceCache() {
57
+ if (workspaceConnectionCache.size <= MAX_WORKSPACE_CACHE_SIZE) return;
58
+ const now = Date.now();
59
+ for (const [key, entry] of workspaceConnectionCache) if (entry.expiresAt <= now) workspaceConnectionCache.delete(key);
60
+ if (workspaceConnectionCache.size > MAX_WORKSPACE_CACHE_SIZE) {
61
+ const excess = workspaceConnectionCache.size - MAX_WORKSPACE_CACHE_SIZE;
62
+ const keys = workspaceConnectionCache.keys();
63
+ for (let i = 0; i < excess; i++) {
64
+ const { value } = keys.next();
65
+ if (value) workspaceConnectionCache.delete(value);
66
+ }
67
+ }
68
+ }
69
+ function getSlackNango() {
70
+ const secretKey = env.NANGO_SLACK_SECRET_KEY || env.NANGO_SECRET_KEY;
71
+ if (!secretKey) throw new Error("NANGO_SLACK_SECRET_KEY or NANGO_SECRET_KEY is required for Slack integration");
72
+ return new Nango({ secretKey });
73
+ }
74
+ function getSlackIntegrationId() {
75
+ return env.NANGO_SLACK_INTEGRATION_ID || "slack-agent";
76
+ }
77
+ async function createConnectSession(params) {
78
+ try {
79
+ const nango = getSlackNango();
80
+ const integrationId = getSlackIntegrationId();
81
+ const session = await nango.createConnectSession({
82
+ end_user: {
83
+ id: params.userId,
84
+ email: params.userEmail,
85
+ display_name: params.userName
86
+ },
87
+ organization: {
88
+ id: params.tenantId,
89
+ display_name: params.tenantId
90
+ },
91
+ allowed_integrations: [integrationId]
92
+ });
93
+ logger.info({
94
+ userId: params.userId,
95
+ userEmail: params.userEmail,
96
+ integrationId
97
+ }, "Created Nango connect session");
98
+ return { sessionToken: session.data.token };
99
+ } catch (error) {
100
+ logger.error({ error }, "Failed to create Nango connect session");
101
+ return null;
102
+ }
103
+ }
104
+ async function getConnectionAccessToken(connectionId) {
105
+ try {
106
+ const nango = getSlackNango();
107
+ const integrationId = getSlackIntegrationId();
108
+ return (await nango.getConnection(integrationId, connectionId)).credentials?.access_token || null;
109
+ } catch (error) {
110
+ logger.error({
111
+ error,
112
+ connectionId
113
+ }, "Failed to get connection access token");
114
+ return null;
115
+ }
116
+ }
117
+ /**
118
+ * Find a workspace connection by Slack team ID.
119
+ * Uses PostgreSQL first (O(1)) with in-memory caching, then falls back to Nango.
120
+ *
121
+ * Performance: This function is called on every @mention and command.
122
+ * The PostgreSQL-first approach with caching provides O(1) lookups.
123
+ */
124
+ async function findWorkspaceConnectionByTeamId(teamId) {
125
+ const cached = workspaceConnectionCache.get(teamId);
126
+ if (cached && cached.expiresAt > Date.now()) {
127
+ logger.debug({ teamId }, "Workspace connection cache hit");
128
+ return cached.connection;
129
+ }
130
+ try {
131
+ const dbWorkspace = await findWorkAppSlackWorkspaceBySlackTeamId(runDbClient_default)(teamId);
132
+ if (dbWorkspace?.nangoConnectionId) {
133
+ const botToken = await getConnectionAccessToken(dbWorkspace.nangoConnectionId);
134
+ if (botToken) {
135
+ const connection = {
136
+ connectionId: dbWorkspace.nangoConnectionId,
137
+ teamId,
138
+ teamName: dbWorkspace.slackTeamName || void 0,
139
+ botToken,
140
+ tenantId: dbWorkspace.tenantId,
141
+ defaultAgent: void 0
142
+ };
143
+ const defaultAgentConfig = await getWorkspaceDefaultAgentFromNangoByConnectionId(dbWorkspace.nangoConnectionId);
144
+ if (defaultAgentConfig) connection.defaultAgent = defaultAgentConfig;
145
+ evictWorkspaceCache();
146
+ workspaceConnectionCache.set(teamId, {
147
+ connection,
148
+ expiresAt: Date.now() + CACHE_TTL_MS
149
+ });
150
+ logger.debug({ teamId }, "Workspace connection found via PostgreSQL");
151
+ return connection;
152
+ }
153
+ }
154
+ logger.debug({ teamId }, "PostgreSQL lookup failed, falling back to Nango iteration");
155
+ return findWorkspaceConnectionByTeamIdFromNango(teamId);
156
+ } catch (error) {
157
+ logger.error({
158
+ error,
159
+ teamId
160
+ }, "Failed to find workspace connection by team ID");
161
+ return findWorkspaceConnectionByTeamIdFromNango(teamId);
162
+ }
163
+ }
164
+ async function getWorkspaceDefaultAgentFromNangoByConnectionId(connectionId) {
165
+ try {
166
+ const nango = getSlackNango();
167
+ const integrationId = getSlackIntegrationId();
168
+ const metadata = (await nango.getConnection(integrationId, connectionId)).metadata;
169
+ if (metadata?.default_agent) try {
170
+ return JSON.parse(metadata.default_agent);
171
+ } catch {
172
+ return null;
173
+ }
174
+ return null;
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+ /**
180
+ * Legacy fallback: Find workspace by iterating all Nango connections.
181
+ * Only used when PostgreSQL lookup fails.
182
+ */
183
+ async function findWorkspaceConnectionByTeamIdFromNango(teamId) {
184
+ try {
185
+ const nango = getSlackNango();
186
+ const integrationId = getSlackIntegrationId();
187
+ const connections = await nango.listConnections();
188
+ for (const conn of connections.connections) if (conn.provider_config_key === integrationId) try {
189
+ const fullConn = await nango.getConnection(integrationId, conn.connection_id);
190
+ const connectionConfig = fullConn.connection_config;
191
+ const metadata = fullConn.metadata;
192
+ const credentials = fullConn;
193
+ if ((connectionConfig?.["team.id"] || metadata?.slack_team_id) === teamId && credentials.credentials?.access_token) {
194
+ let defaultAgent;
195
+ if (metadata?.default_agent) try {
196
+ defaultAgent = JSON.parse(metadata.default_agent);
197
+ } catch {}
198
+ const connection = {
199
+ connectionId: conn.connection_id,
200
+ teamId,
201
+ teamName: metadata?.slack_team_name,
202
+ botToken: credentials.credentials.access_token,
203
+ tenantId: metadata?.tenant_id || "default",
204
+ defaultAgent
205
+ };
206
+ evictWorkspaceCache();
207
+ workspaceConnectionCache.set(teamId, {
208
+ connection,
209
+ expiresAt: Date.now() + CACHE_TTL_MS
210
+ });
211
+ return connection;
212
+ }
213
+ } catch {}
214
+ return null;
215
+ } catch (error) {
216
+ logger.error({
217
+ error,
218
+ teamId
219
+ }, "Failed to find workspace connection from Nango");
220
+ return null;
221
+ }
222
+ }
223
+ function clearWorkspaceConnectionCache(teamId) {
224
+ if (teamId) workspaceConnectionCache.delete(teamId);
225
+ else workspaceConnectionCache.clear();
226
+ }
227
+ async function updateConnectionMetadata(connectionId, metadata) {
228
+ try {
229
+ const nango = getSlackNango();
230
+ const integrationId = getSlackIntegrationId();
231
+ await nango.updateMetadata(integrationId, connectionId, metadata);
232
+ return true;
233
+ } catch (error) {
234
+ logger.error({
235
+ error,
236
+ connectionId
237
+ }, "Failed to update connection metadata");
238
+ return false;
239
+ }
240
+ }
241
+ async function setWorkspaceDefaultAgent(teamId, defaultAgent) {
242
+ try {
243
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
244
+ if (!workspace) {
245
+ logger.warn({ teamId }, "No workspace connection found to set default agent");
246
+ return false;
247
+ }
248
+ const success = await updateConnectionMetadata(workspace.connectionId, { default_agent: defaultAgent ? JSON.stringify(defaultAgent) : "" });
249
+ if (success) clearWorkspaceConnectionCache(teamId);
250
+ return success;
251
+ } catch (error) {
252
+ logger.error({
253
+ error,
254
+ teamId
255
+ }, "Failed to set workspace default agent");
256
+ return false;
257
+ }
258
+ }
259
+ async function getWorkspaceDefaultAgentFromNango(teamId) {
260
+ try {
261
+ return (await findWorkspaceConnectionByTeamId(teamId))?.defaultAgent || null;
262
+ } catch (error) {
263
+ logger.error({
264
+ error,
265
+ teamId
266
+ }, "Failed to get workspace default agent");
267
+ return null;
268
+ }
269
+ }
270
+ /**
271
+ * Compute a stable, deterministic connection ID for a Slack workspace.
272
+ * Format: "T:<team_id>" or "E:<enterprise_id>:T:<team_id>" for Enterprise Grid
273
+ */
274
+ function computeWorkspaceConnectionId(params) {
275
+ const { teamId, enterpriseId } = params;
276
+ if (enterpriseId) return `E:${enterpriseId}:T:${teamId}`;
277
+ return `T:${teamId}`;
278
+ }
279
+ /**
280
+ * Store a workspace installation in Nango.
281
+ * Uses upsert semantics - will update if the connection already exists.
282
+ */
283
+ async function storeWorkspaceInstallation(data) {
284
+ const connectionId = computeWorkspaceConnectionId({
285
+ teamId: data.teamId,
286
+ enterpriseId: data.enterpriseId
287
+ });
288
+ try {
289
+ const integrationId = getSlackIntegrationId();
290
+ const secretKey = env.NANGO_SLACK_SECRET_KEY || env.NANGO_SECRET_KEY;
291
+ if (!secretKey) {
292
+ logger.error({}, "No Nango secret key available");
293
+ return {
294
+ connectionId,
295
+ success: false
296
+ };
297
+ }
298
+ const nangoApiUrl = env.NANGO_SERVER_URL || "https://api.nango.dev";
299
+ logger.info({
300
+ integrationId,
301
+ connectionId,
302
+ teamId: data.teamId,
303
+ teamName: data.teamName
304
+ }, "Importing connection to Nango");
305
+ const displayName = data.enterpriseName ? `${data.teamName || data.teamId} (${data.enterpriseName})` : data.teamName || data.teamId;
306
+ const workspaceUrl = data.workspaceUrl || (data.teamDomain ? `https://${data.teamDomain}.slack.com` : "");
307
+ const now = (/* @__PURE__ */ new Date()).toISOString();
308
+ const requestBody = {
309
+ provider_config_key: integrationId,
310
+ connection_id: connectionId,
311
+ credentials: {
312
+ type: "OAUTH2",
313
+ access_token: data.botToken
314
+ },
315
+ metadata: {
316
+ display_name: displayName,
317
+ connection_type: "workspace",
318
+ slack_team_id: data.teamId,
319
+ slack_team_name: data.teamName || "",
320
+ slack_team_domain: data.teamDomain || "",
321
+ slack_workspace_url: workspaceUrl,
322
+ slack_workspace_icon_url: data.workspaceIconUrl || "",
323
+ slack_enterprise_id: data.enterpriseId || "",
324
+ slack_enterprise_name: data.enterpriseName || "",
325
+ is_enterprise_install: String(data.isEnterpriseInstall || false),
326
+ slack_bot_user_id: data.botUserId || "",
327
+ slack_bot_scopes: data.botScopes || "",
328
+ slack_app_id: data.appId || "",
329
+ installed_by_slack_user_id: data.installerUserId || "",
330
+ installed_by_slack_user_name: data.installerUserName || "",
331
+ installed_at: now,
332
+ last_updated_at: now,
333
+ installation_source: data.installationSource || "dashboard",
334
+ inkeep_tenant_id: data.tenantId || "default",
335
+ status: "active"
336
+ },
337
+ connection_config: { "team.id": data.teamId }
338
+ };
339
+ const response = await retryWithBackoff(async () => {
340
+ const controller = new AbortController();
341
+ const timeout = setTimeout(() => controller.abort(), 1e4);
342
+ try {
343
+ const res = await fetch(`${nangoApiUrl}/connections`, {
344
+ method: "POST",
345
+ headers: {
346
+ Authorization: `Bearer ${secretKey}`,
347
+ "Content-Type": "application/json"
348
+ },
349
+ body: JSON.stringify(requestBody),
350
+ signal: controller.signal
351
+ });
352
+ if (!res.ok && res.status >= 500) {
353
+ const errorBody = await res.text().catch(() => "Unknown error");
354
+ const err = /* @__PURE__ */ new Error(`Nango API error ${res.status}: ${errorBody}`);
355
+ err.status = res.status;
356
+ throw err;
357
+ }
358
+ return res;
359
+ } finally {
360
+ clearTimeout(timeout);
361
+ }
362
+ });
363
+ const responseText = await response.text();
364
+ if (!response.ok) {
365
+ logger.error({
366
+ status: response.status,
367
+ errorBody: responseText,
368
+ connectionId
369
+ }, "Failed to import connection to Nango");
370
+ return {
371
+ connectionId,
372
+ success: false
373
+ };
374
+ }
375
+ logger.info({
376
+ connectionId,
377
+ teamId: data.teamId,
378
+ teamName: data.teamName
379
+ }, "Stored workspace installation in Nango");
380
+ return {
381
+ connectionId,
382
+ success: true
383
+ };
384
+ } catch (error) {
385
+ logger.error({
386
+ error,
387
+ connectionId,
388
+ teamId: data.teamId
389
+ }, "Failed to store workspace installation");
390
+ return {
391
+ connectionId,
392
+ success: false
393
+ };
394
+ }
395
+ }
396
+ /**
397
+ * List all workspace installations from Nango.
398
+ */
399
+ async function listWorkspaceInstallations() {
400
+ try {
401
+ const nango = getSlackNango();
402
+ const integrationId = getSlackIntegrationId();
403
+ const connections = await nango.listConnections();
404
+ const workspaces = [];
405
+ for (const conn of connections.connections) if (conn.provider_config_key === integrationId) try {
406
+ const fullConn = await nango.getConnection(integrationId, conn.connection_id);
407
+ const metadata = fullConn.metadata;
408
+ const credentials = fullConn;
409
+ if (metadata?.connection_type === "workspace" && credentials.credentials?.access_token) {
410
+ let defaultAgent;
411
+ if (metadata?.default_agent) try {
412
+ defaultAgent = JSON.parse(metadata.default_agent);
413
+ } catch {}
414
+ workspaces.push({
415
+ connectionId: conn.connection_id,
416
+ teamId: metadata.slack_team_id || "",
417
+ teamName: metadata.slack_team_name,
418
+ botToken: credentials.credentials.access_token,
419
+ tenantId: metadata.tenant_id || "default",
420
+ defaultAgent
421
+ });
422
+ }
423
+ } catch {}
424
+ return workspaces;
425
+ } catch (error) {
426
+ logger.error({ error }, "Failed to list workspace installations");
427
+ return [];
428
+ }
429
+ }
430
+ /**
431
+ * Delete a workspace installation from Nango.
432
+ */
433
+ async function deleteWorkspaceInstallation(connectionId) {
434
+ try {
435
+ const nango = getSlackNango();
436
+ const integrationId = getSlackIntegrationId();
437
+ logger.info({
438
+ connectionId,
439
+ integrationId
440
+ }, "Attempting to delete workspace installation");
441
+ await nango.deleteConnection(integrationId, connectionId);
442
+ logger.info({ connectionId }, "Deleted workspace installation from Nango");
443
+ return true;
444
+ } catch (error) {
445
+ const errorObj = error;
446
+ const errorMessage = errorObj?.message || String(error);
447
+ const statusCode = errorObj?.status;
448
+ if (statusCode === 404 || errorMessage.includes("404") || errorMessage.includes("not found")) {
449
+ logger.warn({ connectionId }, "Connection not found in Nango, treating as already deleted");
450
+ return true;
451
+ }
452
+ logger.error({
453
+ error: errorMessage,
454
+ statusCode,
455
+ connectionId
456
+ }, "Failed to delete workspace installation");
457
+ return false;
458
+ }
459
+ }
460
+
461
+ //#endregion
462
+ export { clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createConnectSession, deleteWorkspaceInstallation, findWorkspaceConnectionByTeamId, getConnectionAccessToken, getSlackIntegrationId, getSlackNango, getWorkspaceDefaultAgentFromNango, listWorkspaceInstallations, setWorkspaceDefaultAgent, storeWorkspaceInstallation, updateConnectionMetadata };
@@ -0,0 +1,35 @@
1
+ //#region src/slack/services/security.d.ts
2
+ /**
3
+ * Slack Security Utilities
4
+ *
5
+ * Provides security functions for verifying Slack requests and parsing payloads.
6
+ * All incoming Slack requests are verified using HMAC-SHA256 signatures.
7
+ */
8
+ /**
9
+ * Verify that a request originated from Slack using HMAC-SHA256 signature.
10
+ *
11
+ * @param signingSecret - The Slack signing secret from app settings
12
+ * @param requestBody - The raw request body string
13
+ * @param timestamp - The X-Slack-Request-Timestamp header value
14
+ * @param signature - The X-Slack-Signature header value
15
+ * @returns true if the signature is valid, false otherwise
16
+ */
17
+ declare function verifySlackRequest(signingSecret: string, requestBody: string, timestamp: string, signature: string): boolean;
18
+ /**
19
+ * Parse a URL-encoded Slack command body into key-value pairs.
20
+ *
21
+ * @param body - The URL-encoded request body from a slash command
22
+ * @returns Parsed parameters as a string record
23
+ */
24
+ declare function parseSlackCommandBody(body: string): Record<string, string>;
25
+ /**
26
+ * Parse a Slack event body based on content type.
27
+ * Handles both JSON and URL-encoded payloads (for interactive components).
28
+ *
29
+ * @param body - The raw request body
30
+ * @param contentType - The Content-Type header value
31
+ * @returns Parsed event payload
32
+ */
33
+ declare function parseSlackEventBody(body: string, contentType: string): Record<string, unknown>;
34
+ //#endregion
35
+ export { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest };
@@ -0,0 +1,65 @@
1
+ import { getLogger } from "../../logger.js";
2
+ import crypto from "node:crypto";
3
+
4
+ //#region src/slack/services/security.ts
5
+ /**
6
+ * Slack Security Utilities
7
+ *
8
+ * Provides security functions for verifying Slack requests and parsing payloads.
9
+ * All incoming Slack requests are verified using HMAC-SHA256 signatures.
10
+ */
11
+ const logger = getLogger("slack-security");
12
+ /**
13
+ * Verify that a request originated from Slack using HMAC-SHA256 signature.
14
+ *
15
+ * @param signingSecret - The Slack signing secret from app settings
16
+ * @param requestBody - The raw request body string
17
+ * @param timestamp - The X-Slack-Request-Timestamp header value
18
+ * @param signature - The X-Slack-Signature header value
19
+ * @returns true if the signature is valid, false otherwise
20
+ */
21
+ function verifySlackRequest(signingSecret, requestBody, timestamp, signature) {
22
+ try {
23
+ const fiveMinutesAgo = Math.floor(Date.now() / 1e3) - 300;
24
+ if (Number.parseInt(timestamp, 10) < fiveMinutesAgo) {
25
+ logger.warn({}, "Slack request timestamp too old");
26
+ return false;
27
+ }
28
+ const sigBaseString = `v0:${timestamp}:${requestBody}`;
29
+ const mySignature = `v0=${crypto.createHmac("sha256", signingSecret).update(sigBaseString).digest("hex")}`;
30
+ return crypto.timingSafeEqual(Buffer.from(mySignature), Buffer.from(signature));
31
+ } catch (error) {
32
+ logger.error({ error }, "Error verifying Slack request");
33
+ return false;
34
+ }
35
+ }
36
+ /**
37
+ * Parse a URL-encoded Slack command body into key-value pairs.
38
+ *
39
+ * @param body - The URL-encoded request body from a slash command
40
+ * @returns Parsed parameters as a string record
41
+ */
42
+ function parseSlackCommandBody(body) {
43
+ const params = new URLSearchParams(body);
44
+ return Object.fromEntries(params.entries());
45
+ }
46
+ /**
47
+ * Parse a Slack event body based on content type.
48
+ * Handles both JSON and URL-encoded payloads (for interactive components).
49
+ *
50
+ * @param body - The raw request body
51
+ * @param contentType - The Content-Type header value
52
+ * @returns Parsed event payload
53
+ */
54
+ function parseSlackEventBody(body, contentType) {
55
+ if (contentType.includes("application/x-www-form-urlencoded")) {
56
+ const params = new URLSearchParams(body);
57
+ const payload = params.get("payload");
58
+ if (payload) return JSON.parse(payload);
59
+ return Object.fromEntries(params.entries());
60
+ }
61
+ return JSON.parse(body);
62
+ }
63
+
64
+ //#endregion
65
+ export { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest };
@@ -0,0 +1,26 @@
1
+ import { MessageAttachment } from "@slack/types";
2
+
3
+ //#region src/slack/services/types.d.ts
4
+ interface SlackCommandPayload {
5
+ command: string;
6
+ text: string;
7
+ userId: string;
8
+ userName: string;
9
+ teamId: string;
10
+ teamDomain: string;
11
+ enterpriseId?: string;
12
+ channelId: string;
13
+ channelName: string;
14
+ responseUrl: string;
15
+ triggerId: string;
16
+ }
17
+ interface SlackCommandResponse {
18
+ response_type?: 'ephemeral' | 'in_channel';
19
+ text?: string;
20
+ blocks?: unknown[];
21
+ attachments?: MessageAttachment[];
22
+ replace_original?: boolean;
23
+ delete_original?: boolean;
24
+ }
25
+ //#endregion
26
+ export { SlackCommandPayload, SlackCommandResponse };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,25 @@
1
+ //#region src/slack/services/workspace-tokens.d.ts
2
+ /**
3
+ * Workspace Token Service
4
+ *
5
+ * In-memory cache for Slack bot tokens during OAuth installation flow.
6
+ * Primary token storage is in Nango; this is a temporary fallback.
7
+ *
8
+ * Note: Tokens stored here do not persist across server restarts.
9
+ * Always prefer fetching tokens from Nango for production use.
10
+ */
11
+ /**
12
+ * Get the cached bot token for a Slack team.
13
+ * Falls back to null if not cached (caller should fetch from Nango).
14
+ */
15
+ declare function getBotTokenForTeam(teamId: string): string | null;
16
+ /**
17
+ * Cache a bot token for a Slack team (used during OAuth installation).
18
+ */
19
+ declare function setBotTokenForTeam(teamId: string, data: {
20
+ botToken: string;
21
+ teamName: string;
22
+ installedAt: string;
23
+ }): void;
24
+ //#endregion
25
+ export { getBotTokenForTeam, setBotTokenForTeam };
@@ -0,0 +1,27 @@
1
+ //#region src/slack/services/workspace-tokens.ts
2
+ /**
3
+ * Workspace Token Service
4
+ *
5
+ * In-memory cache for Slack bot tokens during OAuth installation flow.
6
+ * Primary token storage is in Nango; this is a temporary fallback.
7
+ *
8
+ * Note: Tokens stored here do not persist across server restarts.
9
+ * Always prefer fetching tokens from Nango for production use.
10
+ */
11
+ const workspaceBotTokens = /* @__PURE__ */ new Map();
12
+ /**
13
+ * Get the cached bot token for a Slack team.
14
+ * Falls back to null if not cached (caller should fetch from Nango).
15
+ */
16
+ function getBotTokenForTeam(teamId) {
17
+ return workspaceBotTokens.get(teamId)?.botToken || null;
18
+ }
19
+ /**
20
+ * Cache a bot token for a Slack team (used during OAuth installation).
21
+ */
22
+ function setBotTokenForTeam(teamId, data) {
23
+ workspaceBotTokens.set(teamId, data);
24
+ }
25
+
26
+ //#endregion
27
+ export { getBotTokenForTeam, setBotTokenForTeam };
@@ -0,0 +1,10 @@
1
+ //#region src/slack/types.d.ts
2
+ interface WorkAppsVariables {
3
+ tenantId?: string;
4
+ userId?: string;
5
+ userEmail?: string;
6
+ tenantRole?: string;
7
+ }
8
+ type ManageAppVariables = WorkAppsVariables;
9
+ //#endregion
10
+ export { ManageAppVariables, WorkAppsVariables };
@@ -0,0 +1 @@
1
+ export { };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkeep/agents-work-apps",
3
- "version": "0.0.0-dev-20260211191741",
3
+ "version": "0.0.0-dev-20260211220939",
4
4
  "description": "First party integrations for Inkeep Agents",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -13,18 +13,26 @@
13
13
  "./github": {
14
14
  "types": "./dist/github/index.d.ts",
15
15
  "import": "./dist/github/index.js"
16
+ },
17
+ "./slack": {
18
+ "types": "./dist/slack/index.d.ts",
19
+ "import": "./dist/slack/index.js"
16
20
  }
17
21
  },
18
22
  "dependencies": {
19
23
  "@modelcontextprotocol/sdk": "^1.25.3",
24
+ "@nangohq/node": "^0.69.20",
20
25
  "@octokit/auth-app": "^8.1.2",
21
26
  "@octokit/rest": "^22.0.1",
27
+ "@slack/types": "^2.18.0",
28
+ "@slack/web-api": "^7.9.1",
29
+ "slack-block-builder": "^2.8.0",
22
30
  "drizzle-orm": "^0.44.7",
23
31
  "fetch-to-node": "^2.1.0",
24
32
  "hono": "^4.11.7",
25
33
  "jose": "^6.1.0",
26
34
  "minimatch": "^10.1.1",
27
- "@inkeep/agents-core": "0.0.0-dev-20260211191741"
35
+ "@inkeep/agents-core": "0.0.0-dev-20260211220939"
28
36
  },
29
37
  "peerDependencies": {
30
38
  "@hono/zod-openapi": "^1.1.5",