@inkeep/agents-work-apps 0.65.1 → 0.66.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.
@@ -19,7 +19,7 @@ function getGitHubAppConfig() {
19
19
  });
20
20
  if (!result.success) {
21
21
  const errorMessage = `GitHub App credentials are not configured. ${result.error.issues.map((issue) => issue.message).join(". ")}. Please set GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY environment variables.`;
22
- logger.error({}, errorMessage);
22
+ logger.error(errorMessage);
23
23
  throw new Error(errorMessage);
24
24
  }
25
25
  cachedConfig = result.data;
@@ -7,7 +7,7 @@ const GITHUB_OIDC_JWKS_URL = "https://token.actions.githubusercontent.com/.well-
7
7
  const CACHE_TTL_MS = 3600 * 1e3;
8
8
  let jwksCache = null;
9
9
  function createJwksWithLogging() {
10
- logger.info({}, "Creating new JWKS fetch function for GitHub OIDC");
10
+ logger.info("Creating new JWKS fetch function for GitHub OIDC");
11
11
  return createRemoteJWKSet(new URL(GITHUB_OIDC_JWKS_URL), { cacheMaxAge: CACHE_TTL_MS });
12
12
  }
13
13
  function isCacheExpired() {
@@ -70,7 +70,7 @@ async function getJwkForToken(header) {
70
70
  }
71
71
  function clearJwksCache() {
72
72
  jwksCache = null;
73
- logger.debug({}, "JWKS cache cleared");
73
+ logger.debug("JWKS cache cleared");
74
74
  }
75
75
  function getJwksCacheStatus() {
76
76
  if (!jwksCache) return { cached: false };
@@ -1,7 +1,7 @@
1
- import * as hono0 from "hono";
1
+ import * as hono2 from "hono";
2
2
 
3
3
  //#region src/github/mcp/auth.d.ts
4
- declare const githubMcpAuth: () => hono0.MiddlewareHandler<{
4
+ declare const githubMcpAuth: () => hono2.MiddlewareHandler<{
5
5
  Variables: {
6
6
  toolId: string;
7
7
  tenantId: string;
@@ -1,5 +1,5 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types7 from "hono/types";
2
+ import * as hono_types8 from "hono/types";
3
3
 
4
4
  //#region src/github/mcp/index.d.ts
5
5
  declare const app: Hono<{
@@ -8,6 +8,6 @@ declare const app: Hono<{
8
8
  tenantId: string;
9
9
  projectId: string;
10
10
  };
11
- }, hono_types7.BlankSchema, "/">;
11
+ }, hono_types8.BlankSchema, "/">;
12
12
  //#endregion
13
13
  export { app as default };
@@ -72,7 +72,7 @@ async function validateOidcToken(token) {
72
72
  };
73
73
  } catch (error) {
74
74
  if (error instanceof errors.JWTExpired) {
75
- logger.warn({}, "OIDC token has expired");
75
+ logger.warn("OIDC token has expired");
76
76
  return {
77
77
  success: false,
78
78
  errorType: "expired",
@@ -108,7 +108,7 @@ async function validateOidcToken(token) {
108
108
  };
109
109
  }
110
110
  if (error instanceof errors.JWSSignatureVerificationFailed) {
111
- logger.warn({}, "Invalid OIDC token signature");
111
+ logger.warn("Invalid OIDC token signature");
112
112
  return {
113
113
  success: false,
114
114
  errorType: "invalid_signature",
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types3 from "hono/types";
2
+ import * as hono_types4 from "hono/types";
3
3
 
4
4
  //#region src/github/routes/setup.d.ts
5
- declare const app: Hono<hono_types3.BlankEnv, hono_types3.BlankSchema, "/">;
5
+ declare const app: Hono<hono_types4.BlankEnv, hono_types4.BlankSchema, "/">;
6
6
  //#endregion
7
7
  export { app as default };
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types5 from "hono/types";
2
+ import * as hono_types6 from "hono/types";
3
3
 
4
4
  //#region src/github/routes/tokenExchange.d.ts
5
- declare const app: Hono<hono_types5.BlankEnv, hono_types5.BlankSchema, "/">;
5
+ declare const app: Hono<hono_types6.BlankEnv, hono_types6.BlankSchema, "/">;
6
6
  //#endregion
7
7
  export { app as default };
@@ -35,9 +35,9 @@ app.post("/", async (c) => {
35
35
  }, 400);
36
36
  }
37
37
  const body = parseResult.data;
38
- logger.info({}, "Processing token exchange request");
38
+ logger.info("Processing token exchange request");
39
39
  if (!isGitHubAppConfigured()) {
40
- logger.error({}, "GitHub App credentials not configured");
40
+ logger.error("GitHub App credentials not configured");
41
41
  const errorMessage = "GitHub App credentials are not configured. Please contact the administrator to set up GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY.";
42
42
  c.header("Content-Type", "application/problem+json");
43
43
  return c.json({
@@ -42,7 +42,7 @@ function verifyWebhookSignature(payload, signature, secret) {
42
42
  const app = new Hono();
43
43
  app.post("/", async (c) => {
44
44
  if (!isWebhookConfigured()) {
45
- logger.error({}, "GitHub webhook secret not configured");
45
+ logger.error("GitHub webhook secret not configured");
46
46
  return c.json({ error: "GitHub webhook secret not configured" }, 500);
47
47
  }
48
48
  const rawBody = await c.req.text();
@@ -8,6 +8,7 @@ import { handleMessageShortcut, handleOpenAgentSelectorModal, handleToolApproval
8
8
  import { handleDirectMessage } from "./services/events/direct-message.js";
9
9
  import { handleModalSubmission } from "./services/events/modal-submission.js";
10
10
  import "./services/events/index.js";
11
+ import { cleanupWorkspaceInstallation } from "./services/workspace-cleanup.js";
11
12
  import "./services/index.js";
12
13
  import { flushTraces } from "@inkeep/agents-core";
13
14
 
@@ -131,6 +132,55 @@ async function dispatchSlackEvent(eventType, payload, options, span) {
131
132
  errorStack
132
133
  }, "Failed to handle direct message (outer catch)");
133
134
  }).finally(() => flushTraces()));
135
+ } else if (innerEventType === "app_uninstalled" && teamId) {
136
+ outcome = "handled";
137
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
138
+ span.updateName("slack.webhook app_uninstalled");
139
+ logger.info({ teamId }, "Handling event: app_uninstalled");
140
+ registerBackgroundWork(cleanupWorkspaceInstallation({
141
+ teamId,
142
+ skipTokenRevocation: true
143
+ }).then((result) => {
144
+ logger.info({
145
+ teamId,
146
+ success: result.success,
147
+ dbCleaned: result.dbCleaned
148
+ }, "app_uninstalled cleanup completed");
149
+ }).catch((err) => {
150
+ const errorMessage = err instanceof Error ? err.message : String(err);
151
+ logger.error({
152
+ errorMessage,
153
+ teamId
154
+ }, "Failed to handle app_uninstalled");
155
+ }).finally(() => flushTraces()));
156
+ } else if (innerEventType === "tokens_revoked" && teamId) {
157
+ outcome = "handled";
158
+ span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
159
+ span.updateName("slack.webhook tokens_revoked");
160
+ const tokensEvent = payload.event;
161
+ const revokedBotTokens = tokensEvent?.tokens?.bot ?? [];
162
+ const revokedOauthTokens = tokensEvent?.tokens?.oauth ?? [];
163
+ logger.info({
164
+ teamId,
165
+ revokedBotCount: revokedBotTokens.length,
166
+ revokedOauthCount: revokedOauthTokens.length
167
+ }, "Handling event: tokens_revoked");
168
+ if (revokedBotTokens.length > 0) registerBackgroundWork(cleanupWorkspaceInstallation({
169
+ teamId,
170
+ skipTokenRevocation: true
171
+ }).then((result) => {
172
+ logger.info({
173
+ teamId,
174
+ success: result.success,
175
+ dbCleaned: result.dbCleaned
176
+ }, "tokens_revoked (bot) cleanup completed");
177
+ }).catch((err) => {
178
+ const errorMessage = err instanceof Error ? err.message : String(err);
179
+ logger.error({
180
+ errorMessage,
181
+ teamId
182
+ }, "Failed to handle tokens_revoked (bot)");
183
+ }).finally(() => flushTraces()));
134
184
  } else {
135
185
  outcome = "ignored_unknown_event";
136
186
  span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
@@ -1,5 +1,5 @@
1
1
  import { Hono } from "hono";
2
- import * as hono_types8 from "hono/types";
2
+ import * as hono_types3 from "hono/types";
3
3
 
4
4
  //#region src/slack/mcp/index.d.ts
5
5
  interface ChannelInfo {
@@ -18,6 +18,6 @@ declare const app: Hono<{
18
18
  tenantId: string;
19
19
  projectId: string;
20
20
  };
21
- }, hono_types8.BlankSchema, "/">;
21
+ }, hono_types3.BlankSchema, "/">;
22
22
  //#endregion
23
23
  export { ChannelInfo, app as default, pruneStaleChannelIds };
@@ -1,5 +1,5 @@
1
1
  import { ManageAppVariables } from "../types.js";
2
- import * as hono1 from "hono";
2
+ import * as hono0 from "hono";
3
3
 
4
4
  //#region src/slack/middleware/permissions.d.ts
5
5
  /**
@@ -14,7 +14,7 @@ declare const requireWorkspaceAdmin: <Env extends {
14
14
  Variables: ManageAppVariables;
15
15
  } = {
16
16
  Variables: ManageAppVariables;
17
- }>() => hono1.MiddlewareHandler<Env, string, {}, Response>;
17
+ }>() => hono0.MiddlewareHandler<Env, string, {}, Response>;
18
18
  /**
19
19
  * Middleware that requires either:
20
20
  * 1. Org admin/owner role (can modify any channel), OR
@@ -26,6 +26,6 @@ declare const requireChannelMemberOrAdmin: <Env extends {
26
26
  Variables: ManageAppVariables;
27
27
  } = {
28
28
  Variables: ManageAppVariables;
29
- }>() => hono1.MiddlewareHandler<Env, string, {}, Response>;
29
+ }>() => hono0.MiddlewareHandler<Env, string, {}, Response>;
30
30
  //#endregion
31
31
  export { isOrgAdmin, requireChannelMemberOrAdmin, requireWorkspaceAdmin };
@@ -28,14 +28,14 @@ app.post("/commands", async (c) => {
28
28
  const timestamp = c.req.header("x-slack-request-timestamp") || "";
29
29
  const signature = c.req.header("x-slack-signature") || "";
30
30
  if (!env.SLACK_SIGNING_SECRET) {
31
- logger.error({}, "SLACK_SIGNING_SECRET not configured - rejecting request");
31
+ logger.error("SLACK_SIGNING_SECRET not configured - rejecting request");
32
32
  return c.json({
33
33
  response_type: "ephemeral",
34
34
  text: "Server configuration error"
35
35
  }, 500);
36
36
  }
37
37
  if (!verifySlackRequest(env.SLACK_SIGNING_SECRET, body, timestamp, signature)) {
38
- logger.error({}, "Invalid Slack request signature");
38
+ logger.error("Invalid Slack request signature");
39
39
  return c.json({
40
40
  response_type: "ephemeral",
41
41
  text: "Invalid request signature"
@@ -100,7 +100,7 @@ app.post("/events", async (c) => {
100
100
  if (!env.SLACK_SIGNING_SECRET) {
101
101
  outcome = "error";
102
102
  span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
103
- logger.error({}, "SLACK_SIGNING_SECRET not configured - rejecting request");
103
+ logger.error("SLACK_SIGNING_SECRET not configured - rejecting request");
104
104
  span.end();
105
105
  return c.json({ error: "Server configuration error" }, 500);
106
106
  }
@@ -114,7 +114,7 @@ app.post("/events", async (c) => {
114
114
  if (eventType === "url_verification") {
115
115
  outcome = "url_verification";
116
116
  span.setAttribute(SLACK_SPAN_KEYS.OUTCOME, outcome);
117
- logger.info({}, "Responding to Slack URL verification challenge");
117
+ logger.info("Responding to Slack URL verification challenge");
118
118
  span.end();
119
119
  return c.text(String(eventBody.challenge));
120
120
  }
@@ -146,12 +146,12 @@ app.post("/nango-webhook", async (c) => {
146
146
  const body = await c.req.text();
147
147
  const nangoSecret = env.NANGO_SLACK_SECRET_KEY || env.NANGO_SECRET_KEY;
148
148
  if (!nangoSecret) {
149
- logger.error({}, "No Nango secret key configured — rejecting webhook");
149
+ logger.error("No Nango secret key configured — rejecting webhook");
150
150
  return c.json({ error: "Server configuration error" }, 503);
151
151
  }
152
152
  const signature = c.req.header("x-nango-signature");
153
153
  if (!signature) {
154
- logger.warn({}, "Missing Nango webhook signature");
154
+ logger.warn("Missing Nango webhook signature");
155
155
  return c.json({ error: "Missing signature" }, 401);
156
156
  }
157
157
  const crypto = await import("node:crypto");
@@ -44,12 +44,12 @@ function parseOAuthState(stateStr) {
44
44
  try {
45
45
  const [data, signature] = stateStr.split(".");
46
46
  if (!data || !signature) {
47
- logger.warn({}, "OAuth state missing signature");
47
+ logger.warn("OAuth state missing signature");
48
48
  return null;
49
49
  }
50
50
  const expectedSignature = crypto$1.createHmac("sha256", getStateSigningSecret()).update(data).digest("base64url");
51
51
  if (signature.length !== expectedSignature.length || !crypto$1.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
52
- logger.warn({}, "Invalid OAuth state signature");
52
+ logger.warn("Invalid OAuth state signature");
53
53
  return null;
54
54
  }
55
55
  const decoded = Buffer.from(data, "base64url").toString("utf-8");
@@ -139,7 +139,7 @@ app.openapi(createProtectedRoute({
139
139
  return c.redirect(`${dashboardUrl}?error=${encodeURIComponent(error)}`);
140
140
  }
141
141
  if (!code) {
142
- logger.error({}, "No code provided in OAuth callback");
142
+ logger.error("No code provided in OAuth callback");
143
143
  return c.redirect(`${dashboardUrl}?error=no_code`);
144
144
  }
145
145
  try {
@@ -161,7 +161,7 @@ app.openapi(createProtectedRoute({
161
161
  } catch (fetchErr) {
162
162
  clearTimeout(timeout);
163
163
  if (fetchErr.name === "AbortError") {
164
- logger.error({}, "Slack token exchange timed out");
164
+ logger.error("Slack token exchange timed out");
165
165
  return c.redirect(`${dashboardUrl}?error=timeout`);
166
166
  }
167
167
  throw fetchErr;
@@ -1,12 +1,13 @@
1
1
  import { getLogger } from "../../logger.js";
2
2
  import runDbClient_default from "../../db/runDbClient.js";
3
- import { clearWorkspaceConnectionCache, computeWorkspaceConnectionId, deleteWorkspaceInstallation, findWorkspaceConnectionByTeamId, listWorkspaceInstallations, setWorkspaceDefaultAgent } from "../services/nango.js";
3
+ import { findWorkspaceConnectionByTeamId, listWorkspaceInstallations, setWorkspaceDefaultAgent } from "../services/nango.js";
4
4
  import { lookupAgentName } from "../services/agent-resolution.js";
5
- import { getBotMemberChannels, getSlackChannels, getSlackClient, revokeSlackToken } from "../services/client.js";
5
+ import { getBotMemberChannels, getSlackChannels, getSlackClient } from "../services/client.js";
6
+ import { cleanupWorkspaceInstallation } from "../services/workspace-cleanup.js";
6
7
  import "../services/index.js";
7
8
  import { requireChannelMemberOrAdmin, requireWorkspaceAdmin } from "../middleware/permissions.js";
8
9
  import { OpenAPIHono, z } from "@hono/zod-openapi";
9
- import { WorkAppSlackAgentConfigRequestSchema, deleteAllSlackMcpToolAccessConfigsByTenant, deleteAllWorkAppSlackChannelAgentConfigsByTeam, deleteAllWorkAppSlackUserMappingsByTeam, deleteWorkAppSlackChannelAgentConfig, deleteWorkAppSlackWorkspaceByNangoConnectionId, findWorkAppSlackChannelAgentConfig, findWorkAppSlackWorkspaceByTeamId, listWorkAppSlackChannelAgentConfigsByTeam, listWorkAppSlackUserMappingsByTeam, updateWorkAppSlackWorkspace, upsertWorkAppSlackChannelAgentConfig } from "@inkeep/agents-core";
10
+ import { WorkAppSlackAgentConfigRequestSchema, deleteWorkAppSlackChannelAgentConfig, findWorkAppSlackChannelAgentConfig, findWorkAppSlackWorkspaceByTeamId, listWorkAppSlackChannelAgentConfigsByTeam, listWorkAppSlackUserMappingsByTeam, updateWorkAppSlackWorkspace, upsertWorkAppSlackChannelAgentConfig } from "@inkeep/agents-core";
10
11
  import { createProtectedRoute, inheritedWorkAppsAuth } from "@inkeep/agents-core/middleware";
11
12
 
12
13
  //#region src/slack/routes/workspaces.ts
@@ -72,7 +73,7 @@ app.openapi(createProtectedRoute({
72
73
  try {
73
74
  const sessionTenantId = c.get("tenantId");
74
75
  if (!sessionTenantId) {
75
- logger.warn({}, "No tenantId in session context — cannot list workspaces");
76
+ logger.warn("No tenantId in session context — cannot list workspaces");
76
77
  return c.json({ workspaces: [] });
77
78
  }
78
79
  const workspaces = await listWorkspaceInstallations(sessionTenantId);
@@ -347,47 +348,21 @@ app.openapi(createProtectedRoute({
347
348
  }), async (c) => {
348
349
  const { teamId: workspaceIdentifier } = c.req.valid("param");
349
350
  let teamId;
350
- let connectionId;
351
351
  try {
352
352
  if (workspaceIdentifier.includes(":")) {
353
- connectionId = workspaceIdentifier;
354
353
  const teamMatch = workspaceIdentifier.match(/T:([A-Z0-9]+)/);
355
354
  if (!teamMatch) return c.json({ error: "Invalid connectionId format" }, 400);
356
355
  teamId = teamMatch[1];
357
- } else {
358
- teamId = workspaceIdentifier;
359
- connectionId = computeWorkspaceConnectionId({
360
- teamId,
361
- enterpriseId: void 0
362
- });
363
- }
356
+ } else teamId = workspaceIdentifier;
364
357
  const workspace = await findWorkspaceConnectionByTeamId(teamId);
365
358
  if (!workspace) return c.json({ error: "Workspace not found" }, 404);
366
- if (workspace.botToken) if (await revokeSlackToken(workspace.botToken)) logger.info({ teamId }, "Revoked Slack bot token");
367
- else logger.warn({ teamId }, "Failed to revoke Slack bot token, continuing with uninstall");
368
- const tenantId = workspace.tenantId;
369
- const deletedChannelConfigs = await deleteAllWorkAppSlackChannelAgentConfigsByTeam(runDbClient_default)(tenantId, teamId);
370
- if (deletedChannelConfigs > 0) logger.info({
371
- teamId,
372
- deletedChannelConfigs
373
- }, "Deleted channel configs for uninstalled workspace");
374
- const deletedMappings = await deleteAllWorkAppSlackUserMappingsByTeam(runDbClient_default)(tenantId, teamId);
375
- if (deletedMappings > 0) logger.info({
359
+ if (!verifyTenantOwnership(c, workspace.tenantId)) return c.json({ error: "Access denied" }, 403);
360
+ const result = await cleanupWorkspaceInstallation({ teamId });
361
+ if (!result.success) logger.error({
376
362
  teamId,
377
- deletedMappings
378
- }, "Deleted user mappings for uninstalled workspace");
379
- const deletedMcpConfigs = await deleteAllSlackMcpToolAccessConfigsByTenant(runDbClient_default)(tenantId);
380
- if (deletedMcpConfigs > 0) logger.info({
381
- teamId,
382
- deletedMcpConfigs
383
- }, "Deleted MCP tool access configs for uninstalled workspace");
384
- if (await deleteWorkAppSlackWorkspaceByNangoConnectionId(runDbClient_default)(connectionId)) logger.info({ connectionId }, "Deleted workspace from database");
385
- if (!await deleteWorkspaceInstallation(connectionId)) logger.error({ connectionId }, "deleteWorkspaceInstallation returned false (DB already cleaned up)");
386
- clearWorkspaceConnectionCache(teamId);
387
- logger.info({
388
- connectionId,
389
- teamId
390
- }, "Deleted workspace installation and cleared cache");
363
+ result
364
+ }, "Workspace uninstall partially failed");
365
+ logger.info({ teamId }, "Workspace uninstalled via API");
391
366
  return c.json({ success: true });
392
367
  } catch (error) {
393
368
  logger.error({
@@ -380,7 +380,7 @@ async function revokeSlackToken(token) {
380
380
  try {
381
381
  const result = await new WebClient(token).auth.revoke();
382
382
  if (result.ok) {
383
- logger.info({}, "Successfully revoked Slack token");
383
+ logger.info("Successfully revoked Slack token");
384
384
  return true;
385
385
  }
386
386
  logger.warn({ error: result.error }, "Token revocation returned non-ok status");
@@ -388,7 +388,7 @@ async function revokeSlackToken(token) {
388
388
  } catch (error) {
389
389
  const errorMessage = error instanceof Error ? error.message : String(error);
390
390
  if (errorMessage.includes("token_revoked") || errorMessage.includes("invalid_auth")) {
391
- logger.info({}, "Token already revoked or invalid");
391
+ logger.info("Token already revoked or invalid");
392
392
  return true;
393
393
  }
394
394
  logger.error({ error }, "Failed to revoke Slack token");
@@ -9,10 +9,10 @@ import { AgentOption } from "../modals.js";
9
9
  * Called on every @mention and /inkeep command — caching avoids redundant DB queries.
10
10
  */
11
11
  declare function findCachedUserMapping(tenantId: string, slackUserId: string, teamId: string, clientId?: string): Promise<{
12
+ slackUserId: string;
12
13
  id: string;
13
14
  createdAt: string;
14
15
  updatedAt: string;
15
- slackUserId: string;
16
16
  tenantId: string;
17
17
  clientId: string;
18
18
  slackTeamId: string;
@@ -14,5 +14,6 @@ import { PublicExecutionParams, executeAgentPublicly } from "./events/execution.
14
14
  import { handleModalSubmission } from "./events/modal-submission.js";
15
15
  import "./events/index.js";
16
16
  import { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest } from "./security.js";
17
+ import { WorkspaceCleanupResult, cleanupWorkspaceInstallation } from "./workspace-cleanup.js";
17
18
  import { getBotTokenForTeam, setBotTokenForTeam } from "./workspace-tokens.js";
18
- export { AgentConfigSources, AgentOption, AgentResolutionParams, BuildAgentSelectorModalParams, BuildMessageShortcutModalParams, ContextBlockParams, DefaultAgentConfig, InlineSelectorMetadata, ModalMetadata, PublicExecutionParams, ResolvedAgentConfig, SlackCommandPayload, SlackCommandResponse, SlackErrorType, SlackWorkspaceConnection, StreamResult, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, WorkspaceInstallData, buildAgentSelectorModal, buildCitationsBlock, buildDataArtifactBlocks, buildDataComponentBlocks, buildMessageShortcutModal, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createContextBlockFromText, createCreateInkeepAccountMessage, createErrorMessage, createNotLinkedMessage, createSmartLinkMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, executeAgentPublicly, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotMemberChannels, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserByEmail, getSlackUserInfo, getSlackUsers, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentPickerCommand, handleAppMention, handleCommand, handleDirectMessage, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleQuestionCommand, handleStatusCommand, handleToolApproval, handleUnlinkCommand, listWorkspaceInstallations, lookupAgentName, lookupProjectName, markdownToMrkdwn, openDmConversation, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, validateBotChannelMembership, verifySlackRequest };
19
+ export { AgentConfigSources, AgentOption, AgentResolutionParams, BuildAgentSelectorModalParams, BuildMessageShortcutModalParams, ContextBlockParams, DefaultAgentConfig, InlineSelectorMetadata, ModalMetadata, PublicExecutionParams, ResolvedAgentConfig, SlackCommandPayload, SlackCommandResponse, SlackErrorType, SlackWorkspaceConnection, StreamResult, ToolApprovalButtonValue, ToolApprovalButtonValueSchema, WorkspaceCleanupResult, WorkspaceInstallData, buildAgentSelectorModal, buildCitationsBlock, buildDataArtifactBlocks, buildDataComponentBlocks, buildMessageShortcutModal, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, checkIfBotThread, checkUserIsChannelMember, classifyError, cleanupWorkspaceInstallation, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createContextBlockFromText, createCreateInkeepAccountMessage, createErrorMessage, createNotLinkedMessage, createSmartLinkMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, executeAgentPublicly, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotMemberChannels, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserByEmail, getSlackUserInfo, getSlackUsers, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentPickerCommand, handleAppMention, handleCommand, handleDirectMessage, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleQuestionCommand, handleStatusCommand, handleToolApproval, handleUnlinkCommand, listWorkspaceInstallations, lookupAgentName, lookupProjectName, markdownToMrkdwn, openDmConversation, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, validateBotChannelMembership, verifySlackRequest };
@@ -13,6 +13,7 @@ import { handleDirectMessage } from "./events/direct-message.js";
13
13
  import { handleModalSubmission } from "./events/modal-submission.js";
14
14
  import "./events/index.js";
15
15
  import { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest } from "./security.js";
16
+ import { cleanupWorkspaceInstallation } from "./workspace-cleanup.js";
16
17
  import { getBotTokenForTeam, setBotTokenForTeam } from "./workspace-tokens.js";
17
18
 
18
- export { SlackErrorType, ToolApprovalButtonValueSchema, buildAgentSelectorModal, buildCitationsBlock, buildDataArtifactBlocks, buildDataComponentBlocks, buildMessageShortcutModal, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createContextBlockFromText, createCreateInkeepAccountMessage, createErrorMessage, createNotLinkedMessage, createSmartLinkMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, executeAgentPublicly, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotMemberChannels, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserByEmail, getSlackUserInfo, getSlackUsers, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentPickerCommand, handleAppMention, handleCommand, handleDirectMessage, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleQuestionCommand, handleStatusCommand, handleToolApproval, handleUnlinkCommand, listWorkspaceInstallations, lookupAgentName, lookupProjectName, markdownToMrkdwn, openDmConversation, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, validateBotChannelMembership, verifySlackRequest };
19
+ export { SlackErrorType, ToolApprovalButtonValueSchema, buildAgentSelectorModal, buildCitationsBlock, buildDataArtifactBlocks, buildDataComponentBlocks, buildMessageShortcutModal, buildSummaryBreadcrumbBlock, buildToolApprovalBlocks, buildToolApprovalDoneBlocks, buildToolApprovalExpiredBlocks, buildToolOutputErrorBlock, checkIfBotThread, checkUserIsChannelMember, classifyError, cleanupWorkspaceInstallation, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createContextBlockFromText, createCreateInkeepAccountMessage, createErrorMessage, createNotLinkedMessage, createSmartLinkMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, executeAgentPublicly, extractApiErrorMessage, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotMemberChannels, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannelInfo, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserByEmail, getSlackUserInfo, getSlackUsers, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentPickerCommand, handleAppMention, handleCommand, handleDirectMessage, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleQuestionCommand, handleStatusCommand, handleToolApproval, handleUnlinkCommand, listWorkspaceInstallations, lookupAgentName, lookupProjectName, markdownToMrkdwn, openDmConversation, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, validateBotChannelMembership, verifySlackRequest };
@@ -57,7 +57,7 @@ function getSlackIntegrationId() {
57
57
  }
58
58
  async function createConnectSession(params) {
59
59
  if (isSlackDevMode()) {
60
- logger.debug({}, "Skipping Nango connect session in dev mode");
60
+ logger.debug("Skipping Nango connect session in dev mode");
61
61
  return null;
62
62
  }
63
63
  try {
@@ -340,7 +340,7 @@ async function storeWorkspaceInstallation(data) {
340
340
  const integrationId = getSlackIntegrationId();
341
341
  const secretKey = env.NANGO_SLACK_SECRET_KEY || env.NANGO_SECRET_KEY;
342
342
  if (!secretKey) {
343
- logger.error({}, "No Nango secret key available");
343
+ logger.error("No Nango secret key available");
344
344
  return {
345
345
  connectionId,
346
346
  success: false
@@ -22,7 +22,7 @@ function verifySlackRequest(signingSecret, requestBody, timestamp, signature) {
22
22
  try {
23
23
  const fiveMinutesAgo = Math.floor(Date.now() / 1e3) - 300;
24
24
  if (Number.parseInt(timestamp, 10) < fiveMinutesAgo) {
25
- logger.warn({}, "Slack request timestamp too old");
25
+ logger.warn("Slack request timestamp too old");
26
26
  return false;
27
27
  }
28
28
  const sigBaseString = `v0:${timestamp}:${requestBody}`;
@@ -0,0 +1,17 @@
1
+ //#region src/slack/services/workspace-cleanup.d.ts
2
+ interface WorkspaceCleanupResult {
3
+ success: boolean;
4
+ teamId: string;
5
+ tokenRevoked: boolean;
6
+ dbCleaned: boolean;
7
+ nangoCleaned: boolean;
8
+ }
9
+ declare function cleanupWorkspaceInstallation({
10
+ teamId,
11
+ skipTokenRevocation
12
+ }: {
13
+ teamId: string;
14
+ skipTokenRevocation?: boolean;
15
+ }): Promise<WorkspaceCleanupResult>;
16
+ //#endregion
17
+ export { WorkspaceCleanupResult, cleanupWorkspaceInstallation };
@@ -0,0 +1,82 @@
1
+ import { getLogger } from "../../logger.js";
2
+ import runDbClient_default from "../../db/runDbClient.js";
3
+ import { clearWorkspaceConnectionCache, deleteWorkspaceInstallation, findWorkspaceConnectionByTeamId } from "./nango.js";
4
+ import { revokeSlackToken } from "./client.js";
5
+ import "./index.js";
6
+ import { deleteAllSlackMcpToolAccessConfigsByTenant, deleteAllWorkAppSlackChannelAgentConfigsByTeam, deleteAllWorkAppSlackUserMappingsByTeam, deleteWorkAppSlackWorkspaceByNangoConnectionId } from "@inkeep/agents-core";
7
+
8
+ //#region src/slack/services/workspace-cleanup.ts
9
+ const logger = getLogger("slack-workspace-cleanup");
10
+ async function cleanupWorkspaceInstallation({ teamId, skipTokenRevocation = false }) {
11
+ const result = {
12
+ success: false,
13
+ teamId,
14
+ tokenRevoked: false,
15
+ dbCleaned: false,
16
+ nangoCleaned: false
17
+ };
18
+ const workspace = await findWorkspaceConnectionByTeamId(teamId);
19
+ if (!workspace) {
20
+ logger.warn({ teamId }, "No workspace found for cleanup");
21
+ clearWorkspaceConnectionCache(teamId);
22
+ result.success = true;
23
+ return result;
24
+ }
25
+ const { tenantId, botToken, connectionId } = workspace;
26
+ if (!skipTokenRevocation && botToken) {
27
+ result.tokenRevoked = await revokeSlackToken(botToken);
28
+ if (result.tokenRevoked) logger.info({ teamId }, "Revoked Slack bot token during cleanup");
29
+ else logger.warn({ teamId }, "Failed to revoke Slack bot token, continuing with cleanup");
30
+ }
31
+ const steps = [
32
+ {
33
+ name: "channel_configs",
34
+ run: () => deleteAllWorkAppSlackChannelAgentConfigsByTeam(runDbClient_default)(tenantId, teamId)
35
+ },
36
+ {
37
+ name: "user_mappings",
38
+ run: () => deleteAllWorkAppSlackUserMappingsByTeam(runDbClient_default)(tenantId, teamId)
39
+ },
40
+ {
41
+ name: "mcp_configs",
42
+ run: () => deleteAllSlackMcpToolAccessConfigsByTenant(runDbClient_default)(tenantId)
43
+ },
44
+ {
45
+ name: "workspace_row",
46
+ run: () => deleteWorkAppSlackWorkspaceByNangoConnectionId(runDbClient_default)(connectionId)
47
+ },
48
+ {
49
+ name: "nango_connection",
50
+ run: () => deleteWorkspaceInstallation(connectionId)
51
+ }
52
+ ];
53
+ const failures = [];
54
+ for (const step of steps) try {
55
+ await step.run();
56
+ } catch (error) {
57
+ failures.push(step.name);
58
+ logger.error({
59
+ error,
60
+ teamId,
61
+ connectionId,
62
+ step: step.name
63
+ }, "Cleanup step failed");
64
+ }
65
+ result.dbCleaned = !failures.some((f) => f !== "nango_connection");
66
+ result.nangoCleaned = !failures.includes("nango_connection");
67
+ clearWorkspaceConnectionCache(teamId);
68
+ result.success = failures.length === 0;
69
+ if (failures.length > 0) logger.error({
70
+ teamId,
71
+ connectionId,
72
+ failures
73
+ }, "Workspace cleanup completed with failures");
74
+ else logger.info({
75
+ teamId,
76
+ connectionId
77
+ }, "Workspace cleanup completed");
78
+ return result;
79
+ }
80
+
81
+ //#endregion
82
+ export { cleanupWorkspaceInstallation };
@@ -7,7 +7,6 @@ var oauth_config = {
7
7
  "app_mentions:read",
8
8
  "channels:history",
9
9
  "channels:read",
10
- "channels:join",
11
10
  "chat:write",
12
11
  "chat:write.public",
13
12
  "commands",
@@ -23,10 +22,7 @@ var oauth_config = {
23
22
  "mpim:write",
24
23
  "team:read",
25
24
  "users:read",
26
- "users:read.email",
27
- "search:read.public",
28
- "search:read.files",
29
- "search:read.users"
25
+ "users:read.email"
30
26
  ]
31
27
  }
32
28
  };
@@ -11,7 +11,7 @@ const logger = getLogger("slack-socket-mode");
11
11
  const GLOBAL_KEY = "__inkeep_slack_socket_mode_client__";
12
12
  async function startSocketMode(appToken) {
13
13
  if (globalThis[GLOBAL_KEY]) {
14
- logger.info({}, "Socket Mode client already running (HMR reload detected), skipping");
14
+ logger.info("Socket Mode client already running (HMR reload detected), skipping");
15
15
  return;
16
16
  }
17
17
  const { SocketModeClient } = await import("@slack/socket-mode");
@@ -19,7 +19,7 @@ async function startSocketMode(appToken) {
19
19
  setupSocketModeListeners(client);
20
20
  await client.start();
21
21
  globalThis[GLOBAL_KEY] = client;
22
- logger.info({}, "Slack Socket Mode client started");
22
+ logger.info("Slack Socket Mode client started");
23
23
  }
24
24
  function registerBackgroundWork(work) {
25
25
  work.catch((err) => {
@@ -31,10 +31,10 @@ function setupSocketModeListeners(client) {
31
31
  logger.error({ error: args[0] }, "Socket Mode client error");
32
32
  });
33
33
  client.on("disconnected", () => {
34
- logger.warn({}, "Socket Mode client disconnected");
34
+ logger.warn("Socket Mode client disconnected");
35
35
  });
36
36
  client.on("reconnecting", () => {
37
- logger.info({}, "Socket Mode client reconnecting...");
37
+ logger.info("Socket Mode client reconnecting...");
38
38
  });
39
39
  client.on("slack_event", async (...args) => {
40
40
  const { ack, body, type } = args[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkeep/agents-work-apps",
3
- "version": "0.65.1",
3
+ "version": "0.66.0",
4
4
  "description": "First party integrations for Inkeep Agents",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -35,7 +35,7 @@
35
35
  "minimatch": "^10.2.1",
36
36
  "oxfmt": "^0.42.0",
37
37
  "slack-block-builder": "^2.8.0",
38
- "@inkeep/agents-core": "0.65.1"
38
+ "@inkeep/agents-core": "0.66.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@hono/zod-openapi": "^1.1.5",