feat(secrets): finalize external secrets runtime and migration hardening

This commit is contained in:
joshavant
2026-02-24 19:34:29 -06:00
committed by Peter Steinberger
parent c5b89fbaea
commit 0e69660c41
22 changed files with 442 additions and 38 deletions

View File

@@ -150,6 +150,61 @@ describe("ensureApiKeyFromEnvOrPrompt", () => {
}),
);
});
it("uses explicit inline env ref when secret-input-mode=ref selects existing env key", async () => {
process.env.MINIMAX_API_KEY = "env-key";
delete process.env.MINIMAX_OAUTH_TOKEN;
const { confirm, text } = createPromptSpies({
confirmResult: true,
textResult: "prompt-key",
});
const setCredential = vi.fn(async () => undefined);
const result = await ensureApiKeyFromEnvOrPrompt({
provider: "minimax",
envLabel: "MINIMAX_API_KEY",
promptMessage: "Enter key",
normalize: (value) => value.trim(),
validate: () => undefined,
prompter: createPrompter({ confirm, text }),
secretInputMode: "ref",
setCredential,
});
expect(result).toBe("env-key");
expect(setCredential).toHaveBeenCalledWith("${MINIMAX_API_KEY}", "ref");
expect(text).not.toHaveBeenCalled();
});
it("shows a ref-mode note when plaintext input is provided in ref mode", async () => {
delete process.env.MINIMAX_API_KEY;
delete process.env.MINIMAX_OAUTH_TOKEN;
const { confirm, note, text } = createPromptSpies({
confirmResult: false,
textResult: " prompted-key ",
});
const setCredential = vi.fn(async () => undefined);
const result = await ensureApiKeyFromEnvOrPrompt({
provider: "minimax",
envLabel: "MINIMAX_API_KEY",
promptMessage: "Enter key",
normalize: (value) => value.trim(),
validate: () => undefined,
prompter: createPrompter({ confirm, note, text }),
secretInputMode: "ref",
setCredential,
});
expect(result).toBe("prompted-key");
expect(setCredential).toHaveBeenCalledWith("prompted-key", "ref");
expect(note).toHaveBeenCalledWith(
expect.stringContaining("secret-input-mode=ref stores an env reference"),
"Ref mode note",
);
});
});
describe("ensureApiKeyFromOptionEnvOrPrompt", () => {

View File

@@ -5,6 +5,14 @@ import type { ApplyAuthChoiceParams } from "./auth-choice.apply.js";
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
import type { SecretInputMode } from "./onboard-types.js";
const INLINE_ENV_REF_RE = /^\$\{([A-Z][A-Z0-9_]*)\}$/;
const ENV_SOURCE_LABEL_RE = /(?:^|:\s)([A-Z][A-Z0-9_]*)$/;
function extractEnvVarFromSourceLabel(source: string): string | undefined {
const match = ENV_SOURCE_LABEL_RE.exec(source.trim());
return match?.[1];
}
export function createAuthChoiceAgentModelNoter(
params: ApplyAuthChoiceParams,
): (model: string) => Promise<void> {
@@ -205,7 +213,14 @@ export async function ensureApiKeyFromEnvOrPrompt(params: {
prompter: params.prompter,
explicitMode: params.secretInputMode,
});
await params.setCredential(envKey.apiKey, mode);
const explicitEnvRef =
mode === "ref"
? (() => {
const envVar = extractEnvVarFromSourceLabel(envKey.source);
return envVar ? `\${${envVar}}` : envKey.apiKey;
})()
: envKey.apiKey;
await params.setCredential(explicitEnvRef, mode);
return envKey.apiKey;
}
}
@@ -215,6 +230,12 @@ export async function ensureApiKeyFromEnvOrPrompt(params: {
validate: params.validate,
});
const apiKey = params.normalize(String(key ?? ""));
if (params.secretInputMode === "ref" && !INLINE_ENV_REF_RE.test(apiKey)) {
await params.prompter.note(
"secret-input-mode=ref stores an env reference, not plaintext key input. Enter ${ENV_VAR} to target a specific variable, or keep current input to use the provider default env var.",
"Ref mode note",
);
}
await params.setCredential(apiKey, params.secretInputMode);
return apiKey;
}

View File

@@ -229,6 +229,35 @@ describe("modelsStatusCommand auth overview", () => {
).toBe(true);
});
it("does not emit raw short api-key values in JSON labels", async () => {
const localRuntime = createRuntime();
const shortSecret = "abc123";
const originalProfiles = { ...mocks.store.profiles };
mocks.store.profiles = {
...mocks.store.profiles,
"openai:default": {
type: "api_key",
provider: "openai",
key: shortSecret,
},
};
try {
await modelsStatusCommand({ json: true }, localRuntime as never);
const payload = JSON.parse(String((localRuntime.log as Mock).mock.calls[0]?.[0]));
const providers = payload.auth.providers as Array<{
provider: string;
profiles: { labels: string[] };
}>;
const openai = providers.find((p) => p.provider === "openai");
const labels = openai?.profiles.labels ?? [];
expect(labels.join(" ")).toContain("...");
expect(labels.join(" ")).not.toContain(shortSecret);
} finally {
mocks.store.profiles = originalProfiles;
}
});
it("uses agent overrides and reports sources", async () => {
const localRuntime = createRuntime();
await withAgentScopeOverrides(