mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-05 23:57:53 +02:00
The design binary previously called process.env.OPENAI_API_KEY without checking where the key came from. If a user ran $D inside someone else's project that had OPENAI_API_KEY in its .env, the resulting generation billed that project's account. Silent and irreversible. Fix: resolveApiKeyInfo() returns both the key and its source. When the env-var path matches an OPENAI_API_KEY entry in the current directory's .env, .env.<NODE_ENV>, or .env.local file, we set a warning. requireApiKey() prints "Using OpenAI key from <source>" plus the warning before the run — never the key itself. Adds 6 unit tests covering: config-vs-env precedence, env-only (no match), env+cwd .env match, quoted/exported values, value-mismatch (no false positive), and the no-leak invariant for requireApiKey stderr output. Contributed by @jbetala7 via #1278. Closes #1248. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Tests for $D OpenAI auth source reporting (#1278, closes #1248).
|
||||
*
|
||||
* Verifies that resolveApiKey + requireApiKey:
|
||||
* - prefer ~/.gstack/openai.json over OPENAI_API_KEY
|
||||
* - report when the env-var key matches a cwd .env / .env.local
|
||||
* - never echo the key itself to stderr (only the source label)
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import {
|
||||
describeApiKeySource,
|
||||
requireApiKey,
|
||||
resolveApiKey,
|
||||
resolveApiKeyInfo,
|
||||
saveApiKey,
|
||||
} from "../src/auth";
|
||||
|
||||
let tmpDir: string;
|
||||
let tmpHome: string;
|
||||
let originalHome: string | undefined;
|
||||
let originalKey: string | undefined;
|
||||
let originalNodeEnv: string | undefined;
|
||||
let originalCwd: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "gstack-design-auth-"));
|
||||
tmpHome = path.join(tmpDir, "home");
|
||||
fs.mkdirSync(tmpHome, { recursive: true });
|
||||
|
||||
originalHome = process.env.HOME;
|
||||
originalKey = process.env.OPENAI_API_KEY;
|
||||
originalNodeEnv = process.env.NODE_ENV;
|
||||
originalCwd = process.cwd();
|
||||
|
||||
process.env.HOME = tmpHome;
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
delete process.env.NODE_ENV;
|
||||
process.chdir(tmpDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(originalCwd);
|
||||
if (originalHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = originalHome;
|
||||
if (originalKey === undefined) delete process.env.OPENAI_API_KEY;
|
||||
else process.env.OPENAI_API_KEY = originalKey;
|
||||
if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
|
||||
else process.env.NODE_ENV = originalNodeEnv;
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("resolveApiKeyInfo", () => {
|
||||
test("uses ~/.gstack/openai.json before OPENAI_API_KEY", () => {
|
||||
saveApiKey("sk-config");
|
||||
process.env.OPENAI_API_KEY = "sk-env";
|
||||
|
||||
const resolution = resolveApiKeyInfo();
|
||||
|
||||
expect(resolution?.key).toBe("sk-config");
|
||||
expect(resolution?.source).toBe("config");
|
||||
expect(describeApiKeySource(resolution!)).toBe("~/.gstack/openai.json");
|
||||
expect(resolveApiKey()).toBe("sk-config");
|
||||
});
|
||||
|
||||
test("uses OPENAI_API_KEY when no config file exists", () => {
|
||||
process.env.OPENAI_API_KEY = "sk-env";
|
||||
|
||||
const resolution = resolveApiKeyInfo();
|
||||
|
||||
expect(resolution?.key).toBe("sk-env");
|
||||
expect(resolution?.source).toBe("env");
|
||||
expect(resolution?.envFile).toBeUndefined();
|
||||
expect(describeApiKeySource(resolution!)).toBe("OPENAI_API_KEY environment variable");
|
||||
});
|
||||
|
||||
test("reports when OPENAI_API_KEY matches current-directory .env", () => {
|
||||
fs.writeFileSync(path.join(tmpDir, ".env"), "OPENAI_API_KEY=sk-project\n");
|
||||
process.env.OPENAI_API_KEY = "sk-project";
|
||||
|
||||
const resolution = resolveApiKeyInfo();
|
||||
|
||||
expect(resolution?.key).toBe("sk-project");
|
||||
expect(resolution?.envFile).toBe(".env");
|
||||
expect(describeApiKeySource(resolution!)).toBe("OPENAI_API_KEY environment variable (matches .env in current directory)");
|
||||
expect(resolution?.warning).toContain("may bill that project's OpenAI account");
|
||||
});
|
||||
|
||||
test("detects quoted and exported env-file values", () => {
|
||||
fs.writeFileSync(path.join(tmpDir, ".env.local"), "export OPENAI_API_KEY=\"sk-local\"\n");
|
||||
process.env.OPENAI_API_KEY = "sk-local";
|
||||
|
||||
const resolution = resolveApiKeyInfo();
|
||||
|
||||
expect(resolution?.envFile).toBe(".env.local");
|
||||
expect(resolution?.warning).toContain(".env.local");
|
||||
});
|
||||
|
||||
test("does not claim env-file source when values differ", () => {
|
||||
fs.writeFileSync(path.join(tmpDir, ".env"), "OPENAI_API_KEY=sk-other\n");
|
||||
process.env.OPENAI_API_KEY = "sk-shell";
|
||||
|
||||
const resolution = resolveApiKeyInfo();
|
||||
|
||||
expect(resolution?.key).toBe("sk-shell");
|
||||
expect(resolution?.envFile).toBeUndefined();
|
||||
expect(resolution?.warning).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("requireApiKey", () => {
|
||||
test("prints source disclosure without leaking the key", () => {
|
||||
process.env.OPENAI_API_KEY = "sk-secret-value";
|
||||
const messages: string[] = [];
|
||||
const originalError = console.error;
|
||||
console.error = (...args: unknown[]) => {
|
||||
messages.push(args.map(String).join(" "));
|
||||
};
|
||||
|
||||
try {
|
||||
expect(requireApiKey()).toBe("sk-secret-value");
|
||||
} finally {
|
||||
console.error = originalError;
|
||||
}
|
||||
|
||||
const stderr = messages.join("\n");
|
||||
expect(stderr).toContain("Using OpenAI key from OPENAI_API_KEY environment variable.");
|
||||
expect(stderr).not.toContain("sk-secret-value");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user