diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..0dcdcf3a Binary files /dev/null and b/.DS_Store differ diff --git a/src/main/app/ipc-handlers.ts b/src/main/app/ipc-handlers.ts index c72af8d5..abaa673a 100644 --- a/src/main/app/ipc-handlers.ts +++ b/src/main/app/ipc-handlers.ts @@ -329,16 +329,16 @@ export function registerIpcHandlers(deps: IpcDeps): void { const composioConnections = new ComposioConnectionService(deps.connection.db); const connectorGrants = new ConnectorGrantsService(deps.connection.db); - ipcMain.handle(IPC_CHANNELS.listComposioToolkits, async () => { + ipcMain.handle(IPC_CHANNELS.listConnectorCatalog, async () => { try { - return ok(await composioConnections.listToolkits()); + return ok(await composioConnections.listCatalog()); } catch (error) { - return fail("composio_toolkits_failed", error instanceof Error ? error.message : String(error)); + return fail("composio_catalog_failed", error instanceof Error ? error.message : String(error)); } }); ipcMain.handle(IPC_CHANNELS.connectComposioToolkit, async (_event, input) => { try { - const result = await composioConnections.connectToolkit(input.orgId, input.toolkitSlug, input.toolkitName); + const result = await composioConnections.connectToolkit(input.orgId, input.toolkitSlug); notifyDataChanged(); return ok(result); } catch (error) { diff --git a/src/main/database/migrations/047-auth-config-purge.ts b/src/main/database/migrations/047-auth-config-purge.ts new file mode 100644 index 00000000..45586d4b --- /dev/null +++ b/src/main/database/migrations/047-auth-config-purge.ts @@ -0,0 +1,9 @@ +/** + * Auth configs are configured in the Composio dashboard and mirrored through + * the API at runtime — the app no longer creates them, so the create-once + * auth_config:* cache rows in composio_settings are never read again and are + * purged. entity_id rows stay: connected-account calls still use them. + */ +export const authConfigPurgeSchema = ` + delete from composio_settings where key like 'auth_config:%'; +`; diff --git a/src/main/database/schema.ts b/src/main/database/schema.ts index 933e0d35..44c39ea5 100644 --- a/src/main/database/schema.ts +++ b/src/main/database/schema.ts @@ -45,6 +45,7 @@ import { composioIntegrationSchema } from "./migrations/043-composio-integration import { mentionWakeStateSchema } from "./migrations/044-mention-wake-state"; import { connectorGrantsSchema } from "./migrations/045-connector-grants"; import { composioKeyPurgeSchema } from "./migrations/046-composio-key-purge"; +import { authConfigPurgeSchema } from "./migrations/047-auth-config-purge"; interface Migration { version: number; @@ -99,6 +100,7 @@ const MIGRATIONS: Migration[] = [ { version: 44, name: "mention_wake_state", sql: mentionWakeStateSchema }, { version: 45, name: "connector_grants", sql: connectorGrantsSchema }, { version: 46, name: "composio_key_purge", sql: composioKeyPurgeSchema }, + { version: 47, name: "auth_config_purge", sql: authConfigPurgeSchema }, ]; const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version; diff --git a/src/main/services/composio/composio-client.ts b/src/main/services/composio/composio-client.ts index 1da70d39..85270005 100644 --- a/src/main/services/composio/composio-client.ts +++ b/src/main/services/composio/composio-client.ts @@ -13,15 +13,11 @@ export interface ComposioToolSchema { }; } -export interface ComposioToolkitSummary { - slug: string; - name: string; - description: string; - logo: string | null; - categories: string[]; - authSchemes: string[]; - noAuth: boolean; - toolsCount: number; +export interface ComposioAuthConfigSummary { + id: string; + toolkitSlug: string; + authScheme: string; + status: string; } export interface ComposioConnectedAccount { @@ -40,34 +36,28 @@ export interface ComposioToolExecutionResult { export class ComposioClient { constructor(private readonly apiKey: string) {} - /** The toolkit catalog, most popular first. */ - async listToolkits(limit: number): Promise { - const toolkits: ComposioToolkitSummary[] = []; + /** Every auth config in the Composio project — the connector catalog. */ + async listAuthConfigs(): Promise { + const configs: ComposioAuthConfigSummary[] = []; let cursor: string | null = null; - while (toolkits.length < limit) { - const params = new URLSearchParams({ limit: String(Math.min(100, limit - toolkits.length)) }); + do { + const params = new URLSearchParams({ limit: "100" }); if (cursor) params.set("cursor", cursor); - const response = await this.request("GET", `${BASE_URL}/toolkits?${params.toString()}`); + const response = await this.request("GET", `${BASE_URL}/auth_configs?${params.toString()}`); const items = Array.isArray(response.items) ? response.items as Array> : []; for (const item of items) { - const meta = (item.meta ?? {}) as Record; - toolkits.push({ - slug: String(item.slug ?? ""), - name: String(item.name ?? ""), - description: String(meta.description ?? "").slice(0, 200), - logo: typeof meta.logo === "string" ? meta.logo : null, - categories: Array.isArray(meta.categories) - ? (meta.categories as Array>).map((c) => String(c.name ?? c.id ?? "")).filter(Boolean) - : [], - authSchemes: Array.isArray(item.auth_schemes) ? (item.auth_schemes as unknown[]).map(String) : [], - noAuth: item.no_auth === true, - toolsCount: Number(meta.tools_count ?? 0), + const toolkit = (item.toolkit ?? {}) as Record; + configs.push({ + id: String(item.id ?? ""), + toolkitSlug: String(toolkit.slug ?? "").toLowerCase(), + authScheme: String(item.auth_scheme ?? ""), + status: String(item.status ?? ""), }); } cursor = typeof response.next_cursor === "string" && response.next_cursor ? response.next_cursor : null; - if (!cursor || items.length === 0) break; - } - return toolkits; + if (items.length === 0) break; + } while (cursor); + return configs; } async getTools(toolkits: string[]): Promise { @@ -83,17 +73,6 @@ export class ComposioClient { return all; } - /** Creates a Composio-managed auth config for a toolkit. */ - async createAuthConfig(toolkitSlug: string): Promise { - const response = await this.request("POST", `${BASE_URL}/auth_configs`, { - toolkit: { slug: toolkitSlug }, - }); - const authConfig = (response.auth_config ?? {}) as Record; - const id = String(authConfig.id ?? ""); - if (!id) throw new Error("Composio auth config creation returned no id."); - return id; - } - /** * Starts a connection for a user; the redirect URL completes it in the * browser. Composio-managed auth configs only accept the link flow — diff --git a/src/main/services/composio/composio-connection-service.ts b/src/main/services/composio/composio-connection-service.ts index 83784cf9..5b533997 100644 --- a/src/main/services/composio/composio-connection-service.ts +++ b/src/main/services/composio/composio-connection-service.ts @@ -1,42 +1,44 @@ import type { DatabaseSync } from "node:sqlite"; -import type { ComposioToolkitSummaryDto, ConnectComposioToolkitResult } from "../../../shared/commands"; -import { ComposioClient, type ComposioToolkitSummary } from "./composio-client"; +import type { ConnectorCatalogEntry, ConnectComposioToolkitResult } from "../../../shared/commands"; +import { ComposioClient } from "./composio-client"; import { composioApiKey } from "./composio-credentials"; import { getComposioUserId } from "./composio-tool-bridge"; +import { connectorDisplayName, connectorLogoUrl } from "./connector-brands"; import { normalizeSlug } from "./connector-grants"; -const TOOLKIT_CATALOG_LIMIT = 200; const CATALOG_TTL_MS = 10 * 60 * 1000; -let catalogCache: { fetchedAt: number; toolkits: ComposioToolkitSummary[] } | null = null; +let catalogCache: { fetchedAt: number; entries: ConnectorCatalogEntry[] } | null = null; /** - * Company-level Composio connections. One connected account per toolkit per - * organization, mirrored into connector_accounts so agent runs and the - * Connectors page read local state instead of calling out on every frame. + * Company-level Composio connections. The catalog mirrors the auth configs + * of the Composio project — the dashboard is the source of truth for which + * connectors exist; the app never creates auth configs, it only connects + * accounts against them. One connected account per toolkit per organization, + * mirrored into connector_accounts so agent runs and the Connectors page + * read local state instead of calling out on every frame. */ export class ComposioConnectionService { constructor(private readonly db: DatabaseSync) {} - listToolkits(): Promise { - const client = this.requireClient(); - return this.catalog(client); + listCatalog(): Promise { + return this.catalog(this.requireClient()); } /** - * Starts (or restarts) the company's connection for a toolkit: creates a - * Composio-managed auth config once per toolkit, initiates the connected - * account for the company user via the link flow, and records the pending - * account locally as INITIALIZING — the poll that follows adopts Composio's - * real status. The returned redirect URL completes authorization in the - * browser. + * Starts (or restarts) the company's connection for a toolkit: initiates a + * connected account against the toolkit's configured auth config via the + * link flow and records the pending account locally as INITIALIZING — the + * poll that follows adopts Composio's real status. The returned redirect + * URL completes authorization (or API-key entry) in the browser. */ - async connectToolkit(orgId: string, toolkitSlug: string, toolkitName: string): Promise { + async connectToolkit(orgId: string, toolkitSlug: string): Promise { const slug = normalizeSlug(toolkitSlug); const client = this.requireClient(); - const authConfigId = await this.ensureAuthConfig(client, orgId, slug); + const entry = (await this.catalog(client)).find((item) => item.slug === slug); + if (!entry) throw new Error(`No auth config for "${slug}" exists in the Composio project.`); const account = await client.createConnectedAccount({ - authConfigId, + authConfigId: entry.authConfigId, userId: getComposioUserId(this.db, orgId), }); if (!account.id) throw new Error("Composio did not return a connected account id."); @@ -50,7 +52,7 @@ export class ComposioConnectionService { auth_config_id = excluded.auth_config_id, status = excluded.status, updated_at = excluded.updated_at - `).run(account.id, orgId, slug, toolkitName.trim() || slug, authConfigId, "INITIALIZING", now, now); + `).run(account.id, orgId, slug, entry.name, entry.authConfigId, "INITIALIZING", now, now); return { accountId: account.id, redirectUrl: account.redirectUrl }; } @@ -73,7 +75,7 @@ export class ComposioConnectionService { account.id, orgId, account.toolkitSlug, - account.toolkitSlug, + connectorDisplayName(account.toolkitSlug), account.authConfigId, account.status, account.createdAt || now, @@ -88,27 +90,24 @@ export class ComposioConnectionService { return new ComposioClient(apiKey); } - private async ensureAuthConfig(client: ComposioClient, orgId: string, slug: string): Promise { - const key = `auth_config:${slug}`; - const existing = this.db.prepare( - "select value from composio_settings where org_id = ? and key = ? limit 1", - ).get(orgId, key) as { value: string } | undefined; - if (existing?.value) return existing.value; - const authConfigId = await client.createAuthConfig(slug); - this.db.prepare(` - insert into composio_settings(org_id, key, value, updated_at) values (?, ?, ?, ?) - on conflict(org_id, key) do update set value = excluded.value, updated_at = excluded.updated_at - `).run(orgId, key, authConfigId, new Date().toISOString()); - return authConfigId; - } - - private async catalog(client: ComposioClient): Promise { + private async catalog(client: ComposioClient): Promise { if (catalogCache && Date.now() - catalogCache.fetchedAt < CATALOG_TTL_MS) { - return catalogCache.toolkits; + return catalogCache.entries; } - const toolkits = await client.listToolkits(TOOLKIT_CATALOG_LIMIT); - catalogCache = { fetchedAt: Date.now(), toolkits }; - return toolkits; + const configs = await client.listAuthConfigs(); + const bySlug = new Map(); + for (const config of configs) { + if (config.status !== "ENABLED" || !config.toolkitSlug || bySlug.has(config.toolkitSlug)) continue; + bySlug.set(config.toolkitSlug, { + authConfigId: config.id, + slug: config.toolkitSlug, + name: connectorDisplayName(config.toolkitSlug), + authScheme: config.authScheme, + logoUrl: connectorLogoUrl(config.toolkitSlug), + }); + } + const entries = [...bySlug.values()].sort((a, b) => a.name.localeCompare(b.name)); + catalogCache = { fetchedAt: Date.now(), entries }; + return entries; } - } diff --git a/src/main/services/composio/connector-brands.ts b/src/main/services/composio/connector-brands.ts new file mode 100644 index 00000000..b3150c0e --- /dev/null +++ b/src/main/services/composio/connector-brands.ts @@ -0,0 +1,67 @@ +/** + * Display names and logo domains for the toolkits configured in the Composio + * project. Logos come from img.logo.dev keyed by company domain (publishable + * token — safe to embed). A slug outside this table renders with a prettified + * name and the letter tile until its brand row is added here. + */ +const LOGO_DEV_TOKEN = "***REMOVED***"; + +const BRANDS: Record = { + airtable: { name: "Airtable", domain: "airtable.com" }, + attio: { name: "Attio", domain: "attio.com" }, + cal: { name: "Cal.com", domain: "cal.com" }, + calendly: { name: "Calendly", domain: "calendly.com" }, + canva: { name: "Canva", domain: "canva.com" }, + cloudflare: { name: "Cloudflare", domain: "cloudflare.com" }, + discord: { name: "Discord", domain: "discord.com" }, + elevenlabs: { name: "ElevenLabs", domain: "elevenlabs.io" }, + figma: { name: "Figma", domain: "figma.com" }, + firecrawl: { name: "Firecrawl", domain: "firecrawl.dev" }, + github: { name: "GitHub", domain: "github.com" }, + gmail: { name: "Gmail", domain: "gmail.com" }, + google_analytics: { name: "Google Analytics", domain: "analytics.google.com" }, + googleads: { name: "Google Ads", domain: "ads.google.com" }, + googlecalendar: { name: "Google Calendar", domain: "calendar.google.com" }, + googledocs: { name: "Google Docs", domain: "docs.google.com" }, + googledrive: { name: "Google Drive", domain: "drive.google.com" }, + googlesheets: { name: "Google Sheets", domain: "sheets.google.com" }, + googletasks: { name: "Google Tasks", domain: "tasks.google.com" }, + heygen: { name: "HeyGen", domain: "heygen.com" }, + junglescout: { name: "Jungle Scout", domain: "junglescout.com" }, + linear: { name: "Linear", domain: "linear.app" }, + linkedin: { name: "LinkedIn", domain: "linkedin.com" }, + mailchimp: { name: "Mailchimp", domain: "mailchimp.com" }, + monday: { name: "Monday", domain: "monday.com" }, + notion: { name: "Notion", domain: "notion.so" }, + one_drive: { name: "OneDrive", domain: "onedrive.com" }, + outlook: { name: "Outlook", domain: "outlook.com" }, + perplexityai: { name: "Perplexity", domain: "perplexity.ai" }, + reddit: { name: "Reddit", domain: "reddit.com" }, + salesforce: { name: "Salesforce", domain: "salesforce.com" }, + sentry: { name: "Sentry", domain: "sentry.io" }, + share_point: { name: "SharePoint", domain: "sharepoint.com" }, + shopify: { name: "Shopify", domain: "shopify.com" }, + slack: { name: "Slack", domain: "slack.com" }, + stripe: { name: "Stripe", domain: "stripe.com" }, + supabase: { name: "Supabase", domain: "supabase.com" }, + tavily: { name: "Tavily", domain: "tavily.com" }, + twitter: { name: "X", domain: "x.com" }, + whatsapp: { name: "WhatsApp", domain: "whatsapp.com" }, + zoho: { name: "Zoho", domain: "zoho.com" }, +}; + +export function connectorDisplayName(slug: string): string { + const brand = BRANDS[slug]; + if (brand) return brand.name; + return slug + .split(/[_-]+/) + .filter(Boolean) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} + +export function connectorLogoUrl(slug: string): string | null { + const brand = BRANDS[slug]; + if (!brand) return null; + return `https://img.logo.dev/${brand.domain}?token=${LOGO_DEV_TOKEN}`; +} diff --git a/src/preload/index.ts b/src/preload/index.ts index ef51b7ab..0b32fe07 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -83,7 +83,7 @@ const api: DesktopApi = { setLegalHoldState: (input) => invoke(IPC_CHANNELS.setLegalHoldState, input), createRetentionFreeze: (input) => invoke(IPC_CHANNELS.createRetentionFreeze, input), setRetentionFreezeState: (input) => invoke(IPC_CHANNELS.setRetentionFreezeState, input), - listComposioToolkits: (input) => invoke(IPC_CHANNELS.listComposioToolkits, input), + listConnectorCatalog: () => invoke(IPC_CHANNELS.listConnectorCatalog), connectComposioToolkit: (input) => invoke(IPC_CHANNELS.connectComposioToolkit, input), refreshComposioAccounts: (input) => invoke(IPC_CHANNELS.refreshComposioAccounts, input), grantConnector: (input) => invoke(IPC_CHANNELS.grantConnector, input), diff --git a/src/renderer/index.html b/src/renderer/index.html index e6056b45..714c469f 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -4,7 +4,7 @@ AgentCompany diff --git a/src/renderer/src/pages/ConnectorsPage.tsx b/src/renderer/src/pages/ConnectorsPage.tsx index db1b3f6e..46594988 100644 --- a/src/renderer/src/pages/ConnectorsPage.tsx +++ b/src/renderer/src/pages/ConnectorsPage.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { Button } from "../shared/ui"; -import type { ComposioToolkitSummaryDto } from "../../../shared/commands"; +import type { ConnectorCatalogEntry } from "../../../shared/commands"; import type { PageProps } from "./types"; import { activeOrgId, requireActiveEmployee } from "./types"; import { ConnectedList } from "./connectors/ConnectedList"; @@ -8,7 +8,7 @@ import { ToolkitCatalog } from "./connectors/ToolkitCatalog"; type CatalogState = | { phase: "loading" } - | { phase: "ready"; toolkits: ComposioToolkitSummaryDto[] } + | { phase: "ready"; entries: ConnectorCatalogEntry[] } | { phase: "error"; message: string }; type CatalogSlot = { orgId: string; state: CatalogState } | null; @@ -47,14 +47,22 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) { [accounts], ); + const logoBySlug = useMemo(() => { + const map = new Map(); + if (catalog.phase === "ready") { + for (const entry of catalog.entries) map.set(entry.slug, entry.logoUrl); + } + return map; + }, [catalog]); + useEffect(() => { let cancelled = false; - void window.agentCompany.listComposioToolkits({ orgId }).then((result) => { + void window.agentCompany.listConnectorCatalog().then((result) => { if (cancelled) return; setCatalogSlot({ orgId, state: result.ok - ? { phase: "ready", toolkits: result.data } + ? { phase: "ready", entries: result.data } : { phase: "error", message: result.error.message }, }); }); @@ -89,17 +97,17 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) { }, CONNECT_POLL_INTERVAL_MS); }; - const connect = (toolkit: ComposioToolkitSummaryDto) => { - setBusySlug(toolkit.slug); + const connect = (entry: ConnectorCatalogEntry) => { + setBusySlug(entry.slug); setConnectError(null); void window.agentCompany - .connectComposioToolkit({ orgId, toolkitSlug: toolkit.slug, toolkitName: toolkit.name }) + .connectComposioToolkit({ orgId, toolkitSlug: entry.slug }) .then(async (result) => { if (!result.ok) { setConnectError(result.error.message); return; } - pollUntilActive(toolkit.slug, 0); + pollUntilActive(entry.slug, 0); if (result.data.redirectUrl) { const opened = await window.agentCompany.openExternalUrl(result.data.redirectUrl); if (!opened.ok) setConnectError(opened.error.message); @@ -133,11 +141,15 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) { )} - + - {catalog.phase === "loading" && ( -
Loading catalog…
- )} {catalog.phase === "error" && (
-
- {filtered.map((toolkit) => { - const connected = connectedSlugs.has(toolkit.slug); +
+ {filtered.map((entry) => { + const connected = connectedSlugs.has(entry.slug); return (
- -
-
- - {toolkit.name} - - {toolkit.categories[0] && {toolkit.categories[0]}} -
-
- {toolkit.description} -
-
- {connected ? ( - Connected - ) : ( - - )} -
-
+ + + {entry.name} + + {connected ? ( + Connected + ) : ( + + )}
); })} diff --git a/src/shared/commands/composio.ts b/src/shared/commands/composio.ts index 35c61268..17af3b9d 100644 --- a/src/shared/commands/composio.ts +++ b/src/shared/commands/composio.ts @@ -1,22 +1,14 @@ -export interface ComposioToolkitSummaryDto { +export interface ConnectorCatalogEntry { + authConfigId: string; slug: string; name: string; - description: string; - logo: string | null; - categories: string[]; - authSchemes: string[]; - noAuth: boolean; - toolsCount: number; -} - -export interface ListComposioToolkitsInput { - orgId: string; + authScheme: string; + logoUrl: string | null; } export interface ConnectComposioToolkitInput { orgId: string; toolkitSlug: string; - toolkitName: string; } export interface ConnectComposioToolkitResult { diff --git a/src/shared/desktop-api.ts b/src/shared/desktop-api.ts index 9d35e121..3b1fcfe6 100644 --- a/src/shared/desktop-api.ts +++ b/src/shared/desktop-api.ts @@ -64,11 +64,10 @@ import type { SyncSkillsFromDiskInput, RevokeProviderCredentialInput, SaveProviderCredentialInput, - ComposioToolkitSummaryDto, ConnectComposioToolkitInput, ConnectComposioToolkitResult, + ConnectorCatalogEntry, ConnectorGrantInput, - ListComposioToolkitsInput, RefreshComposioAccountsInput, SearchInput, SendMessageInput, @@ -180,7 +179,7 @@ export interface DesktopApi { setLegalHoldState(input: SetLegalHoldStateInput): Promise>; createRetentionFreeze(input: CreateRetentionFreezeInput): Promise>; setRetentionFreezeState(input: SetRetentionFreezeStateInput): Promise>; - listComposioToolkits(input: ListComposioToolkitsInput): Promise>; + listConnectorCatalog(): Promise>; connectComposioToolkit(input: ConnectComposioToolkitInput): Promise>; refreshComposioAccounts(input: RefreshComposioAccountsInput): Promise>; grantConnector(input: ConnectorGrantInput): Promise>; diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index 94559b6e..a85485cc 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -75,7 +75,7 @@ export const IPC_CHANNELS = { deleteMeeting: "agentcompany:delete-meeting", createSprint: "agentcompany:create-sprint", deleteSprint: "agentcompany:delete-sprint", - listComposioToolkits: "agentcompany:list-composio-toolkits", + listConnectorCatalog: "agentcompany:list-connector-catalog", connectComposioToolkit: "agentcompany:connect-composio-toolkit", refreshComposioAccounts: "agentcompany:refresh-composio-accounts", grantConnector: "agentcompany:grant-connector", diff --git a/tests/unit/auth-config-purge-migration.test.ts b/tests/unit/auth-config-purge-migration.test.ts new file mode 100644 index 00000000..37ff3146 --- /dev/null +++ b/tests/unit/auth-config-purge-migration.test.ts @@ -0,0 +1,31 @@ +import { randomUUID } from "node:crypto"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; +import { DatabaseConnection } from "../../src/main/database/connection"; +import { authConfigPurgeSchema } from "../../src/main/database/migrations/047-auth-config-purge"; +import { OrganizationService } from "../../src/main/services/organization-service"; + +describe("auth config purge migration", () => { + test("removes cached auth_config rows but keeps the entity id", () => { + const connection = new DatabaseConnection(join(tmpdir(), `ac-authconfig-purge-${randomUUID()}.db`)); + const organization = new OrganizationService(connection.db); + let orgId = ""; + connection.transaction(() => { + orgId = organization.createOrganization({ organizationName: "OrbitOps", employeeName: "Yang", mission: "m", goal: "g" }); + }); + const now = new Date().toISOString(); + const insert = connection.db.prepare( + "insert into composio_settings(org_id, key, value, updated_at) values (?, ?, ?, ?)", + ); + insert.run(orgId, "entity_id", orgId, now); + insert.run(orgId, "auth_config:github", "ac_123", now); + insert.run(orgId, "auth_config:stripe", "ac_456", now); + + connection.db.exec(authConfigPurgeSchema); + + const keys = (connection.db.prepare("select key from composio_settings where org_id = ? order by key asc").all(orgId) as Array<{ key: string }>) + .map((row) => row.key); + expect(keys).toEqual(["entity_id"]); + }); +}); diff --git a/tests/unit/connector-brands.test.ts b/tests/unit/connector-brands.test.ts new file mode 100644 index 00000000..8eaab8af --- /dev/null +++ b/tests/unit/connector-brands.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "vitest"; +import { connectorDisplayName, connectorLogoUrl } from "../../src/main/services/composio/connector-brands"; + +describe("connector brands", () => { + test("known toolkits resolve a display name and a logo.dev URL", () => { + expect(connectorDisplayName("google_analytics")).toBe("Google Analytics"); + expect(connectorLogoUrl("google_analytics")).toBe( + "https://img.logo.dev/analytics.google.com?token=***REMOVED***", + ); + expect(connectorDisplayName("share_point")).toBe("SharePoint"); + expect(connectorLogoUrl("stripe")).toContain("img.logo.dev/stripe.com"); + }); + + test("unknown toolkits prettify the slug and carry no logo", () => { + expect(connectorDisplayName("acme_billing")).toBe("Acme Billing"); + expect(connectorLogoUrl("acme_billing")).toBeNull(); + }); +}); diff --git a/tests/unit/domain-services.test.ts b/tests/unit/domain-services.test.ts index f3a42097..75f08d71 100644 --- a/tests/unit/domain-services.test.ts +++ b/tests/unit/domain-services.test.ts @@ -24,7 +24,7 @@ describe("domain services", () => { const connection = createConnection(); const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>; - expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46]); + expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]); connection.close(); });