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,7 +1,7 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { ComposioToolkitSummaryDto, ConnectComposioToolkitResult } from "../../../shared/commands";
|
||||
import { ComposioClient, type ComposioToolkitSummary } from "./composio-client";
|
||||
import { resolveComposioApiKey } from "./composio-credentials";
|
||||
import { composioApiKey } from "./composio-credentials";
|
||||
import { getComposioUserId } from "./composio-tool-bridge";
|
||||
import { normalizeSlug } from "./connector-grants";
|
||||
|
||||
@@ -18,8 +18,8 @@ let catalogCache: { fetchedAt: number; toolkits: ComposioToolkitSummary[] } | nu
|
||||
export class ComposioConnectionService {
|
||||
constructor(private readonly db: DatabaseSync) {}
|
||||
|
||||
listToolkits(orgId: string): Promise<ComposioToolkitSummaryDto[]> {
|
||||
const client = this.requireClient(orgId);
|
||||
listToolkits(): Promise<ComposioToolkitSummaryDto[]> {
|
||||
const client = this.requireClient();
|
||||
return this.catalog(client);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export class ComposioConnectionService {
|
||||
*/
|
||||
async connectToolkit(orgId: string, toolkitSlug: string, toolkitName: string): Promise<ConnectComposioToolkitResult> {
|
||||
const slug = normalizeSlug(toolkitSlug);
|
||||
const client = this.requireClient(orgId);
|
||||
const client = this.requireClient();
|
||||
const authConfigId = await this.ensureAuthConfig(client, orgId, slug);
|
||||
const account = await client.createConnectedAccount({
|
||||
authConfigId,
|
||||
@@ -54,7 +54,7 @@ export class ComposioConnectionService {
|
||||
|
||||
/** Pulls account statuses from Composio and updates the local mirror. */
|
||||
async refreshAccounts(orgId: string): Promise<void> {
|
||||
const client = this.requireClient(orgId);
|
||||
const client = this.requireClient();
|
||||
const accounts = await client.listConnectedAccounts(getComposioUserId(this.db, orgId));
|
||||
const now = new Date().toISOString();
|
||||
for (const account of accounts) {
|
||||
@@ -80,9 +80,9 @@ export class ComposioConnectionService {
|
||||
}
|
||||
}
|
||||
|
||||
private requireClient(orgId: string): ComposioClient {
|
||||
const apiKey = resolveComposioApiKey(this.db, orgId);
|
||||
if (!apiKey) throw new Error("Composio API key is not configured for this company.");
|
||||
private requireClient(): ComposioClient {
|
||||
const apiKey = composioApiKey();
|
||||
if (!apiKey) throw new Error("The Composio integration is not configured in this build.");
|
||||
return new ComposioClient(apiKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { ToolDefinition, ToolCall } from "../provider-tool-turn";
|
||||
import { ComposioClient, type ComposioToolSchema } from "./composio-client";
|
||||
import { resolveComposioApiKey } from "./composio-credentials";
|
||||
import { composioApiKey } from "./composio-credentials";
|
||||
import { ConnectorGrantsService } from "./connector-grants";
|
||||
|
||||
const COMPOSIO_TOOL_PREFIX = "composio_";
|
||||
@@ -53,7 +53,7 @@ export function composioSchemaToToolDefinition(schema: ComposioToolSchema): Tool
|
||||
* granted this agent.
|
||||
*/
|
||||
export async function loadComposioTools(db: DatabaseSync, orgId: string, agentId: string): Promise<ToolDefinition[]> {
|
||||
const apiKey = getComposioApiKey(db, orgId);
|
||||
const apiKey = composioApiKey();
|
||||
if (!apiKey) return [];
|
||||
const grants = new ConnectorGrantsService(db);
|
||||
const toolkits = grants.toolkitsForAgent(orgId, agentId);
|
||||
@@ -84,8 +84,8 @@ export async function executeComposioTool(
|
||||
agentId: string,
|
||||
call: ToolCall,
|
||||
): Promise<string> {
|
||||
const apiKey = getComposioApiKey(db, orgId);
|
||||
if (!apiKey) return "ERROR: Composio API key is not configured for this company.";
|
||||
const apiKey = composioApiKey();
|
||||
if (!apiKey) return "ERROR: The Composio integration is not configured in this build.";
|
||||
const toolSlug = originalToolName(call.name);
|
||||
const toolkit = toolkitForTool(toolSlug);
|
||||
const grants = new ConnectorGrantsService(db);
|
||||
@@ -114,10 +114,6 @@ export async function executeComposioTool(
|
||||
}
|
||||
}
|
||||
|
||||
function getComposioApiKey(db: DatabaseSync, orgId: string): string | null {
|
||||
return resolveComposioApiKey(db, orgId);
|
||||
}
|
||||
|
||||
export function getComposioUserId(db: DatabaseSync, orgId: string): string {
|
||||
const row = db.prepare(
|
||||
"select value from composio_settings where org_id = ? and key = 'entity_id' limit 1",
|
||||
|
||||
Reference in new issue
Block a user