/** * Muse Password Vault — Form Detection & Autofill * * Injected into every child webview page via initialization_script. * Detects login forms and signals Rust via muse-action://vault beacon. * Autofill is triggered separately by Rust calling window.__museAutofill(). * * ISOLATION: This runs inside the app's own WebView profile, completely * separate from the system browser (Edge/Safari/etc). No credential leakage. */ (function() { 'use strict'; if (window.__museVaultDetector) return; window.__museVaultDetector = true; // ─── Beacon to Rust via muse-action:// protocol ────────────────────── function beacon(action, data) { var params = []; data.action = action; for (var k in data) { if (data[k] != null) params.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k])); } var img = new Image(); img.src = 'muse-action://vault?' + params.join('&'); } // ─── Find password and username fields ─────────────────────────────── function findPasswordFields() { return Array.from(document.querySelectorAll('input[type="password"]:not([disabled]):not([hidden])')); } function findUsernameFor(pwField) { var form = pwField.closest('form') || document; var inputs = Array.from(form.querySelectorAll('input')); var pwIdx = inputs.indexOf(pwField); var candidates = inputs.filter(function(el, idx) { if (idx >= pwIdx) return false; var t = (el.type || '').toLowerCase(); if (t === 'hidden' || t === 'submit' || t === 'button' || t === 'checkbox' || t === 'radio') return false; var n = (el.name + el.id + el.autocomplete).toLowerCase(); if (t === 'email' || t === 'text' || t === 'tel') return true; if (n.match(/user|email|login|acct|phone/)) return true; return false; }); return candidates.pop() || null; } // ─── Capture credentials on form submission ────────────────────────── var lastCaptured = null; function captureFromField(pwField) { if (!pwField || !pwField.value) return null; var userField = findUsernameFor(pwField); return { origin: location.origin, username: userField ? userField.value : '', password: pwField.value }; } function onFormSubmit(e) { var form = e.target || e.currentTarget; var pw = form.querySelector('input[type="password"]'); var creds = captureFromField(pw); if (creds && creds.password) { lastCaptured = creds; beacon('save-prompt', creds); } } function attachFormListeners() { document.querySelectorAll('form').forEach(function(form) { if (form.__museVault) return; form.__museVault = true; form.addEventListener('submit', onFormSubmit, true); }); } // ─── SPA detection: intercept network calls while password field has value var origFetch = window.fetch; window.fetch = function() { checkPendingCredentials(); return origFetch.apply(this, arguments); }; var origXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function() { checkPendingCredentials(); return origXHRSend.apply(this, arguments); }; function checkPendingCredentials() { var pwFields = findPasswordFields(); for (var i = 0; i < pwFields.length; i++) { var creds = captureFromField(pwFields[i]); if (creds && creds.password && (!lastCaptured || lastCaptured.password !== creds.password)) { lastCaptured = creds; // Delay — if page navigates away or password field disappears, login likely succeeded setTimeout(function() { if (lastCaptured) beacon('save-prompt', lastCaptured); }, 2000); } } } // ─── Submit button click detection (for forms without