194 lines
7.1 KiB
JavaScript
194 lines
7.1 KiB
JavaScript
// Injected into every account view (top page and CCP iframes). Three jobs:
|
|
// 1. Detect the signed-in agent (name + state) and report it to main.
|
|
// 2. Detect contacts arriving/leaving for the unified alert strip.
|
|
// 3. Execute status commands ("go Available"/"go Offline") inside the CCP.
|
|
//
|
|
// The probe must run in the PAGE's main world to see `window.connect`. It is
|
|
// injected with webFrame.executeJavaScript — NOT a <script> tag, because the
|
|
// CCP's CSP (script-src 'self', no unsafe-inline) silently blocks injected
|
|
// tags. webFrame.executeJavaScript is not subject to page CSP.
|
|
//
|
|
// Agent Workspace keeps Streams in a CCP iframe, so this preload also runs in
|
|
// subframes (nodeIntegrationInSubFrames). Autofill stays top-frame only.
|
|
|
|
const { ipcRenderer, webFrame } = require("electron");
|
|
const { probeScript } = require("./probe");
|
|
|
|
function inject() {
|
|
webFrame.executeJavaScript(probeScript).catch(() => {});
|
|
}
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", inject, { once: true });
|
|
} else {
|
|
inject();
|
|
}
|
|
|
|
// Relay page -> main. Same-origin iframe probes post to window.top; only the
|
|
// top frame forwards those. Cross-origin iframes can't reach top, so they send
|
|
// IPC themselves.
|
|
window.addEventListener("message", (e) => {
|
|
const d = e.data;
|
|
if (!d || d.__cdmsg !== true) return;
|
|
if (
|
|
!["agent", "detection", "state", "auth", "status-result", "incoming", "gone", "reply", "accepted"].includes(
|
|
d.type
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
try {
|
|
if (window !== window.top) {
|
|
try {
|
|
void window.top.location.href; // throws if this iframe is cross-origin
|
|
return; // same-origin: the top frame's listener will relay
|
|
} catch {
|
|
ipcRenderer.send("contact", d);
|
|
return;
|
|
}
|
|
}
|
|
} catch {}
|
|
ipcRenderer.send("contact", d);
|
|
});
|
|
|
|
// Relay main -> page (status commands). Forward into same-origin iframes so a
|
|
// top-level send still reaches the CCP frame that owns window.connect.
|
|
ipcRenderer.on("cd-set-status", (_e, kind) => {
|
|
const msg = { __cdcmd: true, cmd: "set-status", kind };
|
|
window.postMessage(msg, "*");
|
|
try {
|
|
if (window.top !== window) return;
|
|
for (const f of document.querySelectorAll("iframe")) {
|
|
try {
|
|
f.contentWindow.postMessage(msg, "*");
|
|
} catch (err) {}
|
|
}
|
|
} catch (err) {}
|
|
});
|
|
|
|
// ---------- saved sign-in autofill ----------
|
|
// Runs in the preload's isolated world (direct DOM access, invisible to the
|
|
// page's scripts). Credentials come from the main process, which releases them
|
|
// only when this view's URL is the account's real Amazon login host, at most
|
|
// twice per 5 minutes. One attempt per page load. Iframes skip this entirely.
|
|
(function autofill() {
|
|
try {
|
|
if (window !== window.top) return;
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
let done = false;
|
|
let busy = false;
|
|
|
|
// Written against the real Amazon Connect sign-in markup (a GWT app):
|
|
// <input type="text" id="wdc_organization"> visibility:hidden (decoy)
|
|
// <input type="username" id="wdc_username"> <-- NOT a valid HTML
|
|
// type; filtering for
|
|
// text/email misses it
|
|
// <input type="password" id="wdc_password">
|
|
// <input type="password" id="wdc_mfa"> display:none
|
|
// <button id="wdc_login_button">Sign In</button>
|
|
|
|
const cssHidden = (el) => {
|
|
const c = getComputedStyle(el);
|
|
return c.display === "none" || c.visibility === "hidden";
|
|
};
|
|
|
|
const idText = (el) =>
|
|
`${el.name || ""} ${el.id || ""} ${el.getAttribute("placeholder") || ""} ${
|
|
el.getAttribute("aria-label") || ""
|
|
}`;
|
|
|
|
// GWT and React both ignore a plain .value write: go through the native
|
|
// setter, then fire the events either framework might be listening for.
|
|
const setValue = (el, value) => {
|
|
if (!el) return;
|
|
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set;
|
|
setter.call(el, value);
|
|
for (const type of ["input", "change", "keyup", "blur"]) {
|
|
el.dispatchEvent(new Event(type, { bubbles: true }));
|
|
}
|
|
};
|
|
|
|
// Skip display:none password fields — #wdc_mfa is one, and grabbing it
|
|
// instead of the real box would silently fill nothing.
|
|
const findPassword = () =>
|
|
[...document.querySelectorAll('input[type="password"]')].find((el) => !cssHidden(el)) || null;
|
|
|
|
// Identify the username box by NAME, not by type — its type is the
|
|
// non-standard "username". Exclude by what a field can't be, then prefer one
|
|
// named like a username, then fall back to the nearest box above the
|
|
// password field.
|
|
const findUsername = (pass) => {
|
|
const NON_TEXT = new Set([
|
|
"password", "checkbox", "radio", "hidden", "submit", "button",
|
|
"file", "image", "reset", "range", "color",
|
|
]);
|
|
const cands = [...document.querySelectorAll("input")].filter(
|
|
(el) => !NON_TEXT.has((el.getAttribute("type") || "text").toLowerCase())
|
|
);
|
|
const named = cands.filter((el) => /user|email|login/i.test(idText(el)));
|
|
return (
|
|
named.find((el) => !cssHidden(el)) || // #wdc_username
|
|
named[0] ||
|
|
cands
|
|
.filter((el) => !cssHidden(el) && el.compareDocumentPosition(pass) & 4)
|
|
.pop() ||
|
|
cands.find((el) => !cssHidden(el)) ||
|
|
null
|
|
);
|
|
};
|
|
|
|
const tryFill = async () => {
|
|
if (done || busy) return done;
|
|
const pass = findPassword();
|
|
if (!pass) return false;
|
|
const text = (document.body && document.body.innerText) || "";
|
|
if (!/sign\s?in|log\s?in/i.test(text)) return false; // password box, but not a login page
|
|
|
|
busy = true;
|
|
const cred = await ipcRenderer.invoke("login:request").catch(() => null);
|
|
busy = false;
|
|
if (done) return true;
|
|
if (!cred) {
|
|
done = true; // nothing saved (or rate-limited) — stop asking this load
|
|
return true;
|
|
}
|
|
|
|
done = true;
|
|
const user = findUsername(pass);
|
|
setValue(user, cred.username);
|
|
setValue(pass, cred.password);
|
|
|
|
// Verify before submitting. A submit with an empty username burns an auth
|
|
// attempt and moves the account toward a lockout, so report instead.
|
|
setTimeout(() => {
|
|
const userOk = user && user.value === cred.username;
|
|
const passOk = pass.value === cred.password;
|
|
if (!userOk || !passOk) {
|
|
ipcRenderer.send("contact", {
|
|
type: "autofill-failed",
|
|
reason: !userOk ? "could not fill the username field" : "could not fill the password field",
|
|
});
|
|
return;
|
|
}
|
|
const btn =
|
|
document.getElementById("wdc_login_button") ||
|
|
document.querySelector('button[type="submit"], input[type="submit"]') ||
|
|
[...document.querySelectorAll("button")].find((b) =>
|
|
/sign\s?in|log\s?in/i.test(b.textContent)
|
|
);
|
|
if (btn) btn.click();
|
|
else ipcRenderer.send("contact", { type: "autofill-failed", reason: "no Sign In button found" });
|
|
}, 400);
|
|
return true;
|
|
};
|
|
|
|
// Login forms often render late — poll for a while, then give up quietly.
|
|
const timer = setInterval(async () => {
|
|
if (await tryFill()) clearInterval(timer);
|
|
}, 900);
|
|
setTimeout(() => clearInterval(timer), 45000);
|
|
})();
|