From d6af2a66581168860284762f3b6d5914956e8329 Mon Sep 17 00:00:00 2001 From: Krishnadas Date: Tue, 12 Aug 2025 10:26:34 +0530 Subject: [PATCH 1/5] readline: add skipHistory option to rl.question() Add a boolean skipHistory option to readline.Interface.question(). When true, the input will not be saved in the readline history. Useful for sensitive inputs like passwords. Includes tests verifying both skipHistory enabled and disabled cases. Docs to be added later after maintainer review. Fixes: https://github.com/nodejs/node/issues/59390 --- lib/internal/readline/interface.js | 14 ++++++- lib/internal/repl/history.js | 2 + lib/readline.js | 4 ++ test/parallel/test-readline-skip-history.js | 42 +++++++++++++++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-readline-skip-history.js diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 5ebfa44ecba068..5b006bdb66b348 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -156,6 +156,7 @@ const kPreviousCursorCols = Symbol('_previousCursorCols'); const kMultilineMove = Symbol('_multilineMove'); const kPreviousPrevRows = Symbol('_previousPrevRows'); const kAddNewLineOnTTY = Symbol('_addNewLineOnTTY'); +const kSkipHistory = Symbol('_skipHistory'); function InterfaceConstructor(input, output, completer, terminal) { this[kSawReturnAt] = 0; @@ -166,6 +167,8 @@ function InterfaceConstructor(input, output, completer, terminal) { this[kPreviousKey] = null; this.escapeCodeTimeout = ESCAPE_CODE_TIMEOUT; this.tabSize = 8; + // Initialise skipHistory flag + this[kSkipHistory] = false; FunctionPrototypeCall(EventEmitter, this); @@ -491,7 +494,15 @@ class Interface extends InterfaceConstructor { } [kAddHistory]() { - return this.historyManager.addHistory(this[kIsMultiline], this[kLastCommandErrored]); + const currentLine = this.line; + if (this[kSkipHistory]) { + this[kSkipHistory] = false; // reset flag for next time + return currentLine; // keep the input but don't store in history + } + return this.historyManager.addHistory( + this[kIsMultiline], + this[kLastCommandErrored] + ); } [kRefreshLine]() { @@ -1605,4 +1616,5 @@ module.exports = { kRestorePreviousState, kAddNewLineOnTTY, kLastCommandErrored, + kSkipHistory, }; diff --git a/lib/internal/repl/history.js b/lib/internal/repl/history.js index d05bb19d733a22..5b2b7e2d8864a2 100644 --- a/lib/internal/repl/history.js +++ b/lib/internal/repl/history.js @@ -44,6 +44,7 @@ const kIsFlushing = Symbol('_kIsFlushing'); const kHistory = Symbol('_kHistory'); const kSize = Symbol('_kSize'); const kIndex = Symbol('_kIndex'); +const kSkipHistory = Symbol('_skipHistory'); // Class methods const kNormalizeLineEndings = Symbol('_kNormalizeLineEndings'); @@ -74,6 +75,7 @@ class ReplHistory { this[kSize] = options.size ?? context.historySize ?? kHistorySize; this[kHistory] = options.history ?? []; this[kIndex] = -1; + this[kSkipHistory] = options.skipHistory || false; } initialize(onReadyCallback) { diff --git a/lib/readline.js b/lib/readline.js index 1eaf35d75029e2..4c8276d9f2ab18 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -95,6 +95,7 @@ const { kWordLeft, kWordRight, kWriteToOutput, + kSkipHistory, } = require('internal/readline/interface'); let addAbortListener; @@ -136,6 +137,9 @@ Interface.prototype.question = function question(query, options, cb) { options = kEmptyObject; } + // Set skip history flag if requested + if (options?.skipHistory) this[kSkipHistory] = true; + if (options.signal) { validateAbortSignal(options.signal, 'options.signal'); if (options.signal.aborted) { diff --git a/test/parallel/test-readline-skip-history.js b/test/parallel/test-readline-skip-history.js new file mode 100644 index 00000000000000..9ac99a38a8ee85 --- /dev/null +++ b/test/parallel/test-readline-skip-history.js @@ -0,0 +1,42 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const readline = require('readline'); +const { PassThrough } = require('stream'); + +// Test with skipHistory: true +{ + const input = new PassThrough(); + const output = new PassThrough(); + const rl = readline.createInterface({ input, output }); + + rl.question('Password? ', { skipHistory: true }, (pw) => { + assert.strictEqual(pw, 'secret-pass'); + assert.strictEqual(rl.history.includes('secret-pass'), false, + 'Password should not be in history'); + rl.close(); + }); + + // Simulate user typing "secret-pass" + Enter + input.write('secret-pass\n'); +} + +// Test with skipHistory: false (default behavior) +{ + const input = new PassThrough(); + input.isTTY = true; // simulate TTY + const output = new PassThrough(); + output.isTTY = true; // simulate TTY + const rl = readline.createInterface({ input, output }); + + rl.question('Password? ', (pw) => { + assert.strictEqual(pw, 'normal-input'); + assert.strictEqual(rl.history.includes('normal-input'), true, + 'Input should be in history'); + rl.close(); + }); + + // Simulate user typing "normal-input" + Enter + input.write('normal-input\n'); +} From 02799935ff3f017058695aaaf51ea9cf7bf0372b Mon Sep 17 00:00:00 2001 From: Krishnadas Date: Tue, 12 Aug 2025 16:03:16 +0530 Subject: [PATCH 2/5] readline: enhance readline skipHistory and tests - Added common.mustCall to ensure callbacks are invoked as expected - Used assert.partialDeepStrictEqual to verify rl.history contents precisely --- lib/internal/readline/interface.js | 2 +- lib/internal/readline/symbols.js | 8 ++++++++ lib/internal/repl/history.js | 2 +- lib/readline.js | 2 +- test/parallel/test-readline-skip-history.js | 16 +++++++--------- 5 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 lib/internal/readline/symbols.js diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 5b006bdb66b348..9c7afa147ed6b5 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -156,7 +156,7 @@ const kPreviousCursorCols = Symbol('_previousCursorCols'); const kMultilineMove = Symbol('_multilineMove'); const kPreviousPrevRows = Symbol('_previousPrevRows'); const kAddNewLineOnTTY = Symbol('_addNewLineOnTTY'); -const kSkipHistory = Symbol('_skipHistory'); +const { kSkipHistory } = require('internal/readline/symbols'); function InterfaceConstructor(input, output, completer, terminal) { this[kSawReturnAt] = 0; diff --git a/lib/internal/readline/symbols.js b/lib/internal/readline/symbols.js new file mode 100644 index 00000000000000..f9b5bf330802dc --- /dev/null +++ b/lib/internal/readline/symbols.js @@ -0,0 +1,8 @@ +'use strict'; + +// Shared Symbols for internal readline modules +const kSkipHistory = Symbol('_skipHistory'); + +module.exports = { + kSkipHistory, +}; diff --git a/lib/internal/repl/history.js b/lib/internal/repl/history.js index 5b2b7e2d8864a2..2c72bb1b6fba41 100644 --- a/lib/internal/repl/history.js +++ b/lib/internal/repl/history.js @@ -44,7 +44,7 @@ const kIsFlushing = Symbol('_kIsFlushing'); const kHistory = Symbol('_kHistory'); const kSize = Symbol('_kSize'); const kIndex = Symbol('_kIndex'); -const kSkipHistory = Symbol('_skipHistory'); +const { kSkipHistory } = require('internal/readline/symbols'); // Class methods const kNormalizeLineEndings = Symbol('_kNormalizeLineEndings'); diff --git a/lib/readline.js b/lib/readline.js index 4c8276d9f2ab18..2cfa9513ae9cb5 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -95,8 +95,8 @@ const { kWordLeft, kWordRight, kWriteToOutput, - kSkipHistory, } = require('internal/readline/interface'); +const { kSkipHistory } = require('internal/readline/symbols'); let addAbortListener; function Interface(input, output, completer, terminal) { diff --git a/test/parallel/test-readline-skip-history.js b/test/parallel/test-readline-skip-history.js index 9ac99a38a8ee85..4216a0ffa95d9c 100644 --- a/test/parallel/test-readline-skip-history.js +++ b/test/parallel/test-readline-skip-history.js @@ -1,6 +1,6 @@ 'use strict'; -require('../common'); +const common = require('../common'); const assert = require('assert'); const readline = require('readline'); const { PassThrough } = require('stream'); @@ -11,12 +11,11 @@ const { PassThrough } = require('stream'); const output = new PassThrough(); const rl = readline.createInterface({ input, output }); - rl.question('Password? ', { skipHistory: true }, (pw) => { + rl.question('Password? ', { skipHistory: true }, common.mustCall((pw) => { assert.strictEqual(pw, 'secret-pass'); - assert.strictEqual(rl.history.includes('secret-pass'), false, - 'Password should not be in history'); + assert.ok(!rl.history.includes('secret-pass'), 'Password should not be in history'); rl.close(); - }); + })); // Simulate user typing "secret-pass" + Enter input.write('secret-pass\n'); @@ -30,12 +29,11 @@ const { PassThrough } = require('stream'); output.isTTY = true; // simulate TTY const rl = readline.createInterface({ input, output }); - rl.question('Password? ', (pw) => { + rl.question('Password? ', common.mustCall((pw) => { assert.strictEqual(pw, 'normal-input'); - assert.strictEqual(rl.history.includes('normal-input'), true, - 'Input should be in history'); + assert.partialDeepStrictEqual(rl.history, ['normal-input']); rl.close(); - }); + })); // Simulate user typing "normal-input" + Enter input.write('normal-input\n'); From a498f41c376000767411caeccb8b37b80d88c74f Mon Sep 17 00:00:00 2001 From: Krishnadas Date: Tue, 12 Aug 2025 16:21:32 +0530 Subject: [PATCH 3/5] readline: fix lint errors in readline interface and symbols - Capitalized comments per style guide - Added missing trailing commas in objects - Use primordials.Symbol instead of global Symbol - Address node-core linting rules for internal modules --- lib/internal/readline/interface.js | 6 +++--- lib/internal/readline/symbols.js | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 9c7afa147ed6b5..7a153b23dd7dd5 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -496,12 +496,12 @@ class Interface extends InterfaceConstructor { [kAddHistory]() { const currentLine = this.line; if (this[kSkipHistory]) { - this[kSkipHistory] = false; // reset flag for next time - return currentLine; // keep the input but don't store in history + this[kSkipHistory] = false; // Reset flag for next time + return currentLine; // Keep the input but don't store in history } return this.historyManager.addHistory( this[kIsMultiline], - this[kLastCommandErrored] + this[kLastCommandErrored], ); } diff --git a/lib/internal/readline/symbols.js b/lib/internal/readline/symbols.js index f9b5bf330802dc..5bb4a875c612a5 100644 --- a/lib/internal/readline/symbols.js +++ b/lib/internal/readline/symbols.js @@ -1,6 +1,9 @@ 'use strict'; // Shared Symbols for internal readline modules +const { + Symbol, +} = primordials; const kSkipHistory = Symbol('_skipHistory'); module.exports = { From fff75a7ef1f097b28787412ad4a9f66787eea395 Mon Sep 17 00:00:00 2001 From: Krishnadas Date: Tue, 12 Aug 2025 22:53:16 +0530 Subject: [PATCH 4/5] readline: fix lint errors in readline interface and symbols - Capitalized comments per style guide - Added missing trailing commas in objects - Use primordials.Symbol instead of global Symbol - Address node-core linting rules for internal modules --- lib/internal/readline/interface.js | 2 +- lib/internal/readline/symbols.js | 11 ----------- lib/internal/readline/utils.js | 2 ++ lib/internal/repl/history.js | 2 +- lib/readline.js | 2 +- 5 files changed, 5 insertions(+), 14 deletions(-) delete mode 100644 lib/internal/readline/symbols.js diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 7a153b23dd7dd5..980b7b2cb3269b 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -156,7 +156,7 @@ const kPreviousCursorCols = Symbol('_previousCursorCols'); const kMultilineMove = Symbol('_multilineMove'); const kPreviousPrevRows = Symbol('_previousPrevRows'); const kAddNewLineOnTTY = Symbol('_addNewLineOnTTY'); -const { kSkipHistory } = require('internal/readline/symbols'); +const { kSkipHistory } = require('internal/readline/utils'); function InterfaceConstructor(input, output, completer, terminal) { this[kSawReturnAt] = 0; diff --git a/lib/internal/readline/symbols.js b/lib/internal/readline/symbols.js deleted file mode 100644 index 5bb4a875c612a5..00000000000000 --- a/lib/internal/readline/symbols.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -// Shared Symbols for internal readline modules -const { - Symbol, -} = primordials; -const kSkipHistory = Symbol('_skipHistory'); - -module.exports = { - kSkipHistory, -}; diff --git a/lib/internal/readline/utils.js b/lib/internal/readline/utils.js index 93029df7c8da30..98b9d014ef57d6 100644 --- a/lib/internal/readline/utils.js +++ b/lib/internal/readline/utils.js @@ -15,6 +15,7 @@ const { const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16 const kEscape = '\x1b'; const kSubstringSearch = Symbol('kSubstringSearch'); +const kSkipHistory = Symbol('_skipHistory'); function CSI(strings, ...args) { let ret = `${kEscape}[`; @@ -418,4 +419,5 @@ module.exports = { reverseString, kSubstringSearch, CSI, + kSkipHistory, }; diff --git a/lib/internal/repl/history.js b/lib/internal/repl/history.js index 2c72bb1b6fba41..c90f348735fff9 100644 --- a/lib/internal/repl/history.js +++ b/lib/internal/repl/history.js @@ -44,7 +44,7 @@ const kIsFlushing = Symbol('_kIsFlushing'); const kHistory = Symbol('_kHistory'); const kSize = Symbol('_kSize'); const kIndex = Symbol('_kIndex'); -const { kSkipHistory } = require('internal/readline/symbols'); +const { kSkipHistory } = require('internal/readline/utils'); // Class methods const kNormalizeLineEndings = Symbol('_kNormalizeLineEndings'); diff --git a/lib/readline.js b/lib/readline.js index 2cfa9513ae9cb5..22bf00e2ae6563 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -96,7 +96,7 @@ const { kWordRight, kWriteToOutput, } = require('internal/readline/interface'); -const { kSkipHistory } = require('internal/readline/symbols'); +const { kSkipHistory } = require('internal/readline/utils'); let addAbortListener; function Interface(input, output, completer, terminal) { From 11c50c92a227cf35f16876124af00b425ef6bdee Mon Sep 17 00:00:00 2001 From: Krishnadas Date: Wed, 13 Aug 2025 00:40:20 +0530 Subject: [PATCH 5/5] readline: fixed linting issues, test case for skipHistory false - Address node-core linting rules for internal modules - test case for skipHistory false --- lib/internal/readline/interface.js | 2 +- lib/internal/repl/history.js | 2 +- test/parallel/test-readline-skip-history.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/internal/readline/interface.js b/lib/internal/readline/interface.js index 980b7b2cb3269b..07f416c66dd860 100644 --- a/lib/internal/readline/interface.js +++ b/lib/internal/readline/interface.js @@ -63,6 +63,7 @@ const { charLengthLeft, commonPrefix, kSubstringSearch, + kSkipHistory, } = require('internal/readline/utils'); let emitKeypressEvents; let kFirstEventParam; @@ -156,7 +157,6 @@ const kPreviousCursorCols = Symbol('_previousCursorCols'); const kMultilineMove = Symbol('_multilineMove'); const kPreviousPrevRows = Symbol('_previousPrevRows'); const kAddNewLineOnTTY = Symbol('_addNewLineOnTTY'); -const { kSkipHistory } = require('internal/readline/utils'); function InterfaceConstructor(input, output, completer, terminal) { this[kSawReturnAt] = 0; diff --git a/lib/internal/repl/history.js b/lib/internal/repl/history.js index c90f348735fff9..3ca11c41d477c5 100644 --- a/lib/internal/repl/history.js +++ b/lib/internal/repl/history.js @@ -26,6 +26,7 @@ const permission = require('internal/process/permission'); const { clearTimeout, setTimeout } = require('timers'); const { reverseString, + kSkipHistory, } = require('internal/readline/utils'); // The debounce is to guard against code pasted into the REPL. @@ -44,7 +45,6 @@ const kIsFlushing = Symbol('_kIsFlushing'); const kHistory = Symbol('_kHistory'); const kSize = Symbol('_kSize'); const kIndex = Symbol('_kIndex'); -const { kSkipHistory } = require('internal/readline/utils'); // Class methods const kNormalizeLineEndings = Symbol('_kNormalizeLineEndings'); diff --git a/test/parallel/test-readline-skip-history.js b/test/parallel/test-readline-skip-history.js index 4216a0ffa95d9c..e2cb64dfcb46af 100644 --- a/test/parallel/test-readline-skip-history.js +++ b/test/parallel/test-readline-skip-history.js @@ -29,7 +29,7 @@ const { PassThrough } = require('stream'); output.isTTY = true; // simulate TTY const rl = readline.createInterface({ input, output }); - rl.question('Password? ', common.mustCall((pw) => { + rl.question('Password? ', { skipHistory: false }, common.mustCall((pw) => { assert.strictEqual(pw, 'normal-input'); assert.partialDeepStrictEqual(rl.history, ['normal-input']); rl.close();