-
Notifications
You must be signed in to change notification settings - Fork 24
let's integreate intercom with newtab #327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -181,3 +181,6 @@ log.txt | |
|
|
||
| # Testing iteration temp files | ||
| tmp/ | ||
|
|
||
| # Coding agent artifacts | ||
| .agent/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <!doctype html> | ||
| <html> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <style> | ||
| html, body { | ||
| margin: 0; | ||
| padding: 0; | ||
| background: transparent; | ||
| overflow: hidden; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <script src="./main.ts" type="module"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import type { IntercomParentMessage } from '../../lib/intercom/intercom' | ||
|
|
||
| type IntercomFn = ((...args: unknown[]) => void) & { q?: unknown[][] } | ||
|
|
||
| declare global { | ||
| interface Window { | ||
| Intercom: IntercomFn | ||
| intercomSettings: Record<string, unknown> | ||
| } | ||
| } | ||
|
|
||
| function sendToParent(message: { type: string }) { | ||
| parent.postMessage(message, '*') | ||
| } | ||
|
|
||
| function initIntercomStub() { | ||
| const i = ((...args: unknown[]) => { | ||
| i.c(args) | ||
| }) as ((...args: unknown[]) => void) & { | ||
| q: unknown[][] | ||
| c: (args: unknown[]) => void | ||
| } | ||
| i.q = [] | ||
| i.c = (args: unknown[]) => { | ||
| i.q.push(args) | ||
| } | ||
| window.Intercom = i as IntercomFn | ||
| } | ||
|
|
||
| function loadIntercomScript(appId: string): Promise<void> { | ||
| return new Promise((resolve, reject) => { | ||
| const script = document.createElement('script') | ||
| script.src = `https://widget.intercom.io/widget/${appId}` | ||
| script.async = true | ||
| script.onload = () => resolve() | ||
| script.onerror = () => reject(new Error('Failed to load Intercom script')) | ||
| document.head.appendChild(script) | ||
| }) | ||
| } | ||
|
|
||
| let booted = false | ||
|
|
||
| async function handleBoot(appId: string) { | ||
| if (booted) return | ||
| booted = true | ||
|
|
||
| initIntercomStub() | ||
| await loadIntercomScript(appId) | ||
|
|
||
| window.Intercom('boot', { | ||
| app_id: appId, | ||
| vertical_padding: 0, | ||
| horizontal_padding: 0, | ||
| }) | ||
|
|
||
| window.Intercom('onShow', () => { | ||
| sendToParent({ type: 'intercom:messenger-opened' }) | ||
| }) | ||
|
|
||
| window.Intercom('onHide', () => { | ||
| sendToParent({ type: 'intercom:messenger-closed' }) | ||
| }) | ||
|
|
||
| sendToParent({ type: 'intercom:ready' }) | ||
| } | ||
|
|
||
| function handleUpdate(userId: string, email: string, name: string) { | ||
| if (!window.Intercom) return | ||
| window.Intercom('update', { | ||
| user_id: userId, | ||
| email, | ||
| name, | ||
| }) | ||
| } | ||
|
|
||
| function handleShutdown() { | ||
| if (!window.Intercom) return | ||
| window.Intercom('shutdown') | ||
| } | ||
|
|
||
| window.addEventListener('message', (event: MessageEvent) => { | ||
| const data = event.data as IntercomParentMessage | ||
| if ( | ||
| !data || | ||
| typeof data.type !== 'string' || | ||
| !data.type.startsWith('intercom:') | ||
| ) | ||
| return | ||
|
|
||
| switch (data.type) { | ||
| case 'intercom:boot': | ||
| handleBoot(data.appId) | ||
| break | ||
| case 'intercom:update': | ||
| handleUpdate(data.userId, data.email, data.name) | ||
| break | ||
| case 'intercom:shutdown': | ||
| handleShutdown() | ||
| break | ||
| } | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import type { FC, PropsWithChildren } from 'react' | ||
| import { useEffect, useRef, useState } from 'react' | ||
| import { useSessionInfo } from '@/lib/auth/sessionStorage' | ||
|
Comment on lines
+1
to
+3
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sandbox boot may be missed Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/agent/lib/intercom/IntercomProvider.tsx
Line: 1:3
Comment:
**Sandbox boot may be missed**
If `sessionInfo` resolves before the iframe `onLoad` fires, the identity sync effect posts `intercom:update` while `iframeWindowRef.current` is still `null`, and there’s no retry after boot. In that scenario, Intercom will boot without user identity until a later session change occurs. Consider sending an `intercom:update` after `intercom:ready` (or after `handleIframeLoad`) when `sessionInfo.user` is already available.
How can I resolve this? If you propose a fix, please make it concise. |
||
| import { env } from '@/lib/env' | ||
| import { sentry } from '@/lib/sentry/sentry' | ||
| import { | ||
| INTERCOM_LAUNCHER_HEIGHT, | ||
| INTERCOM_LAUNCHER_WIDTH, | ||
| INTERCOM_MESSENGER_HEIGHT, | ||
| INTERCOM_MESSENGER_WIDTH, | ||
| isIntercomSandboxEvent, | ||
| } from './intercom' | ||
|
|
||
| export const IntercomProvider: FC<PropsWithChildren> = ({ children }) => { | ||
| const appId = env.VITE_PUBLIC_INTERCOM_APP_ID | ||
| if (!appId) return <>{children}</> | ||
|
|
||
| return <IntercomProviderInner appId={appId}>{children}</IntercomProviderInner> | ||
| } | ||
|
|
||
| const IntercomProviderInner: FC<PropsWithChildren<{ appId: string }>> = ({ | ||
| appId, | ||
| children, | ||
| }) => { | ||
| const iframeRef = useRef<HTMLIFrameElement>(null) | ||
| const iframeWindowRef = useRef<Window | null>(null) | ||
| const [isMessengerOpen, setIsMessengerOpen] = useState(false) | ||
| const { sessionInfo, isLoading } = useSessionInfo() | ||
|
|
||
| const handleIframeLoad = () => { | ||
|
Comment on lines
+27
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. postMessage accepts any origin Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/agent/lib/intercom/IntercomProvider.tsx
Line: 27:30
Comment:
**postMessage accepts any origin**
`postMessage(..., '*')` combined with a sandbox that also posts with `'*'` means any frame/window can spoof `intercom:*` messages to this page. Even though the listener checks `type`, it doesn’t verify `event.source === iframeRef.current?.contentWindow` (or an expected origin), so a malicious page could toggle state or trigger shutdown/update. Tighten validation to only accept messages from the sandbox iframe window.
How can I resolve this? If you propose a fix, please make it concise. |
||
| iframeWindowRef.current = iframeRef.current?.contentWindow ?? null | ||
| try { | ||
| iframeWindowRef.current?.postMessage( | ||
| { type: 'intercom:boot', appId }, | ||
| '*', | ||
| ) | ||
| } catch (error) { | ||
| sentry.captureException(error, { | ||
| extra: { message: 'Failed to send boot command to Intercom sandbox' }, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| useEffect(() => { | ||
| const handler = (event: MessageEvent) => { | ||
| if (!isIntercomSandboxEvent(event.data)) return | ||
|
|
||
| switch (event.data.type) { | ||
| case 'intercom:ready': | ||
| break | ||
| case 'intercom:messenger-opened': | ||
| setIsMessengerOpen(true) | ||
| break | ||
| case 'intercom:messenger-closed': | ||
| setIsMessengerOpen(false) | ||
| break | ||
| } | ||
| } | ||
|
|
||
| window.addEventListener('message', handler) | ||
| return () => window.removeEventListener('message', handler) | ||
| }, []) | ||
|
|
||
| useEffect(() => { | ||
| if (isLoading) return | ||
|
|
||
| try { | ||
| if (sessionInfo.user) { | ||
| iframeWindowRef.current?.postMessage( | ||
| { | ||
| type: 'intercom:update', | ||
| userId: sessionInfo.user.id, | ||
| email: sessionInfo.user.email, | ||
| name: sessionInfo.user.name, | ||
| }, | ||
| '*', | ||
| ) | ||
| } else { | ||
| iframeWindowRef.current?.postMessage({ type: 'intercom:shutdown' }, '*') | ||
| } | ||
| } catch (error) { | ||
| sentry.captureException(error, { | ||
| extra: { message: 'Failed to sync identity with Intercom sandbox' }, | ||
| }) | ||
| } | ||
| }, [sessionInfo.user, isLoading]) | ||
|
|
||
| const width = isMessengerOpen | ||
| ? INTERCOM_MESSENGER_WIDTH | ||
| : INTERCOM_LAUNCHER_WIDTH | ||
| const height = isMessengerOpen | ||
| ? INTERCOM_MESSENGER_HEIGHT | ||
| : INTERCOM_LAUNCHER_HEIGHT | ||
|
|
||
| return ( | ||
| <> | ||
| {children} | ||
| <iframe | ||
| ref={iframeRef} | ||
| title="Intercom Messenger" | ||
| src={chrome.runtime.getURL('intercom.html')} | ||
| onLoad={handleIframeLoad} | ||
| allow="microphone" | ||
| style={{ | ||
| position: 'fixed', | ||
| bottom: 20, | ||
| right: 20, | ||
| zIndex: 9999, | ||
| border: 'none', | ||
| background: 'transparent', | ||
| width, | ||
| height, | ||
| transition: 'width 0.2s ease, height 0.2s ease', | ||
| pointerEvents: 'auto', | ||
| }} | ||
| /> | ||
| </> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| export type IntercomBootCommand = { | ||
| type: 'intercom:boot' | ||
| appId: string | ||
| } | ||
|
|
||
| export type IntercomUpdateCommand = { | ||
| type: 'intercom:update' | ||
| userId: string | ||
| email: string | ||
| name: string | ||
| } | ||
|
|
||
| export type IntercomShutdownCommand = { | ||
| type: 'intercom:shutdown' | ||
| } | ||
|
|
||
| export type IntercomParentMessage = | ||
| | IntercomBootCommand | ||
| | IntercomUpdateCommand | ||
| | IntercomShutdownCommand | ||
|
|
||
| export type IntercomReadyEvent = { | ||
| type: 'intercom:ready' | ||
| } | ||
|
|
||
| export type IntercomMessengerOpenedEvent = { | ||
| type: 'intercom:messenger-opened' | ||
| } | ||
|
|
||
| export type IntercomMessengerClosedEvent = { | ||
| type: 'intercom:messenger-closed' | ||
| } | ||
|
|
||
| export type IntercomSandboxEvent = | ||
| | IntercomReadyEvent | ||
| | IntercomMessengerOpenedEvent | ||
| | IntercomMessengerClosedEvent | ||
|
|
||
| export const INTERCOM_LAUNCHER_WIDTH = 80 | ||
| export const INTERCOM_LAUNCHER_HEIGHT = 80 | ||
| export const INTERCOM_MESSENGER_WIDTH = 420 | ||
| export const INTERCOM_MESSENGER_HEIGHT = 650 | ||
|
|
||
| const KNOWN_SANDBOX_EVENT_TYPES = new Set([ | ||
| 'intercom:ready', | ||
| 'intercom:messenger-opened', | ||
| 'intercom:messenger-closed', | ||
| ]) | ||
|
|
||
| export function isIntercomSandboxEvent( | ||
| data: unknown, | ||
| ): data is IntercomSandboxEvent { | ||
| if (typeof data !== 'object' || data === null) return false | ||
| const msg = data as { type?: unknown } | ||
| if (typeof msg.type !== 'string') return false | ||
| return KNOWN_SANDBOX_EVENT_TYPES.has(msg.type) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
postMessage targetOrigin too broad
The sandbox sends events via
parent.postMessage(message, '*'), which allows any embedding origin to receive Intercom state events if this page is ever embedded outside the extension context. Since the parent is expected to be the extension page, pass an explicittargetOrigin(or at least validateevent.originon the parent side and restrictevent.source).Prompt To Fix With AI