mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
fix(prompt): strip bracketed paste escape sequences from password input
Terminals using bracketed paste mode wrap pasted content with ESC[200~ (start) and ESC[201~ (end) sequences. These were incorrectly passed through to API keys, causing "[200~API_KEY[201~" instead of "API_KEY". Now buffers ESC sequences and discards recognized paste markers.
This commit is contained in:
@@ -143,6 +143,11 @@ export class InteractivePrompt {
|
||||
|
||||
/**
|
||||
* Get password/secret input (masked)
|
||||
*
|
||||
* Handles bracketed paste mode escape sequences that terminals send:
|
||||
* - Start paste: ESC[200~
|
||||
* - End paste: ESC[201~
|
||||
* These are stripped automatically so pasted API keys work correctly.
|
||||
*/
|
||||
static async password(message: string, options: PasswordOptions = {}): Promise<string> {
|
||||
const { mask = '*' } = options;
|
||||
@@ -162,6 +167,7 @@ export class InteractivePrompt {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let input = '';
|
||||
let escapeBuffer = ''; // Buffer for escape sequence detection
|
||||
|
||||
const cleanup = (): void => {
|
||||
if (process.stdin.setRawMode) {
|
||||
@@ -177,6 +183,32 @@ export class InteractivePrompt {
|
||||
for (const char of str) {
|
||||
const charCode = char.charCodeAt(0);
|
||||
|
||||
// ESC character (start of escape sequence)
|
||||
if (charCode === 27) {
|
||||
escapeBuffer = '\x1b';
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're in an escape sequence, buffer chars until we detect the pattern
|
||||
if (escapeBuffer) {
|
||||
escapeBuffer += char;
|
||||
|
||||
// Check for bracketed paste sequences: ESC[200~ (start) or ESC[201~ (end)
|
||||
if (escapeBuffer === '\x1b[200~' || escapeBuffer === '\x1b[201~') {
|
||||
// Discard bracketed paste markers
|
||||
escapeBuffer = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
// If buffer is getting too long without match, it's not a paste sequence
|
||||
// Flush buffer as regular input (shouldn't happen with API keys)
|
||||
if (escapeBuffer.length > 6) {
|
||||
// Not a recognized sequence - skip it entirely (likely other escape seq)
|
||||
escapeBuffer = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Enter key (CR or LF)
|
||||
if (charCode === 13 || charCode === 10) {
|
||||
cleanup();
|
||||
|
||||
@@ -152,6 +152,93 @@ describe('InteractivePrompt', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('password - bracketed paste handling', () => {
|
||||
/**
|
||||
* Test helper: Simulates the escape sequence filtering logic from password()
|
||||
* This mirrors the implementation to verify bracketed paste sequences are stripped
|
||||
*/
|
||||
function stripBracketedPaste(input) {
|
||||
let result = '';
|
||||
let escapeBuffer = '';
|
||||
|
||||
for (const char of input) {
|
||||
const charCode = char.charCodeAt(0);
|
||||
|
||||
// ESC character (start of escape sequence)
|
||||
if (charCode === 27) {
|
||||
escapeBuffer = '\x1b';
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're in an escape sequence, buffer chars until we detect the pattern
|
||||
if (escapeBuffer) {
|
||||
escapeBuffer += char;
|
||||
|
||||
// Check for bracketed paste sequences: ESC[200~ (start) or ESC[201~ (end)
|
||||
if (escapeBuffer === '\x1b[200~' || escapeBuffer === '\x1b[201~') {
|
||||
escapeBuffer = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
// If buffer is getting too long without match, it's not a paste sequence
|
||||
if (escapeBuffer.length > 6) {
|
||||
escapeBuffer = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular printable character
|
||||
if (charCode >= 32) {
|
||||
result += char;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
it('strips ESC[200~ (start paste) sequence', () => {
|
||||
const input = '\x1b[200~sk-ant-api-key\x1b[201~';
|
||||
const result = stripBracketedPaste(input);
|
||||
assert.strictEqual(result, 'sk-ant-api-key');
|
||||
});
|
||||
|
||||
it('handles API key pasted with bracketed paste mode', () => {
|
||||
const pastedKey = '\x1b[200~sk-ant-api03-abcdefghijklmnop\x1b[201~';
|
||||
const result = stripBracketedPaste(pastedKey);
|
||||
assert.strictEqual(result, 'sk-ant-api03-abcdefghijklmnop');
|
||||
});
|
||||
|
||||
it('passes through normal typed input without escape sequences', () => {
|
||||
const typedKey = 'sk-ant-api03-normal-typing';
|
||||
const result = stripBracketedPaste(typedKey);
|
||||
assert.strictEqual(result, 'sk-ant-api03-normal-typing');
|
||||
});
|
||||
|
||||
it('handles only start paste sequence', () => {
|
||||
const input = '\x1b[200~my-api-key';
|
||||
const result = stripBracketedPaste(input);
|
||||
assert.strictEqual(result, 'my-api-key');
|
||||
});
|
||||
|
||||
it('handles only end paste sequence', () => {
|
||||
const input = 'my-api-key\x1b[201~';
|
||||
const result = stripBracketedPaste(input);
|
||||
assert.strictEqual(result, 'my-api-key');
|
||||
});
|
||||
|
||||
it('handles multiple paste sequences', () => {
|
||||
const input = '\x1b[200~first\x1b[201~\x1b[200~second\x1b[201~';
|
||||
const result = stripBracketedPaste(input);
|
||||
assert.strictEqual(result, 'firstsecond');
|
||||
});
|
||||
|
||||
it('handles empty paste', () => {
|
||||
const input = '\x1b[200~\x1b[201~';
|
||||
const result = stripBracketedPaste(input);
|
||||
assert.strictEqual(result, '');
|
||||
});
|
||||
});
|
||||
|
||||
describe('confirm', () => {
|
||||
it('returns true when CCS_YES=1', async () => {
|
||||
process.env.CCS_YES = '1';
|
||||
|
||||
Reference in New Issue
Block a user