musealpha / src-tauri /resources /scripts /vault_detector.js
asdf98's picture
Upload 112 files
3d7d9b5 verified
/**
* 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);
})();