@almightygpt/core 0.9.1 → 0.9.2

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.
Files changed (40) hide show
  1. package/dist/adapters/claude.d.ts.map +1 -1
  2. package/dist/adapters/claude.js +3 -1
  3. package/dist/adapters/claude.js.map +1 -1
  4. package/dist/adapters/defaults.d.ts +34 -0
  5. package/dist/adapters/defaults.d.ts.map +1 -0
  6. package/dist/adapters/defaults.js +41 -0
  7. package/dist/adapters/defaults.js.map +1 -0
  8. package/dist/adapters/gemini.d.ts.map +1 -1
  9. package/dist/adapters/gemini.js +3 -1
  10. package/dist/adapters/gemini.js.map +1 -1
  11. package/dist/adapters/openai.d.ts.map +1 -1
  12. package/dist/adapters/openai.js +3 -1
  13. package/dist/adapters/openai.js.map +1 -1
  14. package/dist/auth/keychain.d.ts +11 -1
  15. package/dist/auth/keychain.d.ts.map +1 -1
  16. package/dist/auth/keychain.js +13 -4
  17. package/dist/auth/keychain.js.map +1 -1
  18. package/dist/auth/resolver.d.ts.map +1 -1
  19. package/dist/auth/resolver.js +19 -3
  20. package/dist/auth/resolver.js.map +1 -1
  21. package/dist/auth/types.d.ts +18 -5
  22. package/dist/auth/types.d.ts.map +1 -1
  23. package/dist/auth/types.js +8 -6
  24. package/dist/auth/types.js.map +1 -1
  25. package/dist/auth/validator.d.ts +23 -3
  26. package/dist/auth/validator.d.ts.map +1 -1
  27. package/dist/auth/validator.js +126 -21
  28. package/dist/auth/validator.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +1 -1
  31. package/package.json +1 -1
  32. package/src/adapters/claude.ts +3 -1
  33. package/src/adapters/defaults.ts +44 -0
  34. package/src/adapters/gemini.ts +3 -1
  35. package/src/adapters/openai.ts +3 -1
  36. package/src/auth/keychain.ts +20 -6
  37. package/src/auth/resolver.ts +23 -3
  38. package/src/auth/types.ts +27 -8
  39. package/src/auth/validator.ts +149 -25
  40. package/src/index.ts +1 -1
@@ -13,26 +13,41 @@
13
13
  * - Google: tiny generateContent call against gemini-2.5-flash
14
14
  *
15
15
  * Cost is fractions of a cent per call. Latency is ~1-3 seconds.
16
- * Failure surfaces the provider's exact error message so the user
17
- * can fix it.
16
+ *
17
+ * **Error handling (per Codex v0.8 P2 #6):** failure responses are
18
+ * parsed into a short user-safe message + status code. We never
19
+ * return the raw provider body to the user — those bodies can include
20
+ * request IDs, billing/account state, org/project details, quota
21
+ * metadata, or other account information that's too noisy for CLI
22
+ * logs. The raw body is available via the `rawBody` field for callers
23
+ * that explicitly want it (e.g. debug mode).
24
+ *
25
+ * **Default models (per Codex v0.8 P2 #5):** sourced from
26
+ * `adapters/defaults.ts` — the single source of truth — so the
27
+ * validator can't drift from the adapters.
18
28
  */
19
29
 
20
30
  import type { ProviderId } from "./types.js";
31
+ import { DEFAULT_MODELS } from "../adapters/defaults.js";
21
32
 
22
33
  export interface ValidationResult {
23
34
  ok: boolean;
24
35
  /** Provider-reported model used (e.g. "gpt-4o-2024-08-06"). */
25
36
  model?: string;
26
- /** Provider's error message verbatim if ok === false. */
37
+ /** Short user-safe message never includes raw provider body. */
27
38
  error?: string;
39
+ /** HTTP status code from the provider, if applicable. */
40
+ statusCode?: number;
41
+ /** Latency in ms — useful for `auth status --validate` JSON output. */
42
+ latencyMs?: number;
43
+ /**
44
+ * Raw provider response body. Present iff a request was made and
45
+ * returned non-OK. Callers should NOT surface this to users without
46
+ * explicit debug intent; it may contain account metadata.
47
+ */
48
+ rawBody?: string;
28
49
  }
29
50
 
30
- const DEFAULT_VALIDATION_MODELS: Record<ProviderId, string> = {
31
- openai: "gpt-4o",
32
- anthropic: "claude-sonnet-4-6",
33
- google: "gemini-2.5-flash",
34
- };
35
-
36
51
  const VALIDATION_TIMEOUT_MS = 15_000;
37
52
 
38
53
  /**
@@ -44,25 +59,33 @@ export async function validateKey(
44
59
  provider: ProviderId,
45
60
  key: string,
46
61
  ): Promise<ValidationResult> {
62
+ const start = Date.now();
47
63
  try {
64
+ let result: ValidationResult;
48
65
  switch (provider) {
49
66
  case "openai":
50
- return await validateOpenAI(key);
67
+ result = await validateOpenAI(key);
68
+ break;
51
69
  case "anthropic":
52
- return await validateAnthropic(key);
70
+ result = await validateAnthropic(key);
71
+ break;
53
72
  case "google":
54
- return await validateGoogle(key);
73
+ result = await validateGoogle(key);
74
+ break;
55
75
  }
76
+ result.latencyMs = Date.now() - start;
77
+ return result;
56
78
  } catch (err) {
57
79
  return {
58
80
  ok: false,
59
- error: err instanceof Error ? err.message : String(err),
81
+ error: friendlyNetworkError(err),
82
+ latencyMs: Date.now() - start,
60
83
  };
61
84
  }
62
85
  }
63
86
 
64
87
  async function validateOpenAI(key: string): Promise<ValidationResult> {
65
- const model = DEFAULT_VALIDATION_MODELS.openai;
88
+ const model = DEFAULT_MODELS.openai;
66
89
  const controller = new AbortController();
67
90
  const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
68
91
  try {
@@ -80,7 +103,13 @@ async function validateOpenAI(key: string): Promise<ValidationResult> {
80
103
  signal: controller.signal,
81
104
  });
82
105
  if (!res.ok) {
83
- return { ok: false, error: await res.text().catch(() => res.statusText) };
106
+ const rawBody = await res.text().catch(() => "");
107
+ return {
108
+ ok: false,
109
+ statusCode: res.status,
110
+ error: normalizeOpenAIError(res.status, rawBody),
111
+ rawBody,
112
+ };
84
113
  }
85
114
  const data = (await res.json()) as { model?: string };
86
115
  return { ok: true, model: data.model ?? model };
@@ -90,7 +119,7 @@ async function validateOpenAI(key: string): Promise<ValidationResult> {
90
119
  }
91
120
 
92
121
  async function validateAnthropic(key: string): Promise<ValidationResult> {
93
- const model = DEFAULT_VALIDATION_MODELS.anthropic;
122
+ const model = DEFAULT_MODELS.anthropic;
94
123
  const controller = new AbortController();
95
124
  const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
96
125
  try {
@@ -109,7 +138,13 @@ async function validateAnthropic(key: string): Promise<ValidationResult> {
109
138
  signal: controller.signal,
110
139
  });
111
140
  if (!res.ok) {
112
- return { ok: false, error: await res.text().catch(() => res.statusText) };
141
+ const rawBody = await res.text().catch(() => "");
142
+ return {
143
+ ok: false,
144
+ statusCode: res.status,
145
+ error: normalizeAnthropicError(res.status, rawBody),
146
+ rawBody,
147
+ };
113
148
  }
114
149
  const data = (await res.json()) as { model?: string };
115
150
  return { ok: true, model: data.model ?? model };
@@ -119,16 +154,12 @@ async function validateAnthropic(key: string): Promise<ValidationResult> {
119
154
  }
120
155
 
121
156
  async function validateGoogle(key: string): Promise<ValidationResult> {
122
- const model = DEFAULT_VALIDATION_MODELS.google;
157
+ const model = DEFAULT_MODELS.google;
123
158
  const controller = new AbortController();
124
159
  const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
125
160
  try {
126
- // Use x-goog-api-key header rather than ?key= URL parameter. URLs
127
- // leak into proxy logs, HTTP tooling, crash diagnostics. The header
128
- // path is supported by all v1beta endpoints. Codex's v0.8 security
129
- // review flagged the URL-key approach as a P1.
130
- const url =
131
- `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
161
+ // x-goog-api-key header (per v0.9.1 fix) — never the URL query.
162
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
132
163
  const res = await fetch(url, {
133
164
  method: "POST",
134
165
  headers: {
@@ -142,10 +173,103 @@ async function validateGoogle(key: string): Promise<ValidationResult> {
142
173
  signal: controller.signal,
143
174
  });
144
175
  if (!res.ok) {
145
- return { ok: false, error: await res.text().catch(() => res.statusText) };
176
+ const rawBody = await res.text().catch(() => "");
177
+ return {
178
+ ok: false,
179
+ statusCode: res.status,
180
+ error: normalizeGoogleError(res.status, rawBody, key),
181
+ rawBody,
182
+ };
146
183
  }
147
184
  return { ok: true, model };
148
185
  } finally {
149
186
  clearTimeout(timer);
150
187
  }
151
188
  }
189
+
190
+ // ─── Error normalization (Codex v0.8 P2 #6) ──────────────────────────
191
+ //
192
+ // Parse known provider JSON error shapes into short, user-safe messages.
193
+ // Never echo the raw key back even by accident (defense in depth: we
194
+ // also redact anything that looks like the submitted key).
195
+
196
+ function normalizeOpenAIError(status: number, rawBody: string): string {
197
+ // OpenAI shape: { "error": { "message": "...", "type": "...", "code": "..." } }
198
+ try {
199
+ const parsed = JSON.parse(rawBody) as {
200
+ error?: { message?: string; type?: string; code?: string };
201
+ };
202
+ const msg = parsed.error?.message;
203
+ if (msg) return `[${status}] OpenAI: ${truncate(msg, 200)}`;
204
+ } catch {
205
+ /* fall through */
206
+ }
207
+ return statusOnlyMessage("OpenAI", status);
208
+ }
209
+
210
+ function normalizeAnthropicError(status: number, rawBody: string): string {
211
+ // Anthropic shape: { "type": "error", "error": { "type": "...", "message": "..." } }
212
+ try {
213
+ const parsed = JSON.parse(rawBody) as {
214
+ error?: { type?: string; message?: string };
215
+ };
216
+ const msg = parsed.error?.message;
217
+ if (msg) return `[${status}] Anthropic: ${truncate(msg, 200)}`;
218
+ } catch {
219
+ /* fall through */
220
+ }
221
+ return statusOnlyMessage("Anthropic", status);
222
+ }
223
+
224
+ function normalizeGoogleError(
225
+ status: number,
226
+ rawBody: string,
227
+ submittedKey: string,
228
+ ): string {
229
+ // Google shape: { "error": { "code": N, "message": "...", "status": "..." } }
230
+ try {
231
+ const parsed = JSON.parse(rawBody) as {
232
+ error?: { code?: number; message?: string; status?: string };
233
+ };
234
+ let msg = parsed.error?.message ?? "";
235
+ // Belt-and-braces redaction: Google sometimes echoes the key in
236
+ // error messages (e.g. "API key not valid. Pass a valid API key.")
237
+ // — we don't ship the actual key value if it ever ends up here.
238
+ if (submittedKey && msg.includes(submittedKey)) {
239
+ msg = msg.replace(submittedKey, "<redacted-key>");
240
+ }
241
+ if (msg) return `[${status}] Google: ${truncate(msg, 200)}`;
242
+ } catch {
243
+ /* fall through */
244
+ }
245
+ return statusOnlyMessage("Google", status);
246
+ }
247
+
248
+ function statusOnlyMessage(provider: string, status: number): string {
249
+ if (status === 401 || status === 403) {
250
+ return `[${status}] ${provider} rejected the key (unauthorized).`;
251
+ }
252
+ if (status === 429) {
253
+ return `[${status}] ${provider} rate-limited or quota exceeded.`;
254
+ }
255
+ if (status >= 500) {
256
+ return `[${status}] ${provider} returned a server error; try again.`;
257
+ }
258
+ return `[${status}] ${provider} returned an unrecognized error.`;
259
+ }
260
+
261
+ function truncate(s: string, n: number): string {
262
+ return s.length <= n ? s : s.slice(0, n - 1) + "…";
263
+ }
264
+
265
+ function friendlyNetworkError(err: unknown): string {
266
+ const msg = err instanceof Error ? err.message : String(err);
267
+ if (msg.includes("abort")) {
268
+ return "Validation timed out (network or provider too slow).";
269
+ }
270
+ if (msg.includes("ENOTFOUND") || msg.includes("ECONNREFUSED")) {
271
+ return "Could not reach the provider (network issue).";
272
+ }
273
+ // Generic — but never include random stack data; just the message.
274
+ return `Network error: ${truncate(msg, 200)}`;
275
+ }
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * - budget/ ✅ task #14 BudgetTracker + BudgetExceededError
14
14
  */
15
15
 
16
- export const VERSION = "0.9.1";
16
+ export const VERSION = "0.9.2";
17
17
 
18
18
  // MCP server (v0.9.0+) — exposes AlmightyGPT's review surface as MCP tools.
19
19
  export { startMcpServer } from "./mcp/server.js";