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
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ export interface ComfyDesktop2Bridge {
/** Reports whether the backend server is cloud/remote, not the user's location.
* Optional: desktop builds predating it are still in the wild. */
isRemote?(): boolean
/** Returns the per-installation id: a non-PII, stable-per-machine value
* (`SHA-256(machine_id + salt)`, see main/lib/deviceId). The hosted
* frontend sends it to cloud as the `X-Comfy-Install-Id` header so cloud
* can attribute requests to a Desktop install. Not an identity/secret.
* Undefined outside cloud-hosted pages or when the id is not
* machine-derived. Absent on Desktop builds predating it. */
getInstallId?(): string | undefined
openTerminal?: () => Promise<boolean>
openMcpSetup?: () => Promise<boolean>
/** Opens a model provider access page in the hosted frontend's browser session.
Expand Down
2 changes: 1 addition & 1 deletion packages/comfyui-desktop-bridge-types/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-desktop-bridge-types",
"version": "0.5.0",
"version": "0.6.0",
"description": "TypeScript definitions for the Comfy Desktop hosted frontend bridge",
"type": "module",
"main": "./index.js",
Expand Down
101 changes: 100 additions & 1 deletion src/main/lib/ipc/registerDownloadHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ const mocks = vi.hoisted(() => ({
handle: vi.fn(),
on: vi.fn(),
openModelAccessPageWindow: vi.fn(),
showItemInFolder: vi.fn()
showItemInFolder: vi.fn(),
getDeviceId: vi.fn(),
getIdClass: vi.fn()
}))

vi.mock('electron', () => ({
Expand Down Expand Up @@ -35,6 +37,11 @@ vi.mock('../modelAccessPage', () => ({
openModelAccessPageWindow: mocks.openModelAccessPageWindow
}))

vi.mock('../deviceId', () => ({
getDeviceId: mocks.getDeviceId,
getIdClass: mocks.getIdClass
}))

import { registerDownloadHandlers } from './registerDownloadHandlers'

type IpcHandler = (
Expand Down Expand Up @@ -66,3 +73,95 @@ describe('registerDownloadHandlers model access bridge', () => {
expect(mocks.openModelAccessPageWindow).toHaveBeenCalledWith(sender, url)
})
})

type SyncIpcEvent = { sender: unknown; senderFrame: unknown; returnValue?: unknown }

const CLOUD_URL = 'https://cloud.comfy.org/'

function onHandler(channel: string): (event: SyncIpcEvent) => void {
const call = mocks.on.mock.calls.find(([name]) => name === channel)
expect(call).toBeDefined()
return call![1] as (event: SyncIpcEvent) => void
}

type FrameShape = 'main' | 'subframe' | 'missing'

function requestInstallId({ url = CLOUD_URL, frame = 'main' as FrameShape } = {}): unknown {
const mainFrame = { processId: 3, routingId: 1, url }
const senderFrames = {
main: { processId: 3, routingId: 1, url },
subframe: { processId: 3, routingId: 2, url },
missing: null
}
const event: SyncIpcEvent = {
sender: { id: 7, mainFrame },
senderFrame: senderFrames[frame]
}
onHandler('desktop2-get-install-id')(event)
return event.returnValue
}

describe('registerDownloadHandlers install id bridge', () => {
beforeEach(() => {
vi.clearAllMocks()
registerDownloadHandlers()
})

it('answers a cloud page with the machine-derived id', () => {
mocks.findEntryByComfySender.mockReturnValue({ sourceCategory: 'cloud' })
mocks.getIdClass.mockReturnValue('machine_derived')
mocks.getDeviceId.mockReturnValue('install-abc')

expect(requestInstallId()).toBe('install-abc')
})

it.for(['remote', 'local', null])('withholds the id from a %s page', (sourceCategory) => {
mocks.findEntryByComfySender.mockReturnValue({ sourceCategory })
mocks.getIdClass.mockReturnValue('machine_derived')
mocks.getDeviceId.mockReturnValue('install-abc')

expect(requestInstallId()).toBeUndefined()
})

it('withholds the id once a cloud view has navigated off the trusted cloud origin', () => {
mocks.findEntryByComfySender.mockReturnValue({ sourceCategory: 'cloud' })
mocks.getIdClass.mockReturnValue('machine_derived')
mocks.getDeviceId.mockReturnValue('install-abc')

expect(requestInstallId({ url: 'https://example.com/' })).toBeUndefined()
})

it('withholds the id from a subframe of a cloud page', () => {
mocks.findEntryByComfySender.mockReturnValue({ sourceCategory: 'cloud' })
mocks.getIdClass.mockReturnValue('machine_derived')
mocks.getDeviceId.mockReturnValue('install-abc')

expect(requestInstallId({ frame: 'subframe' })).toBeUndefined()
})

it('withholds the id when the sending frame is gone', () => {
mocks.findEntryByComfySender.mockReturnValue({ sourceCategory: 'cloud' })
mocks.getIdClass.mockReturnValue('machine_derived')
mocks.getDeviceId.mockReturnValue('install-abc')

expect(requestInstallId({ frame: 'missing' })).toBeUndefined()
})

it('withholds the id from an unregistered sender', () => {
mocks.findEntryByComfySender.mockReturnValue(null)
mocks.getIdClass.mockReturnValue('machine_derived')

expect(requestInstallId()).toBeUndefined()
})

it.for(['random_fallback', 'placeholder_fallback'])(
'withholds a %s id rather than sending one that changes every launch',
(idClass) => {
mocks.findEntryByComfySender.mockReturnValue({ sourceCategory: 'cloud' })
mocks.getIdClass.mockReturnValue(idClass)
mocks.getDeviceId.mockReturnValue('random-per-launch')

expect(requestInstallId()).toBeUndefined()
}
)
})
21 changes: 21 additions & 0 deletions src/main/lib/ipc/registerDownloadHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BrowserWindow, ipcMain, shell } from 'electron'
import type { IpcMainEvent } from 'electron'
import path from 'path'
import { findEntryByComfySender } from '../../host/registry'
import {
Expand All @@ -12,14 +13,34 @@ import {
retryDownload,
startModelDownload
} from '../comfyDownloadManager'
import { getDeviceId, getIdClass } from '../deviceId'
import { openModelAccessPageWindow } from '../modelAccessPage'
import { isTrustedCloudUrl } from '../trustedCloudUrl'

function isTrustedCloudSender({ sender, senderFrame }: IpcMainEvent): boolean {
if (findEntryByComfySender(sender)?.sourceCategory !== 'cloud' || !senderFrame) return false
const mainFrame = sender.mainFrame
return (
senderFrame.processId === mainFrame.processId &&
senderFrame.routingId === mainFrame.routingId &&
isTrustedCloudUrl(senderFrame.url)
)
}

export function registerDownloadHandlers(): void {
ipcMain.on('desktop2-is-remote', (event) => {
const entry = findEntryByComfySender(event.sender)
event.returnValue = entry?.sourceCategory === 'cloud' || entry?.sourceCategory === 'remote'
})

// Per-installation id for cloud attribution (sent as X-Comfy-Install-Id by
// the cloud-hosted frontend). Only the main frame of a trusted cloud page
// gets it, and only a machine-derived id: a fallback id changes every launch.
ipcMain.on('desktop2-get-install-id', (event) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: getInstallId() is typed as returning string, and the preload casts with as string, but there are two paths here that do not produce an install id.

Unregistered handler. ipcRenderer.sendSync on a channel with no listener leaves event.returnValue unset and resolves to undefined. This handler is registered inside registerDownloadHandlers(), called at src/main/index.ts:2211. If the hosted frontend can reach the bridge before that call runs, the cast silently yields undefined and the frontend puts undefined into a header.

Pre-init fallback, which worries me more. The comment on this block says "getDeviceId() is safe post-initDeviceId()", but nothing in this file enforces that — it is an assumption about startup ordering held in a comment. And deviceId.ts is explicit about what happens when the assumption does not hold: "It must only be called after initDeviceId() has resolved; if called earlier it falls back to a random UUID flagged as 'random_fallback'."

That failure is silent and points the wrong way. A random UUID is a perfectly well-formed string, so nothing downstream notices — cloud receives a valid-looking X-Comfy-Install-Id that is different on every launch, and the attribution this feature exists for quietly stops working while every dashboard still shows data. The description says the frontend reads this "inline while building request headers", so the first requests after launch are precisely where a race would land.

I could not prove the race is reachable without tracing the full startup against window creation, so I am not claiming it fires today. The ask is to make the contract honest rather than ordering-dependent: return undefined (or an explicit sentinel) when the id is not yet initialised and let the frontend omit the header, rather than returning something indistinguishable from a real id. getIdClass() is already exported next door and would let this answer "not ready" precisely. The preload signature and the .d.ts would then want string | undefined, which the frontend is already prepared for since it feature-detects the method anyway.

The new test covers the happy path well. A case for "id not initialised" would pin whichever behaviour you choose.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On the return type: agreed. It now returns the id only when it's machine-derived and omits it otherwise, typed string | undefined, with a test. Done in b82bfaf.

event.returnValue =
isTrustedCloudSender(event) && getIdClass() === 'machine_derived' ? getDeviceId() : undefined
})

Comment thread
coderabbitai[bot] marked this conversation as resolved.
ipcMain.handle('desktop2-open-model-access-page', (event, payload?: { url?: unknown }) =>
openModelAccessPageWindow(event.sender, payload?.url)
)
Expand Down
4 changes: 4 additions & 0 deletions src/preload/comfyPreload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,12 @@ const Auth: ComfyDesktop2AuthBridge = {
}
}

let installId: string | undefined

const bridge = {
isRemote: (): boolean => ipcRenderer.sendSync('desktop2-is-remote') as boolean,
getInstallId: (): string | undefined =>
(installId ??= ipcRenderer.sendSync('desktop2-get-install-id') as string | undefined),
openModelAccessPage: (url: string): Promise<boolean> => {
return ipcRenderer.invoke('desktop2-open-model-access-page', { url })
},
Expand Down
7 changes: 7 additions & 0 deletions src/types/comfyDesktopBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ export interface ComfyDesktop2Bridge {
/** Reports whether the backend server is cloud/remote, not the user's location.
* Optional: desktop builds predating it are still in the wild. */
isRemote?(): boolean
/** Returns the per-installation id: a non-PII, stable-per-machine value
* (`SHA-256(machine_id + salt)`, see main/lib/deviceId). The hosted
* frontend sends it to cloud as the `X-Comfy-Install-Id` header so cloud
* can attribute requests to a Desktop install. Not an identity/secret.
* Undefined outside cloud-hosted pages or when the id is not
* machine-derived. Absent on Desktop builds predating it. */
getInstallId?(): string | undefined
openTerminal?: () => Promise<boolean>
openMcpSetup?: () => Promise<boolean>
/** Opens a model provider access page in the hosted frontend's browser session.
Expand Down
Loading