Skip to content

Commit c7e1fdf

Browse files
anurag-bg-neuclaude
andcommitted
fix(models): mask paste-token input in CLI auth prompt
The interactive 'Paste token for <provider>' prompt echoed the pasted bearer/setup token in cleartext because readPastedSecret was called with masked: false. Its sibling paste-api-key command already renders bullets. Flip the flag to masked: true so the secret routes through the styled password prompt. Masking is render-only; both prompt branches share normalizeSecretInput, so the stored profile value is unchanged. Reroute the paste-token unit tests onto the password mock and add a regression test asserting the masked prompt is used. Co-Authored-By: Claude Opus 4.8 <[email protected]>
1 parent 92242f4 commit c7e1fdf

2 files changed

Lines changed: 26 additions & 13 deletions

File tree

src/commands/models/auth.test.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,7 +1288,7 @@ describe("modelsAuthLoginCommand", () => {
12881288
}) as typeof process.exit);
12891289
try {
12901290
const cancelSymbol = Symbol.for("clack:cancel");
1291-
mocks.clackText.mockResolvedValue(cancelSymbol);
1291+
mocks.clackPassword.mockResolvedValue(cancelSymbol);
12921292
mocks.clackIsCancel.mockImplementation((value: unknown) => value === cancelSymbol);
12931293

12941294
await expect(modelsAuthPasteTokenCommand({ provider: "openai" }, runtime)).rejects.toThrow(
@@ -1303,9 +1303,24 @@ describe("modelsAuthLoginCommand", () => {
13031303
}
13041304
});
13051305

1306+
it("reads the pasted token through the masked password prompt", async () => {
1307+
const runtime = createRuntime();
1308+
mocks.clackPassword.mockResolvedValue("openai-token");
1309+
1310+
await modelsAuthPasteTokenCommand({ provider: "openai" }, runtime);
1311+
1312+
expect(mocks.clackPassword).toHaveBeenCalledTimes(1);
1313+
expect(mocks.clackText).not.toHaveBeenCalled();
1314+
expect(mocks.upsertAuthProfileWithLock).toHaveBeenCalledWith(
1315+
expect.objectContaining({
1316+
credential: expect.objectContaining({ type: "token", token: "openai-token" }),
1317+
}),
1318+
);
1319+
});
1320+
13061321
it("writes pasted Anthropic setup-tokens and logs the preference note", async () => {
13071322
const runtime = createRuntime();
1308-
mocks.clackText.mockResolvedValue(`sk-ant-oat01-${"a".repeat(80)}`);
1323+
mocks.clackPassword.mockResolvedValue(`sk-ant-oat01-${"a".repeat(80)}`);
13091324

13101325
await modelsAuthPasteTokenCommand({ provider: "anthropic" }, runtime);
13111326

@@ -1332,7 +1347,7 @@ describe("modelsAuthLoginCommand", () => {
13321347
it("writes pasted tokens to the requested agent store", async () => {
13331348
const runtime = createRuntime();
13341349
useCoderAgentConfig();
1335-
mocks.clackText.mockResolvedValue("openai-token");
1350+
mocks.clackPassword.mockResolvedValue("openai-token");
13361351

13371352
await modelsAuthPasteTokenCommand({ provider: "openai", agent: "coder" }, runtime);
13381353

@@ -1351,7 +1366,7 @@ describe("modelsAuthLoginCommand", () => {
13511366
it("rejects pasted token expiries that cannot fit in the Date timestamp range", async () => {
13521367
const runtime = createRuntime();
13531368
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(MAX_DATE_TIMESTAMP_MS);
1354-
mocks.clackText.mockResolvedValue("openai-token");
1369+
mocks.clackPassword.mockResolvedValue("openai-token");
13551370
try {
13561371
await expect(
13571372
modelsAuthPasteTokenCommand({ provider: "openai", expiresIn: "1ms" }, runtime),
@@ -1367,7 +1382,7 @@ describe("modelsAuthLoginCommand", () => {
13671382
it("rejects OpenAI API keys pasted as OpenAI Codex token material", async () => {
13681383
const runtime = createRuntime();
13691384
const validateMessages: string[] = [];
1370-
mocks.clackText.mockImplementation(
1385+
mocks.clackPassword.mockImplementation(
13711386
async (params: { validate?: (value: string) => string | undefined }) => {
13721387
const message = params.validate?.("sk-openai-chatgpt-api-key-value");
13731388
if (message) {
@@ -1398,7 +1413,7 @@ describe("modelsAuthLoginCommand", () => {
13981413
"paste-api-key --provider openai",
13991414
);
14001415

1401-
expect(mocks.clackText).not.toHaveBeenCalled();
1416+
expect(mocks.clackPassword).not.toHaveBeenCalled();
14021417
expect(mocks.upsertAuthProfileWithLock).not.toHaveBeenCalled();
14031418
expect(mocks.updateConfig).not.toHaveBeenCalled();
14041419
});
@@ -1412,7 +1427,7 @@ describe("modelsAuthLoginCommand", () => {
14121427
"paste-api-key --provider openai",
14131428
);
14141429

1415-
expect(mocks.clackText).not.toHaveBeenCalled();
1430+
expect(mocks.clackPassword).not.toHaveBeenCalled();
14161431
expect(mocks.upsertAuthProfileWithLock).not.toHaveBeenCalled();
14171432
expect(mocks.updateConfig).not.toHaveBeenCalled();
14181433
});
@@ -1523,7 +1538,7 @@ describe("modelsAuthLoginCommand", () => {
15231538
'Unknown agent id "missing". Use "openclaw agents list" to see configured agents.',
15241539
);
15251540

1526-
expect(mocks.clackText).not.toHaveBeenCalled();
1541+
expect(mocks.clackPassword).not.toHaveBeenCalled();
15271542
expect(mocks.upsertAuthProfileWithLock).not.toHaveBeenCalled();
15281543
expect(mocks.updateConfig).not.toHaveBeenCalled();
15291544
});
@@ -1659,10 +1674,8 @@ describe("modelsAuthLoginCommand", () => {
16591674
useCoderAgentConfig();
16601675
mocks.resolvePluginProviders.mockReturnValue([]);
16611676
mocks.clackSelect.mockResolvedValue("custom");
1662-
mocks.clackText
1663-
.mockResolvedValueOnce("openai")
1664-
.mockResolvedValueOnce("openai:manual")
1665-
.mockResolvedValueOnce("openai-token");
1677+
mocks.clackText.mockResolvedValueOnce("openai").mockResolvedValueOnce("openai:manual");
1678+
mocks.clackPassword.mockResolvedValue("openai-token");
16661679
mocks.clackConfirm.mockResolvedValue(false);
16671680

16681681
await modelsAuthAddCommand({ agent: "coder" }, runtime);

src/commands/models/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ export async function modelsAuthPasteTokenCommand(
679679
};
680680
const tokenInput = await readPastedSecret({
681681
message: `Paste token for ${provider}`,
682-
masked: false,
682+
masked: true,
683683
validate: validateTokenInput,
684684
});
685685
const token =

0 commit comments

Comments
 (0)