From 95dbc047cc213fa228aac64aeec44d49ac137824 Mon Sep 17 00:00:00 2001 From: kjxcodez Date: Wed, 5 Aug 2026 18:10:14 +0530 Subject: [PATCH 1/8] fix(desktop): whitelist missing campaigns and scheduler IPC channels in preload script to resolve enrollment unauthorized errors --- apps/desktop/src/preload/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index a4a1f0e..4514ecb 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -135,7 +135,13 @@ contextBridge.exposeInMainWorld('ipc', { 'updater:download', 'updater:install', 'agent:execute', - 'agent:workflow:execute' + 'agent:workflow:execute', + 'campaigns:enroll', + 'campaigns:enrollments:list', + 'campaigns:bulk-pause-enrollments', + 'campaigns:bulk-resume-enrollments', + 'campaigns:bulk-remove-enrollments', + 'scheduler:queue:list' ]; if (validChannels.includes(channel as string)) { return ipcRenderer.invoke(channel, payload); From bc5f6b4ab9c9afa1adb8908bc60bbdfa274c4330 Mon Sep 17 00:00:00 2001 From: kjxcodez Date: Wed, 5 Aug 2026 18:25:09 +0530 Subject: [PATCH 2/8] fix(desktop): handle sequence steps safely as parsed array or string to prevent render crash --- apps/desktop/src/renderer/screens/CampaignsScreen.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/screens/CampaignsScreen.tsx b/apps/desktop/src/renderer/screens/CampaignsScreen.tsx index d6e5b6d..086e628 100644 --- a/apps/desktop/src/renderer/screens/CampaignsScreen.tsx +++ b/apps/desktop/src/renderer/screens/CampaignsScreen.tsx @@ -837,7 +837,7 @@ export default function CampaignsScreen() { {sequence?.name || '—'} - Steps: {sequence?.steps ? JSON.parse(sequence.steps).length : 0} steps + Steps: {sequence?.steps ? (typeof sequence.steps === 'string' ? JSON.parse(sequence.steps).length : sequence.steps.length) : 0} steps @@ -1179,7 +1179,7 @@ export default function CampaignsScreen() { {getEnrollmentStatusBadge(selectedEnrollment.status)} Step {selectedEnrollment.currentStepIndex || 0} of{' '} - {sequence?.steps ? JSON.parse(sequence.steps).length : 0} + {sequence?.steps ? (typeof sequence.steps === 'string' ? JSON.parse(sequence.steps).length : sequence.steps.length) : 0} {selectedEnrollment.status === 'replied' && ( From 8ab18d8084d6754eae8dd6d41e155b662c733af6 Mon Sep 17 00:00:00 2001 From: kjxcodez Date: Wed, 5 Aug 2026 18:33:09 +0530 Subject: [PATCH 3/8] fix(worker): make sequence status check case-insensitive in automation worker --- apps/desktop/src/main/workers/plugins/automation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/main/workers/plugins/automation.ts b/apps/desktop/src/main/workers/plugins/automation.ts index ba782e2..36023c6 100644 --- a/apps/desktop/src/main/workers/plugins/automation.ts +++ b/apps/desktop/src/main/workers/plugins/automation.ts @@ -914,7 +914,7 @@ export async function executeAutomationWorkflow(ctx: JobContext): Promise { `Automation workflow: sequence "${sequenceId}" not found in workspace "${ctx.workspaceId}".` ); } - if (sequence.status !== 'active') { + if (sequence.status?.toLowerCase() !== 'active') { throw new Error( `Automation workflow: sequence "${sequence.name}" is not active (status: "${sequence.status}").` ); From e4414ecb60d3ac38234f1f92a5cec0279e2ab659 Mon Sep 17 00:00:00 2001 From: kjxcodez Date: Wed, 5 Aug 2026 18:33:42 +0530 Subject: [PATCH 4/8] fix(ipc): bind type parameter in getAvgDuration metrics query --- apps/desktop/src/main/ipc/observability-ipc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/main/ipc/observability-ipc.ts b/apps/desktop/src/main/ipc/observability-ipc.ts index 7d5c914..19ba18e 100644 --- a/apps/desktop/src/main/ipc/observability-ipc.ts +++ b/apps/desktop/src/main/ipc/observability-ipc.ts @@ -289,7 +289,7 @@ export function registerObservabilityIpc() { WHERE type = ? AND status = 'completed' AND durationMs IS NOT NULL ` ) - .get() as any; + .get(type) as any; return Math.round(row?.avgVal || 0); }; From c4f53c7ab72d66f624646ae33216477ee1aa2991 Mon Sep 17 00:00:00 2001 From: kjxcodez Date: Wed, 5 Aug 2026 18:34:47 +0530 Subject: [PATCH 5/8] fix(ipc): select both id and jobId in scheduler queue list query --- apps/desktop/src/main/ipc/campaigns-ipc.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/src/main/ipc/campaigns-ipc.ts b/apps/desktop/src/main/ipc/campaigns-ipc.ts index 2f55e24..c5801b0 100644 --- a/apps/desktop/src/main/ipc/campaigns-ipc.ts +++ b/apps/desktop/src/main/ipc/campaigns-ipc.ts @@ -300,6 +300,7 @@ export function registerCampaignsIpc(): void { .prepare( ` SELECT + j.id, j.id as jobId, j.type, j.status, From e3a3f7da724c3b9f9589a4c0a81b205d783c7cf6 Mon Sep 17 00:00:00 2001 From: kjxcodez Date: Wed, 5 Aug 2026 18:53:21 +0530 Subject: [PATCH 6/8] fix(sqlite): add tags column to contacts table via migration 025 --- apps/desktop/src/main/database/runner.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/desktop/src/main/database/runner.ts b/apps/desktop/src/main/database/runner.ts index c291dfc..8138bff 100644 --- a/apps/desktop/src/main/database/runner.ts +++ b/apps/desktop/src/main/database/runner.ts @@ -663,6 +663,12 @@ export const MIGRATIONS: Migration[] = [ CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_memory_key ON workspace_memory(workspaceId, scope, key); ` + }, + { + name: '025_add_contacts_tags', + up: ` + ALTER TABLE contacts ADD COLUMN tags TEXT; + ` } ]; From f4d1f45932079086d63284821672bc9d0a198484 Mon Sep 17 00:00:00 2001 From: kjxcodez Date: Wed, 5 Aug 2026 19:03:42 +0530 Subject: [PATCH 7/8] fix(worker): decrypt and bind SMTP credentials from connected sender accounts --- .../src/main/workers/plugins/automation.ts | 66 ++++++++++++++----- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/main/workers/plugins/automation.ts b/apps/desktop/src/main/workers/plugins/automation.ts index 36023c6..175e0b9 100644 --- a/apps/desktop/src/main/workers/plugins/automation.ts +++ b/apps/desktop/src/main/workers/plugins/automation.ts @@ -3,6 +3,7 @@ import { randomUUID } from 'crypto'; import nodemailer from 'nodemailer'; import { AIRuntime, PromptsLibrary } from '@leadforge/ai'; import type { JobContext } from '../../../shared/types/job'; +import { decryptSecret } from '../../lib/crypto'; // ── Types ───────────────────────────────────────────────────────────────────── @@ -1827,32 +1828,67 @@ async function handleSendEmailStep( const renderedBody = resolveVariables(tpl.body, renderCtx); const settings = loadSettings(db, workspaceId); - const account = db - .prepare( - ` - SELECT id, email, name - FROM email_accounts - WHERE workspaceId = ? AND status = 'connected' AND deletedAt IS NULL - ORDER BY createdAt ASC LIMIT 1 - ` - ) - .get(workspaceId) as { id: string; email: string; name: string } | undefined; - let host = resolveSettingValue(ctx.payload._secrets, settings, 'smtp.host', 'smtpHost', 'host'); + let accountId: string | undefined = undefined; + if (execCtx.execution.campaignId) { + try { + const campaignRow = db + .prepare('SELECT sendingAccountId FROM campaigns WHERE id = ? AND deletedAt IS NULL') + .get(execCtx.execution.campaignId) as { sendingAccountId: string } | undefined; + accountId = campaignRow?.sendingAccountId; + } catch {} + } + + let account: any = undefined; + if (accountId) { + account = db + .prepare( + ` + SELECT id, email, name, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword + FROM email_accounts + WHERE id = ? AND workspaceId = ? AND status = 'connected' AND deletedAt IS NULL + ` + ) + .get(accountId, workspaceId); + } + + if (!account) { + account = db + .prepare( + ` + SELECT id, email, name, smtpHost, smtpPort, smtpSecure, smtpUsername, smtpPassword + FROM email_accounts + WHERE workspaceId = ? AND status = 'connected' AND deletedAt IS NULL + ORDER BY createdAt ASC LIMIT 1 + ` + ) + .get(workspaceId); + } + + let accountPassword = ''; + if (account?.smtpPassword) { + try { + accountPassword = decryptSecret(account.smtpPassword); + } catch { + accountPassword = account.smtpPassword; + } + } + + let host = resolveSettingValue(ctx.payload._secrets, settings, 'smtp.host', 'smtpHost', 'host') || account?.smtpHost; let portStr = resolveSettingValue( ctx.payload._secrets, settings, 'smtp.port', 'smtpPort', 'port' - ); + ) || (account?.smtpPort ? String(account.smtpPort) : undefined); let secureStr = resolveSettingValue( ctx.payload._secrets, settings, 'smtp.secure', 'smtpSecure', 'secure' - ); + ) || (account?.smtpSecure ? String(account.smtpSecure) : undefined); let username = resolveSettingValue( ctx.payload._secrets, settings, @@ -1860,7 +1896,7 @@ async function handleSendEmailStep( 'smtp.user', 'smtpUsername', 'username' - ); + ) || account?.smtpUsername; let password = resolveSettingValue( ctx.payload._secrets, settings, @@ -1868,7 +1904,7 @@ async function handleSendEmailStep( 'smtp.pass', 'smtpPassword', 'password' - ); + ) || accountPassword; let senderName = resolveSettingValue( ctx.payload._secrets, From 8aa293138c247d649689227308ef3a44bfcd2d65 Mon Sep 17 00:00:00 2001 From: kjxcodez Date: Wed, 5 Aug 2026 19:06:24 +0530 Subject: [PATCH 8/8] fix(worker): fetch campaignId from database to fix type error on ExecutionContext --- apps/desktop/src/main/workers/plugins/automation.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/main/workers/plugins/automation.ts b/apps/desktop/src/main/workers/plugins/automation.ts index 175e0b9..3853968 100644 --- a/apps/desktop/src/main/workers/plugins/automation.ts +++ b/apps/desktop/src/main/workers/plugins/automation.ts @@ -1830,14 +1830,17 @@ async function handleSendEmailStep( const settings = loadSettings(db, workspaceId); let accountId: string | undefined = undefined; - if (execCtx.execution.campaignId) { - try { + try { + const execRow = db + .prepare('SELECT campaignId FROM sequence_executions WHERE id = ?') + .get(execCtx.execution.id) as { campaignId: string | null } | undefined; + if (execRow?.campaignId) { const campaignRow = db .prepare('SELECT sendingAccountId FROM campaigns WHERE id = ? AND deletedAt IS NULL') - .get(execCtx.execution.campaignId) as { sendingAccountId: string } | undefined; + .get(execRow.campaignId) as { sendingAccountId: string } | undefined; accountId = campaignRow?.sendingAccountId; - } catch {} - } + } + } catch {} let account: any = undefined; if (accountId) {