feat(clipboard): user-configurable auto-clear delay (Never/15/30/60/120s)
Single choke point: copySecure overrides any positive clearAfterMs with the clipboardClearSeconds setting (0 = user disabled). The 15 call sites keep passing 30000 unchanged — positive just means "auto-clear this secret"; explicit 0 (username copies) still never clears. Synced setting + a select in Settings > Security (Clipboard privacy). Win+V history exclusion is deliberately NOT exposed — a password manager must not offer to leak into history / cloud clipboard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -68,12 +68,15 @@ const Bridge = (() => {
|
||||
active,
|
||||
|
||||
// Copy text to clipboard, excluded from Win+V history.
|
||||
// clearAfterMs: Delphi auto-clears after this many ms (0 = never).
|
||||
// clearAfterMs: 0 = never auto-clear (username copies). Any positive
|
||||
// value means "auto-clear this secret" and is overridden by the user's
|
||||
// clipboardClearSeconds setting (0 there = user disabled auto-clear).
|
||||
// Returns true when the bridge handled the copy, false as fallback signal.
|
||||
copySecure(text, clearAfterMs = 30000) {
|
||||
if (!active) return false;
|
||||
const ms = clearAfterMs > 0 ? (state.clipboardClearSeconds * 1000) : 0;
|
||||
cmd('cmd://clipboard/copy?text=' + encodeURIComponent(text) +
|
||||
'&clear=' + clearAfterMs);
|
||||
'&clear=' + ms);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -646,6 +649,7 @@ const state = {
|
||||
recoveryConfigured: false, // refreshed by refreshRecoveryStatus on Settings open
|
||||
autofillEnabled: localStorage.getItem('autofillEnabled') !== '0', // default ON
|
||||
autofillClearField: localStorage.getItem('autofillClearField') !== '0', // default ON
|
||||
clipboardClearSeconds: parseInt(localStorage.getItem('clipboardClearSeconds') ?? '30', 10), // 0 = never
|
||||
autofillFailBalloon: localStorage.getItem('autofillFailBalloon') !== '0', // default ON
|
||||
// Hotkey combos. Each combo = { ctrl, shift, alt, win, key }.
|
||||
// key is the uppercase character or VK label ('A'..'Z', '0'..'9',
|
||||
@@ -6365,6 +6369,7 @@ function openSettings() {
|
||||
$('#settingTrayNotifRow').style.display = Bridge.active ? '' : 'none';
|
||||
$('#settingTrashPurge').value = String(state.trashAutoPurgeDays || 0);
|
||||
$('#settingPasswordExpiry').value = String(state.passwordExpiryDays || 0);
|
||||
$('#settingClipboardClear').value = String(state.clipboardClearSeconds ?? 30);
|
||||
$('#settingEditorPosition').value = state.editorPosition || 'right';
|
||||
$('#settingConfirmUnsaved').checked = state.confirmOnUnsaved !== false;
|
||||
// PIN unlock — only meaningful when DPAPI is available.
|
||||
@@ -6761,6 +6766,8 @@ const SYNCED_SETTING_KEYS = [
|
||||
'autofillClearField',
|
||||
// Tray balloon when a fill fails while the window is hidden (default ON).
|
||||
'autofillFailBalloon',
|
||||
// Seconds before a copied secret is auto-cleared (0 = never). Default 30.
|
||||
'clipboardClearSeconds',
|
||||
];
|
||||
|
||||
// Sets `data-editor-position` on <body> so CSS can swap the slideover
|
||||
@@ -6847,6 +6854,9 @@ async function loadServerSettings() {
|
||||
case 'autofillFailBalloon':
|
||||
localStorage.setItem('autofillFailBalloon', v ? '1' : '0');
|
||||
break;
|
||||
case 'clipboardClearSeconds':
|
||||
localStorage.setItem('clipboardClearSeconds', String(v));
|
||||
break;
|
||||
}
|
||||
});
|
||||
// Apply visual settings immediately.
|
||||
@@ -7531,6 +7541,13 @@ async function init() {
|
||||
if (n === 0) toast('Trash auto-purge disabled');
|
||||
else toast('Trash will auto-purge after ' + n + ' days (next unlock)');
|
||||
});
|
||||
$('#settingClipboardClear').addEventListener('change', e => {
|
||||
const n = parseInt(e.target.value, 10) || 0;
|
||||
state.clipboardClearSeconds = n;
|
||||
localStorage.setItem('clipboardClearSeconds', String(n));
|
||||
saveServerSettings();
|
||||
toast(n === 0 ? 'Clipboard auto-clear disabled' : 'Copied secrets clear after ' + n + 's');
|
||||
});
|
||||
$('#settingPasswordExpiry').addEventListener('change', e => {
|
||||
const n = parseInt(e.target.value, 10) || 0;
|
||||
state.passwordExpiryDays = n;
|
||||
|
||||
Reference in New Issue
Block a user