@@ -808,6 +808,50 @@ export function shouldSkipSlackTriggerEvent(
808808 return false
809809}
810810
811+ /**
812+ * Resolves the bot token a Slack webhook can act with, across the three trigger
813+ * backends: a pasted bot token (legacy `slack_webhook`), a reusable custom-bot
814+ * credential's stored token, and a native `slack_app` OAuth credential. The
815+ * OAuth credential resolves via its OWNER (not the execution actor in
816+ * `workflow.userId`, who may not own the credential) so reaction-message text
817+ * and file downloads work. `credentialOwnerUserId` short-circuits the
818+ * credential → account → owner chain when the caller already resolved it.
819+ */
820+ async function resolveSlackWebhookBotToken (
821+ providerConfig : Record < string , unknown > ,
822+ requestId : string ,
823+ credentialOwnerUserId ?: string
824+ ) : Promise < string | undefined > {
825+ const pastedToken = providerConfig . botToken as string | undefined
826+ if ( pastedToken || typeof providerConfig . credentialId !== 'string' ) {
827+ return pastedToken
828+ }
829+ const credentialId = providerConfig . credentialId
830+
831+ const botCredential = await getSlackBotCredential ( credentialId )
832+ if ( botCredential ?. botToken ) {
833+ return botCredential . botToken
834+ }
835+
836+ let ownerUserId = credentialOwnerUserId
837+ if ( ! ownerUserId ) {
838+ const resolved = await resolveOAuthAccountId ( credentialId )
839+ if ( ! resolved ?. accountId ) {
840+ return undefined
841+ }
842+ const [ owner ] = await db
843+ . select ( { userId : account . userId } )
844+ . from ( account )
845+ . where ( eq ( account . id , resolved . accountId ) )
846+ . limit ( 1 )
847+ ownerUserId = owner ?. userId
848+ }
849+ if ( ! ownerUserId ) {
850+ return undefined
851+ }
852+ return ( await refreshAccessTokenIfNeeded ( credentialId , ownerUserId , requestId ) ) ?? undefined
853+ }
854+
811855export const slackHandler : WebhookProviderHandler = {
812856 verifyAuth ( { request, rawBody, requestId, providerConfig } : AuthContext ) {
813857 const signingSecret = providerConfig . signingSecret as string | undefined
@@ -870,34 +914,19 @@ export const slackHandler: WebhookProviderHandler = {
870914 * `actions[]` and no Events-API `event` envelope), and the Events API
871915 * (app_mention, message, reaction_added, ... nested under `event`).
872916 */
873- async formatInput ( { body, webhook, requestId } : FormatInputContext ) : Promise < FormatInputResult > {
917+ async formatInput ( {
918+ body,
919+ webhook,
920+ requestId,
921+ credentialOwnerUserId,
922+ } : FormatInputContext ) : Promise < FormatInputResult > {
874923 const b = isRecordLike ( body ) ? body : { }
875924 const providerConfig = ( webhook . providerConfig as Record < string , unknown > ) || { }
876- let botToken = providerConfig . botToken as string | undefined
877- // Reusable custom Slack bot credential: use its stored bot token directly.
878- if ( ! botToken && typeof providerConfig . credentialId === 'string' ) {
879- const botCredential = await getSlackBotCredential ( providerConfig . credentialId )
880- if ( botCredential ) botToken = botCredential . botToken
881- }
882- // Native (slack_app) triggers carry an OAuth credential rather than a pasted
883- // bot token; resolve it via the credential's OWNER (not the execution actor
884- // in workflow.userId, who may not own the credential) so reaction-message
885- // text and file downloads work.
886- if ( ! botToken && typeof providerConfig . credentialId === 'string' ) {
887- const credentialId = providerConfig . credentialId
888- const resolved = await resolveOAuthAccountId ( credentialId )
889- if ( resolved ?. accountId ) {
890- const [ owner ] = await db
891- . select ( { userId : account . userId } )
892- . from ( account )
893- . where ( eq ( account . id , resolved . accountId ) )
894- . limit ( 1 )
895- if ( owner ?. userId ) {
896- botToken =
897- ( await refreshAccessTokenIfNeeded ( credentialId , owner . userId , requestId ) ) ?? undefined
898- }
899- }
900- }
925+ const botToken = await resolveSlackWebhookBotToken (
926+ providerConfig ,
927+ requestId ,
928+ credentialOwnerUserId
929+ )
901930 const includeFiles = Boolean ( providerConfig . includeFiles )
902931
903932 if ( typeof b ?. command === 'string' && b . command . startsWith ( '/' ) ) {
0 commit comments