Ctrl K
ComunidadGuías…

src/main/clipboard-guard.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { clipboard, ClipboardItem } from 'electron'

import { logError } from './log'
import { getPrefs } from './prefs'

// The recovery key grants full access, so it is cleared even when the user turned the
// auto-clear off.
const SECRET_CLEAR_SECONDS = 30

const rawFormat = (name: string): string => `electron application/osclipboard;format="${name}"`
const dwordZero = (): Blob => new Blob([new Uint32Array([0])])

let timer: ReturnType<typeof setTimeout> | undefined
// What locking may still have to clear, see clearClipboardIfOurs.
let lastWritten: string | undefined
// The secret the armed timer still has to clear.
let pendingSecret: string | undefined
// Bumped by every lock, so a write still in flight when a lock cleared can tell it was missed.
let lockGeneration = 0

export function scheduleClipboardClear(copiedValue: string, seconds?: number): void {
  if (timer) clearTimeout(timer)

  const delay = seconds ?? getPrefs().clipboardClearSeconds
  if (delay <= 0) return

  timer = setTimeout(() => {
    lastWritten = pendingSecret = undefined
    // readText() is async since Electron 44
    clipboard
      .readText()
      .then((text) => {
        if (text === copiedValue) clipboard.clear()
      })
      .catch((e: unknown) => logError('[clipboard] auto-clear failed:', e))
  }, delay * 1000)
}

// Clipboard history, cloud sync and clipboard managers decide whether to keep an item when it
// is written, so the opt-out markers must ride in the same atomic write as the text.
export async function writeClipboard(text: string, { secret = false } = {}): Promise<void> {
  const generation = lockGeneration
  await clipboard.write([
    new ClipboardItem({
      'text/plain': text,
      [rawFormat('ExcludeClipboardContentFromMonitorProcessing')]: dwordZero(),
      [rawFormat('CanIncludeInClipboardHistory')]: dwordZero(),
      [rawFormat('CanUploadToCloudClipboard')]: dwordZero(),
      // Electron drops MIME types it doesn't know, so the pasteboard type goes in raw.
      [rawFormat('org.nspasteboard.ConcealedType')]: '',
      'x-kde-passwordManagerHint': 'secret'
    })
  ])
  // Copying the same secret again (e.g. selecting the words) must not relax its clear.
  const isSecret = secret || text === pendingSecret
  if (generation !== lockGeneration && !isSecret && getPrefs().clearClipboardOnLock) {
    try {
      // Another app may have copied since the write landed; that copy isn't ours to clear.
      if ((await clipboard.readText()) === text) clipboard.clear()
      return
    } catch (e) {
      // Falls through, so the timer or the next lock or quit can still clear it.
      logError('[clipboard] clearing a copy that landed after a lock failed:', e)
    }
  }
  pendingSecret = isSecret ? text : undefined
  lastWritten = text
  scheduleClipboardClear(text, isSecret ? SECRET_CLEAR_SECONDS : undefined)
}

// A locked vault must not leave its content on the clipboard, and the clear timer dies with the
// process, so every lock, quit and crash calls this. Never throws: it runs on those paths.
// The recovery key is shown only once, so a lock leaves it to its 30 s clear: clearing it there
// could wipe it before the user pastes it, with no way to see it again. Quitting kills that
// timer, so quit clears it, regardless of the prefs.
export async function clearClipboardIfOurs(reason: 'lock' | 'quit'): Promise<void> {
  if (reason === 'lock') lockGeneration++
  const ours = lastWritten
  if (ours === undefined) return
  const keep = ours === pendingSecret ? reason === 'lock' : !getPrefs().clearClipboardOnLock
  if (keep) return
  lastWritten = pendingSecret = undefined
  if (timer) clearTimeout(timer)
  try {
    if ((await clipboard.readText()) === ours) clipboard.clear()
  } catch (e) {
    logError('[clipboard] clearing on lock failed:', e)
  }
}