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:
+13
@@ -691,6 +691,19 @@
|
|||||||
automatically when the app is minimised — no manual setup needed.
|
automatically when the app is minimised — no manual setup needed.
|
||||||
</p>
|
</p>
|
||||||
<button class="btn btn-ghost btn-sm" id="openClipboardSettings">How?</button>
|
<button class="btn btn-ghost btn-sm" id="openClipboardSettings">How?</button>
|
||||||
|
<div class="setting-row" style="margin-top:8px">
|
||||||
|
<span>
|
||||||
|
Auto-clear copied secrets after
|
||||||
|
<small class="setting-hint">Only clears if you haven't copied something else since.</small>
|
||||||
|
</span>
|
||||||
|
<select id="settingClipboardClear">
|
||||||
|
<option value="0">Never</option>
|
||||||
|
<option value="15">15 seconds</option>
|
||||||
|
<option value="30">30 seconds</option>
|
||||||
|
<option value="60">1 minute</option>
|
||||||
|
<option value="120">2 minutes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="slideover-field">
|
<div class="slideover-field">
|
||||||
|
|||||||
@@ -68,12 +68,15 @@ const Bridge = (() => {
|
|||||||
active,
|
active,
|
||||||
|
|
||||||
// Copy text to clipboard, excluded from Win+V history.
|
// 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.
|
// Returns true when the bridge handled the copy, false as fallback signal.
|
||||||
copySecure(text, clearAfterMs = 30000) {
|
copySecure(text, clearAfterMs = 30000) {
|
||||||
if (!active) return false;
|
if (!active) return false;
|
||||||
|
const ms = clearAfterMs > 0 ? (state.clipboardClearSeconds * 1000) : 0;
|
||||||
cmd('cmd://clipboard/copy?text=' + encodeURIComponent(text) +
|
cmd('cmd://clipboard/copy?text=' + encodeURIComponent(text) +
|
||||||
'&clear=' + clearAfterMs);
|
'&clear=' + ms);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -646,6 +649,7 @@ const state = {
|
|||||||
recoveryConfigured: false, // refreshed by refreshRecoveryStatus on Settings open
|
recoveryConfigured: false, // refreshed by refreshRecoveryStatus on Settings open
|
||||||
autofillEnabled: localStorage.getItem('autofillEnabled') !== '0', // default ON
|
autofillEnabled: localStorage.getItem('autofillEnabled') !== '0', // default ON
|
||||||
autofillClearField: localStorage.getItem('autofillClearField') !== '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
|
autofillFailBalloon: localStorage.getItem('autofillFailBalloon') !== '0', // default ON
|
||||||
// Hotkey combos. Each combo = { ctrl, shift, alt, win, key }.
|
// Hotkey combos. Each combo = { ctrl, shift, alt, win, key }.
|
||||||
// key is the uppercase character or VK label ('A'..'Z', '0'..'9',
|
// key is the uppercase character or VK label ('A'..'Z', '0'..'9',
|
||||||
@@ -6365,6 +6369,7 @@ function openSettings() {
|
|||||||
$('#settingTrayNotifRow').style.display = Bridge.active ? '' : 'none';
|
$('#settingTrayNotifRow').style.display = Bridge.active ? '' : 'none';
|
||||||
$('#settingTrashPurge').value = String(state.trashAutoPurgeDays || 0);
|
$('#settingTrashPurge').value = String(state.trashAutoPurgeDays || 0);
|
||||||
$('#settingPasswordExpiry').value = String(state.passwordExpiryDays || 0);
|
$('#settingPasswordExpiry').value = String(state.passwordExpiryDays || 0);
|
||||||
|
$('#settingClipboardClear').value = String(state.clipboardClearSeconds ?? 30);
|
||||||
$('#settingEditorPosition').value = state.editorPosition || 'right';
|
$('#settingEditorPosition').value = state.editorPosition || 'right';
|
||||||
$('#settingConfirmUnsaved').checked = state.confirmOnUnsaved !== false;
|
$('#settingConfirmUnsaved').checked = state.confirmOnUnsaved !== false;
|
||||||
// PIN unlock — only meaningful when DPAPI is available.
|
// PIN unlock — only meaningful when DPAPI is available.
|
||||||
@@ -6761,6 +6766,8 @@ const SYNCED_SETTING_KEYS = [
|
|||||||
'autofillClearField',
|
'autofillClearField',
|
||||||
// Tray balloon when a fill fails while the window is hidden (default ON).
|
// Tray balloon when a fill fails while the window is hidden (default ON).
|
||||||
'autofillFailBalloon',
|
'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
|
// Sets `data-editor-position` on <body> so CSS can swap the slideover
|
||||||
@@ -6847,6 +6854,9 @@ async function loadServerSettings() {
|
|||||||
case 'autofillFailBalloon':
|
case 'autofillFailBalloon':
|
||||||
localStorage.setItem('autofillFailBalloon', v ? '1' : '0');
|
localStorage.setItem('autofillFailBalloon', v ? '1' : '0');
|
||||||
break;
|
break;
|
||||||
|
case 'clipboardClearSeconds':
|
||||||
|
localStorage.setItem('clipboardClearSeconds', String(v));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Apply visual settings immediately.
|
// Apply visual settings immediately.
|
||||||
@@ -7531,6 +7541,13 @@ async function init() {
|
|||||||
if (n === 0) toast('Trash auto-purge disabled');
|
if (n === 0) toast('Trash auto-purge disabled');
|
||||||
else toast('Trash will auto-purge after ' + n + ' days (next unlock)');
|
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 => {
|
$('#settingPasswordExpiry').addEventListener('change', e => {
|
||||||
const n = parseInt(e.target.value, 10) || 0;
|
const n = parseInt(e.target.value, 10) || 0;
|
||||||
state.passwordExpiryDays = n;
|
state.passwordExpiryDays = n;
|
||||||
|
|||||||
Reference in New Issue
Block a user