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}
+