/** * Factory Droid API key resolution. * * Resolution order: * 1. `FACTORY_API_KEY` env var * 2. `~/.factory/settings.json` → `apiKey` (the droid CLI config) * 3. pi's auth file (`/auth.json`) under a `factory`/`factory-droid` key * * The env-literal filter (`$` prefix) is applied so pi's `$FACTORY_API_KEY` * placeholder is treated as "not set" rather than sent upstream. */ import { existsSync, readFileSync } from "node:fs" import { homedir } from "node:os" import { join } from "node:path" import { FACTORY_API_KEY_ENV } from "./config.ts" import type { FactoryDroidDependencies } from "./types.ts" export const ENV_LITERAL_PREFIX = "$" export function isEnvLiteral(value: string | undefined): boolean { return !!value && value.startsWith(ENV_LITERAL_PREFIX) } function factorySettingsPath(home: string): string { return join(home, ".factory", "settings.json") } function defaultAuthPaths(home: string): string[] { return [ join(home, ".factory", "settings.json"), join(home, ".pi", "agent", "auth.json"), ] } /** Read `apiKey` (or a nested provider record) from a JSON file. */ function apiKeyFromFile(path: string): string | undefined { try { if (!existsSync(path)) return undefined const parsed: unknown = JSON.parse(readFileSync(path, "utf8")) if (!isRecord(parsed)) return undefined // ~/.factory/settings.json → { apiKey: "fk-..." } const direct = typeof parsed.apiKey === "string" ? parsed.apiKey : undefined if (direct) return direct // pi auth.json → { "factory": { "type":"api","key":... } } or { "factory-droid": {...} } for (const key of ["factory", "factory-droid", "factoryDroid"]) { const entry = parsed[key] if (!isRecord(entry)) continue const nested = typeof entry.key === "string" ? entry.key : undefined if (nested) return nested } return undefined } catch { return undefined } } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value) } export function getFactoryDroidApiKey( options: { env?: NodeJS.ProcessEnv authPaths?: readonly string[] homeDir?: () => string } = {}, ): string | undefined { const env = options.env ?? process.env const envKey = env[FACTORY_API_KEY_ENV] if (envKey && !isEnvLiteral(envKey)) return envKey const home = options.homeDir?.() ?? homedir() const authPaths = options.authPaths ?? defaultAuthPaths(home) for (const authPath of authPaths) { const key = apiKeyFromFile(authPath) if (key && !isEnvLiteral(key)) return key } return undefined } /** Resolve the API key with the same fallbacks, using the stream deps. */ export function resolveFactoryDroidApiKey( deps: Pick, ): string | undefined { return getFactoryDroidApiKey({ env: deps.env, authPaths: deps.authPaths, homeDir: deps.homeDir, }) }