app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
master
John Lauer
rust: Phase 3 complete (vault, cards, wiki auto-sign + view flip, annotate, tab + screen recording, highFps lease, down…
4869577
2mo ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
(() => {
if (window !== window.top) return;
if (window.__pupCapV === '${BRIDGE_VERSION}') return;
window.__pupCapV = '${BRIDGE_VERSION}';
function vis(el){ return !!el && !el.disabled && el.offsetParent !== null && el.getClientRects().length > 0; }
// v2.0.123: a password field is not always type=password — Gusto and many SSO/Keycloak themes use
// type=text with autocomplete="current-password" (or name/id "password") and mask it themselves.
function isPwd(el){ return el && el.tagName === 'INPUT' && (el.type === 'password' || el.autocomplete === 'current-password' || el.autocomplete === 'new-password' || /(^|[^a-z])password([^a-z]|$)/i.test(el.name || '') || /(^|[^a-z])password([^a-z]|$)/i.test(el.id || '')); }
function grab(form){
try {
var pwd = [].slice.call((form || document).querySelectorAll('input')).filter(vis).filter(isPwd)[0];
if (!pwd || !pwd.value) return;
var all = [].slice.call((form || document).querySelectorAll('input'));
var pi = all.indexOf(pwd);
var user = [].slice.call((form || document).querySelectorAll('input[autocomplete=username], input[type=email]')).filter(vis).filter(function(e){ return !isPwd(e); })[0];
if (!user) {
var cands = [].slice.call((form || document).querySelectorAll('input[type=text], input[type=tel], input:not([type])')).filter(vis).filter(function(e){ return !isPwd(e); });
user = cands.slice().reverse().filter(function(c){ return all.indexOf(c) < pi; })[0] || cands[0] || null;
}
var rec = { host: location.hostname, u: user ? user.value : '', p: pwd.value, ts: Date.now() };
// Prefer the SYNCHRONOUS binding — it reaches the bridge BEFORE the submit navigates the
// page away (the global stash below would be wiped by that navigation, missing the capture).
if (typeof window.__pupCredCapture === 'function') { try { window.__pupCredCapture(JSON.stringify(rec)); } catch (e) {} }
window.__pupCapturedLogin = rec; // fallback for SPA logins that never navigate (sweep harvests it)
} catch (e) {}
}
function formHasPwd(f){ try { return !!f && [].slice.call(f.querySelectorAll('input')).some(isPwd); } catch (e) { return false; } }
addEventListener('submit', function(e){ grab(e.target && e.target.tagName === 'FORM' ? e.target : null); }, true);
addEventListener('click', function(e){
try {
var t = e.target && e.target.closest && e.target.closest('button, input[type=submit], [role=button], a');
if (!t) return;
var f = t.form || (t.closest && t.closest('form'));
// v2.0.128 (consumer bug report #4): was a literal input[type=password] check, so a masked
// type=text password field (Rails/Keycloak) never matched and capture missed on click. Use the
// isPwd matcher; if there is no enclosing form, sweep the document (SPA login with no <form>).
if (formHasPwd(f)) grab(f); else if (!f) grab(null);
} catch (e2) {}
}, true);
// v2.0.121 (John, Gusto miss): ALSO capture the instant the password field loses focus or
// changes — this fires BEFORE the form submits and, crucially, before a cross-origin SSO redirect
// (login.gusto.com -> app.gusto.com) tears the page down. Submit-only capture raced that redirect
// and lost; grabbing on blur/change delivers the creds through the synchronous binding first.
addEventListener('change', function(e){ try { if (isPwd(e.target) && e.target.value) grab(e.target.form || null); } catch (e2) {} }, true);
addEventListener('blur', function(e){ try { if (isPwd(e.target) && e.target.value) grab(e.target.form || null); } catch (e2) {} }, true);
// v2.0.128 (consumer bug report #4): a STANDARD Rails login often submits PROGRAMMATICALLY —
// form.submit() (and requestSubmit's older cousins) does NOT dispatch the 'submit' event, so
// submit/click capture silently missed it. Hook the prototype so a programmatic submit still grabs.
try {
var _origSubmit = HTMLFormElement.prototype.submit;
HTMLFormElement.prototype.submit = function(){ try { if (formHasPwd(this)) grab(this); } catch (e) {} return _origSubmit.apply(this, arguments); };
} catch (e) {}
// Last-resort net: on Enter inside a login form, and on pagehide before ANY navigation tears the
// page down, grab whatever password field is currently filled. Cheap, synchronous, dedup is
// downstream (same host+user+pwd within the session is coalesced).
addEventListener('keydown', function(e){ try { if (e.key === 'Enter' && isPwd(document.activeElement) && document.activeElement.value) grab(document.activeElement.form || null); } catch (e2) {} }, true);
addEventListener('pagehide', function(){ try { grab(null); } catch (e2) {} }, true);
})()