feat(intercom): Closing intercom session on org change#112549
feat(intercom): Closing intercom session on org change#112549sentaur-athena merged 2 commits intomasterfrom
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Shutdown ignores in-flight boot causing cross-org session leak
- I confirmed the race was real and fixed it by tracking the booting org, invalidating in-flight boots during shutdown/org changes, and preventing stale boot promises from showing Intercom for the wrong org.
Or push these changes by commenting:
@cursor push 66d79886a2
Preview (66d79886a2)
diff --git a/static/app/utils/intercom.tsx b/static/app/utils/intercom.tsx
--- a/static/app/utils/intercom.tsx
+++ b/static/app/utils/intercom.tsx
@@ -24,6 +24,7 @@
let hasBooted = false;
let bootPromise: Promise<void> | null = null;
+let bootingOrgSlug: string | null = null;
let bootedOrgSlug: string | null = null;
/**
@@ -31,16 +32,21 @@
* Only fetches JWT and boots on first call.
*/
async function initIntercom(orgSlug: string): Promise<void> {
- if (hasBooted) {
+ if (hasBooted && bootedOrgSlug === orgSlug) {
return;
}
- // Prevent concurrent initialization
- if (bootPromise) {
+ // Prevent concurrent initialization for the same organization
+ if (bootPromise && bootingOrgSlug === orgSlug) {
return bootPromise;
}
- bootPromise = (async () => {
+ // If a different org is currently booting, invalidate it first.
+ if (bootPromise && bootingOrgSlug !== orgSlug) {
+ await shutdownIntercom();
+ }
+
+ const currentBootPromise = (async () => {
try {
const intercomAppId = ConfigStore.get('intercomAppId');
if (!intercomAppId) {
@@ -55,6 +61,12 @@
// Boot Intercom with user data
const {default: Intercom} = await import('@intercom/messenger-js-sdk');
+
+ // Boot may have been invalidated while async work was in-flight.
+ if (bootPromise !== currentBootPromise || bootingOrgSlug !== orgSlug) {
+ return;
+ }
+
Intercom({
app_id: intercomAppId,
user_id: jwtData.userData.userId,
@@ -71,14 +83,25 @@
hasBooted = true;
bootedOrgSlug = orgSlug;
+
+ if (bootPromise === currentBootPromise) {
+ bootPromise = null;
+ bootingOrgSlug = null;
+ }
} catch (error) {
- // Reset so user can retry on next click
- bootPromise = null;
- throw error;
+ // Reset so user can retry on next click.
+ if (bootPromise === currentBootPromise) {
+ bootPromise = null;
+ bootingOrgSlug = null;
+ throw error;
+ }
}
})();
- return bootPromise;
+ bootPromise = currentBootPromise;
+ bootingOrgSlug = orgSlug;
+
+ return currentBootPromise;
}
/**
@@ -86,6 +109,10 @@
* Call this when user logs out or switches organizations.
*/
export async function shutdownIntercom(): Promise<void> {
+ // Invalidate any in-flight boot immediately.
+ bootPromise = null;
+ bootingOrgSlug = null;
+
if (!hasBooted) {
return;
}
@@ -105,11 +132,18 @@
*/
export async function showIntercom(orgSlug: string): Promise<void> {
// If booted for a different org, shutdown first to re-initialize with new org context
- if (hasBooted && bootedOrgSlug !== orgSlug) {
+ if (
+ (hasBooted && bootedOrgSlug !== orgSlug) ||
+ (bootPromise && bootingOrgSlug !== orgSlug)
+ ) {
await shutdownIntercom();
}
await initIntercom(orgSlug);
+ if (!hasBooted || bootedOrgSlug !== orgSlug) {
+ return;
+ }
+
const {show} = await import('@intercom/messenger-js-sdk');
show();
}This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 27a607c. Configure here.
| hasBooted = false; | ||
| bootPromise = null; | ||
| bootedOrgSlug = null; | ||
| } |
There was a problem hiding this comment.
Shutdown ignores in-flight boot causing cross-org session leak
High Severity
shutdownIntercom only checks hasBooted but ignores a pending bootPromise. If a user switches orgs while Intercom is still booting (e.g., JWT fetch in progress), hasBooted is false, so shutdown is a no-op. The boot then completes for the old org, creating the exact cross-org session leak this PR intends to prevent. The same gap exists in showIntercom: the guard hasBooted && bootedOrgSlug !== orgSlug misses the in-progress case, and initIntercom would reuse the stale bootPromise for the wrong org.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 27a607c. Configure here.
There was a problem hiding this comment.
@scttcper is this actually possible? I would think that hasBooted is always true if we got a bootPromise.
There was a problem hiding this comment.
i guess. Start the request -> switch orgs -> jwtData comes back for the previous org or something
There was a problem hiding this comment.
doesn't really seem like it would be that bad, i'm not sure switching orgs and contacting support is the most common thing
There was a problem hiding this comment.
@sentaur-athena actually thinking about this more, there's no way for this to happen for us because we make them switch subdomains and its a full page reload
There was a problem hiding this comment.
Thanks for checking. I can address this but it would make the code extremely confusing and dirty. Will merge as is.
When user changes or org changes we need to call intercom authentication API again. Each user<>org pair represents an intercom user and we don't want the same user in org A has access to their chats on org B.



When user changes or org changes we need to call intercom authentication API again. Each user<>org pair represents an intercom user and we don't want the same user in org A has access to their chats on org B.