import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { type CredentialStore, createModels, type Provider } from "@earendil-works/pi-ai"; import lockfile from "proper-lockfile"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { AuthStorage, FileAuthStorageBackend } from "../src/core/auth-storage.ts"; describe("AuthStorage", () => { let tempDir: string; let authJsonPath: string; beforeEach(() => { tempDir = join(tmpdir(), `pi-test-auth-storage-${Date.now()}-${Math.random().toString(36).slice(2)}`); mkdirSync(tempDir, { recursive: true }); authJsonPath = join(tempDir, "auth.json"); }); afterEach(() => { if (existsSync(tempDir)) rmSync(tempDir, { recursive: true }); vi.restoreAllMocks(); }); function writeAuthJson(data: Record): void { writeFileSync(authJsonPath, JSON.stringify(data)); } test("reads and resolves stored API-key credentials", async () => { const original = process.env.TEST_AUTH_STORAGE_KEY; process.env.TEST_AUTH_STORAGE_KEY = "environment-key"; try { writeAuthJson({ anthropic: { type: "api_key", key: "$TEST_AUTH_STORAGE_KEY" } }); const storage = AuthStorage.create(authJsonPath); expect(await storage.read("anthropic")).toEqual({ type: "api_key", key: "environment-key" }); } finally { if (original === undefined) delete process.env.TEST_AUTH_STORAGE_KEY; else process.env.TEST_AUTH_STORAGE_KEY = original; } }); test("resolves command-backed API-key credentials", async () => { writeAuthJson({ anthropic: { type: "api_key", key: "!printf 'command-key'" } }); const storage = AuthStorage.create(authJsonPath); expect(await storage.read("anthropic")).toEqual({ type: "api_key", key: "command-key" }); }); test("returns OAuth credentials unchanged", async () => { const credential = { type: "oauth" as const, access: "access-token", refresh: "refresh-token", expires: Date.now() + 60_000, }; const storage = AuthStorage.inMemory({ anthropic: credential }); expect(await storage.read("anthropic")).toEqual(credential); }); test("credential-scoped env takes precedence and remains inspectable", async () => { writeAuthJson({ anthropic: { type: "api_key", key: "$SCOPED_KEY", env: { SCOPED_KEY: "scoped-value", REGION: "test-region" }, }, }); const storage = AuthStorage.create(authJsonPath); expect(await storage.read("anthropic")).toMatchObject({ key: "scoped-value", env: { SCOPED_KEY: "scoped-value", REGION: "test-region" }, }); }); test("coalesces file reloads across concurrent readers and storage instances", async () => { writeAuthJson({ anthropic: { type: "api_key", key: "old" } }); const first = AuthStorage.create(authJsonPath); const second = AuthStorage.create(authJsonPath); const lockSpy = vi.spyOn(lockfile, "lock"); writeAuthJson({ anthropic: { type: "api_key", key: "new" }, openai: { type: "api_key", key: "openai-key" }, }); const [anthropic, openai, credentials] = await Promise.all([ first.read("anthropic"), second.read("openai"), first.list(), ]); expect(anthropic).toEqual({ type: "api_key", key: "new" }); expect(openai).toEqual({ type: "api_key", key: "openai-key" }); expect(credentials).toEqual([ { providerId: "anthropic", type: "api_key" }, { providerId: "openai", type: "api_key" }, ]); expect(lockSpy).toHaveBeenCalledTimes(1); await expect(second.read("anthropic")).resolves.toEqual({ type: "api_key", key: "new" }); expect(lockSpy).toHaveBeenCalledTimes(1); writeAuthJson({ anthropic: { type: "api_key", key: "newest" } }); await expect(first.read("anthropic")).resolves.toEqual({ type: "api_key", key: "newest" }); expect(lockSpy).toHaveBeenCalledTimes(2); }); test("modify persists a credential while preserving unrelated external edits", async () => { writeAuthJson({ anthropic: { type: "api_key", key: "old" } }); const storage = AuthStorage.create(authJsonPath); writeAuthJson({ anthropic: { type: "api_key", key: "old" }, openai: { type: "api_key", key: "external" }, }); await storage.modify("anthropic", async () => ({ type: "api_key", key: "new" })); expect(JSON.parse(readFileSync(authJsonPath, "utf8"))).toEqual({ anthropic: { type: "api_key", key: "new" }, openai: { type: "api_key", key: "external" }, }); }); test("modify with undefined leaves the current credential unchanged", async () => { writeAuthJson({ anthropic: { type: "api_key", key: "stored" } }); const storage = AuthStorage.create(authJsonPath); expect(await storage.modify("anthropic", async () => undefined)).toEqual({ type: "api_key", key: "stored" }); expect(await storage.read("anthropic")).toEqual({ type: "api_key", key: "stored" }); }); test("serializes concurrent modifications", async () => { writeAuthJson({}); const first = AuthStorage.create(authJsonPath); const second = AuthStorage.create(authJsonPath); await Promise.all([ first.modify("anthropic", async () => ({ type: "api_key", key: "anthropic-key" })), second.modify("openai", async () => ({ type: "api_key", key: "openai-key" })), ]); expect(JSON.parse(readFileSync(authJsonPath, "utf8"))).toEqual({ anthropic: { type: "api_key", key: "anthropic-key" }, openai: { type: "api_key", key: "openai-key" }, }); }); test("delete removes one credential while preserving others", async () => { writeAuthJson({ anthropic: { type: "api_key", key: "anthropic-key" }, openai: { type: "api_key", key: "openai-key" }, }); const storage = AuthStorage.create(authJsonPath); writeAuthJson({ anthropic: { type: "api_key", key: "anthropic-key" }, openai: { type: "api_key", key: "openai-key" }, google: { type: "api_key", key: "external-key" }, }); await storage.delete("anthropic"); await expect(storage.list()).resolves.toEqual([ { providerId: "openai", type: "api_key" }, { providerId: "google", type: "api_key" }, ]); expect(await storage.read("anthropic")).toBeUndefined(); expect(await storage.read("openai")).toEqual({ type: "api_key", key: "openai-key" }); expect(await storage.read("google")).toEqual({ type: "api_key", key: "external-key" }); }); test("in-memory storage implements the same credential-store behavior", async () => { const storage = AuthStorage.inMemory({ anthropic: { type: "api_key", key: "initial" } }); expect(await storage.read("anthropic")).toEqual({ type: "api_key", key: "initial" }); await storage.modify("anthropic", async () => ({ type: "api_key", key: "updated" })); expect(await storage.read("anthropic")).toEqual({ type: "api_key", key: "updated" }); await storage.delete("anthropic"); await expect(storage.list()).resolves.toEqual([]); }); test("does not write after lock acquisition failure and recovers on retry", async () => { writeAuthJson({ anthropic: { type: "api_key", key: "stored" } }); const storage = AuthStorage.create(authJsonPath); const lockSpy = vi.spyOn(lockfile, "lock").mockRejectedValueOnce(new Error("lock unavailable")); await expect(storage.modify("openai", async () => ({ type: "api_key", key: "new" }))).rejects.toThrow( "lock unavailable", ); expect(JSON.parse(readFileSync(authJsonPath, "utf8"))).toEqual({ anthropic: { type: "api_key", key: "stored" }, }); lockSpy.mockRestore(); await storage.modify("openai", async () => ({ type: "api_key", key: "new" })); expect(JSON.parse(readFileSync(authJsonPath, "utf8"))).toEqual({ anthropic: { type: "api_key", key: "stored" }, openai: { type: "api_key", key: "new" }, }); }); test("surfaces a compromised file storage lock", async () => { writeAuthJson({ anthropic: { type: "api_key", key: "stored" } }); const backend = new FileAuthStorageBackend(authJsonPath); const update = vi.fn(async () => ({ result: undefined, next: JSON.stringify({}) })); const compromised = new Error("lock compromised"); vi.spyOn(lockfile, "lock").mockImplementation(async (_file, options) => { options?.onCompromised?.(compromised); return async () => {}; }); await expect(backend.withLockAsync(update)).rejects.toThrow(compromised); expect(update).not.toHaveBeenCalled(); expect(JSON.parse(readFileSync(authJsonPath, "utf8"))).toEqual({ anthropic: { type: "api_key", key: "stored" }, }); }); test("translates a credential-store refresh failure and allows a later retry", async () => { const providerId = "oauth-provider"; const base = AuthStorage.inMemory({ [providerId]: { type: "oauth", access: "expired-access", refresh: "refresh-token", expires: 0, }, }); let failNextModify = true; const credentials: CredentialStore = { read: (id) => base.read(id), list: () => base.list(), modify: (id, fn) => { if (failNextModify) { failNextModify = false; return Promise.reject(new Error("credential store unavailable")); } return base.modify(id, fn); }, delete: (id) => base.delete(id), }; const provider: Provider = { id: providerId, name: "OAuth Provider", auth: { oauth: { name: "OAuth", login: async () => { throw new Error("not used"); }, refresh: async (credential) => ({ ...credential, access: "refreshed-access", expires: Date.now() + 60_000, }), toAuth: async (credential) => ({ apiKey: credential.access }), }, }, getModels: () => [], stream: () => { throw new Error("not used"); }, streamSimple: () => { throw new Error("not used"); }, }; const models = createModels({ credentials }); models.setProvider(provider); await expect(models.getAuth(providerId)).rejects.toMatchObject({ code: "auth" }); await expect(models.getAuth(providerId)).resolves.toMatchObject({ auth: { apiKey: "refreshed-access" } }); }); test("does not overwrite malformed auth files", async () => { writeAuthJson({ anthropic: { type: "api_key", key: "stored" } }); const storage = AuthStorage.create(authJsonPath); writeFileSync(authJsonPath, "{invalid-json", "utf8"); await expect(storage.modify("openai", async () => ({ type: "api_key", key: "new" }))).rejects.toThrow(); expect(readFileSync(authJsonPath, "utf8")).toBe("{invalid-json"); }); });