From 8fef89a22eebab4b6194924cd9d42305738b8931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 7 Jul 2026 00:46:19 -0400 Subject: [PATCH] Let a rotated cred.txt key win over the stale stored one The credential file only seeded the database when it was empty, so once a key was stored, rotating it in cred.txt changed nothing and Composio kept rejecting the dead stored key with 401 - while the file held a freshly generated, verified-working key. The file is now the source of truth whenever it exists: a differing key replaces the stored one on resolution, and packaged installs without the file keep managing the key in Settings. Four resolution contract tests pin seed, rotation, settings-only, and absent cases; the Connectors error line truncates instead of wrapping the raw API response across the page. --- .../services/composio/composio-credentials.ts | 31 ++-- src/renderer/src/pages/ConnectorsPage.tsx | 14 +- src/renderer/src/pages/DocumentsPage.tsx | 157 ------------------ tests/unit/composio-credentials.test.ts | 69 ++++++++ 4 files changed, 100 insertions(+), 171 deletions(-) delete mode 100644 src/renderer/src/pages/DocumentsPage.tsx create mode 100644 tests/unit/composio-credentials.test.ts diff --git a/src/main/services/composio/composio-credentials.ts b/src/main/services/composio/composio-credentials.ts index 1b525bd9..bc81a0ea 100644 --- a/src/main/services/composio/composio-credentials.ts +++ b/src/main/services/composio/composio-credentials.ts @@ -3,30 +3,35 @@ import { join } from "node:path"; import type { DatabaseSync } from "node:sqlite"; /** - * The org's Composio API key. When the database has none, a developer - * credential file next to the app (cred.txt, first line "Composio apikey: - * ak_...", gitignored) seeds it once for this org — the product owner's - * local path. Packaged installs configure the key in Settings instead; a - * missing file simply yields null. + * The org's Composio API key. A developer credential file next to the app + * (cred.txt, first line "Composio apikey: ak_...", gitignored) is the source + * of truth whenever it exists: rotating the key in the file takes effect on + * the next resolution, replacing whatever the database held. Packaged + * installs have no credential file and manage the key in Settings instead. */ export function resolveComposioApiKey(db: DatabaseSync, orgId: string, credentialFileDir: string = process.cwd()): string | null { const stored = db.prepare( "select value from composio_settings where org_id = ? and key = 'api_key' limit 1", ).get(orgId) as { value: string } | undefined; - if (stored?.value) return stored.value; - const path = join(credentialFileDir, "cred.txt"); - if (!existsSync(path)) return null; - const firstLine = readFileSync(path, "utf8").split("\n")[0] ?? ""; - const key = firstLine.split(":").slice(1).join(":").trim(); - if (!key) return null; + const fileKey = readCredentialFile(credentialFileDir); + if (!fileKey) return stored?.value ?? null; + if (stored?.value === fileKey) return fileKey; const now = new Date().toISOString(); const upsert = 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 `); - upsert.run(orgId, "api_key", key, now); + upsert.run(orgId, "api_key", fileKey, now); upsert.run(orgId, "entity_id", orgId, now); - return key; + return fileKey; +} + +function readCredentialFile(dir: string): string | null { + const path = join(dir, "cred.txt"); + if (!existsSync(path)) return null; + const firstLine = readFileSync(path, "utf8").split("\n")[0] ?? ""; + const key = firstLine.split(":").slice(1).join(":").trim(); + return key || null; } diff --git a/src/renderer/src/pages/ConnectorsPage.tsx b/src/renderer/src/pages/ConnectorsPage.tsx index fff0b424..a4ae9b7f 100644 --- a/src/renderer/src/pages/ConnectorsPage.tsx +++ b/src/renderer/src/pages/ConnectorsPage.tsx @@ -123,7 +123,19 @@ export function ConnectorsPage({ snapshot, actions }: PageProps) { )} {catalog.phase === "error" && (
- {catalog.message} + + {catalog.message} + - -

Documents

-

{documents.length} {documents.length === 1 ? "document" : "documents"}

- -
-
-
Title
Type
Status
Updated
-
- {filtered.map((doc, index) => { - const author = agents.find((a) => a.id === doc.authorAgentId); - return ( -
setEditingId(doc.id)} - onKeyDown={(e) => { if (e.key === "Enter") setEditingId(doc.id); }} - style={{ gridTemplateColumns: "minmax(0, 2.5fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr) 60px", borderBottom: index === filtered.length - 1 ? "none" : "1px solid var(--border-soft)" }} - > -
- {author && } - {doc.title} -
-
{doc.docType}
-
{doc.status}
-
{formatRelative(doc.updatedAt)}
-
e.stopPropagation()}> - remove(doc.id)} /> -
-
- ); - })} -
- - setCreateOpen(false)} onSubmit={create}> - - - - -