Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/.env.dist.local
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ VUE_APP_DISCORD_INSTALLATION_URL=
VUE_APP_CONVERSATIONS_PUBLIC_URL=http://localhost:3000
VUE_APP_NANGO_URL=http://localhost:3003
VUE_APP_ENV=local
VUE_APP_INTERCOM_APP_ID=mxl90k6y
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.env.dist.local is typically a template; committing a concrete app id can cause accidental usage against the wrong Intercom workspace in local/dev setups. Consider leaving this blank or using an obvious placeholder value (with a short comment) to encourage explicit configuration per environment.

Suggested change
VUE_APP_INTERCOM_APP_ID=mxl90k6y
# Set your Intercom app id for this environment (leave blank if not used).
VUE_APP_INTERCOM_APP_ID=

Copilot uses AI. Check for mistakes.
12 changes: 12 additions & 0 deletions frontend/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ const defaultConfig = {
permissions: {
teamUserIds: import.meta.env.VUE_APP_TEAM_USER_IDS,
},
intercom: {
appId: import.meta.env.VUE_APP_INTERCOM_APP_ID,
apiBase: 'https://api-iam.intercom.io',
auth0IntercomClaim: 'http://lfx.dev/claims/intercom',
auth0UsernameClaim: 'https://sso.linuxfoundation.org/claims/username',
},
};

const composedConfig = {
Expand Down Expand Up @@ -104,6 +110,12 @@ const composedConfig = {
permissions: {
teamUserIds: 'CROWD_VUE_APP_TEAM_USER_IDS',
},
intercom: {
appId: 'CROWD_VUE_APP_INTERCOM_APP_ID',
apiBase: 'https://api-iam.intercom.io',
auth0IntercomClaim: 'http://lfx.dev/claims/intercom',
auth0UsernameClaim: 'https://sso.linuxfoundation.org/claims/username',
},
};

const config = defaultConfig.backendUrl ? defaultConfig : composedConfig;
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/modules/auth/store/auth.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { watch } from 'vue';
import config from '@/config';
import { setRumUser } from '@/utils/datadog/rum';
import useSessionTracking from '@/shared/modules/monitoring/useSessionTracking';
import { boot as bootIntercom, shutdown as shutdownIntercom } from '@/utils/intercom';

export default {
init() {
Expand Down Expand Up @@ -36,6 +37,19 @@ export default {
if (user) {
setRumUser(user);
lfxHeader.authuser = user;

const intercomJwt = user[config.intercom.auth0IntercomClaim];
const userId = user[config.intercom.auth0UsernameClaim];
if (userId && intercomJwt) {
bootIntercom({
user_id: userId,
name: user.name,
email: user.email,
intercom_user_jwt: intercomJwt,
}).catch((error: any) => {
console.error('Intercom: Boot failed', error);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging the raw error object can inadvertently include sensitive context (depending on how errors are produced/serialized), and this path is triggered during authentication. Prefer logging a sanitized message (e.g., error?.message) and avoid dumping full objects that might contain user identifiers or token-related info.

Suggested change
console.error('Intercom: Boot failed', error);
console.error('Intercom: Boot failed:', (error && error.message) ? error.message : String(error));

Copilot uses AI. Check for mistakes.
});
}
}
});
},
Expand Down Expand Up @@ -155,6 +169,7 @@ export default {
},
logout() {
disconnectSocket();
shutdownIntercom();
this.user = null;
return Auth0Service.logout();
},
Expand Down
167 changes: 167 additions & 0 deletions frontend/src/utils/intercom/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-use-before-define */
import config from '@/config';

declare global {
interface Window {
Intercom?: any;
intercomSettings?: any;
}
}

let isLoaded = false;
let isBooted = false;
let isLoading = false;

export interface IntercomBootOptions {
user_id: string;
name?: string;
email?: string;
intercom_user_jwt?: string;
}

const loadScript = (): void => {
if (isLoaded || isLoading || typeof window === 'undefined') {
return;
}
isLoading = true;

// Create stub so queued commands work before script loads
const w = window as any;
const ic = w.Intercom;
if (typeof ic === 'function') {
ic('reattach_activator');
ic('update', w.intercomSettings);
} else {
const i: any = (...args: any[]) => { i.c(args); };
i.q = [];
i.c = (args: any) => { i.q.push(args); };
w.Intercom = i;
}

// Pre-set app settings
window.intercomSettings = {
api_base: config.intercom.apiBase,
app_id: config.intercom.appId,
};

const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = `https://widget.intercom.io/widget/${config.intercom.appId}`;
script.onload = () => {
isLoaded = true;
isLoading = false;
};
script.onerror = (error) => {
isLoading = false;
console.error('Intercom: Failed to load script', error);
};
Comment on lines +56 to +59
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the script fails quickly (onerror), boot() will still poll for up to 10s before rejecting, which delays auth lifecycle completion and adds avoidable background work. Consider making loadScript() return a shared Promise (cached across calls) that resolves on onload and rejects on onerror, and have boot() await that instead of polling; this will fail fast, simplify control flow, and avoid repeated intervals/timeouts for concurrent boots.

Copilot uses AI. Check for mistakes.

const firstScript = document.getElementsByTagName('script')[0];
if (firstScript?.parentNode) {
firstScript.parentNode.insertBefore(script, firstScript);
} else {
(document.head || document.body).appendChild(script);
}
};

export const boot = (options: IntercomBootOptions): Promise<void> => new Promise((resolve, reject) => {
if (typeof window === 'undefined') {
reject(new Error('Window is undefined'));
return;
}

if (!config.intercom.appId) {
console.info('Intercom: Disabled (no appId configured)');
reject(new Error('No Intercom app ID configured'));
return;
}
Comment on lines +75 to +79
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Treating “Intercom disabled by configuration” as a rejected Promise forces callers to handle this as an error path, even though it’s an expected state. Prefer resolving as a no-op when appId is missing (and/or exposing an isEnabled() helper) so downstream auth flows don’t need to special-case this.

Copilot uses AI. Check for mistakes.

if (isBooted) {
const { intercom_user_jwt: _jwt, ...updateOptions } = options;
update(updateOptions);
resolve();
return;
}

if (!isLoaded && !isLoading) {
loadScript();
}

// Set JWT in intercomSettings before boot — required for identity verification
if (options.intercom_user_jwt) {
window.intercomSettings = window.intercomSettings || {};
window.intercomSettings.intercom_user_jwt = options.intercom_user_jwt;
}
Comment on lines +92 to +96
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intercom_user_jwt is stripped out of the payload passed to Intercom('boot', ...). If identity verification relies on providing intercom_user_jwt in the boot call (as Intercom’s standard flow does), this can lead to users being booted without verification. Include intercom_user_jwt in the boot payload (or pass window.intercomSettings as the boot object after merging user fields) so verification is reliably applied.

Copilot uses AI. Check for mistakes.

const checkLoaded = setInterval(() => {
if (isLoaded && window.Intercom) {
clearInterval(checkLoaded);
clearTimeout(timeoutHandle);

if (isBooted) {
const { intercom_user_jwt: _jwt, ...updateOptions } = options;
update(updateOptions);
resolve();
return;
}

isBooted = true;
try {
const { intercom_user_jwt: _jwt, ...bootOptions } = options;
window.Intercom('boot', {
api_base: config.intercom.apiBase,
app_id: config.intercom.appId,
...bootOptions,
Comment on lines +112 to +116
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intercom_user_jwt is stripped out of the payload passed to Intercom('boot', ...). If identity verification relies on providing intercom_user_jwt in the boot call (as Intercom’s standard flow does), this can lead to users being booted without verification. Include intercom_user_jwt in the boot payload (or pass window.intercomSettings as the boot object after merging user fields) so verification is reliably applied.

Suggested change
const { intercom_user_jwt: _jwt, ...bootOptions } = options;
window.Intercom('boot', {
api_base: config.intercom.apiBase,
app_id: config.intercom.appId,
...bootOptions,
window.Intercom('boot', {
api_base: config.intercom.apiBase,
app_id: config.intercom.appId,
...options,

Copilot uses AI. Check for mistakes.
});
resolve();
} catch (error) {
isBooted = false;
console.error('Intercom: Boot failed', error);
reject(error);
}
}
}, 100);

const timeoutHandle = setTimeout(() => {
clearInterval(checkLoaded);
if (!isBooted) {
isLoading = false;
reject(new Error('Intercom script failed to load'));
}
}, 10000);
Comment on lines +98 to +133
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the script fails quickly (onerror), boot() will still poll for up to 10s before rejecting, which delays auth lifecycle completion and adds avoidable background work. Consider making loadScript() return a shared Promise (cached across calls) that resolves on onload and rejects on onerror, and have boot() await that instead of polling; this will fail fast, simplify control flow, and avoid repeated intervals/timeouts for concurrent boots.

Copilot uses AI. Check for mistakes.
});

export const update = (data?: Partial<IntercomBootOptions>): void => {
if (typeof window !== 'undefined' && window.Intercom && isBooted) {
try {
window.Intercom('update', data || {});
} catch (error) {
console.error('Intercom: Update failed', error);
}
}
};

export const shutdown = (): void => {
if (typeof window === 'undefined') {
return;
}
if (window.intercomSettings?.intercom_user_jwt) {
delete window.intercomSettings.intercom_user_jwt;
}
if (window.Intercom && isBooted) {
try {
window.Intercom('shutdown');
isBooted = false;
} catch (error) {
console.error('Intercom: Shutdown failed', error);
}
}
};

export const useIntercom = () => ({
boot,
update,
shutdown,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exported useIntercom composable is never used

Low Severity

The useIntercom composable is exported but never imported anywhere in the codebase. The auth actions module directly imports boot and shutdown instead. This is dead code that adds unnecessary surface area to the module.

Fix in Cursor Fix in Web

Loading