feat: purge legacy agent hosts and land vault Chrome extension path
Retire ACP/Hermes/OpenCode surfaces and rename hermes_tools to mnote_agent_tools so Page AI stays on Pi Lab only. Add Chrome vault extension + extension token route, pre-release purge design, and soft-retire legacy smokes for the small-group production cut.
This commit is contained in:
@@ -0,0 +1,655 @@
|
||||
/**
|
||||
* Content script: detect password form submit, confirm UI, message background.
|
||||
* Does NOT hold tokens or cookie secrets.
|
||||
*
|
||||
* KeePassXC-style flow:
|
||||
* - submit / submit-button click → PENDING_SAVE (background)
|
||||
* - after navigation, background opens OPEN_SAVE_UI_WITH_DRAFT
|
||||
* - SPA same-page: background spa fallback also opens draft UI
|
||||
* - blur is NOT a primary save trigger
|
||||
*/
|
||||
|
||||
(() => {
|
||||
if (window.__mnoteVaultContentInstalled) return;
|
||||
window.__mnoteVaultContentInstalled = true;
|
||||
|
||||
const HOST_ID = "mnote-vault-save-host";
|
||||
/** Last known password on this document only (lost on full navigation). */
|
||||
let lastPassword = "";
|
||||
let lastUsername = "";
|
||||
let lastEmail = "";
|
||||
/** Prevent double PENDING_SAVE from submit + button click. */
|
||||
let submitLockUntil = 0;
|
||||
const SUBMIT_LOCK_MS = 1200;
|
||||
|
||||
function findUsernameNear(passwordInput) {
|
||||
const form =
|
||||
passwordInput?.form || passwordInput?.closest?.("form") || document;
|
||||
const candidates = form.querySelectorAll(
|
||||
'input[type="email"], input[type="text"], input[name*="user" i], input[name*="login" i], input[name*="email" i], input[autocomplete="username"], input[autocomplete="email"]'
|
||||
);
|
||||
for (const el of candidates) {
|
||||
if (el === passwordInput) continue;
|
||||
if (el.type === "password" || el.type === "hidden") continue;
|
||||
const v = (el.value || "").trim();
|
||||
if (v) return { username: v, email: el.type === "email" ? v : "" };
|
||||
}
|
||||
// Fallback: any non-empty text-like input in form
|
||||
const more = form.querySelectorAll(
|
||||
"input:not([type]), input[type=text], input[type=email], input[type=tel]"
|
||||
);
|
||||
for (const el of more) {
|
||||
if (el.type === "password" || el.type === "hidden") continue;
|
||||
const v = (el.value || "").trim();
|
||||
if (v) return { username: v, email: el.type === "email" ? v : "" };
|
||||
}
|
||||
return { username: "", email: "" };
|
||||
}
|
||||
|
||||
function findPasswordIn(root) {
|
||||
const scope = root || document;
|
||||
const list = scope.querySelectorAll?.('input[type="password"]') || [];
|
||||
for (const el of list) {
|
||||
if (el.value && el.value.length >= 1) return el;
|
||||
}
|
||||
return list[0] || null;
|
||||
}
|
||||
|
||||
function isRegisterPath() {
|
||||
const p = location.pathname.toLowerCase();
|
||||
return /sign[-_]?up|register|signup|join|create[-_]?account/.test(p);
|
||||
}
|
||||
|
||||
function buildTitle() {
|
||||
let title = (document.title || "").slice(0, 80);
|
||||
if (isRegisterPath() && title && !/注册|register|sign.?up/i.test(title)) {
|
||||
title = `注册 · ${title}`;
|
||||
}
|
||||
return title || location.hostname;
|
||||
}
|
||||
|
||||
function collectDraftFromDom(preferPasswordInput) {
|
||||
const pwEl =
|
||||
preferPasswordInput ||
|
||||
(document.activeElement?.type === "password"
|
||||
? document.activeElement
|
||||
: null) ||
|
||||
findPasswordIn(document);
|
||||
const pw = (pwEl && pwEl.value) || lastPassword || "";
|
||||
const near = pwEl
|
||||
? findUsernameNear(pwEl)
|
||||
: { username: lastUsername, email: lastEmail };
|
||||
const username = (near.username || lastUsername || "").trim();
|
||||
const email = (near.email || lastEmail || "").trim();
|
||||
if (pw) lastPassword = pw;
|
||||
if (username) lastUsername = username;
|
||||
if (email) lastEmail = email;
|
||||
return {
|
||||
pageUrl: location.href,
|
||||
title: buildTitle(),
|
||||
username,
|
||||
email,
|
||||
password: pw,
|
||||
};
|
||||
}
|
||||
|
||||
function removeHost() {
|
||||
const el = document.getElementById(HOST_ID);
|
||||
if (el) el.remove();
|
||||
}
|
||||
|
||||
function sendPending(draft) {
|
||||
if (!draft?.password) return;
|
||||
const now = Date.now();
|
||||
if (now < submitLockUntil) return;
|
||||
submitLockUntil = now + SUBMIT_LOCK_MS;
|
||||
|
||||
lastPassword = draft.password;
|
||||
if (draft.username) lastUsername = draft.username;
|
||||
if (draft.email) lastEmail = draft.email;
|
||||
|
||||
chrome.runtime.sendMessage(
|
||||
{
|
||||
type: "PENDING_SAVE",
|
||||
pageUrl: draft.pageUrl || location.href,
|
||||
title: draft.title || buildTitle(),
|
||||
username: draft.username || "",
|
||||
password: draft.password,
|
||||
email: draft.email || "",
|
||||
fromSubmit: true,
|
||||
},
|
||||
() => {
|
||||
// ignore response; SW may be waking
|
||||
void chrome.runtime.lastError;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} draft
|
||||
* @param {{ fromPending?: boolean }} [opts]
|
||||
*/
|
||||
async function showConfirm(draft, opts = {}) {
|
||||
removeHost();
|
||||
const host = document.createElement("div");
|
||||
host.id = HOST_ID;
|
||||
host.style.cssText =
|
||||
"all:initial;position:fixed;z-index:2147483646;right:16px;bottom:16px;font-family:system-ui,sans-serif;";
|
||||
const shadow = host.attachShadow({ mode: "closed" });
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = `
|
||||
.card {
|
||||
width: 320px; max-width: 92vw;
|
||||
background: #1a1d23; color: #e8eaed;
|
||||
border: 1px solid #3a3f4b; border-radius: 12px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,.45);
|
||||
padding: 14px 14px 12px; font-size: 13px; line-height: 1.4;
|
||||
}
|
||||
h3 { margin: 0 0 10px; font-size: 14px; font-weight: 600; }
|
||||
label { display:block; margin: 8px 0 2px; color:#9aa0a6; font-size:11px; }
|
||||
input[type=text], input[type=password] {
|
||||
width: 100%; box-sizing: border-box;
|
||||
background:#0f1115; color:#e8eaed; border:1px solid #3a3f4b;
|
||||
border-radius:6px; padding:6px 8px; font-size:13px;
|
||||
}
|
||||
.pw-wrap {
|
||||
display:flex; align-items:stretch; gap:0;
|
||||
border:1px solid #3a3f4b; border-radius:6px; overflow:hidden;
|
||||
background:#0f1115;
|
||||
}
|
||||
.pw-wrap input {
|
||||
flex:1; min-width:0; border:0; border-radius:0; padding:6px 8px;
|
||||
}
|
||||
.pw-wrap input:focus { outline:none; }
|
||||
.btn-pw-toggle {
|
||||
flex:0 0 auto; min-width:52px; margin:0; border:0; border-left:1px solid #3a3f4b;
|
||||
border-radius:0; background:#2a2f3a; color:#e8eaed; padding:0 10px;
|
||||
font-size:11px; cursor:pointer; line-height:1;
|
||||
}
|
||||
.btn-pw-toggle:hover { background:#343b49; }
|
||||
.btn-pw-toggle:focus-visible { outline:2px solid #3b82f6; outline-offset:-2px; }
|
||||
.row { display:flex; gap:8px; align-items:center; margin-top:8px; font-size:12px; }
|
||||
.row input { width:auto; }
|
||||
.actions { display:flex; gap:8px; justify-content:flex-end; margin-top:12px; }
|
||||
button {
|
||||
border:0; border-radius:8px; padding:7px 12px; cursor:pointer; font-size:12px;
|
||||
}
|
||||
.btn-cancel { background:#2a2f3a; color:#e8eaed; }
|
||||
.btn-save { background:#3b82f6; color:#fff; font-weight:600; }
|
||||
.btn-save:disabled { opacity:.5; cursor:wait; }
|
||||
.msg { margin-top:8px; font-size:11px; color:#9aa0a6; min-height:14px; }
|
||||
.msg.err { color:#f87171; }
|
||||
.msg.ok { color:#4ade80; }
|
||||
select {
|
||||
width:100%; box-sizing:border-box; background:#0f1115; color:#e8eaed;
|
||||
border:1px solid #3a3f4b; border-radius:6px; padding:6px 8px;
|
||||
}
|
||||
.muted { color:#9aa0a6; font-size:11px; margin-top:4px; }
|
||||
.banner {
|
||||
margin: 0 0 8px; padding: 6px 8px; border-radius: 6px;
|
||||
background: #243044; color: #93c5fd; font-size: 11px;
|
||||
}
|
||||
.folder-new {
|
||||
display:none; margin-top:6px;
|
||||
}
|
||||
.folder-new.is-open { display:block; }
|
||||
.folder-hint { margin-top:4px; }
|
||||
`;
|
||||
|
||||
const fromPending = Boolean(draft.fromPending || opts.fromPending);
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
card.innerHTML = `
|
||||
<h3>保存到 MNote 密码箱</h3>
|
||||
${
|
||||
fromPending
|
||||
? `<div class="banner">已捕获登录提交${draft.pendingReason === "nav_complete" ? "(登录后页面)" : ""} · 请确认后保存</div>`
|
||||
: ""
|
||||
}
|
||||
<label>标题</label>
|
||||
<input type="text" id="title" />
|
||||
<label>URL</label>
|
||||
<input type="text" id="url" />
|
||||
<label>用户名</label>
|
||||
<input type="text" id="username" autocomplete="off" />
|
||||
<label>密码</label>
|
||||
<div class="pw-wrap">
|
||||
<input type="password" id="password" autocomplete="off" />
|
||||
<button type="button" class="btn-pw-toggle" id="togglePw" aria-label="显示密码" title="显示/隐藏密码">显示</button>
|
||||
</div>
|
||||
<label>分组</label>
|
||||
<select id="folder">
|
||||
<option value="">(无分组)</option>
|
||||
<option value="__new__">+ 新建分组…</option>
|
||||
</select>
|
||||
<div class="folder-new" id="folderNewWrap">
|
||||
<label>新分组名(可用 / 分层,如 工作/客户)</label>
|
||||
<input type="text" id="folderNew" placeholder="例如:个人 / 工作 / 客户A" autocomplete="off" />
|
||||
</div>
|
||||
<div class="muted folder-hint" id="folderHint">读取已有分组…</div>
|
||||
<label>匹配已有条目</label>
|
||||
<select id="match">
|
||||
<option value="create">新建条目</option>
|
||||
</select>
|
||||
<div class="row">
|
||||
<input type="checkbox" id="saveSession" checked />
|
||||
<label for="saveSession" style="margin:0;color:#e8eaed">同时保存登录态(Cookie)</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="checkbox" id="shareAi" />
|
||||
<label for="shareAi" style="margin:0;color:#e8eaed">同步到 AI 密码本</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="checkbox" id="neverAsk" />
|
||||
<label for="neverAsk" style="margin:0;color:#e8eaed">此网站不再询问</label>
|
||||
</div>
|
||||
<div class="muted" id="cookieHint"></div>
|
||||
<div class="msg" id="msg"></div>
|
||||
<div class="actions">
|
||||
<button type="button" class="btn-cancel" id="cancel">取消</button>
|
||||
<button type="button" class="btn-save" id="save">保存</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
shadow.appendChild(style);
|
||||
shadow.appendChild(card);
|
||||
document.documentElement.appendChild(host);
|
||||
|
||||
const $ = (id) => shadow.getElementById(id);
|
||||
$("title").value = draft.title || buildTitle();
|
||||
$("url").value = draft.pageUrl || location.href;
|
||||
$("username").value = draft.username || lastUsername || "";
|
||||
$("password").value = draft.password || lastPassword || "";
|
||||
|
||||
function syncFolderNewVisibility() {
|
||||
const isNew = $("folder").value === "__new__";
|
||||
$("folderNewWrap").classList.toggle("is-open", isNew);
|
||||
if (isNew) {
|
||||
try {
|
||||
$("folderNew").focus();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveFolderPath() {
|
||||
const sel = $("folder").value;
|
||||
if (sel === "__new__") {
|
||||
return String($("folderNew").value || "").trim();
|
||||
}
|
||||
return String(sel || "").trim();
|
||||
}
|
||||
|
||||
$("folder").onchange = syncFolderNewVisibility;
|
||||
|
||||
$("togglePw").onclick = () => {
|
||||
const input = $("password");
|
||||
const btn = $("togglePw");
|
||||
const show = input.type === "password";
|
||||
input.type = show ? "text" : "password";
|
||||
btn.textContent = show ? "隐藏" : "显示";
|
||||
btn.setAttribute("aria-label", show ? "隐藏密码" : "显示密码");
|
||||
};
|
||||
|
||||
// status + match
|
||||
chrome.runtime.sendMessage({ type: "GET_STATUS" }, (st) => {
|
||||
void chrome.runtime.lastError;
|
||||
const r = st?.result || st;
|
||||
if (!st?.ok || !r?.connected) {
|
||||
$("msg").className = "msg err";
|
||||
$("msg").textContent = "未连接:请打开扩展 Options 登录 MNote";
|
||||
$("save").disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
const matchUrl = $("url").value.trim() || location.href;
|
||||
chrome.runtime.sendMessage(
|
||||
{ type: "MATCH_URL", pageUrl: matchUrl },
|
||||
(res) => {
|
||||
void chrome.runtime.lastError;
|
||||
const items = res?.result?.items || [];
|
||||
const sel = $("match");
|
||||
for (const it of items) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = it.id;
|
||||
opt.textContent = `追加到 · ${it.title || it.id}`;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 已有分组 + 上次选用
|
||||
chrome.runtime.sendMessage({ type: "LIST_FOLDERS" }, (res) => {
|
||||
void chrome.runtime.lastError;
|
||||
const r = res?.result || res || {};
|
||||
const folders = Array.isArray(r.folders) ? r.folders : [];
|
||||
const last = String(r.lastFolderPath || "").trim();
|
||||
const sel = $("folder");
|
||||
// 保留「无分组」「新建」两端选项,中间插入已有路径
|
||||
const newOpt = sel.querySelector('option[value="__new__"]');
|
||||
for (const fp of folders) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = fp;
|
||||
opt.textContent = fp;
|
||||
sel.insertBefore(opt, newOpt);
|
||||
}
|
||||
if (last) {
|
||||
// 上次路径若不在列表中,也加一条再选中
|
||||
let found = false;
|
||||
for (const o of sel.options) {
|
||||
if (o.value === last) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = last;
|
||||
opt.textContent = last;
|
||||
sel.insertBefore(opt, newOpt);
|
||||
}
|
||||
sel.value = last;
|
||||
} else {
|
||||
sel.value = "";
|
||||
}
|
||||
syncFolderNewVisibility();
|
||||
$("folderHint").textContent = folders.length
|
||||
? `共 ${folders.length} 个已有分组;可选已有或新建`
|
||||
: "暂无已有分组,可选择「新建分组」并填写名称";
|
||||
});
|
||||
|
||||
// Request broad host access so cookies.getAll can see session cookies.
|
||||
// Best-effort here; the critical grant happens on Save click (user gesture).
|
||||
chrome.runtime.sendMessage(
|
||||
{ type: "ENSURE_PAGE_HOST", pageUrl: matchUrl, broad: true },
|
||||
() => {
|
||||
void chrome.runtime.lastError;
|
||||
chrome.runtime.sendMessage(
|
||||
{ type: "CAPTURE_COOKIE_COUNT", pageUrl: matchUrl },
|
||||
(res) => {
|
||||
void chrome.runtime.lastError;
|
||||
const n = res?.result?.count ?? 0;
|
||||
const err = res?.result?.error;
|
||||
if (n > 0) {
|
||||
$("cookieHint").textContent = `将捕获约 ${n} 条 Cookie`;
|
||||
} else if (err) {
|
||||
$("cookieHint").textContent =
|
||||
`无法读取 Cookie:${err}。点「保存」时会再次申请网站权限;也可在扩展详情设为「所有网站」。`;
|
||||
} else {
|
||||
$("cookieHint").textContent =
|
||||
"当前可能尚无会话 Cookie。点「保存」时会申请 Cookie 权限并捕获;若仍为 0,请检查扩展网站访问权限。";
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
$("cancel").onclick = () => {
|
||||
const forever = Boolean($("neverAsk")?.checked);
|
||||
chrome.runtime.sendMessage(
|
||||
{
|
||||
type: "DISMISS_SAVE",
|
||||
pageUrl: $("url").value.trim() || location.href,
|
||||
username: $("username").value.trim(),
|
||||
forever,
|
||||
},
|
||||
() => {
|
||||
void chrome.runtime.lastError;
|
||||
removeHost();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$("save").onclick = async () => {
|
||||
$("save").disabled = true;
|
||||
$("msg").className = "msg";
|
||||
$("msg").textContent = "保存中…";
|
||||
const matchVal = $("match").value;
|
||||
const folderPath = resolveFolderPath();
|
||||
if ($("folder").value === "__new__" && !folderPath) {
|
||||
$("msg").className = "msg err";
|
||||
$("msg").textContent = "请填写新分组名称";
|
||||
$("save").disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Critical: request cookie host permission in this click gesture.
|
||||
// Without it, chrome.cookies.getAll returns [] and login session cannot be saved.
|
||||
let hostPermissionGranted = false;
|
||||
try {
|
||||
hostPermissionGranted = await new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage(
|
||||
{
|
||||
type: "ENSURE_PAGE_HOST",
|
||||
pageUrl: $("url").value.trim() || location.href,
|
||||
broad: true,
|
||||
},
|
||||
(res) => {
|
||||
void chrome.runtime.lastError;
|
||||
resolve(Boolean(res?.ok || res?.result?.ok || res?.result?.hasHost));
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
hostPermissionGranted = false;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
pageUrl: $("url").value.trim() || location.href,
|
||||
title: $("title").value.trim(),
|
||||
username: $("username").value.trim(),
|
||||
password: $("password").value,
|
||||
email: draft.email || lastEmail || "",
|
||||
folderPath,
|
||||
saveSession: $("saveSession").checked,
|
||||
shareToAi: $("shareAi").checked,
|
||||
mode: matchVal === "create" ? "create" : "append",
|
||||
existingId: matchVal === "create" ? null : matchVal,
|
||||
recaptureSession: true,
|
||||
hostPermissionGranted,
|
||||
};
|
||||
chrome.runtime.sendMessage(
|
||||
{ type: "SAVE_CREDENTIAL", payload },
|
||||
(res) => {
|
||||
void chrome.runtime.lastError;
|
||||
if (!res?.ok) {
|
||||
$("msg").className = "msg err";
|
||||
$("msg").textContent = res?.error || "保存失败";
|
||||
$("save").disabled = false;
|
||||
return;
|
||||
}
|
||||
const r = res.result || {};
|
||||
let text = `已保存 ${r.credentialId || ""}`;
|
||||
if (folderPath && matchVal === "create") text += ` · 分组 ${folderPath}`;
|
||||
if (r.sessionError) text += ` · 登录态: ${r.sessionError}`;
|
||||
else if (payload.saveSession && r.sessionSaved) text += " · 含登录态";
|
||||
else if (payload.saveSession) text += " · 登录态待二次捕获";
|
||||
if (r.sessionRecaptureScheduled && !r.sessionSaved) {
|
||||
text += " · 将二次捕获 Cookie";
|
||||
}
|
||||
$("msg").className = "msg ok";
|
||||
$("msg").textContent = text;
|
||||
setTimeout(removeHost, r.sessionError ? 3200 : 1800);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual toolbar open: prefer pending draft, else live form / last known.
|
||||
*/
|
||||
function openFromManual() {
|
||||
chrome.runtime.sendMessage({ type: "GET_PENDING" }, (res) => {
|
||||
void chrome.runtime.lastError;
|
||||
const pending = res?.result?.pending;
|
||||
if (pending?.password) {
|
||||
showConfirm(
|
||||
{
|
||||
pageUrl: pending.pageUrl || location.href,
|
||||
title: pending.title || buildTitle(),
|
||||
username: pending.username || "",
|
||||
email: pending.email || "",
|
||||
password: pending.password,
|
||||
fromPending: true,
|
||||
pendingReason: "manual",
|
||||
},
|
||||
{ fromPending: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
const draft = collectDraftFromDom();
|
||||
showConfirm(draft);
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const form = e.target;
|
||||
if (!(form instanceof HTMLFormElement)) return;
|
||||
const pw = findPasswordIn(form) || findPasswordIn(document);
|
||||
if (!pw || !pw.value) return;
|
||||
const draft = collectDraftFromDom(pw);
|
||||
if (!draft.password) return;
|
||||
// Do not block navigation; stash in background immediately.
|
||||
sendPending(draft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture click on submit-like controls (including formless login UIs).
|
||||
*/
|
||||
function onPointerDownCapture(e) {
|
||||
const t = e.target;
|
||||
if (!(t instanceof Element)) return;
|
||||
const btn = t.closest(
|
||||
'button[type="submit"], input[type="submit"], button:not([type]), [type="submit"], [role="button"]'
|
||||
);
|
||||
if (!btn) return;
|
||||
// Avoid random buttons far from any password field.
|
||||
const form = btn.closest("form");
|
||||
const pw =
|
||||
(form && findPasswordIn(form)) ||
|
||||
findPasswordIn(btn.closest("div,section,main,body") || document);
|
||||
if (!pw || !pw.value || pw.value.length < 1) return;
|
||||
// Heuristic: prefer buttons that look like login/submit.
|
||||
const label = (
|
||||
btn.getAttribute("aria-label") ||
|
||||
btn.value ||
|
||||
btn.textContent ||
|
||||
""
|
||||
)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const looksSubmit =
|
||||
btn.matches('button[type="submit"], input[type="submit"], [type="submit"]') ||
|
||||
/log\s*in|sign\s*in|sign\s*up|register|提交|登录|注册|continue|next|进入/.test(
|
||||
label
|
||||
) ||
|
||||
Boolean(form);
|
||||
if (!looksSubmit) return;
|
||||
const draft = collectDraftFromDom(pw);
|
||||
if (draft.password) sendPending(draft);
|
||||
}
|
||||
|
||||
// Remember password as user types (still only in this document).
|
||||
function onPasswordInput(e) {
|
||||
const t = e.target;
|
||||
if (!(t instanceof HTMLInputElement) || t.type !== "password") return;
|
||||
if (t.value) lastPassword = t.value;
|
||||
const near = findUsernameNear(t);
|
||||
if (near.username) lastUsername = near.username;
|
||||
if (near.email) lastEmail = near.email;
|
||||
}
|
||||
|
||||
document.addEventListener("submit", onSubmit, true);
|
||||
document.addEventListener("pointerdown", onPointerDownCapture, true);
|
||||
document.addEventListener("input", onPasswordInput, true);
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, _s, sendResponse) => {
|
||||
if (msg?.type === "OPEN_SAVE_UI_WITH_DRAFT") {
|
||||
const d = msg.draft || {};
|
||||
showConfirm(
|
||||
{
|
||||
pageUrl: d.pageUrl || location.href,
|
||||
title: d.title || buildTitle(),
|
||||
username: d.username || lastUsername || "",
|
||||
email: d.email || lastEmail || "",
|
||||
password: d.password || lastPassword || "",
|
||||
fromPending: true,
|
||||
pendingReason: d.pendingReason || "pending",
|
||||
},
|
||||
{ fromPending: true }
|
||||
);
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
if (msg?.type === "OPEN_SAVE_UI") {
|
||||
openFromManual();
|
||||
sendResponse({ ok: true });
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// After full navigation inject: if background still has pending for this tab
|
||||
// and did not manage to message us yet, pull once (belt-and-suspenders).
|
||||
function pullPendingOnLoad() {
|
||||
// Let tabs.onUpdated / OPEN_SAVE_UI_WITH_DRAFT win the race first.
|
||||
setTimeout(() => {
|
||||
if (document.getElementById(HOST_ID)) return;
|
||||
chrome.runtime.sendMessage({ type: "GET_PENDING" }, (res) => {
|
||||
void chrome.runtime.lastError;
|
||||
const p = res?.result?.pending;
|
||||
if (!p?.password || p.prompted) return;
|
||||
if (document.getElementById(HOST_ID)) return;
|
||||
// Dedupe before content-side pull opens UI (same site already saved).
|
||||
chrome.runtime.sendMessage(
|
||||
{
|
||||
type: "CHECK_DEDUPE",
|
||||
pageUrl: p.pageUrl || location.href,
|
||||
username: p.username || "",
|
||||
password: p.password || "",
|
||||
},
|
||||
(dedupeRes) => {
|
||||
void chrome.runtime.lastError;
|
||||
const d = dedupeRes?.result || dedupeRes;
|
||||
if (d?.suppress) {
|
||||
chrome.runtime.sendMessage({ type: "CLEAR_PENDING" }, () => {
|
||||
void chrome.runtime.lastError;
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (document.getElementById(HOST_ID)) return;
|
||||
showConfirm(
|
||||
{
|
||||
pageUrl: p.pageUrl || location.href,
|
||||
title: p.title || buildTitle(),
|
||||
username: p.username || "",
|
||||
email: p.email || "",
|
||||
password: p.password,
|
||||
fromPending: true,
|
||||
pendingReason: "content_pull",
|
||||
},
|
||||
{ fromPending: true }
|
||||
);
|
||||
chrome.runtime.sendMessage(
|
||||
{
|
||||
type: "MARK_PENDING_PROMPTED",
|
||||
promptedUrl: location.href,
|
||||
},
|
||||
() => {
|
||||
void chrome.runtime.lastError;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}, 700);
|
||||
}
|
||||
|
||||
if (document.readyState === "complete") {
|
||||
pullPendingOnLoad();
|
||||
} else {
|
||||
window.addEventListener("load", pullPendingOnLoad, { once: true });
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user