@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
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=keychain.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keychain.test.d.ts","sourceRoot":"","sources":["../../../src/auth/__tests__/keychain.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG"}
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
18
|
+
import { _resetKeychainCache } from "../keychain.js";
|
|
19
|
+
// Track the entry methods per test so we can rewire mid-suite.
|
|
20
|
+
let mockGetPassword;
|
|
21
|
+
let mockSetPassword;
|
|
22
|
+
let mockDeletePassword;
|
|
23
|
+
let constructorThrows = false;
|
|
24
|
+
let importThrows = false;
|
|
25
|
+
vi.mock("@napi-rs/keyring", () => ({
|
|
26
|
+
get Entry() {
|
|
27
|
+
if (importThrows)
|
|
28
|
+
throw new Error("synthetic import failure");
|
|
29
|
+
return class FakeEntry {
|
|
30
|
+
constructor(_service, _account) {
|
|
31
|
+
if (constructorThrows) {
|
|
32
|
+
throw new Error("native binding refused to initialize");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
getPassword() {
|
|
36
|
+
return mockGetPassword();
|
|
37
|
+
}
|
|
38
|
+
setPassword(p) {
|
|
39
|
+
mockSetPassword(p);
|
|
40
|
+
}
|
|
41
|
+
deletePassword() {
|
|
42
|
+
return mockDeletePassword();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
}));
|
|
47
|
+
import { getKeychain } from "../keychain.js";
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
_resetKeychainCache();
|
|
50
|
+
mockGetPassword = () => null;
|
|
51
|
+
mockSetPassword = () => { };
|
|
52
|
+
mockDeletePassword = () => true;
|
|
53
|
+
constructorThrows = false;
|
|
54
|
+
importThrows = false;
|
|
55
|
+
});
|
|
56
|
+
describe("keychain adapter — availability", () => {
|
|
57
|
+
it("reports available=true when native binding loads cleanly", async () => {
|
|
58
|
+
const kc = await getKeychain();
|
|
59
|
+
expect(kc.available).toBe(true);
|
|
60
|
+
expect(kc.describeBackend()).not.toContain("unavailable");
|
|
61
|
+
});
|
|
62
|
+
it("reports available=false + reason when native binding throws on init", async () => {
|
|
63
|
+
constructorThrows = true;
|
|
64
|
+
const kc = await getKeychain();
|
|
65
|
+
expect(kc.available).toBe(false);
|
|
66
|
+
expect(kc.describeBackend()).toContain("unavailable");
|
|
67
|
+
expect(kc.describeBackend()).toMatch(/initialize|native/i);
|
|
68
|
+
});
|
|
69
|
+
it("get on unavailable adapter returns { kind: 'absent' } (not throw)", async () => {
|
|
70
|
+
constructorThrows = true;
|
|
71
|
+
const kc = await getKeychain();
|
|
72
|
+
const r = await kc.get("openai");
|
|
73
|
+
expect(r).toEqual({ kind: "absent" });
|
|
74
|
+
});
|
|
75
|
+
it("set on unavailable adapter throws with a clear hint", async () => {
|
|
76
|
+
constructorThrows = true;
|
|
77
|
+
const kc = await getKeychain();
|
|
78
|
+
await expect(kc.set("openai", "x")).rejects.toThrow(/unavailable|environment/i);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe("keychain adapter — get behavior (Codex P2 #4 regression coverage)", () => {
|
|
82
|
+
it("found path returns { kind: 'found', key }", async () => {
|
|
83
|
+
mockGetPassword = () => "stored-secret";
|
|
84
|
+
const kc = await getKeychain();
|
|
85
|
+
const r = await kc.get("anthropic");
|
|
86
|
+
expect(r).toEqual({ kind: "found", key: "stored-secret" });
|
|
87
|
+
});
|
|
88
|
+
it("absent path returns { kind: 'absent' } when getPassword returns null", async () => {
|
|
89
|
+
mockGetPassword = () => null;
|
|
90
|
+
const kc = await getKeychain();
|
|
91
|
+
const r = await kc.get("openai");
|
|
92
|
+
expect(r).toEqual({ kind: "absent" });
|
|
93
|
+
});
|
|
94
|
+
it("error path returns { kind: 'error', message } when getPassword throws — NOT silently absent", async () => {
|
|
95
|
+
mockGetPassword = () => {
|
|
96
|
+
throw new Error("keyring locked by OS");
|
|
97
|
+
};
|
|
98
|
+
const kc = await getKeychain();
|
|
99
|
+
const r = await kc.get("openai");
|
|
100
|
+
expect(r.kind).toBe("error");
|
|
101
|
+
if (r.kind === "error") {
|
|
102
|
+
expect(r.message).toContain("keyring locked");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
it("error path preserves non-Error throws via String(err)", async () => {
|
|
106
|
+
mockGetPassword = () => {
|
|
107
|
+
throw "raw string error";
|
|
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("raw string error");
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe("keychain adapter — set / remove", () => {
|
|
118
|
+
it("set forwards the key to the underlying Entry", async () => {
|
|
119
|
+
const writes = [];
|
|
120
|
+
mockSetPassword = (p) => writes.push(p);
|
|
121
|
+
const kc = await getKeychain();
|
|
122
|
+
await kc.set("google", "new-key");
|
|
123
|
+
expect(writes).toEqual(["new-key"]);
|
|
124
|
+
});
|
|
125
|
+
it("remove returns true when entry existed", async () => {
|
|
126
|
+
mockDeletePassword = () => true;
|
|
127
|
+
const kc = await getKeychain();
|
|
128
|
+
const removed = await kc.remove("openai");
|
|
129
|
+
expect(removed).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
it("remove returns false (not throws) when entry didn't exist", async () => {
|
|
132
|
+
mockDeletePassword = () => {
|
|
133
|
+
throw new Error("not found");
|
|
134
|
+
};
|
|
135
|
+
const kc = await getKeychain();
|
|
136
|
+
const removed = await kc.remove("openai");
|
|
137
|
+
expect(removed).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe("keychain adapter — caching", () => {
|
|
141
|
+
it("getKeychain returns the same instance across calls (singleton)", async () => {
|
|
142
|
+
const a = await getKeychain();
|
|
143
|
+
const b = await getKeychain();
|
|
144
|
+
expect(a).toBe(b);
|
|
145
|
+
});
|
|
146
|
+
it("_resetKeychainCache forces a fresh load on next getKeychain", async () => {
|
|
147
|
+
const a = await getKeychain();
|
|
148
|
+
_resetKeychainCache();
|
|
149
|
+
constructorThrows = true; // change behavior between loads
|
|
150
|
+
const b = await getKeychain();
|
|
151
|
+
expect(a.available).toBe(true);
|
|
152
|
+
expect(b.available).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
//# sourceMappingURL=keychain.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keychain.test.js","sourceRoot":"","sources":["../../../src/auth/__tests__/keychain.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=resolver.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.test.d.ts","sourceRoot":"","sources":["../../../src/auth/__tests__/resolver.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
13
|
+
import { resolveApiKey, requireApiKey } from "../resolver.js";
|
|
14
|
+
import { AuthMissingError } from "../types.js";
|
|
15
|
+
// Mock the keychain module before any test runs. Each test then
|
|
16
|
+
// overrides the mock's return for getKeychain().
|
|
17
|
+
vi.mock("../keychain.js", () => ({
|
|
18
|
+
getKeychain: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
import { getKeychain } from "../keychain.js";
|
|
21
|
+
const PROVIDER_ENV_VAR = {
|
|
22
|
+
openai: "OPENAI_API_KEY",
|
|
23
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
24
|
+
google: "GOOGLE_API_KEY",
|
|
25
|
+
};
|
|
26
|
+
function makeKeychainStub(behavior) {
|
|
27
|
+
return {
|
|
28
|
+
available: behavior.available,
|
|
29
|
+
describeBackend: () => "test-backend",
|
|
30
|
+
get: behavior.get ?? (async () => ({ kind: "absent" })),
|
|
31
|
+
set: async () => { },
|
|
32
|
+
remove: async () => true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
describe("resolveApiKey — priority order", () => {
|
|
36
|
+
const savedEnv = {};
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
// Snapshot + clear all provider env vars so test inputs are
|
|
39
|
+
// deterministic.
|
|
40
|
+
for (const v of Object.values(PROVIDER_ENV_VAR)) {
|
|
41
|
+
savedEnv[v] = process.env[v];
|
|
42
|
+
delete process.env[v];
|
|
43
|
+
}
|
|
44
|
+
delete process.env.GEMINI_API_KEY;
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
});
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
// Restore original env so we don't pollute the runner / sibling tests.
|
|
49
|
+
for (const [k, v] of Object.entries(savedEnv)) {
|
|
50
|
+
if (v === undefined)
|
|
51
|
+
delete process.env[k];
|
|
52
|
+
else
|
|
53
|
+
process.env[k] = v;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
it("explicit param wins over env (which would otherwise win over keychain)", async () => {
|
|
57
|
+
process.env.OPENAI_API_KEY = "from-env";
|
|
58
|
+
vi.mocked(getKeychain).mockResolvedValue(makeKeychainStub({
|
|
59
|
+
available: true,
|
|
60
|
+
get: async () => ({ kind: "found", key: "from-keychain" }),
|
|
61
|
+
}));
|
|
62
|
+
const r = await resolveApiKey("openai", { explicit: "from-explicit" });
|
|
63
|
+
expect(r.source).toBe("explicit");
|
|
64
|
+
expect(r.key).toBe("from-explicit");
|
|
65
|
+
});
|
|
66
|
+
it("env wins over keychain — prevents stale-keychain-key bug Codex flagged", async () => {
|
|
67
|
+
process.env.OPENAI_API_KEY = "from-env";
|
|
68
|
+
vi.mocked(getKeychain).mockResolvedValue(makeKeychainStub({
|
|
69
|
+
available: true,
|
|
70
|
+
get: async () => ({ kind: "found", key: "from-keychain" }),
|
|
71
|
+
}));
|
|
72
|
+
const r = await resolveApiKey("openai");
|
|
73
|
+
expect(r.source).toBe("env");
|
|
74
|
+
expect(r.key).toBe("from-env");
|
|
75
|
+
expect(r.envVar).toBe("OPENAI_API_KEY");
|
|
76
|
+
});
|
|
77
|
+
it("keychain wins over missing", async () => {
|
|
78
|
+
vi.mocked(getKeychain).mockResolvedValue(makeKeychainStub({
|
|
79
|
+
available: true,
|
|
80
|
+
get: async () => ({ kind: "found", key: "from-keychain" }),
|
|
81
|
+
}));
|
|
82
|
+
const r = await resolveApiKey("openai");
|
|
83
|
+
expect(r.source).toBe("keychain");
|
|
84
|
+
expect(r.key).toBe("from-keychain");
|
|
85
|
+
});
|
|
86
|
+
it("returns missing when no source has the key", async () => {
|
|
87
|
+
vi.mocked(getKeychain).mockResolvedValue(makeKeychainStub({ available: true }));
|
|
88
|
+
const r = await resolveApiKey("openai");
|
|
89
|
+
expect(r.source).toBe("missing");
|
|
90
|
+
expect(r.key).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
it("returns keychain_error (not missing) when keychain read fails", async () => {
|
|
93
|
+
vi.mocked(getKeychain).mockResolvedValue(makeKeychainStub({
|
|
94
|
+
available: true,
|
|
95
|
+
get: async () => ({ kind: "error", message: "denied by OS" }),
|
|
96
|
+
}));
|
|
97
|
+
const r = await resolveApiKey("openai");
|
|
98
|
+
expect(r.source).toBe("keychain_error");
|
|
99
|
+
expect(r.keychainError).toBe("denied by OS");
|
|
100
|
+
});
|
|
101
|
+
it("keychain unavailability degrades to missing (not error) — backward compat", async () => {
|
|
102
|
+
vi.mocked(getKeychain).mockResolvedValue(makeKeychainStub({ available: false }));
|
|
103
|
+
const r = await resolveApiKey("openai");
|
|
104
|
+
expect(r.source).toBe("missing");
|
|
105
|
+
});
|
|
106
|
+
it("skipKeychain option bypasses keychain entirely", async () => {
|
|
107
|
+
process.env.OPENAI_API_KEY = "from-env";
|
|
108
|
+
// Keychain mock would throw if called — proves skipKeychain takes effect.
|
|
109
|
+
vi.mocked(getKeychain).mockImplementation(() => {
|
|
110
|
+
throw new Error("getKeychain should not be called when skipKeychain is true");
|
|
111
|
+
});
|
|
112
|
+
const r = await resolveApiKey("openai", { skipKeychain: true });
|
|
113
|
+
expect(r.source).toBe("env");
|
|
114
|
+
});
|
|
115
|
+
it("Google provider checks BOTH GOOGLE_API_KEY and GEMINI_API_KEY", async () => {
|
|
116
|
+
delete process.env.GOOGLE_API_KEY;
|
|
117
|
+
process.env.GEMINI_API_KEY = "from-gemini-env";
|
|
118
|
+
vi.mocked(getKeychain).mockResolvedValue(makeKeychainStub({ available: false }));
|
|
119
|
+
const r = await resolveApiKey("google");
|
|
120
|
+
expect(r.source).toBe("env");
|
|
121
|
+
expect(r.key).toBe("from-gemini-env");
|
|
122
|
+
expect(r.envVar).toBe("GEMINI_API_KEY");
|
|
123
|
+
});
|
|
124
|
+
it("Google prefers GOOGLE_API_KEY over GEMINI_API_KEY when both set", async () => {
|
|
125
|
+
process.env.GOOGLE_API_KEY = "primary";
|
|
126
|
+
process.env.GEMINI_API_KEY = "secondary";
|
|
127
|
+
vi.mocked(getKeychain).mockResolvedValue(makeKeychainStub({ available: false }));
|
|
128
|
+
const r = await resolveApiKey("google");
|
|
129
|
+
expect(r.source).toBe("env");
|
|
130
|
+
expect(r.envVar).toBe("GOOGLE_API_KEY");
|
|
131
|
+
expect(r.key).toBe("primary");
|
|
132
|
+
});
|
|
133
|
+
it("empty env var is treated as not set (falls through to keychain)", async () => {
|
|
134
|
+
process.env.OPENAI_API_KEY = "";
|
|
135
|
+
vi.mocked(getKeychain).mockResolvedValue(makeKeychainStub({
|
|
136
|
+
available: true,
|
|
137
|
+
get: async () => ({ kind: "found", key: "from-keychain" }),
|
|
138
|
+
}));
|
|
139
|
+
const r = await resolveApiKey("openai");
|
|
140
|
+
expect(r.source).toBe("keychain");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe("requireApiKey — throws AuthMissingError on missing / keychain_error", () => {
|
|
144
|
+
const savedEnv = {};
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
for (const v of Object.values(PROVIDER_ENV_VAR)) {
|
|
147
|
+
savedEnv[v] = process.env[v];
|
|
148
|
+
delete process.env[v];
|
|
149
|
+
}
|
|
150
|
+
delete process.env.GEMINI_API_KEY;
|
|
151
|
+
vi.clearAllMocks();
|
|
152
|
+
});
|
|
153
|
+
afterEach(() => {
|
|
154
|
+
for (const [k, v] of Object.entries(savedEnv)) {
|
|
155
|
+
if (v === undefined)
|
|
156
|
+
delete process.env[k];
|
|
157
|
+
else
|
|
158
|
+
process.env[k] = v;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
it("returns the key when found", async () => {
|
|
162
|
+
process.env.ANTHROPIC_API_KEY = "the-key";
|
|
163
|
+
vi.mocked(getKeychain).mockResolvedValue(makeKeychainStub({ available: false }));
|
|
164
|
+
const key = await requireApiKey("anthropic");
|
|
165
|
+
expect(key).toBe("the-key");
|
|
166
|
+
});
|
|
167
|
+
it("throws AuthMissingError with provider + env var when missing", async () => {
|
|
168
|
+
vi.mocked(getKeychain).mockResolvedValue(makeKeychainStub({ available: true }));
|
|
169
|
+
await expect(requireApiKey("anthropic")).rejects.toThrow(AuthMissingError);
|
|
170
|
+
await expect(requireApiKey("anthropic")).rejects.toThrow(/anthropic/);
|
|
171
|
+
await expect(requireApiKey("anthropic")).rejects.toThrow(/ANTHROPIC_API_KEY/);
|
|
172
|
+
});
|
|
173
|
+
it("throws AuthMissingError with a DIFFERENT message for keychain_error", async () => {
|
|
174
|
+
vi.mocked(getKeychain).mockResolvedValue(makeKeychainStub({
|
|
175
|
+
available: true,
|
|
176
|
+
get: async () => ({ kind: "error", message: "keyring locked" }),
|
|
177
|
+
}));
|
|
178
|
+
await expect(requireApiKey("anthropic")).rejects.toThrow(/keyring locked/);
|
|
179
|
+
await expect(requireApiKey("anthropic")).rejects.toThrow(/Keychain read failed/);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
//# sourceMappingURL=resolver.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.test.js","sourceRoot":"","sources":["../../../src/auth/__tests__/resolver.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validator tests with mocked fetch.
|
|
3
|
+
*
|
|
4
|
+
* Covers Codex's v0.8 security review demand:
|
|
5
|
+
* "Add a regression test that failed validation never logs or returns
|
|
6
|
+
* a URL containing the key."
|
|
7
|
+
*
|
|
8
|
+
* Plus the broader contract:
|
|
9
|
+
* - happy path → ok: true with model
|
|
10
|
+
* - timeout → ok: false with friendly message
|
|
11
|
+
* - non-OK response → normalized error with statusCode, never raw body
|
|
12
|
+
* - submitted key NEVER appears in error / model field for ANY provider
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=validator.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.test.d.ts","sourceRoot":"","sources":["../../../src/auth/__tests__/validator.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validator tests with mocked fetch.
|
|
3
|
+
*
|
|
4
|
+
* Covers Codex's v0.8 security review demand:
|
|
5
|
+
* "Add a regression test that failed validation never logs or returns
|
|
6
|
+
* a URL containing the key."
|
|
7
|
+
*
|
|
8
|
+
* Plus the broader contract:
|
|
9
|
+
* - happy path → ok: true with model
|
|
10
|
+
* - timeout → ok: false with friendly message
|
|
11
|
+
* - non-OK response → normalized error with statusCode, never raw body
|
|
12
|
+
* - submitted key NEVER appears in error / model field for ANY provider
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
15
|
+
import { validateKey } from "../validator.js";
|
|
16
|
+
const fetchMock = vi.fn();
|
|
17
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
18
|
+
// Keys with recognizable patterns so we can grep for them in any output.
|
|
19
|
+
const KEYS = {
|
|
20
|
+
openai: "sk-test-openai-CANARY-VALUE-12345",
|
|
21
|
+
anthropic: "sk-ant-test-anthropic-CANARY-VALUE-67890",
|
|
22
|
+
google: "AIza-test-google-CANARY-VALUE-abcde",
|
|
23
|
+
};
|
|
24
|
+
function jsonResponse(status, body) {
|
|
25
|
+
return new Response(typeof body === "string" ? body : JSON.stringify(body), {
|
|
26
|
+
status,
|
|
27
|
+
headers: { "content-type": "application/json" },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
fetchMock.mockReset();
|
|
32
|
+
});
|
|
33
|
+
describe("validator — OpenAI", () => {
|
|
34
|
+
it("happy path returns ok with model name from response", async () => {
|
|
35
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(200, { model: "gpt-4o-2024-08-06", choices: [] }));
|
|
36
|
+
const r = await validateKey("openai", KEYS.openai);
|
|
37
|
+
expect(r.ok).toBe(true);
|
|
38
|
+
expect(r.model).toBe("gpt-4o-2024-08-06");
|
|
39
|
+
expect(r.latencyMs).toBeGreaterThanOrEqual(0);
|
|
40
|
+
});
|
|
41
|
+
it("posts to /v1/chat/completions with the key as Bearer header (NOT in URL)", async () => {
|
|
42
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(200, { model: "gpt-4o" }));
|
|
43
|
+
await validateKey("openai", KEYS.openai);
|
|
44
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
45
|
+
expect(String(url)).toBe("https://api.openai.com/v1/chat/completions");
|
|
46
|
+
expect(String(url)).not.toContain(KEYS.openai);
|
|
47
|
+
expect(init.headers).toMatchObject({
|
|
48
|
+
authorization: `Bearer ${KEYS.openai}`,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
it("non-OK response returns normalized error with statusCode and no raw body in error", async () => {
|
|
52
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(401, {
|
|
53
|
+
error: {
|
|
54
|
+
message: "Incorrect API key provided",
|
|
55
|
+
type: "invalid_request_error",
|
|
56
|
+
code: "invalid_api_key",
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
59
|
+
const r = await validateKey("openai", KEYS.openai);
|
|
60
|
+
expect(r.ok).toBe(false);
|
|
61
|
+
expect(r.statusCode).toBe(401);
|
|
62
|
+
expect(r.error).toContain("Incorrect API key provided");
|
|
63
|
+
// The error string should be short — not the raw JSON body.
|
|
64
|
+
expect(r.error.length).toBeLessThan(200);
|
|
65
|
+
// rawBody is preserved separately for explicit debug use.
|
|
66
|
+
expect(r.rawBody).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe("validator — Anthropic", () => {
|
|
70
|
+
it("happy path returns ok with model from response", async () => {
|
|
71
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(200, { model: "claude-sonnet-4-6", content: [] }));
|
|
72
|
+
const r = await validateKey("anthropic", KEYS.anthropic);
|
|
73
|
+
expect(r.ok).toBe(true);
|
|
74
|
+
expect(r.model).toBe("claude-sonnet-4-6");
|
|
75
|
+
});
|
|
76
|
+
it("posts to /v1/messages with x-api-key header (NOT in URL)", async () => {
|
|
77
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(200, { model: "claude-sonnet-4-6" }));
|
|
78
|
+
await validateKey("anthropic", KEYS.anthropic);
|
|
79
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
80
|
+
expect(String(url)).toBe("https://api.anthropic.com/v1/messages");
|
|
81
|
+
expect(String(url)).not.toContain(KEYS.anthropic);
|
|
82
|
+
expect(init.headers).toMatchObject({
|
|
83
|
+
"x-api-key": KEYS.anthropic,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
it("non-OK response returns normalized error with parsed message", async () => {
|
|
87
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(400, {
|
|
88
|
+
type: "error",
|
|
89
|
+
error: {
|
|
90
|
+
type: "authentication_error",
|
|
91
|
+
message: "invalid x-api-key",
|
|
92
|
+
},
|
|
93
|
+
}));
|
|
94
|
+
const r = await validateKey("anthropic", KEYS.anthropic);
|
|
95
|
+
expect(r.ok).toBe(false);
|
|
96
|
+
expect(r.statusCode).toBe(400);
|
|
97
|
+
expect(r.error).toContain("invalid x-api-key");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe("validator — Google", () => {
|
|
101
|
+
it("happy path returns ok with the configured model name", async () => {
|
|
102
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(200, { candidates: [{ content: { parts: [] } }] }));
|
|
103
|
+
const r = await validateKey("google", KEYS.google);
|
|
104
|
+
expect(r.ok).toBe(true);
|
|
105
|
+
expect(r.model).toBe("gemini-2.5-flash");
|
|
106
|
+
});
|
|
107
|
+
it("CODEX V0.8 P1: uses x-goog-api-key header — NEVER puts the key in the URL", async () => {
|
|
108
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(200, { candidates: [] }));
|
|
109
|
+
await validateKey("google", KEYS.google);
|
|
110
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
111
|
+
// This is the regression-test Codex specifically requested.
|
|
112
|
+
expect(String(url)).not.toContain(KEYS.google);
|
|
113
|
+
expect(String(url)).not.toContain("?key=");
|
|
114
|
+
// And the key MUST be in the header instead.
|
|
115
|
+
expect(init.headers).toMatchObject({
|
|
116
|
+
"x-goog-api-key": KEYS.google,
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
it("non-OK response returns normalized error with statusCode", async () => {
|
|
120
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(403, {
|
|
121
|
+
error: {
|
|
122
|
+
code: 403,
|
|
123
|
+
message: "API key not valid",
|
|
124
|
+
status: "PERMISSION_DENIED",
|
|
125
|
+
},
|
|
126
|
+
}));
|
|
127
|
+
const r = await validateKey("google", KEYS.google);
|
|
128
|
+
expect(r.ok).toBe(false);
|
|
129
|
+
expect(r.statusCode).toBe(403);
|
|
130
|
+
expect(r.error).toContain("API key not valid");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe("validator — KEY-LEAK REGRESSION (Codex v0.8 P2 #6)", () => {
|
|
134
|
+
/**
|
|
135
|
+
* The submitted key must never appear in `error` or `model` for
|
|
136
|
+
* ANY provider, in ANY failure path. Tests every provider × every
|
|
137
|
+
* failure mode we care about.
|
|
138
|
+
*/
|
|
139
|
+
const failureModes = [
|
|
140
|
+
{ status: 400, body: { error: { message: "bad request" } } },
|
|
141
|
+
{ status: 401, body: { error: { message: "unauthorized" } } },
|
|
142
|
+
{ status: 403, body: { error: { message: "forbidden" } } },
|
|
143
|
+
{ status: 429, body: { error: { message: "rate limited" } } },
|
|
144
|
+
{ status: 500, body: "Internal Server Error (plain text body)" },
|
|
145
|
+
{
|
|
146
|
+
status: 200,
|
|
147
|
+
body: "malformed-not-json{{{",
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
for (const provider of ["openai", "anthropic", "google"]) {
|
|
151
|
+
for (const mode of failureModes) {
|
|
152
|
+
it(`${provider} ${mode.status}: error message never contains the submitted key verbatim`, async () => {
|
|
153
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(mode.status, mode.body));
|
|
154
|
+
const key = KEYS[provider];
|
|
155
|
+
const r = await validateKey(provider, key);
|
|
156
|
+
// Check every string-shaped field we expose to users.
|
|
157
|
+
for (const field of [r.error, r.model]) {
|
|
158
|
+
if (field) {
|
|
159
|
+
expect(field).not.toContain(key);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
it("if a provider echoes the key in its error body, the normalized error MUST NOT include the key", async () => {
|
|
166
|
+
// Worst-case: provider literally echoes the key in the response.
|
|
167
|
+
// Could happen with poorly-written upstream tooling.
|
|
168
|
+
fetchMock.mockResolvedValueOnce(jsonResponse(400, {
|
|
169
|
+
error: {
|
|
170
|
+
message: `key ${KEYS.openai} is malformed`,
|
|
171
|
+
},
|
|
172
|
+
}));
|
|
173
|
+
const r = await validateKey("openai", KEYS.openai);
|
|
174
|
+
expect(r.ok).toBe(false);
|
|
175
|
+
// The normalized error MUST redact the key.
|
|
176
|
+
expect(r.error).not.toContain(KEYS.openai);
|
|
177
|
+
expect(r.error).toContain("<redacted-key>");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe("validator — network failure handling", () => {
|
|
181
|
+
it("network throw returns ok:false with friendly message (not stack trace)", async () => {
|
|
182
|
+
fetchMock.mockRejectedValueOnce(new TypeError("fetch failed: ECONNREFUSED"));
|
|
183
|
+
const r = await validateKey("openai", KEYS.openai);
|
|
184
|
+
expect(r.ok).toBe(false);
|
|
185
|
+
expect(r.error).toBeDefined();
|
|
186
|
+
expect(r.error).not.toContain(KEYS.openai);
|
|
187
|
+
// friendlyNetworkError should produce a short message
|
|
188
|
+
expect(r.error.length).toBeLessThan(150);
|
|
189
|
+
});
|
|
190
|
+
it("abort (timeout) returns ok:false with friendly message", async () => {
|
|
191
|
+
fetchMock.mockRejectedValueOnce(Object.assign(new Error("aborted"), { name: "AbortError" }));
|
|
192
|
+
const r = await validateKey("openai", KEYS.openai);
|
|
193
|
+
expect(r.ok).toBe(false);
|
|
194
|
+
expect(r.error).toMatch(/timed out|abort/i);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
//# sourceMappingURL=validator.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.test.js","sourceRoot":"","sources":["../../../src/auth/__tests__/validator.test.ts"],"names":[],"mappings":"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"}
|