From 6ba92161a84c95a624826c74dfbb31a5fbf35396 Mon Sep 17 00:00:00 2001 From: Agi-Asi <206806952+Agi-Asi@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:46:58 +0000 Subject: [PATCH 1/2] fix: source events' userId from the authorizations array in buildSource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For event payloads, buildSource extracted userId from event fields (event.user, channel.creator, subteam.created_by). Those name whoever *triggered* the event — e.g. the invitee in member_joined_channel — who may never have installed the app. authorize()/fetchInstallation() then look up an installation for the wrong user, which breaks multi-user-token installs: the store can't know which installation the event was actually delivered for (#2271). The envelope's authorizations array names the installing user the event was delivered for, and buildSource already prefers it for teamId and enterpriseId — userId was the odd one out. Do the same: prefer authorizations[0].user_id, falling back to the existing event field extraction when the array is absent (URL-verification and other non-enveloped shapes). Tests cover both paths: authorizations present (installer id wins over event.user) and absent (event.user fallback unchanged). Fixes #2271 --- src/App.ts | 16 ++++- test/unit/App/build-source.spec.ts | 95 ++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 test/unit/App/build-source.spec.ts diff --git a/src/App.ts b/src/App.ts index fd5dc488f..42a7a1482 100644 --- a/src/App.ts +++ b/src/App.ts @@ -1547,7 +1547,21 @@ function buildSource( const userId: string | undefined = (() => { if (type === IncomingEventType.Event) { // NOTE: no type system backed exhaustiveness check within this incoming event type - const { event } = body as SlackEventMiddlewareArgs['body']; + const bodyAsEvent = body as SlackEventMiddlewareArgs['body']; + // The authorizations array names the installing user this event was + // delivered for. Event payload fields like `event.user` reference the + // user that *triggered* the event, who may never have installed the + // app (e.g. the invitee in `member_joined_channel`), which sends + // `authorize`/`fetchInstallation` looking up the wrong installation. + // Mirrors the teamId/enterpriseId extraction above. See #2271. + if ( + Array.isArray(bodyAsEvent.authorizations) && + bodyAsEvent.authorizations[0] !== undefined && + bodyAsEvent.authorizations[0].user_id !== null + ) { + return bodyAsEvent.authorizations[0].user_id; + } + const { event } = bodyAsEvent; if ('user' in event) { if (typeof event.user === 'string') { return event.user; diff --git a/test/unit/App/build-source.spec.ts b/test/unit/App/build-source.spec.ts new file mode 100644 index 000000000..66516ef08 --- /dev/null +++ b/test/unit/App/build-source.spec.ts @@ -0,0 +1,95 @@ +import assert from 'node:assert'; +import sinon, { type SinonSpy } from 'sinon'; +import type App from '../../../src/App'; +import { + createDummyAppMentionEventMiddlewareArgs, + createFakeLogger, + FakeReceiver, + importApp, + mergeOverrides, + noopMiddleware, + type Override, + withConversationContext, + withMemoryStore, + withNoopAppMetadata, + withNoopWebClient, +} from '../helpers'; + +function buildOverrides(secondOverrides: Override[]): Override { + return mergeOverrides( + withNoopAppMetadata(), + withNoopWebClient(), + ...secondOverrides, + withMemoryStore(sinon.fake()), + withConversationContext(sinon.fake.returns(noopMiddleware)), + ); +} + +describe('App authorize source (buildSource)', () => { + let fakeReceiver: FakeReceiver; + let fakeHandler: SinonSpy; + let fakeAck: SinonSpy; + let fakeAuthorize: SinonSpy; + let MockApp: Awaited>; + let app: App; + + beforeEach(async () => { + fakeReceiver = new FakeReceiver(); + fakeHandler = sinon.fake(); + fakeAck = sinon.fake(); + fakeAuthorize = sinon.fake.resolves({ botToken: '', botId: '' }); + MockApp = importApp(buildOverrides([])); + app = new MockApp({ + logger: createFakeLogger(), + receiver: fakeReceiver, + authorize: fakeAuthorize, + }); + }); + + it('should prefer the authorizations array user over the event user for events', async () => { + // The event `user` is whoever triggered the event (e.g. the invitee in + // `member_joined_channel`) and may never have installed the app; the + // authorizations array names the installing user the event was + // delivered for. authorize/fetchInstallation must be keyed on the + // latter. See #2271. + app.event('app_mention', fakeHandler); + await fakeReceiver.sendEvent({ + ...createDummyAppMentionEventMiddlewareArgs(undefined, { + authorizations: [ + { + enterprise_id: null, + team_id: 'T1234', + user_id: 'U-installer', + is_bot: false, + is_enterprise_install: false, + }, + ], + }), + ack: fakeAck, + }); + sinon.assert.calledOnce(fakeAuthorize); + const source = fakeAuthorize.getCall(0).args[0]; + assert.strictEqual(source.userId, 'U-installer'); + }); + + it('should fall back to the event user when no authorizations array is present', async () => { + app.event('app_mention', fakeHandler); + const args = createDummyAppMentionEventMiddlewareArgs({ + event: { + type: 'app_mention', + text: 'hi', + user: 'U-event-user', + channel: 'C1234', + ts: '1234.56', + event_ts: '1234.56', + }, + }); + await fakeReceiver.sendEvent({ + ...args, + ack: fakeAck, + }); + sinon.assert.calledOnce(fakeAuthorize); + const source = fakeAuthorize.getCall(0).args[0]; + assert.strictEqual(source.userId, 'U-event-user'); + }); +}); From d264a6bdc90e01025154cb549fa6a8b89bd198b7 Mon Sep 17 00:00:00 2001 From: Agi-Asi <206806952+Agi-Asi@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:57:57 +0000 Subject: [PATCH 2/2] chore: add changeset --- .changeset/fix-build-source-user-authorizations.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-build-source-user-authorizations.md diff --git a/.changeset/fix-build-source-user-authorizations.md b/.changeset/fix-build-source-user-authorizations.md new file mode 100644 index 000000000..ff65e677d --- /dev/null +++ b/.changeset/fix-build-source-user-authorizations.md @@ -0,0 +1,5 @@ +--- +"@slack/bolt": patch +--- + +Fix `context.userId` being `undefined` for events whose payload does not carry a user field directly, by sourcing the user ID from the request's `authorizations` array in `buildSource`, matching how `teamId` and `enterpriseId` are already resolved.