UNPKG

@earendil-works/pi-coding-agent

Version:

Coding agent CLI with read, bash, edit, write tools and session management

229 lines 7.81 kB
/** * CredentialStore implementation backed by auth.json. * Provider auth orchestration belongs to ModelRuntime and pi-ai Models. */ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { dirname, join } from "path"; import lockfile from "proper-lockfile"; import { getAgentDir } from "../config.js"; import { normalizePath } from "../utils/paths.js"; import { resolveConfigValue } from "./resolve-config-value.js"; const AUTH_FILE_WRITE_OPTIONS = { encoding: "utf-8", mode: 0o600 }; export class FileAuthStorageBackend { authPath; constructor(authPath = join(getAgentDir(), "auth.json")) { this.authPath = normalizePath(authPath); } ensureParentDir() { const dir = dirname(this.authPath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true, mode: 0o700 }); } } ensureFileExists() { if (!existsSync(this.authPath)) { writeFileSync(this.authPath, "{}", AUTH_FILE_WRITE_OPTIONS); chmodSync(this.authPath, 0o600); } } acquireLockSyncWithRetry(path) { const maxAttempts = 10; const delayMs = 20; let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return lockfile.lockSync(path, { realpath: false }); } catch (error) { const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : undefined; if (code !== "ELOCKED" || attempt === maxAttempts) { throw error; } lastError = error; const start = Date.now(); while (Date.now() - start < delayMs) { // Sleep synchronously to avoid changing callers to async. } } } throw lastError ?? new Error("Failed to acquire auth storage lock"); } withLock(fn) { this.ensureParentDir(); this.ensureFileExists(); let release; try { release = this.acquireLockSyncWithRetry(this.authPath); const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined; const { result, next } = fn(current); if (next !== undefined) { writeFileSync(this.authPath, next, AUTH_FILE_WRITE_OPTIONS); chmodSync(this.authPath, 0o600); } return result; } finally { if (release) { release(); } } } async withLockAsync(fn) { this.ensureParentDir(); this.ensureFileExists(); let release; let lockCompromised = false; let lockCompromisedError; const throwIfCompromised = () => { if (lockCompromised) { throw lockCompromisedError ?? new Error("Auth storage lock was compromised"); } }; try { release = await lockfile.lock(this.authPath, { retries: { retries: 10, factor: 2, minTimeout: 100, maxTimeout: 10000, randomize: true, }, stale: 30000, onCompromised: (err) => { lockCompromised = true; lockCompromisedError = err; }, }); throwIfCompromised(); const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined; const { result, next } = await fn(current); throwIfCompromised(); if (next !== undefined) { writeFileSync(this.authPath, next, AUTH_FILE_WRITE_OPTIONS); chmodSync(this.authPath, 0o600); } throwIfCompromised(); return result; } finally { if (release) { try { await release(); } catch { // Ignore unlock errors when lock is compromised. } } } } } export class InMemoryAuthStorageBackend { value; withLock(fn) { const { result, next } = fn(this.value); if (next !== undefined) { this.value = next; } return result; } async withLockAsync(fn) { const { result, next } = await fn(this.value); if (next !== undefined) { this.value = next; } return result; } } /** * Credential storage backed by a JSON file. */ export class AuthStorage { data = {}; storage; constructor(storage) { this.storage = storage; this.reload(); } static create(authPath) { return new AuthStorage(new FileAuthStorageBackend(authPath ?? join(getAgentDir(), "auth.json"))); } static fromStorage(storage) { return new AuthStorage(storage); } static inMemory(data = {}) { const storage = new InMemoryAuthStorageBackend(); storage.withLock(() => ({ result: undefined, next: JSON.stringify(data, null, 2) })); return AuthStorage.fromStorage(storage); } parseStorageData(content) { if (!content) { return {}; } return JSON.parse(content); } /** * Reload credentials from storage. */ reload() { let content; try { this.storage.withLock((current) => { content = current; return { result: undefined }; }); this.data = this.parseStorageData(content); } catch { // Preserve the last valid in-memory snapshot. } } async read(provider) { const credential = this.data[provider]; if (credential?.type !== "api_key") return credential; if (credential.key === undefined) return credential; return { ...credential, key: resolveConfigValue(credential.key, credential.env) }; } async modify(provider, fn) { return this.storage.withLockAsync(async (content) => { const currentData = this.parseStorageData(content); const next = await fn(currentData[provider]); if (next === undefined) { this.data = currentData; return { result: currentData[provider] }; } const merged = { ...currentData, [provider]: next }; this.data = merged; return { result: next, next: JSON.stringify(merged, null, 2) }; }); } async delete(provider) { await this.storage.withLockAsync(async (content) => { const currentData = this.parseStorageData(content); delete currentData[provider]; this.data = currentData; return { result: undefined, next: JSON.stringify(currentData, null, 2) }; }); } /** List credential metadata without resolving configured key values. */ async list() { return Object.entries(this.data).map(([providerId, credential]) => ({ providerId, type: credential.type })); } } /** * One-off synchronous read of a stored credential from an auth.json file, * without instantiating a store or resolving configured key values. */ export function readStoredCredential(providerId, authPath = join(getAgentDir(), "auth.json")) { try { const data = JSON.parse(readFileSync(normalizePath(authPath), "utf-8")); return data[providerId]; } catch { return undefined; } } //# sourceMappingURL=auth-storage.js.map