File size: 6,306 Bytes
3d7d9b5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | /**
* 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 <form> element)
function attachButtonListeners() {
document.querySelectorAll('button[type="submit"], input[type="submit"], button:not([type])').forEach(function(btn) {
if (btn.__museVault) return;
btn.__museVault = true;
btn.addEventListener('click', function() {
setTimeout(checkPendingCredentials, 100);
}, true);
});
}
// βββ MutationObserver for dynamically created forms (SPAs) βββββββββββ
var observer = new MutationObserver(function() {
attachFormListeners();
attachButtonListeners();
});
observer.observe(document.documentElement, { childList: true, subtree: true });
// Initial scan
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() { attachFormListeners(); attachButtonListeners(); });
} else {
attachFormListeners();
attachButtonListeners();
}
// βββ AUTOFILL: called from Rust via webview.eval() βββββββββββββββββββ
// Rust calls: webview.eval("window.__museAutofill('username', 'password')")
window.__museAutofill = function(username, password) {
var pwFields = findPasswordFields();
if (!pwFields.length) return;
var pwField = pwFields[0];
var userField = findUsernameFor(pwField);
function fill(el, value) {
if (!el || !value) return;
// Use native setter to bypass React/Vue/Angular controlled input
var setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
setter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('blur', { bubbles: true }));
}
if (userField) fill(userField, username);
fill(pwField, password);
};
// βββ Notify Rust that this page has login fields (for autofill offer) β
function notifyLoginPage() {
var pwFields = findPasswordFields();
if (pwFields.length > 0) {
beacon('has-login-form', { origin: location.origin, fields: pwFields.length });
}
}
// Check after short delay to let SPA render
setTimeout(notifyLoginPage, 800);
})();
|