// ==UserScript== // @name gov.br autofill fix thank you claude web chat // @namespace local.govbr.onescreen // @version 1.4 // @description Corrige autofill do CPF, adiciona campo de senha na 1ª tela, envia sozinho e preenche a 2ª etapa (handoff da senha só em memória) // @match https://sso.acesso.gov.br/* // @run-at document-end // @grant unsafeWindow // ==/UserScript== (function () { 'use strict'; const AUTOSUBMIT = true; const LAST_AUTO = 'last_autosubmit'; const MAX_AGE_MS = 30 * 1000; const AUTO_COOLDOWN_MS = 30 * 1000; const pageWin = unsafeWindow; // --------------------------------------------------------------------- // sessionStorage no Chromium é gravado em disco (LevelDB do perfil, em // "Session Storage/") para permitir restauração de sessão. Por isso a // senha NUNCA passa por aqui — só o timestamp de cooldown, que não é // sensível. // --------------------------------------------------------------------- const PREFIX = '__govbr_internal_'; const store = { get(k, def) { try { const v = sessionStorage.getItem(PREFIX + k); return v === null ? def : v; } catch (_) { return def; } }, set(k, v) { try { sessionStorage.setItem(PREFIX + k, String(v)); } catch (_) {} }, del(k) { try { sessionStorage.removeItem(PREFIX + k); } catch (_) {} }, }; // --------------------------------------------------------------------- // Handoff da senha: SOMENTE MEMÓRIA. // window.name sobrevive à navegação dentro da mesma aba (é estado do // contexto de navegação, não do documento) e vive apenas na memória do // processo — não é gravado no DOM Storage em disco. É lido e apagado no // primeiro instante da 2ª tela. // --------------------------------------------------------------------- const VAULT_MARK = '__govbr_pw__:'; const vault = { put(password) { let prev = ''; try { prev = pageWin.name || ''; } catch (_) {} if (prev.indexOf(VAULT_MARK) === 0) prev = ''; // não empilha lixo try { pageWin.name = VAULT_MARK + JSON.stringify({ p: password, // senha t: Date.now(), // validade n: prev, // window.name original, para restaurar depois }); } catch (_) {} }, // apaga sem devolver (e restaura o window.name original, se houver) clear() { let raw = ''; try { raw = pageWin.name || ''; } catch (_) { return; } if (raw.indexOf(VAULT_MARK) !== 0) return; let data = null; try { data = JSON.parse(raw.slice(VAULT_MARK.length)); } catch (_) {} try { pageWin.name = (data && data.n) || ''; } catch (_) {} }, // lê e apaga no mesmo instante take() { let raw = ''; try { raw = pageWin.name || ''; } catch (_) { return null; } if (raw.indexOf(VAULT_MARK) !== 0) return null; let data = null; try { data = JSON.parse(raw.slice(VAULT_MARK.length)); } catch (_) {} try { pageWin.name = (data && data.n) || ''; } catch (_) {} return data; }, }; const log = (...a) => console.log('[govbr-login]', ...a); log('carregado em', location.pathname); // rede de segurança: handoff velho esquecido de uma tentativa anterior (function expireVault() { let raw = ''; try { raw = pageWin.name || ''; } catch (_) { return; } if (raw.indexOf(VAULT_MARK) !== 0) return; let d = null; try { d = JSON.parse(raw.slice(VAULT_MARK.length)); } catch (_) {} if (!d || typeof d.t !== 'number' || Date.now() - d.t > MAX_AGE_MS) vault.clear(); })(); const cpfButton = document.getElementById('enter-account-id'); let cpfInput = document.getElementById('accountId'); if (cpfButton && cpfInput) { stepCpf(); } else { const pwInput = document.querySelector('input[type="password"]'); if (pwInput) stepPassword(pwInput); } function whenCaptchaReady(cb) { const started = Date.now(); const timer = setInterval(() => { const hasCaptcha = !!document.querySelector('script[src*="hcaptcha"]'); const ready = !hasCaptcha || typeof pageWin.hcaptchaWidgetId !== 'undefined'; if (ready || Date.now() - started > 6000) { clearInterval(timer); setTimeout(cb, 150); } }, 100); } // ---------- Etapa 1: CPF (+ campo de senha injetado) ---------- function stepCpf() { vault.clear(); const form = document.getElementById('loginData'); if (form) form.setAttribute('autocomplete', 'on'); const hadFocus = document.activeElement === cpfInput; const fresh = cpfInput.cloneNode(false); fresh.type = 'text'; fresh.removeAttribute('inputmode'); fresh.setAttribute('autocomplete', 'username'); fresh.setAttribute('autocapitalize', 'off'); fresh.setAttribute('spellcheck', 'false'); fresh.value = cpfInput.value; cpfInput.replaceWith(fresh); cpfInput = fresh; if (hadFocus) fresh.focus(); const brokenLabel = document.querySelector('label[for="cpf"]'); const cpfLabel = brokenLabel ? brokenLabel.cloneNode(true) : null; if (brokenLabel) brokenLabel.htmlFor = cpfInput.id; const realCpf = document.createElement('input'); realCpf.type = 'hidden'; realCpf.name = 'accountId'; cpfInput.name = 'username'; (form || document.body).appendChild(realCpf); const syncCpf = () => { realCpf.value = cpfInput.value; }; ['input', 'change', 'blur'].forEach((ev) => cpfInput.addEventListener(ev, syncCpf)); syncCpf(); const pw = cpfInput.cloneNode(false); pw.id = 'password'; pw.removeAttribute('name'); pw.removeAttribute('inputmode'); pw.type = 'password'; pw.value = ''; pw.placeholder = 'Digite sua senha'; pw.setAttribute('autocomplete', 'current-password'); const label = cpfLabel ? cpfLabel.cloneNode(true) : document.createElement('label'); label.htmlFor = 'password'; label.textContent = 'Senha'; cpfInput.after(label, pw); document.addEventListener('click', (e) => { const btn = e.target.closest && e.target.closest('#enter-account-id'); if (btn) syncCpf(); if (btn && pw.value) { vault.put(pw.value); // memória apenas — nada de sessionStorage } }, true); if (!AUTOSUBMIT) return; let typed = false; let submitted = false; const markTyped = (e) => { if (e.type === 'paste' || (e.key && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey)) { typed = true; } }; [cpfInput, pw].forEach((el) => { el.addEventListener('keydown', markTyped); el.addEventListener('paste', markTyped); }); let debounce; const maybeSubmit = () => { if (submitted) return; if (typed) { log('sem envio automático: houve digitação/colagem nos campos'); return; } clearTimeout(debounce); debounce = setTimeout(() => { const digits = cpfInput.value.replace(/\D/g, ''); if (digits.length !== 11 || !pw.value) { log('aguardando campos', { dígitosCPF: digits.length, senhaPreenchida: !!pw.value }); return; } const since = Date.now() - Number(store.get(LAST_AUTO, 0)); if (since < AUTO_COOLDOWN_MS) { log('bloqueado pelo cooldown; tente de novo em', Math.ceil((AUTO_COOLDOWN_MS - since) / 1000), 's'); return; } log('enviando automaticamente'); submitted = true; store.set(LAST_AUTO, Date.now()); // só timestamp: sem problema ir pro disco // grava o handoff também no autosubmit — o clique programático não passa pelo // listener de clique em alguns navegadores quando o alvo é removido antes vault.put(pw.value); whenCaptchaReady(() => cpfButton.click()); }, 250); }; cpfInput.addEventListener('input', maybeSubmit); cpfInput.addEventListener('change', maybeSubmit); pw.addEventListener('input', maybeSubmit); pw.addEventListener('change', maybeSubmit); setTimeout(maybeSubmit, 100); setTimeout(maybeSubmit, 1000); } // ---------- Etapa 2: tela de senha real ---------- function stepPassword(input) { input.setAttribute('autocomplete', 'current-password'); const data = vault.take(); // lê e apaga imediatamente if (!data || Date.now() - data.t > MAX_AGE_MS) return; const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; setter.call(input, data.p); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); const form = input.form || document; const buttons = [...form.querySelectorAll('button[type="submit"], input[type="submit"]')]; const target = buttons.find((b) => b.value === 'enter-password') || buttons.find((b) => /continuar|entrar|login|acessar/i.test(b.textContent || b.value)) || buttons[0]; if (!target) return; whenCaptchaReady(() => target.click()); } })();