Skip to content
Draft
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
11 changes: 8 additions & 3 deletions docs/customerio-messaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 24 additions & 4 deletions e2e/customerio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down Expand Up @@ -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<void> => {
Expand Down Expand Up @@ -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'))
Expand All @@ -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()
Expand Down
24 changes: 15 additions & 9 deletions scripts/customerio-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`
}
}
}
64 changes: 64 additions & 0 deletions src/renderer/src/customerIo/environment.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>()
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()
3 changes: 3 additions & 0 deletions src/renderer/src/customerIo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -110,6 +112,7 @@ let lastSession: CustomerIoSession | null = null
messagingGlobal.__comfyCustomerIo = {
update(session) {
lastSession = session
setMessagingPage(session?.page ?? null)
return controller.update(session)
}
}
Expand Down
Loading