/** * OpenCode API-key resolution. * * Resolves the OPENCODE_API_KEY from, in priority order: * 1. the `OPENCODE_API_KEY` environment variable * 2. `~/.pi/agent/auth.json` → `{ "opencode": "" }` or * `{ "openCodeApiKey": "" }` * * Also normalizes the "$OPENCODE_API_KEY" literal that a host may pass when no * real key is configured, and exposes the auth-header convention per protocol * (x-api-key for Anthropic format, Authorization: Bearer otherwise). */ import { readFileSync } from "node:fs" import { AUTH_FILE_PATH, ENV_LITERAL_PREFIX, OPENCODE_API_KEY_ENV, } from "../../config.ts" import type { ProtocolType } from "../../types.ts" export interface OpencodeAuth { /** The resolved API key, or undefined when none is configured. */ apiKey: string | undefined /** Header name to carry the key ("x-api-key" or "Authorization"). */ authHeader: string /** Value prefix ("Bearer " for OpenAI/Google, "" for Anthropic). */ authPrefix: string } /** True when a value is the raw "$ENV_VAR" literal a host forwards un-resolved. */ export function isEnvLiteral(value: string | undefined): boolean { return !!value && value.startsWith(ENV_LITERAL_PREFIX) } /** Read and parse the auth file, returning {} on any error (never throws). */ function readAuthFile(path: string = AUTH_FILE_PATH): Record { try { const raw = readFileSync(path, "utf8") const parsed = JSON.parse(raw) return isJsonObject(parsed) ? (parsed as Record) : {} } catch { return {} } } function isJsonObject(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value) } function asString(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined } /** * Resolve the OpenCode API key. * * @param envInject process.env-like object (injectable for testing) * @param authFileInject parsed auth-file contents (injectable for testing) */ export function resolveOpencodeApiKey( envInject: Record = process.env, authFileInject?: Record ): string | undefined { const fromEnv = asString(envInject[OPENCODE_API_KEY_ENV]) if (fromEnv && !isEnvLiteral(fromEnv)) return fromEnv const file = authFileInject ?? readAuthFile() const fromFile = asString(file["opencode"]) ?? asString(file["openCodeApiKey"]) if (fromFile && !isEnvLiteral(fromFile)) return fromFile return undefined } /** * Build the auth descriptor for a given protocol. Anthropic-format upstreams * use `x-api-key`; everything else uses `Authorization: Bearer`. */ export function authForProtocol( protocol: ProtocolType, apiKey: string | undefined ): OpencodeAuth { if (protocol === "anthropic-messages") { return { apiKey, authHeader: "x-api-key", authPrefix: "" } } return { apiKey, authHeader: "Authorization", authPrefix: "Bearer " } }