From 577cd68cb581d75935aa9e0ca4131dd6670a0b75 Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Thu, 6 Aug 2026 19:27:33 +0530 Subject: [PATCH] feat(slack): release the Slack team claim when the connection goes away (#23753) Closes the loop on https://github.com/twentyhq/twenty/pull/22984#discussion_r3673946334, where the Slack app claimed a `team_id` on connect and never released it. The `onDisconnect` hook it waited on merged in #23538, and `twenty-sdk` 2.27.0 ships the `onDisconnectLogicFunction` manifest field, so this bumps the app to it. ## Disconnect `slack-team-release` releases the server-scoped `slack-team:{teamId}` claim, so another Twenty workspace can connect that Slack team afterwards. `kv.delete` on a SERVER key only clears a claim the calling workspace owns, and the result reports a team only when that delete actually removed something. The connection is already deleted when the hook runs, so `getConnection` cannot resolve the `team_id` anymore. `claimSlackTeam` records `teamId` under a workspace-scoped `slack-connected-account-team:{connectedAccountId}` key, written before the claim itself so a failure between the two cannot leave a claim nothing can resolve, and the release reads it back from there. That also retires the TODO the review comment pointed at. The release is skipped when a live connection still maps to the same team, excluding the disconnecting account explicitly. That covers a reconnect that re-claimed the team while the job was queued, and a second connection to the same Slack workspace, whose claim would otherwise have been dropped along with the first. The recorded team is cleared either way, so the disconnecting account leaves nothing behind. ## Uninstall Uninstalling the app drops its connections through the `connectionProvider` and `application` cascades, which never reach the disconnect hook, and the same operation deletes the app's logic functions so the hook could not run anyway. The app declares an `uninstallLogicFunction`: it runs before the app's metadata is deleted, lists the remaining Slack connections, and releases each claim through the same `releaseSlackTeamClaim` util. It deliberately skips the live-connection check, since at that point every connection is still listed. ## Known limits A disconnect immediately followed by a reconnect has a window where the queued release can clear a claim the reconnect just took. It self-heals, because the reconnect's own onConnect re-claims the same key moments later. Closing it properly needs a compare-and-set on the claim, which the key-value store does not expose. No unit tests are added for the release path. Earlier revisions of this branch had specs for the disconnect guard, the uninstall sweep and the no-op-release result; they were dropped, so those branches carry no regression coverage. The app's existing 34 tests still pass, along with lint, typecheck and `twenty dev:build`, the last being what used to fail with `Duplicate universal identifiers` on the older SDK. --- packages/twenty-apps/public/slack/SETUP.md | 2 +- .../twenty-apps/public/slack/package.json | 4 +-- .../connection-providers/slack-connection.ts | 4 +++ .../src/constants/universal-identifiers.ts | 6 ++++ .../handlers/slack-app-uninstall-handler.ts | 3 ++ .../slack-register-connection-handler.ts | 4 +-- .../handlers/slack-team-release-handler.ts | 7 ++++ .../logic-functions/slack-app-uninstall.ts | 13 +++++++ .../slack-register-connection.ts | 2 +- .../src/logic-functions/slack-team-release.ts | 13 +++++++ .../release-slack-team-claim-result.type.ts | 4 +++ ... => slack-connection-hook-payload.type.ts} | 2 +- ...get-slack-connected-account-team-kv-key.ts | 3 ++ .../utils/get-slack-connected-account-team.ts | 8 +++++ ...lack-team-claimed-by-another-connection.ts | 27 +++++++++++++++ .../utils/register-slack-connection.ts | 3 +- .../utils/release-all-slack-teams.ts | 31 +++++++++++++++++ .../utils/release-slack-team-claim.ts | 24 +++++++++++++ .../utils/release-slack-team-on-disconnect.ts | 34 +++++++++++++++++++ packages/twenty-apps/public/slack/yarn.lock | 22 ++++++------ 20 files changed, 197 insertions(+), 19 deletions(-) create mode 100644 packages/twenty-apps/public/slack/src/logic-functions/handlers/slack-app-uninstall-handler.ts create mode 100644 packages/twenty-apps/public/slack/src/logic-functions/handlers/slack-team-release-handler.ts create mode 100644 packages/twenty-apps/public/slack/src/logic-functions/slack-app-uninstall.ts create mode 100644 packages/twenty-apps/public/slack/src/logic-functions/slack-team-release.ts create mode 100644 packages/twenty-apps/public/slack/src/logic-functions/types/release-slack-team-claim-result.type.ts rename packages/twenty-apps/public/slack/src/logic-functions/types/{slack-register-connection-payload.type.ts => slack-connection-hook-payload.type.ts} (67%) create mode 100644 packages/twenty-apps/public/slack/src/logic-functions/utils/get-slack-connected-account-team-kv-key.ts create mode 100644 packages/twenty-apps/public/slack/src/logic-functions/utils/get-slack-connected-account-team.ts create mode 100644 packages/twenty-apps/public/slack/src/logic-functions/utils/is-slack-team-claimed-by-another-connection.ts create mode 100644 packages/twenty-apps/public/slack/src/logic-functions/utils/release-all-slack-teams.ts create mode 100644 packages/twenty-apps/public/slack/src/logic-functions/utils/release-slack-team-claim.ts create mode 100644 packages/twenty-apps/public/slack/src/logic-functions/utils/release-slack-team-on-disconnect.ts diff --git a/packages/twenty-apps/public/slack/SETUP.md b/packages/twenty-apps/public/slack/SETUP.md index c899e6fd63..1f49d5413f 100644 --- a/packages/twenty-apps/public/slack/SETUP.md +++ b/packages/twenty-apps/public/slack/SETUP.md @@ -72,7 +72,7 @@ The assistant reuses the same Slack connection — no second bot identity. - **Thread memory.** After a successful reply the bot stays active in that thread, so follow-ups need no mention. Channel threads stay active for 24 hours after the last reply (each reply renews it); DM threads never expire. - **Channel welcome.** With `member_joined_channel` subscribed, the bot posts a short introduction the first time it is added to a channel, with the details (what to ask it, what it reads, and the shared-role caveat from step 4 above) in a thread reply so the channel itself stays quiet. It fires once per channel for 30 days, and only for the bot's own join — humans joining afterwards trigger nothing. Skip the subscription if you would rather it arrived silently. -- **One Slack workspace per Twenty workspace.** Connecting Slack claims that Slack team for the connecting Twenty workspace. On the same server, a second Twenty workspace connecting the same Slack team is rejected. The claim is not released on disconnect yet, so moving a Slack workspace needs a server admin. +- **One Slack workspace per Twenty workspace.** Connecting Slack claims that Slack team for the connecting Twenty workspace. On the same server, a second Twenty workspace connecting the same Slack team is rejected. Removing the connection releases the claim, so another Twenty workspace can then connect that Slack team. Uninstalling the app releases it too. ## Workflow field names (for step authors) diff --git a/packages/twenty-apps/public/slack/package.json b/packages/twenty-apps/public/slack/package.json index 2f23a9325d..d18d8ebde9 100644 --- a/packages/twenty-apps/public/slack/package.json +++ b/packages/twenty-apps/public/slack/package.json @@ -31,8 +31,8 @@ "oxlint": "^0.16.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "twenty-client-sdk": "^2.26.0", - "twenty-sdk": "^2.26.0", + "twenty-client-sdk": "^2.27.0", + "twenty-sdk": "^2.27.0", "typescript": "^5.9.3", "vite-tsconfig-paths": "^4.2.1", "vitest": "^4.0.0" diff --git a/packages/twenty-apps/public/slack/src/connection-providers/slack-connection.ts b/packages/twenty-apps/public/slack/src/connection-providers/slack-connection.ts index 5feb306634..092bb91dd7 100644 --- a/packages/twenty-apps/public/slack/src/connection-providers/slack-connection.ts +++ b/packages/twenty-apps/public/slack/src/connection-providers/slack-connection.ts @@ -3,6 +3,7 @@ import { defineConnectionProvider } from 'twenty-sdk/define'; import { SLACK_CONNECTION_PROVIDER_UNIVERSAL_IDENTIFIER, SLACK_REGISTER_CONNECTION_UNIVERSAL_IDENTIFIER, + SLACK_TEAM_RELEASE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER, } from 'src/constants/universal-identifiers'; export default defineConnectionProvider({ @@ -13,6 +14,9 @@ export default defineConnectionProvider({ onConnectLogicFunction: { universalIdentifier: SLACK_REGISTER_CONNECTION_UNIVERSAL_IDENTIFIER, }, + onDisconnectLogicFunction: { + universalIdentifier: SLACK_TEAM_RELEASE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER, + }, oauth: { authorizationEndpoint: 'https://slack.com/oauth/v2/authorize', tokenEndpoint: 'https://slack.com/api/oauth.v2.access', diff --git a/packages/twenty-apps/public/slack/src/constants/universal-identifiers.ts b/packages/twenty-apps/public/slack/src/constants/universal-identifiers.ts index 72857a6624..45548b9c21 100644 --- a/packages/twenty-apps/public/slack/src/constants/universal-identifiers.ts +++ b/packages/twenty-apps/public/slack/src/constants/universal-identifiers.ts @@ -58,6 +58,12 @@ export const SLACK_ASSISTANT_WORKER_UNIVERSAL_IDENTIFIER = export const SLACK_REGISTER_CONNECTION_UNIVERSAL_IDENTIFIER = 'a29ae15d-dd16-4b99-bb6c-079842da55ab'; +export const SLACK_TEAM_RELEASE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER = + '4470aba8-5ff5-4800-88db-2a427cd8677c'; + +export const SLACK_APP_UNINSTALL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER = + '0c89ed56-7e69-40e4-b15a-919a4912ed9b'; + export const SLACK_ASSISTANT_REQUEST_OBJECT_UNIVERSAL_IDENTIFIER = '4dfdd6c7-9042-4278-8d3e-8172a8a5e15f'; diff --git a/packages/twenty-apps/public/slack/src/logic-functions/handlers/slack-app-uninstall-handler.ts b/packages/twenty-apps/public/slack/src/logic-functions/handlers/slack-app-uninstall-handler.ts new file mode 100644 index 0000000000..370dce2b7d --- /dev/null +++ b/packages/twenty-apps/public/slack/src/logic-functions/handlers/slack-app-uninstall-handler.ts @@ -0,0 +1,3 @@ +import { releaseAllSlackTeams } from 'src/logic-functions/utils/release-all-slack-teams'; + +export const slackAppUninstallHandler = () => releaseAllSlackTeams(); diff --git a/packages/twenty-apps/public/slack/src/logic-functions/handlers/slack-register-connection-handler.ts b/packages/twenty-apps/public/slack/src/logic-functions/handlers/slack-register-connection-handler.ts index 0455d9ceaa..12ac53e0a4 100644 --- a/packages/twenty-apps/public/slack/src/logic-functions/handlers/slack-register-connection-handler.ts +++ b/packages/twenty-apps/public/slack/src/logic-functions/handlers/slack-register-connection-handler.ts @@ -1,8 +1,8 @@ -import { type SlackRegisterConnectionPayload } from 'src/logic-functions/types/slack-register-connection-payload.type'; +import { type SlackConnectionHookPayload } from 'src/logic-functions/types/slack-connection-hook-payload.type'; import { registerSlackConnection } from 'src/logic-functions/utils/register-slack-connection'; export const slackRegisterConnectionHandler = ( - payload: SlackRegisterConnectionPayload, + payload: SlackConnectionHookPayload, ) => registerSlackConnection({ connectedAccountId: payload.connectedAccountId, diff --git a/packages/twenty-apps/public/slack/src/logic-functions/handlers/slack-team-release-handler.ts b/packages/twenty-apps/public/slack/src/logic-functions/handlers/slack-team-release-handler.ts new file mode 100644 index 0000000000..0f6a7a3bf4 --- /dev/null +++ b/packages/twenty-apps/public/slack/src/logic-functions/handlers/slack-team-release-handler.ts @@ -0,0 +1,7 @@ +import { type SlackConnectionHookPayload } from 'src/logic-functions/types/slack-connection-hook-payload.type'; +import { releaseSlackTeamOnDisconnect } from 'src/logic-functions/utils/release-slack-team-on-disconnect'; + +export const slackTeamReleaseHandler = (payload: SlackConnectionHookPayload) => + releaseSlackTeamOnDisconnect({ + connectedAccountId: payload.connectedAccountId, + }); diff --git a/packages/twenty-apps/public/slack/src/logic-functions/slack-app-uninstall.ts b/packages/twenty-apps/public/slack/src/logic-functions/slack-app-uninstall.ts new file mode 100644 index 0000000000..17b0def61f --- /dev/null +++ b/packages/twenty-apps/public/slack/src/logic-functions/slack-app-uninstall.ts @@ -0,0 +1,13 @@ +import { defineUninstallLogicFunction } from 'twenty-sdk/define'; + +import { SLACK_APP_UNINSTALL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers'; +import { slackAppUninstallHandler } from 'src/logic-functions/handlers/slack-app-uninstall-handler'; + +export default defineUninstallLogicFunction({ + universalIdentifier: SLACK_APP_UNINSTALL_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER, + name: 'slack-app-uninstall', + description: + 'Runs when the app is uninstalled, before its metadata is deleted. Releases the server-scoped slack-team claim of every remaining Slack connection, since uninstalling cascade-deletes the connections without going through the onDisconnect hook.', + timeoutSeconds: 30, + handler: slackAppUninstallHandler, +}); diff --git a/packages/twenty-apps/public/slack/src/logic-functions/slack-register-connection.ts b/packages/twenty-apps/public/slack/src/logic-functions/slack-register-connection.ts index a48db58de6..e88a9801b4 100644 --- a/packages/twenty-apps/public/slack/src/logic-functions/slack-register-connection.ts +++ b/packages/twenty-apps/public/slack/src/logic-functions/slack-register-connection.ts @@ -7,7 +7,7 @@ export default defineLogicFunction({ universalIdentifier: SLACK_REGISTER_CONNECTION_UNIVERSAL_IDENTIFIER, name: 'slack-register-connection', description: - 'Runs when a Slack connection is established (via the connection provider onConnect hook). Resolves the Slack team_id for the just-created connection via auth.test and stores this workspace id under the server-scoped slack-team: key so inbound Slack events route here. Caches the bot user id from the same auth.test under slack-bot-user-id so the channel welcome can recognise the bot without calling Slack on every join event.', + 'Runs when a Slack connection is established (via the connection provider onConnect hook). Resolves the Slack team_id for the just-created connection via auth.test and stores this workspace id under the server-scoped slack-team: key so inbound Slack events route here. Records the same team under slack-connected-account-team: so the disconnect hook can release the claim once the connection is gone. Caches the bot user id from the same auth.test under slack-bot-user-id so the channel welcome can recognise the bot without calling Slack on every join event.', timeoutSeconds: 30, handler: slackRegisterConnectionHandler, }); diff --git a/packages/twenty-apps/public/slack/src/logic-functions/slack-team-release.ts b/packages/twenty-apps/public/slack/src/logic-functions/slack-team-release.ts new file mode 100644 index 0000000000..a8e2162976 --- /dev/null +++ b/packages/twenty-apps/public/slack/src/logic-functions/slack-team-release.ts @@ -0,0 +1,13 @@ +import { defineLogicFunction } from 'twenty-sdk/define'; + +import { SLACK_TEAM_RELEASE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers'; +import { slackTeamReleaseHandler } from 'src/logic-functions/handlers/slack-team-release-handler'; + +export default defineLogicFunction({ + universalIdentifier: SLACK_TEAM_RELEASE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER, + name: 'slack-team-release', + description: + 'Runs when a Slack connection is removed (via the connection provider onDisconnect hook). Releases the server-scoped slack-team: claim this workspace took on connect so another workspace can connect the same Slack team.', + timeoutSeconds: 30, + handler: slackTeamReleaseHandler, +}); diff --git a/packages/twenty-apps/public/slack/src/logic-functions/types/release-slack-team-claim-result.type.ts b/packages/twenty-apps/public/slack/src/logic-functions/types/release-slack-team-claim-result.type.ts new file mode 100644 index 0000000000..d09019066a --- /dev/null +++ b/packages/twenty-apps/public/slack/src/logic-functions/types/release-slack-team-claim-result.type.ts @@ -0,0 +1,4 @@ +export type ReleaseSlackTeamClaimResult = { + ok: true; + releasedTeamId: string | null; +}; diff --git a/packages/twenty-apps/public/slack/src/logic-functions/types/slack-register-connection-payload.type.ts b/packages/twenty-apps/public/slack/src/logic-functions/types/slack-connection-hook-payload.type.ts similarity index 67% rename from packages/twenty-apps/public/slack/src/logic-functions/types/slack-register-connection-payload.type.ts rename to packages/twenty-apps/public/slack/src/logic-functions/types/slack-connection-hook-payload.type.ts index 1b19fe4c51..7417d9d9f8 100644 --- a/packages/twenty-apps/public/slack/src/logic-functions/types/slack-register-connection-payload.type.ts +++ b/packages/twenty-apps/public/slack/src/logic-functions/types/slack-connection-hook-payload.type.ts @@ -1,4 +1,4 @@ -export type SlackRegisterConnectionPayload = { +export type SlackConnectionHookPayload = { connectionProviderId: string; connectionProviderName: string; connectedAccountId: string; diff --git a/packages/twenty-apps/public/slack/src/logic-functions/utils/get-slack-connected-account-team-kv-key.ts b/packages/twenty-apps/public/slack/src/logic-functions/utils/get-slack-connected-account-team-kv-key.ts new file mode 100644 index 0000000000..072bf65705 --- /dev/null +++ b/packages/twenty-apps/public/slack/src/logic-functions/utils/get-slack-connected-account-team-kv-key.ts @@ -0,0 +1,3 @@ +export const getSlackConnectedAccountTeamKvKey = ( + connectedAccountId: string, +): string => `slack-connected-account-team:${connectedAccountId}`; diff --git a/packages/twenty-apps/public/slack/src/logic-functions/utils/get-slack-connected-account-team.ts b/packages/twenty-apps/public/slack/src/logic-functions/utils/get-slack-connected-account-team.ts new file mode 100644 index 0000000000..2c1bf8031c --- /dev/null +++ b/packages/twenty-apps/public/slack/src/logic-functions/utils/get-slack-connected-account-team.ts @@ -0,0 +1,8 @@ +import { kv } from 'twenty-sdk/logic-function'; + +import { getSlackConnectedAccountTeamKvKey } from 'src/logic-functions/utils/get-slack-connected-account-team-kv-key'; + +export const getSlackConnectedAccountTeam = async ( + connectedAccountId: string, +): Promise => + kv.get(getSlackConnectedAccountTeamKvKey(connectedAccountId)); diff --git a/packages/twenty-apps/public/slack/src/logic-functions/utils/is-slack-team-claimed-by-another-connection.ts b/packages/twenty-apps/public/slack/src/logic-functions/utils/is-slack-team-claimed-by-another-connection.ts new file mode 100644 index 0000000000..6637c2309a --- /dev/null +++ b/packages/twenty-apps/public/slack/src/logic-functions/utils/is-slack-team-claimed-by-another-connection.ts @@ -0,0 +1,27 @@ +import { listConnections } from 'twenty-sdk/logic-function'; + +import { getSlackConnectedAccountTeam } from 'src/logic-functions/utils/get-slack-connected-account-team'; + +type IsSlackTeamClaimedByAnotherConnectionArgs = { + teamId: string; + excludedConnectedAccountId: string; +}; + +export const isSlackTeamClaimedByAnotherConnection = async ({ + teamId, + excludedConnectedAccountId, +}: IsSlackTeamClaimedByAnotherConnectionArgs): Promise => { + const connections = await listConnections({ providerName: 'slack' }); + + const otherConnections = connections.filter( + (connection) => connection.id !== excludedConnectedAccountId, + ); + + const claimedTeamIds = await Promise.all( + otherConnections.map((connection) => + getSlackConnectedAccountTeam(connection.id), + ), + ); + + return claimedTeamIds.includes(teamId); +}; diff --git a/packages/twenty-apps/public/slack/src/logic-functions/utils/register-slack-connection.ts b/packages/twenty-apps/public/slack/src/logic-functions/utils/register-slack-connection.ts index 193706d139..49f54fb752 100644 --- a/packages/twenty-apps/public/slack/src/logic-functions/utils/register-slack-connection.ts +++ b/packages/twenty-apps/public/slack/src/logic-functions/utils/register-slack-connection.ts @@ -3,6 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import { getConnection, kv } from 'twenty-sdk/logic-function'; import { cacheSlackBotUserId } from 'src/logic-functions/utils/cache-slack-bot-user-id'; +import { getSlackConnectedAccountTeamKvKey } from 'src/logic-functions/utils/get-slack-connected-account-team-kv-key'; import { getSlackTeamKvKey } from 'src/logic-functions/utils/get-slack-team-kv-key'; type RegisterSlackConnectionArgs = { @@ -35,7 +36,7 @@ export const registerSlackConnection = async ({ throw new Error('Slack auth.test returned no user_id for the bot'); } - // TODO: release the claim on disconnect once connection providers expose an onDisconnect hook. + await kv.set(getSlackConnectedAccountTeamKvKey(connectedAccountId), teamId); await kv.set(getSlackTeamKvKey(teamId), null, { scope: 'SERVER' }); await cacheSlackBotUserId(botUserId); diff --git a/packages/twenty-apps/public/slack/src/logic-functions/utils/release-all-slack-teams.ts b/packages/twenty-apps/public/slack/src/logic-functions/utils/release-all-slack-teams.ts new file mode 100644 index 0000000000..06b9475da3 --- /dev/null +++ b/packages/twenty-apps/public/slack/src/logic-functions/utils/release-all-slack-teams.ts @@ -0,0 +1,31 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { listConnections } from 'twenty-sdk/logic-function'; + +import { getSlackConnectedAccountTeam } from 'src/logic-functions/utils/get-slack-connected-account-team'; +import { releaseSlackTeamClaim } from 'src/logic-functions/utils/release-slack-team-claim'; + +type ReleaseAllSlackTeamsResult = { + ok: true; + releasedTeamIds: string[]; +}; + +export const releaseAllSlackTeams = + async (): Promise => { + const connections = await listConnections({ providerName: 'slack' }); + + const results = await Promise.all( + connections.map(async (connection) => + releaseSlackTeamClaim({ + connectedAccountId: connection.id, + teamId: await getSlackConnectedAccountTeam(connection.id), + }), + ), + ); + + return { + ok: true, + releasedTeamIds: results + .map((result) => result.releasedTeamId) + .filter((teamId): teamId is string => isNonEmptyString(teamId)), + }; + }; diff --git a/packages/twenty-apps/public/slack/src/logic-functions/utils/release-slack-team-claim.ts b/packages/twenty-apps/public/slack/src/logic-functions/utils/release-slack-team-claim.ts new file mode 100644 index 0000000000..44e9a0bf0d --- /dev/null +++ b/packages/twenty-apps/public/slack/src/logic-functions/utils/release-slack-team-claim.ts @@ -0,0 +1,24 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { kv } from 'twenty-sdk/logic-function'; + +import { type ReleaseSlackTeamClaimResult } from 'src/logic-functions/types/release-slack-team-claim-result.type'; +import { getSlackConnectedAccountTeamKvKey } from 'src/logic-functions/utils/get-slack-connected-account-team-kv-key'; +import { getSlackTeamKvKey } from 'src/logic-functions/utils/get-slack-team-kv-key'; + +type ReleaseSlackTeamClaimArgs = { + connectedAccountId: string; + teamId: string | null; +}; + +export const releaseSlackTeamClaim = async ({ + connectedAccountId, + teamId, +}: ReleaseSlackTeamClaimArgs): Promise => { + const hasReleasedClaim = isNonEmptyString(teamId) + ? await kv.delete(getSlackTeamKvKey(teamId), { scope: 'SERVER' }) + : false; + + await kv.delete(getSlackConnectedAccountTeamKvKey(connectedAccountId)); + + return { ok: true, releasedTeamId: hasReleasedClaim ? teamId : null }; +}; diff --git a/packages/twenty-apps/public/slack/src/logic-functions/utils/release-slack-team-on-disconnect.ts b/packages/twenty-apps/public/slack/src/logic-functions/utils/release-slack-team-on-disconnect.ts new file mode 100644 index 0000000000..970e565f33 --- /dev/null +++ b/packages/twenty-apps/public/slack/src/logic-functions/utils/release-slack-team-on-disconnect.ts @@ -0,0 +1,34 @@ +import { isNonEmptyString } from '@sniptt/guards'; + +import { type ReleaseSlackTeamClaimResult } from 'src/logic-functions/types/release-slack-team-claim-result.type'; +import { getSlackConnectedAccountTeam } from 'src/logic-functions/utils/get-slack-connected-account-team'; +import { isSlackTeamClaimedByAnotherConnection } from 'src/logic-functions/utils/is-slack-team-claimed-by-another-connection'; +import { releaseSlackTeamClaim } from 'src/logic-functions/utils/release-slack-team-claim'; + +type ReleaseSlackTeamOnDisconnectArgs = { + connectedAccountId: string; +}; + +export const releaseSlackTeamOnDisconnect = async ({ + connectedAccountId, +}: ReleaseSlackTeamOnDisconnectArgs): Promise => { + if (!isNonEmptyString(connectedAccountId)) { + throw new Error( + 'Slack team release failed: onDisconnect payload is missing connectedAccountId', + ); + } + + const teamId = await getSlackConnectedAccountTeam(connectedAccountId); + + const isClaimedByAnotherConnection = + isNonEmptyString(teamId) && + (await isSlackTeamClaimedByAnotherConnection({ + teamId, + excludedConnectedAccountId: connectedAccountId, + })); + + return releaseSlackTeamClaim({ + connectedAccountId, + teamId: isClaimedByAnotherConnection ? null : teamId, + }); +}; diff --git a/packages/twenty-apps/public/slack/yarn.lock b/packages/twenty-apps/public/slack/yarn.lock index d67ec26a04..286892912f 100644 --- a/packages/twenty-apps/public/slack/yarn.lock +++ b/packages/twenty-apps/public/slack/yarn.lock @@ -990,8 +990,8 @@ __metadata: oxlint: "npm:^0.16.0" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" - twenty-client-sdk: "npm:^2.26.0" - twenty-sdk: "npm:^2.26.0" + twenty-client-sdk: "npm:^2.27.0" + twenty-sdk: "npm:^2.27.0" typescript: "npm:^5.9.3" vite-tsconfig-paths: "npm:^4.2.1" vitest: "npm:^4.0.0" @@ -3003,22 +3003,22 @@ __metadata: languageName: node linkType: hard -"twenty-client-sdk@npm:2.26.0, twenty-client-sdk@npm:^2.26.0": - version: 2.26.0 - resolution: "twenty-client-sdk@npm:2.26.0" +"twenty-client-sdk@npm:2.27.0, twenty-client-sdk@npm:^2.27.0": + version: 2.27.0 + resolution: "twenty-client-sdk@npm:2.27.0" dependencies: "@genql/runtime": "npm:^2.10.0" esbuild: "npm:^0.28.1" graphql: "npm:^16.8.1" lodash: "npm:^4.17.21" prettier: "npm:^3.8.3" - checksum: 10c0/f5d184567877d10175f887e5ee81510d43c8c5c4d98ad73fbc757a28547da8299ccbe3072d9de33f8c9e9a41eafe06f335b3706196f4a92fd33b0cdcc5d47acf + checksum: 10c0/7b9e90246fc17a0856867b30a7acf083144005e9ba684e92492b92efdf36792f2deeb55499d4bd7564932f2853fe4eb27bad6539ff93ea536e33119e78c032a3 languageName: node linkType: hard -"twenty-sdk@npm:^2.26.0": - version: 2.26.0 - resolution: "twenty-sdk@npm:2.26.0" +"twenty-sdk@npm:^2.27.0": + version: 2.27.0 + resolution: "twenty-sdk@npm:2.27.0" dependencies: "@sniptt/guards": "npm:^0.2.0" axios: "npm:^1.16.0" @@ -3037,12 +3037,12 @@ __metadata: semver: "npm:7.6.3" sharp: "npm:^0.34.5" tinyglobby: "npm:^0.2.15" - twenty-client-sdk: "npm:2.26.0" + twenty-client-sdk: "npm:2.27.0" typescript: "npm:^5.9.3" uuid: "npm:^13.0.2" bin: twenty: dist/cli.cjs - checksum: 10c0/e3edaced59a93bfa3579dc863f799693c9361fc4993eec6942f711a394a678dff21a9c790a3ad6b1683f71ed3f80ec7e6bcc4172d123f538912f88ff7ca32cfa + checksum: 10c0/aa88f4c0359f03a99e0a30092f27d3739d176749f9fed9e1f50b9e99f6c4a21fea7149258973dfe23cf2c706c026800026b5d65e67bd5e5a3c868f33502dc1d3 languageName: node linkType: hard