@almightygpt/core 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/__tests__/keychain.test.d.ts +18 -0
- package/dist/auth/__tests__/keychain.test.d.ts.map +1 -0
- package/dist/auth/__tests__/keychain.test.js +155 -0
- package/dist/auth/__tests__/keychain.test.js.map +1 -0
- package/dist/auth/__tests__/resolver.test.d.ts +13 -0
- package/dist/auth/__tests__/resolver.test.d.ts.map +1 -0
- package/dist/auth/__tests__/resolver.test.js +182 -0
- package/dist/auth/__tests__/resolver.test.js.map +1 -0
- package/dist/auth/__tests__/validator.test.d.ts +15 -0
- package/dist/auth/__tests__/validator.test.d.ts.map +1 -0
- package/dist/auth/__tests__/validator.test.js +197 -0
- package/dist/auth/__tests__/validator.test.js.map +1 -0
- package/dist/auth/validator.js +19 -14
- package/dist/auth/validator.js.map +1 -1
- package/package.json +4 -2
- package/src/auth/__tests__/keychain.test.ts +171 -0
- package/src/auth/__tests__/resolver.test.ts +231 -0
- package/src/auth/__tests__/validator.test.ts +241 -0
- package/src/auth/validator.ts +27 -14
- package/src/index.ts +1 -1
package/dist/auth/validator.js
CHANGED
|
@@ -82,7 +82,7 @@ async function validateOpenAI(key) {
|
|
|
82
82
|
return {
|
|
83
83
|
ok: false,
|
|
84
84
|
statusCode: res.status,
|
|
85
|
-
error: normalizeOpenAIError(res.status, rawBody),
|
|
85
|
+
error: normalizeOpenAIError(res.status, rawBody, key),
|
|
86
86
|
rawBody,
|
|
87
87
|
};
|
|
88
88
|
}
|
|
@@ -117,7 +117,7 @@ async function validateAnthropic(key) {
|
|
|
117
117
|
return {
|
|
118
118
|
ok: false,
|
|
119
119
|
statusCode: res.status,
|
|
120
|
-
error: normalizeAnthropicError(res.status, rawBody),
|
|
120
|
+
error: normalizeAnthropicError(res.status, rawBody, key),
|
|
121
121
|
rawBody,
|
|
122
122
|
};
|
|
123
123
|
}
|
|
@@ -167,26 +167,26 @@ async function validateGoogle(key) {
|
|
|
167
167
|
// Parse known provider JSON error shapes into short, user-safe messages.
|
|
168
168
|
// Never echo the raw key back even by accident (defense in depth: we
|
|
169
169
|
// also redact anything that looks like the submitted key).
|
|
170
|
-
function normalizeOpenAIError(status, rawBody) {
|
|
170
|
+
function normalizeOpenAIError(status, rawBody, submittedKey) {
|
|
171
171
|
// OpenAI shape: { "error": { "message": "...", "type": "...", "code": "..." } }
|
|
172
172
|
try {
|
|
173
173
|
const parsed = JSON.parse(rawBody);
|
|
174
174
|
const msg = parsed.error?.message;
|
|
175
175
|
if (msg)
|
|
176
|
-
return `[${status}] OpenAI: ${truncate(msg, 200)}`;
|
|
176
|
+
return `[${status}] OpenAI: ${truncate(redactKey(msg, submittedKey), 200)}`;
|
|
177
177
|
}
|
|
178
178
|
catch {
|
|
179
179
|
/* fall through */
|
|
180
180
|
}
|
|
181
181
|
return statusOnlyMessage("OpenAI", status);
|
|
182
182
|
}
|
|
183
|
-
function normalizeAnthropicError(status, rawBody) {
|
|
183
|
+
function normalizeAnthropicError(status, rawBody, submittedKey) {
|
|
184
184
|
// Anthropic shape: { "type": "error", "error": { "type": "...", "message": "..." } }
|
|
185
185
|
try {
|
|
186
186
|
const parsed = JSON.parse(rawBody);
|
|
187
187
|
const msg = parsed.error?.message;
|
|
188
188
|
if (msg)
|
|
189
|
-
return `[${status}] Anthropic: ${truncate(msg, 200)}`;
|
|
189
|
+
return `[${status}] Anthropic: ${truncate(redactKey(msg, submittedKey), 200)}`;
|
|
190
190
|
}
|
|
191
191
|
catch {
|
|
192
192
|
/* fall through */
|
|
@@ -197,21 +197,26 @@ function normalizeGoogleError(status, rawBody, submittedKey) {
|
|
|
197
197
|
// Google shape: { "error": { "code": N, "message": "...", "status": "..." } }
|
|
198
198
|
try {
|
|
199
199
|
const parsed = JSON.parse(rawBody);
|
|
200
|
-
|
|
201
|
-
// Belt-and-braces redaction: Google sometimes echoes the key in
|
|
202
|
-
// error messages (e.g. "API key not valid. Pass a valid API key.")
|
|
203
|
-
// — we don't ship the actual key value if it ever ends up here.
|
|
204
|
-
if (submittedKey && msg.includes(submittedKey)) {
|
|
205
|
-
msg = msg.replace(submittedKey, "<redacted-key>");
|
|
206
|
-
}
|
|
200
|
+
const msg = parsed.error?.message ?? "";
|
|
207
201
|
if (msg)
|
|
208
|
-
return `[${status}] Google: ${truncate(msg, 200)}`;
|
|
202
|
+
return `[${status}] Google: ${truncate(redactKey(msg, submittedKey), 200)}`;
|
|
209
203
|
}
|
|
210
204
|
catch {
|
|
211
205
|
/* fall through */
|
|
212
206
|
}
|
|
213
207
|
return statusOnlyMessage("Google", status);
|
|
214
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Belt-and-braces: if a provider echoes the submitted key in its
|
|
211
|
+
* error body, redact before surfacing to the user. Codex's v0.8 P2 #6
|
|
212
|
+
* found this gap (originally Google-only); now applied to all three
|
|
213
|
+
* providers via this shared helper.
|
|
214
|
+
*/
|
|
215
|
+
function redactKey(msg, key) {
|
|
216
|
+
if (!key || !msg.includes(key))
|
|
217
|
+
return msg;
|
|
218
|
+
return msg.split(key).join("<redacted-key>");
|
|
219
|
+
}
|
|
215
220
|
function statusOnlyMessage(provider, status) {
|
|
216
221
|
if (status === 401 || status === 403) {
|
|
217
222
|
return `[${status}] ${provider} rejected the key (unauthorized).`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/auth/validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAoBzD,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAErC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,QAAoB,EACpB,GAAW;IAEX,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,IAAI,CAAC;QACH,IAAI,MAAwB,CAAC;QAC7B,QAAQ,QAAQ,EAAE,CAAC;YACjB,KAAK,QAAQ;gBACX,MAAM,GAAG,MAAM,cAAc,CAAC,GAAG,CAAC,CAAC;gBACnC,MAAM;YACR,KAAK,WAAW;gBACd,MAAM,GAAG,MAAM,iBAAiB,CAAC,GAAG,CAAC,CAAC;gBACtC,MAAM;YACR,KAAK,QAAQ;gBACX,MAAM,GAAG,MAAM,cAAc,CAAC,GAAG,CAAC,CAAC;gBACnC,MAAM;QACV,CAAC;QACD,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QACtC,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,oBAAoB,CAAC,GAAG,CAAC;YAChC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;SAC9B,CAAC;IACJ,CAAC;AACH,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,GAAW;IACvC,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC;IACpC,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,qBAAqB,CAAC,CAAC;IAC1E,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,4CAA4C,EAAE;YACpE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,GAAG,EAAE;aAC/B;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK;gBACL,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;gBAC3C,UAAU,EAAE,CAAC;aACd,CAAC;YACF,MAAM,EAAE,UAAU,CAAC,MAAM;SAC1B,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YACjD,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,UAAU,EAAE,GAAG,CAAC,MAAM;gBACtB,KAAK,EAAE,oBAAoB,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/auth/validator.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@almightygpt/core",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Core orchestrator, adapters, config, runs, and review logic for AlmightyGPT",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc -p tsconfig.json",
|
|
22
22
|
"clean": "rm -rf dist",
|
|
23
|
-
"watch": "tsc -p tsconfig.json --watch"
|
|
23
|
+
"watch": "tsc -p tsconfig.json --watch",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest"
|
|
24
26
|
},
|
|
25
27
|
"dependencies": {
|
|
26
28
|
"@anthropic-ai/sdk": "^0.30.0",
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keychain adapter tests.
|
|
3
|
+
*
|
|
4
|
+
* Strategy (addresses Codex's v0.8 plan-review R3 — "KeychainAdapter
|
|
5
|
+
* dependency injection"): we mock the dynamic import of
|
|
6
|
+
* `@napi-rs/keyring` itself, replacing the Entry class with a fake
|
|
7
|
+
* we control per test. This sidesteps needing to refactor the real
|
|
8
|
+
* code to accept an injected adapter — the dynamic-import boundary
|
|
9
|
+
* IS the seam we test against.
|
|
10
|
+
*
|
|
11
|
+
* Codex's main concern (P2 #4) was that read failures were silently
|
|
12
|
+
* converted into "absent". These tests prove the new behavior:
|
|
13
|
+
* - found → { kind: "found", key }
|
|
14
|
+
* - absent → { kind: "absent" }
|
|
15
|
+
* - throws → { kind: "error", message } (was: silently dropped)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
19
|
+
import { _resetKeychainCache } from "../keychain.js";
|
|
20
|
+
|
|
21
|
+
// Track the entry methods per test so we can rewire mid-suite.
|
|
22
|
+
let mockGetPassword: () => string | null;
|
|
23
|
+
let mockSetPassword: (key: string) => void;
|
|
24
|
+
let mockDeletePassword: () => boolean;
|
|
25
|
+
let constructorThrows = false;
|
|
26
|
+
let importThrows = false;
|
|
27
|
+
|
|
28
|
+
vi.mock("@napi-rs/keyring", () => ({
|
|
29
|
+
get Entry() {
|
|
30
|
+
if (importThrows) throw new Error("synthetic import failure");
|
|
31
|
+
return class FakeEntry {
|
|
32
|
+
constructor(_service: string, _account: string) {
|
|
33
|
+
if (constructorThrows) {
|
|
34
|
+
throw new Error("native binding refused to initialize");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
getPassword() {
|
|
38
|
+
return mockGetPassword();
|
|
39
|
+
}
|
|
40
|
+
setPassword(p: string) {
|
|
41
|
+
mockSetPassword(p);
|
|
42
|
+
}
|
|
43
|
+
deletePassword() {
|
|
44
|
+
return mockDeletePassword();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
import { getKeychain } from "../keychain.js";
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
_resetKeychainCache();
|
|
54
|
+
mockGetPassword = () => null;
|
|
55
|
+
mockSetPassword = () => {};
|
|
56
|
+
mockDeletePassword = () => true;
|
|
57
|
+
constructorThrows = false;
|
|
58
|
+
importThrows = false;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("keychain adapter — availability", () => {
|
|
62
|
+
it("reports available=true when native binding loads cleanly", async () => {
|
|
63
|
+
const kc = await getKeychain();
|
|
64
|
+
expect(kc.available).toBe(true);
|
|
65
|
+
expect(kc.describeBackend()).not.toContain("unavailable");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("reports available=false + reason when native binding throws on init", async () => {
|
|
69
|
+
constructorThrows = true;
|
|
70
|
+
const kc = await getKeychain();
|
|
71
|
+
expect(kc.available).toBe(false);
|
|
72
|
+
expect(kc.describeBackend()).toContain("unavailable");
|
|
73
|
+
expect(kc.describeBackend()).toMatch(/initialize|native/i);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("get on unavailable adapter returns { kind: 'absent' } (not throw)", async () => {
|
|
77
|
+
constructorThrows = true;
|
|
78
|
+
const kc = await getKeychain();
|
|
79
|
+
const r = await kc.get("openai");
|
|
80
|
+
expect(r).toEqual({ kind: "absent" });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("set on unavailable adapter throws with a clear hint", async () => {
|
|
84
|
+
constructorThrows = true;
|
|
85
|
+
const kc = await getKeychain();
|
|
86
|
+
await expect(kc.set("openai", "x")).rejects.toThrow(/unavailable|environment/i);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("keychain adapter — get behavior (Codex P2 #4 regression coverage)", () => {
|
|
91
|
+
it("found path returns { kind: 'found', key }", async () => {
|
|
92
|
+
mockGetPassword = () => "stored-secret";
|
|
93
|
+
const kc = await getKeychain();
|
|
94
|
+
const r = await kc.get("anthropic");
|
|
95
|
+
expect(r).toEqual({ kind: "found", key: "stored-secret" });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("absent path returns { kind: 'absent' } when getPassword returns null", async () => {
|
|
99
|
+
mockGetPassword = () => null;
|
|
100
|
+
const kc = await getKeychain();
|
|
101
|
+
const r = await kc.get("openai");
|
|
102
|
+
expect(r).toEqual({ kind: "absent" });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("error path returns { kind: 'error', message } when getPassword throws — NOT silently absent", async () => {
|
|
106
|
+
mockGetPassword = () => {
|
|
107
|
+
throw new Error("keyring locked by OS");
|
|
108
|
+
};
|
|
109
|
+
const kc = await getKeychain();
|
|
110
|
+
const r = await kc.get("openai");
|
|
111
|
+
expect(r.kind).toBe("error");
|
|
112
|
+
if (r.kind === "error") {
|
|
113
|
+
expect(r.message).toContain("keyring locked");
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("error path preserves non-Error throws via String(err)", async () => {
|
|
118
|
+
mockGetPassword = () => {
|
|
119
|
+
throw "raw string error";
|
|
120
|
+
};
|
|
121
|
+
const kc = await getKeychain();
|
|
122
|
+
const r = await kc.get("openai");
|
|
123
|
+
expect(r.kind).toBe("error");
|
|
124
|
+
if (r.kind === "error") {
|
|
125
|
+
expect(r.message).toContain("raw string error");
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("keychain adapter — set / remove", () => {
|
|
131
|
+
it("set forwards the key to the underlying Entry", async () => {
|
|
132
|
+
const writes: string[] = [];
|
|
133
|
+
mockSetPassword = (p) => writes.push(p);
|
|
134
|
+
const kc = await getKeychain();
|
|
135
|
+
await kc.set("google", "new-key");
|
|
136
|
+
expect(writes).toEqual(["new-key"]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("remove returns true when entry existed", async () => {
|
|
140
|
+
mockDeletePassword = () => true;
|
|
141
|
+
const kc = await getKeychain();
|
|
142
|
+
const removed = await kc.remove("openai");
|
|
143
|
+
expect(removed).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("remove returns false (not throws) when entry didn't exist", async () => {
|
|
147
|
+
mockDeletePassword = () => {
|
|
148
|
+
throw new Error("not found");
|
|
149
|
+
};
|
|
150
|
+
const kc = await getKeychain();
|
|
151
|
+
const removed = await kc.remove("openai");
|
|
152
|
+
expect(removed).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("keychain adapter — caching", () => {
|
|
157
|
+
it("getKeychain returns the same instance across calls (singleton)", async () => {
|
|
158
|
+
const a = await getKeychain();
|
|
159
|
+
const b = await getKeychain();
|
|
160
|
+
expect(a).toBe(b);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("_resetKeychainCache forces a fresh load on next getKeychain", async () => {
|
|
164
|
+
const a = await getKeychain();
|
|
165
|
+
_resetKeychainCache();
|
|
166
|
+
constructorThrows = true; // change behavior between loads
|
|
167
|
+
const b = await getKeychain();
|
|
168
|
+
expect(a.available).toBe(true);
|
|
169
|
+
expect(b.available).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver tests — proves the priority chain.
|
|
3
|
+
*
|
|
4
|
+
* Codex's v0.8 security review (P1 #3) called out that without these
|
|
5
|
+
* tests, the resolver could silently regress and ship — because
|
|
6
|
+
* `npm run test` would pass vacuously.
|
|
7
|
+
*
|
|
8
|
+
* Each test isolates the keychain via vi.mock so we control its
|
|
9
|
+
* behavior per case. Env vars are reset in beforeEach so test order
|
|
10
|
+
* doesn't matter.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
14
|
+
import { resolveApiKey, requireApiKey } from "../resolver.js";
|
|
15
|
+
import { AuthMissingError } from "../types.js";
|
|
16
|
+
|
|
17
|
+
// Mock the keychain module before any test runs. Each test then
|
|
18
|
+
// overrides the mock's return for getKeychain().
|
|
19
|
+
vi.mock("../keychain.js", () => ({
|
|
20
|
+
getKeychain: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
import { getKeychain } from "../keychain.js";
|
|
23
|
+
|
|
24
|
+
const PROVIDER_ENV_VAR = {
|
|
25
|
+
openai: "OPENAI_API_KEY",
|
|
26
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
27
|
+
google: "GOOGLE_API_KEY",
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
function makeKeychainStub(behavior: {
|
|
31
|
+
available: boolean;
|
|
32
|
+
get?: () => Promise<
|
|
33
|
+
| { kind: "found"; key: string }
|
|
34
|
+
| { kind: "absent" }
|
|
35
|
+
| { kind: "error"; message: string }
|
|
36
|
+
>;
|
|
37
|
+
}) {
|
|
38
|
+
return {
|
|
39
|
+
available: behavior.available,
|
|
40
|
+
describeBackend: () => "test-backend",
|
|
41
|
+
get: behavior.get ?? (async () => ({ kind: "absent" as const })),
|
|
42
|
+
set: async () => {},
|
|
43
|
+
remove: async () => true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("resolveApiKey — priority order", () => {
|
|
48
|
+
const savedEnv: Record<string, string | undefined> = {};
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
// Snapshot + clear all provider env vars so test inputs are
|
|
52
|
+
// deterministic.
|
|
53
|
+
for (const v of Object.values(PROVIDER_ENV_VAR)) {
|
|
54
|
+
savedEnv[v] = process.env[v];
|
|
55
|
+
delete process.env[v];
|
|
56
|
+
}
|
|
57
|
+
delete process.env.GEMINI_API_KEY;
|
|
58
|
+
vi.clearAllMocks();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
// Restore original env so we don't pollute the runner / sibling tests.
|
|
63
|
+
for (const [k, v] of Object.entries(savedEnv)) {
|
|
64
|
+
if (v === undefined) delete process.env[k];
|
|
65
|
+
else process.env[k] = v;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("explicit param wins over env (which would otherwise win over keychain)", async () => {
|
|
70
|
+
process.env.OPENAI_API_KEY = "from-env";
|
|
71
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
72
|
+
makeKeychainStub({
|
|
73
|
+
available: true,
|
|
74
|
+
get: async () => ({ kind: "found", key: "from-keychain" }),
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
const r = await resolveApiKey("openai", { explicit: "from-explicit" });
|
|
78
|
+
expect(r.source).toBe("explicit");
|
|
79
|
+
expect(r.key).toBe("from-explicit");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("env wins over keychain — prevents stale-keychain-key bug Codex flagged", async () => {
|
|
83
|
+
process.env.OPENAI_API_KEY = "from-env";
|
|
84
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
85
|
+
makeKeychainStub({
|
|
86
|
+
available: true,
|
|
87
|
+
get: async () => ({ kind: "found", key: "from-keychain" }),
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
const r = await resolveApiKey("openai");
|
|
91
|
+
expect(r.source).toBe("env");
|
|
92
|
+
expect(r.key).toBe("from-env");
|
|
93
|
+
expect(r.envVar).toBe("OPENAI_API_KEY");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("keychain wins over missing", async () => {
|
|
97
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
98
|
+
makeKeychainStub({
|
|
99
|
+
available: true,
|
|
100
|
+
get: async () => ({ kind: "found", key: "from-keychain" }),
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
const r = await resolveApiKey("openai");
|
|
104
|
+
expect(r.source).toBe("keychain");
|
|
105
|
+
expect(r.key).toBe("from-keychain");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("returns missing when no source has the key", async () => {
|
|
109
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
110
|
+
makeKeychainStub({ available: true }),
|
|
111
|
+
);
|
|
112
|
+
const r = await resolveApiKey("openai");
|
|
113
|
+
expect(r.source).toBe("missing");
|
|
114
|
+
expect(r.key).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns keychain_error (not missing) when keychain read fails", async () => {
|
|
118
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
119
|
+
makeKeychainStub({
|
|
120
|
+
available: true,
|
|
121
|
+
get: async () => ({ kind: "error", message: "denied by OS" }),
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
const r = await resolveApiKey("openai");
|
|
125
|
+
expect(r.source).toBe("keychain_error");
|
|
126
|
+
expect(r.keychainError).toBe("denied by OS");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("keychain unavailability degrades to missing (not error) — backward compat", async () => {
|
|
130
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
131
|
+
makeKeychainStub({ available: false }),
|
|
132
|
+
);
|
|
133
|
+
const r = await resolveApiKey("openai");
|
|
134
|
+
expect(r.source).toBe("missing");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("skipKeychain option bypasses keychain entirely", async () => {
|
|
138
|
+
process.env.OPENAI_API_KEY = "from-env";
|
|
139
|
+
// Keychain mock would throw if called — proves skipKeychain takes effect.
|
|
140
|
+
vi.mocked(getKeychain).mockImplementation(() => {
|
|
141
|
+
throw new Error("getKeychain should not be called when skipKeychain is true");
|
|
142
|
+
});
|
|
143
|
+
const r = await resolveApiKey("openai", { skipKeychain: true });
|
|
144
|
+
expect(r.source).toBe("env");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("Google provider checks BOTH GOOGLE_API_KEY and GEMINI_API_KEY", async () => {
|
|
148
|
+
delete process.env.GOOGLE_API_KEY;
|
|
149
|
+
process.env.GEMINI_API_KEY = "from-gemini-env";
|
|
150
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
151
|
+
makeKeychainStub({ available: false }),
|
|
152
|
+
);
|
|
153
|
+
const r = await resolveApiKey("google");
|
|
154
|
+
expect(r.source).toBe("env");
|
|
155
|
+
expect(r.key).toBe("from-gemini-env");
|
|
156
|
+
expect(r.envVar).toBe("GEMINI_API_KEY");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("Google prefers GOOGLE_API_KEY over GEMINI_API_KEY when both set", async () => {
|
|
160
|
+
process.env.GOOGLE_API_KEY = "primary";
|
|
161
|
+
process.env.GEMINI_API_KEY = "secondary";
|
|
162
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
163
|
+
makeKeychainStub({ available: false }),
|
|
164
|
+
);
|
|
165
|
+
const r = await resolveApiKey("google");
|
|
166
|
+
expect(r.source).toBe("env");
|
|
167
|
+
expect(r.envVar).toBe("GOOGLE_API_KEY");
|
|
168
|
+
expect(r.key).toBe("primary");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("empty env var is treated as not set (falls through to keychain)", async () => {
|
|
172
|
+
process.env.OPENAI_API_KEY = "";
|
|
173
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
174
|
+
makeKeychainStub({
|
|
175
|
+
available: true,
|
|
176
|
+
get: async () => ({ kind: "found", key: "from-keychain" }),
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
const r = await resolveApiKey("openai");
|
|
180
|
+
expect(r.source).toBe("keychain");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("requireApiKey — throws AuthMissingError on missing / keychain_error", () => {
|
|
185
|
+
const savedEnv: Record<string, string | undefined> = {};
|
|
186
|
+
|
|
187
|
+
beforeEach(() => {
|
|
188
|
+
for (const v of Object.values(PROVIDER_ENV_VAR)) {
|
|
189
|
+
savedEnv[v] = process.env[v];
|
|
190
|
+
delete process.env[v];
|
|
191
|
+
}
|
|
192
|
+
delete process.env.GEMINI_API_KEY;
|
|
193
|
+
vi.clearAllMocks();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
afterEach(() => {
|
|
197
|
+
for (const [k, v] of Object.entries(savedEnv)) {
|
|
198
|
+
if (v === undefined) delete process.env[k];
|
|
199
|
+
else process.env[k] = v;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("returns the key when found", async () => {
|
|
204
|
+
process.env.ANTHROPIC_API_KEY = "the-key";
|
|
205
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
206
|
+
makeKeychainStub({ available: false }),
|
|
207
|
+
);
|
|
208
|
+
const key = await requireApiKey("anthropic");
|
|
209
|
+
expect(key).toBe("the-key");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("throws AuthMissingError with provider + env var when missing", async () => {
|
|
213
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
214
|
+
makeKeychainStub({ available: true }),
|
|
215
|
+
);
|
|
216
|
+
await expect(requireApiKey("anthropic")).rejects.toThrow(AuthMissingError);
|
|
217
|
+
await expect(requireApiKey("anthropic")).rejects.toThrow(/anthropic/);
|
|
218
|
+
await expect(requireApiKey("anthropic")).rejects.toThrow(/ANTHROPIC_API_KEY/);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("throws AuthMissingError with a DIFFERENT message for keychain_error", async () => {
|
|
222
|
+
vi.mocked(getKeychain).mockResolvedValue(
|
|
223
|
+
makeKeychainStub({
|
|
224
|
+
available: true,
|
|
225
|
+
get: async () => ({ kind: "error", message: "keyring locked" }),
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
await expect(requireApiKey("anthropic")).rejects.toThrow(/keyring locked/);
|
|
229
|
+
await expect(requireApiKey("anthropic")).rejects.toThrow(/Keychain read failed/);
|
|
230
|
+
});
|
|
231
|
+
});
|