@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.
@@ -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
- let msg = parsed.error?.message ?? "";
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":"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"}
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.0",
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
+ });