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:
1 parent
2d43950123
commit
a2de132083
16 files changed
+105
-181
No files matched your search
@@ -1,78 +1,19 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { DatabaseConnection } from "../../src/main/database/connection";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
import { resolveComposioApiKey } from "../../src/main/services/composio/composio-credentials";
|
||||
|
||||
interface Harness {
|
||||
connection: DatabaseConnection;
|
||||
orgId: string;
|
||||
credDir: string;
|
||||
}
|
||||
|
||||
function harness(): Harness {
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-cred-${randomUUID()}.db`));
|
||||
const organization = new OrganizationService(connection.db);
|
||||
let orgId = "";
|
||||
connection.transaction(() => {
|
||||
orgId = organization.createOrganization({ organizationName: "OrbitOps", employeeName: "Yang", mission: "m", goal: "g" });
|
||||
});
|
||||
const credDir = join(tmpdir(), `ac-cred-dir-${randomUUID()}`);
|
||||
mkdirSync(credDir, { recursive: true });
|
||||
return { connection, orgId, credDir };
|
||||
}
|
||||
|
||||
function storedKey(h: Harness): string | null {
|
||||
const row = h.connection.db.prepare(
|
||||
"select value from composio_settings where org_id = ? and key = 'api_key'",
|
||||
).get(h.orgId) as { value: string } | undefined;
|
||||
return row?.value ?? null;
|
||||
}
|
||||
|
||||
function writeCred(h: Harness, key: string): void {
|
||||
writeFileSync(join(h.credDir, "cred.txt"), `Composio apikey: ${key}\nProject id: pr_x\n`, "utf8");
|
||||
}
|
||||
import { composioApiKey, normalizeComposioApiKey } from "../../src/main/services/composio/composio-credentials";
|
||||
|
||||
describe("composio credential resolution", () => {
|
||||
test("seeds the database from the credential file", () => {
|
||||
const h = harness();
|
||||
writeCred(h, "ak_first");
|
||||
expect(resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir])).toBe("ak_first");
|
||||
expect(storedKey(h)).toBe("ak_first");
|
||||
test("trims the embedded key", () => {
|
||||
expect(normalizeComposioApiKey(" ak_live ")).toBe("ak_live");
|
||||
});
|
||||
|
||||
test("a rotated credential file replaces the stale stored key", () => {
|
||||
const h = harness();
|
||||
writeCred(h, "ak_old");
|
||||
resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir]);
|
||||
writeCred(h, "ak_rotated");
|
||||
expect(resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir])).toBe("ak_rotated");
|
||||
expect(storedKey(h)).toBe("ak_rotated");
|
||||
test("a blank key resolves to null so callers report the integration as unconfigured", () => {
|
||||
expect(normalizeComposioApiKey("")).toBeNull();
|
||||
expect(normalizeComposioApiKey(" ")).toBeNull();
|
||||
});
|
||||
|
||||
test("finds the credential file in a later directory when the first has none", () => {
|
||||
const h = harness();
|
||||
writeCred(h, "ak_resources");
|
||||
const emptyDir = join(tmpdir(), `ac-cred-empty-${randomUUID()}`);
|
||||
mkdirSync(emptyDir, { recursive: true });
|
||||
expect(resolveComposioApiKey(h.connection.db, h.orgId, [emptyDir, h.credDir])).toBe("ak_resources");
|
||||
expect(storedKey(h)).toBe("ak_resources");
|
||||
});
|
||||
|
||||
test("without a credential file the stored key stands", () => {
|
||||
const h = harness();
|
||||
const now = new Date().toISOString();
|
||||
h.connection.db.prepare(
|
||||
"insert into composio_settings(org_id, key, value, updated_at) values (?, 'api_key', 'ak_settings', ?)",
|
||||
).run(h.orgId, now);
|
||||
expect(resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir])).toBe("ak_settings");
|
||||
});
|
||||
|
||||
test("no file and no stored key yields null", () => {
|
||||
const h = harness();
|
||||
expect(resolveComposioApiKey(h.connection.db, h.orgId, [h.credDir])).toBeNull();
|
||||
test("without the build-time injection the key is null", () => {
|
||||
// Vitest runs without the electron-vite define — the same shape as a
|
||||
// build from a checkout that has no cred.txt.
|
||||
expect(composioApiKey()).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
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 { composioKeyPurgeSchema } from "../../src/main/database/migrations/046-composio-key-purge";
|
||||
import { OrganizationService } from "../../src/main/services/organization-service";
|
||||
|
||||
describe("composio key purge migration", () => {
|
||||
test("removes stored api_key and toolkits rows but keeps connection state", () => {
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-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, "api_key", "ak_revoked", now);
|
||||
insert.run(orgId, "toolkits", '["github"]', now);
|
||||
insert.run(orgId, "entity_id", orgId, now);
|
||||
insert.run(orgId, "auth_config:github", "ac_123", now);
|
||||
|
||||
connection.db.exec(composioKeyPurgeSchema);
|
||||
|
||||
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(["auth_config:github", "entity_id"]);
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
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]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user