Embed the Composio key in the app bundle instead of shipping cred.txt

Root cause of the recurring 401: a revoked key was hardcoded in
startup-recovery.ts and onboarding-service.ts and reseeded into
composio_settings on every launch, and the interim fix shipped cred.txt
as a visible file under resources/.

- electron.vite.config.ts reads the gitignored cred.txt at build time
  and injects the key into the main-process bundle as
  __COMPOSIO_API_KEY__; no credential file is packaged and the key is
  not user-visible or configurable. The renderer bundle never sees it.
- Delete both hardcoded-key seeders; migration 046 purges stored
  api_key and never-read toolkits rows while keeping entity_id and
  auth_config state.
- Stop shipping composio_settings in the renderer snapshot.
This commit is contained in:
ZacharyZhang-NY committed 2026-07-07 01:40:19 -04:00
1 parent 2d43950123
commit a2de132083
16 files changed
+105 -181

No files matched your search

@@ -1,48 +1,17 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import type { DatabaseSync } from "node:sqlite";
/**
* The Composio integration key ships with the product: cred.txt (first line
* "Composio apikey: ak_...", gitignored) sits at the project root in
* development and inside the packaged app's resources directory in
* production. Wherever the file exists it is the source of truth — rotating
* the key in the file replaces whatever the database held on the next
* resolution. It is not user-configurable.
* The Composio integration key ships inside the compiled main-process
* bundle: electron.vite.config.ts reads the gitignored cred.txt at the
* repository root during build and injects the key as __COMPOSIO_API_KEY__.
* No credential file is packaged, nothing is persisted to the database, and
* the key is never user-visible or configurable. Rotating the key means
* updating cred.txt and rebuilding.
*/
export function resolveComposioApiKey(db: DatabaseSync, orgId: string, credentialFileDirs: string[] = defaultCredentialDirs()): 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;
const fileKey = readCredentialFile(credentialFileDirs);
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", fileKey, now);
upsert.run(orgId, "entity_id", orgId, now);
return fileKey;
export function composioApiKey(): string | null {
return normalizeComposioApiKey(typeof __COMPOSIO_API_KEY__ === "string" ? __COMPOSIO_API_KEY__ : "");
}
/** Project root in dev; the app's resources directory when packaged. */
export function defaultCredentialDirs(): string[] {
const dirs = [process.cwd()];
if (process.resourcesPath) dirs.push(process.resourcesPath);
return dirs;
}
function readCredentialFile(dirs: string[]): string | null {
for (const dir of dirs) {
const path = join(dir, "cred.txt");
if (!existsSync(path)) continue;
const firstLine = readFileSync(path, "utf8").split("\n")[0] ?? "";
const key = firstLine.split(":").slice(1).join(":").trim();
if (key) return key;
}
return null;
/** Null for blank input so callers surface "not configured" explicitly. */
export function normalizeComposioApiKey(raw: string): string | null {
const key = raw.trim();
return key ? key : null;
}