From dfcde55ea16a337045ae10e38aff252d40bc8130 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 15 Sep 2026 20:10:42 -0700 Subject: [PATCH] Isolate messaging SDK from local URL identity and metadata --- docs/customerio-messaging.md | 11 +++- e2e/customerio.test.ts | 28 ++++++++-- scripts/customerio-script.ts | 24 +++++--- src/renderer/src/customerIo/environment.ts | 64 ++++++++++++++++++++++ src/renderer/src/customerIo/index.ts | 3 + 5 files changed, 114 insertions(+), 16 deletions(-) create mode 100644 src/renderer/src/customerIo/environment.ts diff --git a/docs/customerio-messaging.md b/docs/customerio-messaging.md index a487db8b0..79219fb33 100644 --- a/docs/customerio-messaging.md +++ b/docs/customerio-messaging.md @@ -39,9 +39,14 @@ Development builds default off. Environment overrides: | `COMFY_CUSTOMER_IO_SITE_ID` | Override the public in-app site ID | The browser SDK is bundled as a script and executed in ComfyUI's browser context. -It receives no Node access. Its local/session storage accesses are redirected at -build time to private in-memory stores, leaving ComfyUI's authentication storage -intact. Session identity is reset before switching accounts. Network failures do +It receives no Node access. Its location and local/session storage accesses are +redirected at build time to a private synthetic URL and in-memory stores. This +applies before SDK initialization, so URL parameters cannot select an identity, +trigger events, or add campaign/referrer metadata. The SDK's fallback route uses +`/desktop/comfyui`, never the real workflow path. Native link handling owns +navigation; SDK location assignments cannot replace the workflow. Browser +persistence is disabled and ComfyUI's own URL and storage remain intact. +Session identity is reset before switching accounts. Network failures do not block loading or using ComfyUI; another activation or an online event can retry. ## Verification diff --git a/e2e/customerio.test.ts b/e2e/customerio.test.ts index 1eece1e03..62f717131 100644 --- a/e2e/customerio.test.ts +++ b/e2e/customerio.test.ts @@ -110,6 +110,12 @@ document.getElementById('close').onclick = () => parent.postMessage({gist:{insta }, json: { inAppMessages: [ + { + messageId: 'private-route-message', + queueId: 'private-route-queue', + priority: 0, + properties: { gist: { routeRuleWeb: '^/private-workflow-name$' } } + }, { messageId: `fixture-message-${delivery}`, queueId: `fixture-queue-${delivery}`, @@ -137,9 +143,12 @@ document.getElementById('close').onclick = () => parent.postMessage({gist:{insta return route.fulfill({ json: {} }) }) - await page.goto('http://127.0.0.1:8188/private-workflow-name?private-query=workflow-secret', { - referer: 'http://127.0.0.1:8188/private-referrer' - }) + await page.goto( + 'http://127.0.0.1:8188/private-workflow-name?private-query=workflow-secret&ajs_uid=unverified-person&ajs_event=private-event&utm_campaign=private-campaign&btid=private-ad', + { + referer: 'http://127.0.0.1:8188/private-referrer' + } + ) await page.waitForFunction('typeof window.__comfyDesktop2 === "object"') expect(requests).toHaveLength(1) const update = async (session: CustomerIoSession | null): Promise => { @@ -202,7 +211,10 @@ document.getElementById('close').onclick = () => parent.postMessage({gist:{insta expect(events.some(({ body }) => body?.includes('"name":"desktop/comfyui"'))).toBe(true) expect( events.every( - ({ body }) => !/private-workflow|private-query|private-referrer/.test(body ?? '') + ({ body }) => + !/private-workflow|private-query|private-referrer|private-event|private-campaign|private-ad|unverified-person/.test( + body ?? '' + ) ) ).toBe(true) const pages = events.filter(({ url }) => url.endsWith('/p')) @@ -214,6 +226,14 @@ document.getElementById('close').onclick = () => parent.postMessage({gist:{insta const queues = requests.filter(({ url }) => url.includes('/api/v4/users')) expect(queues.length).toBeGreaterThanOrEqual(2) expect(queues.every(({ headers }) => headers['x-cio-site-id'] === identity.siteId)).toBe(true) + expect(requests.every(({ url }) => !url.includes('private-route-message'))).toBe(true) + expect( + queues.every(({ headers }) => + ['desktop-test-user', 'second-test-user'].includes( + Buffer.from(headers['x-gist-encoded-user-token'] ?? '', 'base64').toString() + ) + ) + ).toBe(true) expect(errors).toEqual([]) } finally { releaseViewLog() diff --git a/scripts/customerio-script.ts b/scripts/customerio-script.ts index 7874c930c..0caa699df 100644 --- a/scripts/customerio-script.ts +++ b/scripts/customerio-script.ts @@ -23,20 +23,26 @@ export function customerIoScriptPlugin(): Plugin { target: 'chrome144', minify: true, metafile: true, + inject: [resolve(__dirname, '../src/renderer/src/customerIo/environment.ts')], define: { 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), - localStorage: '__desktopCioLocalStorage', - 'window.localStorage': '__desktopCioLocalStorage', - 'globalThis.localStorage': '__desktopCioLocalStorage', - sessionStorage: '__desktopCioSessionStorage', - 'window.sessionStorage': '__desktopCioSessionStorage', - 'globalThis.sessionStorage': '__desktopCioSessionStorage' + localStorage: 'sdkLocalStorage', + 'window.localStorage': 'sdkLocalStorage', + 'globalThis.localStorage': 'sdkLocalStorage', + sessionStorage: 'sdkSessionStorage', + 'window.sessionStorage': 'sdkSessionStorage', + 'globalThis.sessionStorage': 'sdkSessionStorage', + location: 'sdkLocation', + 'window.location': 'sdkLocation', + 'globalThis.location': 'sdkLocation', + 'document.location': 'sdkLocation', + 'document.URL': 'sdkLocation.href', + 'document.documentURI': 'sdkLocation.href', + 'document.referrer': '""' } }) for (const file of Object.keys(result.metafile!.inputs)) this.addWatchFile(resolve(file)) - const storage = `function storage(){const m=new Map();return {get length(){return m.size},clear(){m.clear()},getItem(k){return m.get(k)??null},key(i){return [...m.keys()][i]??null},removeItem(k){m.delete(k)},setItem(k,v){m.set(k,String(v))}}}` - const source = `(()=>{${storage};const __desktopCioLocalStorage=storage(),__desktopCioSessionStorage=storage();${result.outputFiles[0]!.text}})();` - return `export default ${JSON.stringify(source)}` + return `export default ${JSON.stringify(result.outputFiles[0]!.text)}` } } } diff --git a/src/renderer/src/customerIo/environment.ts b/src/renderer/src/customerIo/environment.ts new file mode 100644 index 000000000..30b6f222e --- /dev/null +++ b/src/renderer/src/customerIo/environment.ts @@ -0,0 +1,64 @@ +import type { CustomerIoPage } from '../../../shared/customerIo' + +let page: CustomerIoPage | null = null + +export function setMessagingPage(value: CustomerIoPage | null): void { + page = value +} + +/** + * SDK-only browser environment, injected at bundle time. The SDK reads URL + * identity parameters before middleware and enriches events again afterward. + * It must never observe the host page's URL or navigate the workflow away. + */ +export const sdkLocation = { + get href(): string { + return `https://desktop.invalid/${page ?? 'inactive'}` + }, + set href(_value: string) { + // Message actions already pass through the validated main-process bridge. + }, + get pathname(): string { + return `/${page ?? 'inactive'}` + }, + origin: 'https://desktop.invalid', + protocol: 'https:', + host: 'desktop.invalid', + hostname: 'desktop.invalid', + port: '', + search: '', + hash: '', + assign(_value: string): void {}, + replace(_value: string): void {}, + reload(): void {}, + toString(): string { + return this.href + } +} + +function memoryStorage(): Storage { + const values = new Map() + return { + get length() { + return values.size + }, + clear() { + values.clear() + }, + getItem(key) { + return values.get(String(key)) ?? null + }, + key(index) { + return [...values.keys()][index] ?? null + }, + removeItem(key) { + values.delete(String(key)) + }, + setItem(key, value) { + values.set(String(key), String(value)) + } + } +} + +export const sdkLocalStorage = memoryStorage() +export const sdkSessionStorage = memoryStorage() diff --git a/src/renderer/src/customerIo/index.ts b/src/renderer/src/customerIo/index.ts index dabb2916b..3c1661991 100644 --- a/src/renderer/src/customerIo/index.ts +++ b/src/renderer/src/customerIo/index.ts @@ -2,6 +2,7 @@ import { AnalyticsBrowser, InAppPlugin } from '@customerio/cdp-analytics-browser import Gist from 'customerio-gist-web' import type { CustomerIoSession } from '../../../shared/customerIo' import { createMessagingController } from './controller' +import { setMessagingPage } from './environment' const controller = createMessagingController(async (session, currentSession) => { const analytics = AnalyticsBrowser.load( @@ -10,6 +11,7 @@ const controller = createMessagingController(async (session, currentSession) => user: { persist: false }, group: { persist: false }, retryQueue: false, + disableClientPersistence: true, integrations: { All: false, 'Customer.io Data Pipelines': true, @@ -110,6 +112,7 @@ let lastSession: CustomerIoSession | null = null messagingGlobal.__comfyCustomerIo = { update(session) { lastSession = session + setMessagingPage(session?.page ?? null) return controller.update(session) } }