Skip to content

Commit 58d2851

Browse files
fix(slack): require bot name; propagate rotated bot_user_id to webhooks on reconnect
- the setup wizard now requires a bot name (canAdvance), so the credential name, manifest app name, and uniqueness key all use the user's choice instead of the shared Slack team-name fallback that collided for a second bot in one workspace - reconnect that changes the bot user id (recreated Slack app) now updates the bot_user_id cached in each bound webhook's providerConfig, so reaction self-drop keeps working instead of letting the bot's own reactions re-enter
1 parent 470e3c5 commit 58d2851

4 files changed

Lines changed: 26 additions & 3 deletions

File tree

apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ export function ConnectSlackBotModal({
200200
title={isReconnect ? 'Reconnect a custom Slack bot' : 'Create a custom Slack bot'}
201201
doneLabel='Done'
202202
>
203-
<Wizard.Step title='Configure your bot'>
203+
{/* Bot name is required so the credential name, the manifest app name, and
204+
uniqueness all use the user's choice — never the shared Slack team name
205+
fallback, which collides for a second bot in the same workspace. */}
206+
<Wizard.Step title='Configure your bot' canAdvance={appName.trim().length > 0}>
204207
<StepConfigure
205208
appName={appName}
206209
onAppNameChange={setAppName}

apps/sim/lib/credentials/orchestration/index.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
22
import { db } from '@sim/db'
3-
import { credential, environment, workspaceEnvironment } from '@sim/db/schema'
3+
import { credential, environment, webhook, workspaceEnvironment } from '@sim/db/schema'
44
import { createLogger } from '@sim/logger'
55
import { generateId } from '@sim/utils/id'
6-
import { eq } from 'drizzle-orm'
6+
import { and, eq, sql } from 'drizzle-orm'
77
import type { NextRequest } from 'next/server'
88
import { encryptSecret } from '@/lib/core/security/encryption'
99
import { getCredentialActorContext } from '@/lib/credentials/access'
@@ -124,6 +124,7 @@ export async function performUpdateCredential(
124124
params.botToken !== undefined ||
125125
params.apiToken !== undefined ||
126126
params.domain !== undefined
127+
let rotatedSlackBotUserId: string | undefined
127128
if (hasRotationSecret && access.credential.type === 'service_account') {
128129
try {
129130
const secret = await verifyAndBuildServiceAccountSecret(
@@ -136,6 +137,7 @@ export async function performUpdateCredential(
136137
}
137138
)
138139
updates.encryptedServiceAccountKey = secret.encryptedServiceAccountKey
140+
rotatedSlackBotUserId = secret.botUserId
139141
} catch (error) {
140142
if (error instanceof ServiceAccountSecretError) {
141143
return { success: false, error: error.message, errorCode: 'validation' }
@@ -169,6 +171,20 @@ export async function performUpdateCredential(
169171
updates.updatedAt = new Date()
170172
await db.update(credential).set(updates).where(eq(credential.id, params.credentialId))
171173

174+
// Reconnecting to a recreated Slack app changes the bot user id, but each
175+
// deployed webhook cached the old one at deploy for reaction self-drop.
176+
// Propagate the rotated id to the credential's live custom-bot webhooks so
177+
// the bot's own reactions keep being dropped (a stale id lets them re-enter).
178+
if (rotatedSlackBotUserId) {
179+
await db
180+
.update(webhook)
181+
.set({
182+
providerConfig: sql`jsonb_set((${webhook.providerConfig})::jsonb, '{bot_user_id}', to_jsonb(${rotatedSlackBotUserId}::text))::json`,
183+
updatedAt: new Date(),
184+
})
185+
.where(and(eq(webhook.provider, 'slack'), eq(webhook.routingKey, params.credentialId)))
186+
}
187+
172188
const updatedFields = Object.keys(updates).filter((key) => key !== 'updatedAt')
173189
recordAudit({
174190
workspaceId: access.credential.workspaceId,

apps/sim/lib/credentials/service-account-secret.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe('verifyAndBuildServiceAccountSecret', () => {
5959
expect(result.providerId).toBe(SLACK_CUSTOM_BOT_PROVIDER_ID)
6060
expect(result.displayName).toBe('Acme')
6161
expect(result.auditMetadata.slackTeamId).toBe('T1')
62+
expect(result.botUserId).toBe('U_BOT')
6263
const blob = JSON.parse(result.encryptedServiceAccountKey)
6364
expect(blob).toMatchObject({
6465
signingSecret: 'sec',

apps/sim/lib/credentials/service-account-secret.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export interface ServiceAccountSecretResult {
2929
encryptedServiceAccountKey: string
3030
displayName: string
3131
auditMetadata: Record<string, string>
32+
/** Slack custom bot: the derived bot user id (for reaction self-drop). */
33+
botUserId?: string
3234
}
3335

3436
/** Thrown when a service-account secret is missing or fails provider verification. */
@@ -114,6 +116,7 @@ export async function verifyAndBuildServiceAccountSecret(
114116
encryptedServiceAccountKey: encrypted,
115117
displayName: teamName || 'Slack bot',
116118
auditMetadata: { slackTeamId: teamId },
119+
botUserId,
117120
}
118121
}
119122

0 commit comments

Comments
 (0)