Skip to content

Commit 8d48d1e

Browse files
committed
fix: address issue #762
Fixes #762
1 parent be177ea commit 8d48d1e

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

src/cli/doctor.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,13 +307,47 @@ export type ConfiguredProxyDiagnostic = {
307307
detail: string;
308308
};
309309

310-
function envReferenceName(value: string): string | null {
310+
export function envReferenceName(value: string): string | null {
311311
const braced = value.match(/^\$\{(\w+)\}$/);
312312
if (braced) return braced[1]!;
313313
const bare = value.match(/^\$(\w+)$/);
314314
return bare ? bare[1]! : null;
315315
}
316316

317+
export type ProviderApiKeyDiagnostic = {
318+
provider: string;
319+
envName: string;
320+
detail: string;
321+
};
322+
323+
/** Warn when a key-auth provider's apiKey env reference resolves empty in this process. */
324+
export function collectProviderApiKeyDiagnostics(
325+
providers: Record<string, { authMode?: string; apiKey?: string }> = readConfigDiagnostics().config.providers ?? {},
326+
env: EnvMap = process.env,
327+
): ProviderApiKeyDiagnostic[] {
328+
const resolveInEnv = (value: string): string | undefined => {
329+
const name = envReferenceName(value);
330+
if (!name) return value;
331+
return env[name];
332+
};
333+
const rows: ProviderApiKeyDiagnostic[] = [];
334+
for (const [provider, config] of Object.entries(providers)) {
335+
if (config.authMode !== "key") continue;
336+
const raw = typeof config.apiKey === "string" ? config.apiKey.trim() : "";
337+
if (!raw) continue;
338+
const envName = envReferenceName(raw);
339+
if (!envName) continue;
340+
const resolved = resolveInEnv(raw);
341+
if (resolved?.trim()) continue;
342+
rows.push({
343+
provider,
344+
envName,
345+
detail: `provider ${provider}: env reference ${envName} is unset or empty in this process`,
346+
});
347+
}
348+
return rows;
349+
}
350+
317351
export function collectConfiguredProxy(): ConfiguredProxyDiagnostic {
318352
const diagnostics = readConfigDiagnostics();
319353
const rawProxy = typeof diagnostics.config.proxy === "string" ? diagnostics.config.proxy.trim() : "";
@@ -742,6 +776,16 @@ export async function runDoctor(args: string[] = []): Promise<void> {
742776
console.log("\nConfigured proxy (value hidden)");
743777
console.log(` ${configuredProxy.present ? "set " : "unset "} ${configuredProxy.key} (${configuredProxy.source}; ${configuredProxy.detail})`);
744778

779+
const providerApiKeys = collectProviderApiKeyDiagnostics(doctorConfig.providers);
780+
console.log("\nProvider API keys (value hidden)");
781+
if (providerApiKeys.length === 0) {
782+
console.log(" ok no empty env-referenced provider keys detected in this process");
783+
} else {
784+
for (const row of providerApiKeys) {
785+
console.log(` !! ${row.detail}`);
786+
}
787+
}
788+
745789
console.log("\nRunning proxy process proxy env (presence only)");
746790
if (runningProxyEnv.status === "not_running") {
747791
console.log(" -- no running ocx proxy process found");
@@ -825,6 +869,9 @@ export async function runDoctor(args: string[] = []): Promise<void> {
825869
serviceViable: startup.serviceViable,
826870
});
827871
if (proxyDown) hints.push(proxyDown);
872+
for (const row of providerApiKeys) {
873+
hints.push(`${row.detail}. Set ${row.envName} in the shell that starts the proxy, or store a literal key in config (value hidden here).`);
874+
}
828875
const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive);
829876
const noProxy = currentProxyEnv.every(p => !p.present) && !configuredProxy.present;
830877
if (!startup.rebootSafe) {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { collectProviderApiKeyDiagnostics } from "../src/cli/doctor";
3+
4+
describe("doctor provider apiKey env diagnostics (#762)", () => {
5+
test("warns when a key-auth env reference resolves empty without printing secrets", () => {
6+
const rows = collectProviderApiKeyDiagnostics({
7+
openrouter: {
8+
authMode: "key",
9+
apiKey: "${OPENROUTER_API_KEY}",
10+
},
11+
}, {});
12+
expect(rows).toEqual([{
13+
provider: "openrouter",
14+
envName: "OPENROUTER_API_KEY",
15+
detail: "provider openrouter: env reference OPENROUTER_API_KEY is unset or empty in this process",
16+
}]);
17+
});
18+
19+
test("passes when the referenced env var is set", () => {
20+
const rows = collectProviderApiKeyDiagnostics({
21+
openrouter: {
22+
authMode: "key",
23+
apiKey: "${OPENROUTER_API_KEY}",
24+
},
25+
}, { OPENROUTER_API_KEY: "secret-value" });
26+
expect(rows).toEqual([]);
27+
});
28+
29+
test("ignores literal keys and non-key providers", () => {
30+
const rows = collectProviderApiKeyDiagnostics({
31+
openrouter: { authMode: "key", apiKey: "sk-live-not-an-env-ref" },
32+
openai: { authMode: "forward", apiKey: "${SHOULD_NOT_MATTER}" },
33+
}, {});
34+
expect(rows).toEqual([]);
35+
});
36+
});

0 commit comments

Comments
 (0)