@inkeep/agents-work-apps 0.47.4 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/env.d.ts +22 -0
  2. package/dist/env.js +13 -2
  3. package/dist/github/index.d.ts +3 -3
  4. package/dist/github/mcp/auth.d.ts +2 -2
  5. package/dist/github/mcp/index.d.ts +2 -2
  6. package/dist/github/mcp/index.js +23 -34
  7. package/dist/github/mcp/schemas.d.ts +1 -1
  8. package/dist/github/routes/setup.d.ts +2 -2
  9. package/dist/github/routes/tokenExchange.d.ts +2 -2
  10. package/dist/github/routes/webhooks.d.ts +2 -2
  11. package/dist/slack/i18n/index.d.ts +2 -0
  12. package/dist/slack/i18n/index.js +3 -0
  13. package/dist/slack/i18n/strings.d.ts +73 -0
  14. package/dist/slack/i18n/strings.js +67 -0
  15. package/dist/slack/index.d.ts +18 -0
  16. package/dist/slack/index.js +28 -0
  17. package/dist/slack/middleware/permissions.d.ts +31 -0
  18. package/dist/slack/middleware/permissions.js +167 -0
  19. package/dist/slack/routes/events.d.ts +10 -0
  20. package/dist/slack/routes/events.js +551 -0
  21. package/dist/slack/routes/index.d.ts +10 -0
  22. package/dist/slack/routes/index.js +47 -0
  23. package/dist/slack/routes/oauth.d.ts +20 -0
  24. package/dist/slack/routes/oauth.js +344 -0
  25. package/dist/slack/routes/users.d.ts +10 -0
  26. package/dist/slack/routes/users.js +365 -0
  27. package/dist/slack/routes/workspaces.d.ts +10 -0
  28. package/dist/slack/routes/workspaces.js +909 -0
  29. package/dist/slack/services/agent-resolution.d.ts +41 -0
  30. package/dist/slack/services/agent-resolution.js +99 -0
  31. package/dist/slack/services/blocks/index.d.ts +73 -0
  32. package/dist/slack/services/blocks/index.js +103 -0
  33. package/dist/slack/services/client.d.ts +108 -0
  34. package/dist/slack/services/client.js +232 -0
  35. package/dist/slack/services/commands/index.d.ts +19 -0
  36. package/dist/slack/services/commands/index.js +553 -0
  37. package/dist/slack/services/events/app-mention.d.ts +40 -0
  38. package/dist/slack/services/events/app-mention.js +297 -0
  39. package/dist/slack/services/events/block-actions.d.ts +40 -0
  40. package/dist/slack/services/events/block-actions.js +265 -0
  41. package/dist/slack/services/events/index.d.ts +6 -0
  42. package/dist/slack/services/events/index.js +7 -0
  43. package/dist/slack/services/events/modal-submission.d.ts +30 -0
  44. package/dist/slack/services/events/modal-submission.js +400 -0
  45. package/dist/slack/services/events/streaming.d.ts +26 -0
  46. package/dist/slack/services/events/streaming.js +255 -0
  47. package/dist/slack/services/events/utils.d.ts +146 -0
  48. package/dist/slack/services/events/utils.js +370 -0
  49. package/dist/slack/services/index.d.ts +16 -0
  50. package/dist/slack/services/index.js +16 -0
  51. package/dist/slack/services/modals.d.ts +86 -0
  52. package/dist/slack/services/modals.js +355 -0
  53. package/dist/slack/services/nango.d.ts +85 -0
  54. package/dist/slack/services/nango.js +476 -0
  55. package/dist/slack/services/security.d.ts +35 -0
  56. package/dist/slack/services/security.js +65 -0
  57. package/dist/slack/services/types.d.ts +26 -0
  58. package/dist/slack/services/types.js +1 -0
  59. package/dist/slack/services/workspace-tokens.d.ts +25 -0
  60. package/dist/slack/services/workspace-tokens.js +27 -0
  61. package/dist/slack/tracer.d.ts +40 -0
  62. package/dist/slack/tracer.js +39 -0
  63. package/dist/slack/types.d.ts +10 -0
  64. package/dist/slack/types.js +1 -0
  65. package/package.json +11 -2
@@ -0,0 +1,344 @@
1
+ import { env } from "../../env.js";
2
+ import { getLogger } from "../../logger.js";
3
+ import runDbClient_default from "../../db/runDbClient.js";
4
+ import { clearWorkspaceConnectionCache, computeWorkspaceConnectionId, deleteWorkspaceInstallation, storeWorkspaceInstallation } from "../services/nango.js";
5
+ import { getSlackClient, getSlackTeamInfo, getSlackUserInfo } from "../services/client.js";
6
+ import { getBotTokenForTeam, setBotTokenForTeam } from "../services/workspace-tokens.js";
7
+ import "../services/index.js";
8
+ import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
9
+ import { createWorkAppSlackWorkspace } from "@inkeep/agents-core";
10
+ import * as crypto$1 from "node:crypto";
11
+
12
+ //#region src/slack/routes/oauth.ts
13
+ /**
14
+ * Slack OAuth Routes
15
+ *
16
+ * Endpoints for Slack workspace installation via OAuth:
17
+ * - GET /install - Redirect to Slack OAuth page
18
+ * - GET /oauth_redirect - Handle OAuth callback
19
+ */
20
+ const logger = getLogger("slack-oauth");
21
+ const STATE_TTL_MS = 600 * 1e3;
22
+ /**
23
+ * Allowed redirect domains for OAuth callbacks.
24
+ * Validates INKEEP_AGENTS_MANAGE_UI_URL to prevent open redirect attacks.
25
+ */
26
+ const ALLOWED_REDIRECT_HOSTNAMES = new Set([
27
+ "localhost",
28
+ "127.0.0.1",
29
+ "agents.inkeep.com"
30
+ ]);
31
+ function isAllowedRedirectUrl(url) {
32
+ try {
33
+ const parsed = new URL(url);
34
+ if (ALLOWED_REDIRECT_HOSTNAMES.has(parsed.hostname)) return true;
35
+ if (parsed.hostname.endsWith(".inkeep.com")) return true;
36
+ return false;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ const manageUiUrl = env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000";
42
+ if (!isAllowedRedirectUrl(manageUiUrl)) throw new Error(`Invalid INKEEP_AGENTS_MANAGE_UI_URL: "${manageUiUrl}" is not in the allowed redirect domains. Allowed: localhost, 127.0.0.1, *.inkeep.com`);
43
+ function getStateSigningSecret() {
44
+ const secret = env.SLACK_SIGNING_SECRET;
45
+ if (!secret) {
46
+ if (env.ENVIRONMENT === "production") throw new Error("SLACK_SIGNING_SECRET is required in production for OAuth state signing");
47
+ logger.warn({}, "SLACK_SIGNING_SECRET not set, using insecure default. DO NOT USE IN PRODUCTION!");
48
+ return "insecure-dev-oauth-state-secret-change-in-production";
49
+ }
50
+ return secret;
51
+ }
52
+ function createOAuthState(tenantId) {
53
+ const state = {
54
+ nonce: crypto$1.randomBytes(16).toString("hex"),
55
+ tenantId: tenantId || "",
56
+ timestamp: Date.now()
57
+ };
58
+ const data = Buffer.from(JSON.stringify(state)).toString("base64url");
59
+ return `${data}.${crypto$1.createHmac("sha256", getStateSigningSecret()).update(data).digest("base64url")}`;
60
+ }
61
+ function parseOAuthState(stateStr) {
62
+ try {
63
+ const [data, signature] = stateStr.split(".");
64
+ if (!data || !signature) {
65
+ logger.warn({}, "OAuth state missing signature");
66
+ return null;
67
+ }
68
+ const expectedSignature = crypto$1.createHmac("sha256", getStateSigningSecret()).update(data).digest("base64url");
69
+ if (signature.length !== expectedSignature.length || !crypto$1.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
70
+ logger.warn({}, "Invalid OAuth state signature");
71
+ return null;
72
+ }
73
+ const decoded = Buffer.from(data, "base64url").toString("utf-8");
74
+ const state = JSON.parse(decoded);
75
+ if (!state.nonce || !state.timestamp) {
76
+ logger.warn({
77
+ hasNonce: !!state.nonce,
78
+ hasTimestamp: !!state.timestamp
79
+ }, "OAuth state missing required fields");
80
+ return null;
81
+ }
82
+ if (Date.now() - state.timestamp > STATE_TTL_MS) {
83
+ logger.warn({ timestamp: state.timestamp }, "OAuth state expired");
84
+ return null;
85
+ }
86
+ return state;
87
+ } catch {
88
+ logger.warn({ stateStr: stateStr?.slice(0, 20) }, "Failed to parse OAuth state");
89
+ return null;
90
+ }
91
+ }
92
+ const app = new OpenAPIHono();
93
+ app.openapi(createRoute({
94
+ method: "get",
95
+ path: "/install",
96
+ summary: "Install Slack App",
97
+ description: "Redirects to Slack OAuth page for workspace installation",
98
+ operationId: "slack-install",
99
+ tags: [
100
+ "Work Apps",
101
+ "Slack",
102
+ "OAuth"
103
+ ],
104
+ request: { query: z.object({ tenant_id: z.string().optional() }) },
105
+ responses: { 302: { description: "Redirect to Slack OAuth" } }
106
+ }), (c) => {
107
+ const { tenant_id: tenantId } = c.req.valid("query");
108
+ const clientId = env.SLACK_CLIENT_ID;
109
+ const redirectUri = `${env.SLACK_APP_URL}/work-apps/slack/oauth_redirect`;
110
+ const botScopes = [
111
+ "app_mentions:read",
112
+ "channels:history",
113
+ "channels:read",
114
+ "chat:write",
115
+ "chat:write.public",
116
+ "commands",
117
+ "groups:history",
118
+ "groups:read",
119
+ "im:history",
120
+ "im:read",
121
+ "im:write",
122
+ "team:read",
123
+ "users:read",
124
+ "users:read.email"
125
+ ].join(",");
126
+ const state = createOAuthState(tenantId);
127
+ const slackAuthUrl = new URL("https://slack.com/oauth/v2/authorize");
128
+ slackAuthUrl.searchParams.set("client_id", clientId || "");
129
+ slackAuthUrl.searchParams.set("scope", botScopes);
130
+ slackAuthUrl.searchParams.set("redirect_uri", redirectUri);
131
+ slackAuthUrl.searchParams.set("state", state);
132
+ logger.info({
133
+ redirectUri,
134
+ tenantId: tenantId || ""
135
+ }, "Redirecting to Slack OAuth");
136
+ return c.redirect(slackAuthUrl.toString());
137
+ });
138
+ app.openapi(createRoute({
139
+ method: "get",
140
+ path: "/oauth_redirect",
141
+ summary: "Slack OAuth Callback",
142
+ description: "Handles the OAuth callback from Slack after workspace installation",
143
+ operationId: "slack-oauth-redirect",
144
+ tags: [
145
+ "Work Apps",
146
+ "Slack",
147
+ "OAuth"
148
+ ],
149
+ request: { query: z.object({
150
+ code: z.string().optional(),
151
+ error: z.string().optional(),
152
+ state: z.string().optional()
153
+ }) },
154
+ responses: { 302: { description: "Redirect to dashboard with workspace data" } }
155
+ }), async (c) => {
156
+ const { code, error, state: stateParam } = c.req.valid("query");
157
+ const parsedState = stateParam ? parseOAuthState(stateParam) : null;
158
+ const tenantId = parsedState?.tenantId || "";
159
+ const dashboardUrl = `${manageUiUrl}/${tenantId}/work-apps/slack`;
160
+ if (!stateParam || !parsedState) {
161
+ logger.error({ hasState: !!stateParam }, "Invalid or missing OAuth state parameter");
162
+ return c.redirect(`${dashboardUrl}?error=invalid_state`);
163
+ }
164
+ if (error) {
165
+ logger.error({ error }, "Slack OAuth error");
166
+ return c.redirect(`${dashboardUrl}?error=${encodeURIComponent(error)}`);
167
+ }
168
+ if (!code) {
169
+ logger.error({}, "No code provided in OAuth callback");
170
+ return c.redirect(`${dashboardUrl}?error=no_code`);
171
+ }
172
+ try {
173
+ const controller = new AbortController();
174
+ const timeout = setTimeout(() => controller.abort(), 1e4);
175
+ let tokenResponse;
176
+ try {
177
+ tokenResponse = await fetch("https://slack.com/api/oauth.v2.access", {
178
+ method: "POST",
179
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
180
+ body: new URLSearchParams({
181
+ client_id: env.SLACK_CLIENT_ID || "",
182
+ client_secret: env.SLACK_CLIENT_SECRET || "",
183
+ code,
184
+ redirect_uri: `${env.SLACK_APP_URL}/work-apps/slack/oauth_redirect`
185
+ }),
186
+ signal: controller.signal
187
+ });
188
+ } catch (fetchErr) {
189
+ clearTimeout(timeout);
190
+ if (fetchErr.name === "AbortError") {
191
+ logger.error({}, "Slack token exchange timed out");
192
+ return c.redirect(`${dashboardUrl}?error=timeout`);
193
+ }
194
+ throw fetchErr;
195
+ } finally {
196
+ clearTimeout(timeout);
197
+ }
198
+ const tokenData = await tokenResponse.json();
199
+ if (!tokenData.ok) {
200
+ logger.error({ error: tokenData.error }, "Slack token exchange failed");
201
+ return c.redirect(`${dashboardUrl}?error=${encodeURIComponent(tokenData.error || "token_exchange_failed")}`);
202
+ }
203
+ const client = getSlackClient(tokenData.access_token);
204
+ const teamInfo = await getSlackTeamInfo(client);
205
+ logger.debug({ teamInfo }, "Retrieved Slack team info");
206
+ const installerUserId = tokenData.authed_user?.id;
207
+ let installerUserName;
208
+ if (installerUserId) try {
209
+ const userInfo = await getSlackUserInfo(client, installerUserId);
210
+ installerUserName = userInfo?.realName || userInfo?.name;
211
+ } catch {
212
+ logger.warn({ installerUserId }, "Could not fetch installer user info");
213
+ }
214
+ const workspaceData = {
215
+ ok: true,
216
+ teamId: tokenData.team?.id,
217
+ teamName: tokenData.team?.name,
218
+ teamDomain: teamInfo?.domain,
219
+ workspaceUrl: teamInfo?.url,
220
+ workspaceIconUrl: teamInfo?.icon,
221
+ enterpriseId: tokenData.enterprise?.id,
222
+ enterpriseName: tokenData.enterprise?.name,
223
+ isEnterpriseInstall: tokenData.is_enterprise_install || false,
224
+ botUserId: tokenData.bot_user_id,
225
+ botToken: tokenData.access_token,
226
+ botScopes: tokenData.scope,
227
+ installerUserId,
228
+ installerUserName,
229
+ appId: tokenData.app_id,
230
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
231
+ };
232
+ if (workspaceData.teamId && workspaceData.botToken) {
233
+ clearWorkspaceConnectionCache(workspaceData.teamId);
234
+ const nangoResult = await storeWorkspaceInstallation({
235
+ teamId: workspaceData.teamId,
236
+ teamName: workspaceData.teamName,
237
+ teamDomain: workspaceData.teamDomain,
238
+ workspaceUrl: workspaceData.workspaceUrl,
239
+ workspaceIconUrl: workspaceData.workspaceIconUrl,
240
+ enterpriseId: workspaceData.enterpriseId,
241
+ enterpriseName: workspaceData.enterpriseName,
242
+ botUserId: workspaceData.botUserId,
243
+ botToken: workspaceData.botToken,
244
+ botScopes: workspaceData.botScopes,
245
+ installerUserId: workspaceData.installerUserId,
246
+ installerUserName: workspaceData.installerUserName,
247
+ isEnterpriseInstall: workspaceData.isEnterpriseInstall,
248
+ appId: workspaceData.appId,
249
+ tenantId,
250
+ installationSource: "dashboard"
251
+ });
252
+ if (nangoResult.success && nangoResult.connectionId) {
253
+ logger.info({
254
+ teamId: workspaceData.teamId,
255
+ connectionId: nangoResult.connectionId
256
+ }, "Stored workspace installation in Nango");
257
+ try {
258
+ await createWorkAppSlackWorkspace(runDbClient_default)({
259
+ tenantId,
260
+ slackTeamId: workspaceData.teamId,
261
+ slackEnterpriseId: workspaceData.enterpriseId,
262
+ slackAppId: workspaceData.appId,
263
+ slackTeamName: workspaceData.teamName,
264
+ nangoConnectionId: nangoResult.connectionId,
265
+ status: "active"
266
+ });
267
+ logger.info({
268
+ teamId: workspaceData.teamId,
269
+ tenantId
270
+ }, "Persisted workspace installation to database");
271
+ } catch (dbError) {
272
+ const dbErrorMessage = dbError instanceof Error ? dbError.message : String(dbError);
273
+ if (dbErrorMessage.includes("duplicate key") || dbErrorMessage.includes("unique constraint")) logger.info({
274
+ teamId: workspaceData.teamId,
275
+ tenantId
276
+ }, "Workspace already exists in database");
277
+ else {
278
+ const pgCode = dbError && typeof dbError === "object" && "code" in dbError ? dbError.code : void 0;
279
+ logger.error({
280
+ err: dbError,
281
+ dbErrorMessage,
282
+ pgCode,
283
+ teamId: workspaceData.teamId,
284
+ tenantId,
285
+ connectionId: nangoResult.connectionId
286
+ }, "Failed to persist workspace to database, rolling back Nango connection");
287
+ try {
288
+ await deleteWorkspaceInstallation(nangoResult.connectionId);
289
+ } catch (rollbackError) {
290
+ logger.error({
291
+ err: rollbackError,
292
+ connectionId: nangoResult.connectionId
293
+ }, "Failed to rollback Nango connection after DB failure");
294
+ }
295
+ return c.redirect(`${dashboardUrl}?error=installation_failed`);
296
+ }
297
+ }
298
+ } else logger.warn({
299
+ teamId: workspaceData.teamId,
300
+ tenantId,
301
+ nangoSuccess: nangoResult.success,
302
+ nangoError: "error" in nangoResult ? nangoResult.error : void 0
303
+ }, "Failed to store in Nango, falling back to memory");
304
+ setBotTokenForTeam(workspaceData.teamId, {
305
+ botToken: workspaceData.botToken,
306
+ teamName: workspaceData.teamName || "",
307
+ installedAt: workspaceData.installedAt
308
+ });
309
+ }
310
+ logger.info({
311
+ teamId: workspaceData.teamId,
312
+ teamName: workspaceData.teamName
313
+ }, "Slack workspace installation successful");
314
+ const safeWorkspaceData = {
315
+ ok: workspaceData.ok,
316
+ teamId: workspaceData.teamId,
317
+ teamName: workspaceData.teamName,
318
+ teamDomain: workspaceData.teamDomain,
319
+ enterpriseId: workspaceData.enterpriseId,
320
+ enterpriseName: workspaceData.enterpriseName,
321
+ isEnterpriseInstall: workspaceData.isEnterpriseInstall,
322
+ botUserId: workspaceData.botUserId,
323
+ botScopes: workspaceData.botScopes,
324
+ installerUserId: workspaceData.installerUserId,
325
+ installedAt: workspaceData.installedAt,
326
+ connectionId: workspaceData.teamId ? computeWorkspaceConnectionId({
327
+ teamId: workspaceData.teamId,
328
+ enterpriseId: workspaceData.enterpriseId
329
+ }) : void 0
330
+ };
331
+ const encodedData = encodeURIComponent(JSON.stringify(safeWorkspaceData));
332
+ return c.redirect(`${dashboardUrl}?success=true&workspace=${encodedData}`);
333
+ } catch (err) {
334
+ logger.error({
335
+ err,
336
+ tenantId
337
+ }, "Slack OAuth callback error");
338
+ return c.redirect(`${dashboardUrl}?error=callback_error`);
339
+ }
340
+ });
341
+ var oauth_default = app;
342
+
343
+ //#endregion
344
+ export { createOAuthState, oauth_default as default, getBotTokenForTeam, getStateSigningSecret, parseOAuthState, setBotTokenForTeam };
@@ -0,0 +1,10 @@
1
+ import { WorkAppsVariables } from "../types.js";
2
+ import { OpenAPIHono } from "@hono/zod-openapi";
3
+
4
+ //#region src/slack/routes/users.d.ts
5
+
6
+ declare const app: OpenAPIHono<{
7
+ Variables: WorkAppsVariables;
8
+ }, {}, "/">;
9
+ //#endregion
10
+ export { app as default };