Jag Pass
Browser plugin
This is the fill and save plugin that runs in Brave, Chrome, Edge, or Firefox. It is not the Jag Pass desktop app. The desktop app stays a signed installer on the download page. Canonical plugin source: https://jagpass.com/extension
The plugin fills login fields on the current tab. Click a username or password field to see the Jag Pass icon and a Log in as list. Type to filter by name. After you submit a login, Jag Pass asks before saving or updating it. Nothing enters the vault until you choose Save. It also handles passkeys. The desktop receives the tab URL only for local matching and stores only the origin when you save. It never fetches the page or uploads your password. Unlock happens in Jag Pass on your machine.
Published plugin: 1.0.0 (453) jag-pass-extension.zip
SHA-256 b7d07d132f8463de15213597845e827494624324305b4fce433cdf139aa0c966
Load it yourself
- Install Jag Pass. Settings, Install browser host. That copies these files to a folder and opens it.
- Or unzip the plugin zip and keep the chromium folder.
- Brave, Chrome, or Edge: open the extensions page, turn on Developer mode, Load unpacked, pick that folder.
- Pin Jag Pass. Click a username or password field, or the toolbar icon, or Ctrl+Shift+U.
- Firefox: about:debugging, This Firefox, Load Temporary Add-on, firefox/manifest.json. Firefox drops that load when it restarts.
Do not move the loaded folder after you load it. Jag Pass cannot click Load unpacked for you. The browser will not let an app install a plugin.
Source
manifest.json
{
"manifest_version": 3,
"name": "Jag Pass",
"version": "1.0.0",
"description": "Fill logins and offer to save submitted credentials through the local Jag Pass desktop app.",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqiecMyS/KCwMPEjMEn7e4hBthqEGAYPAGwKq0/XO8oVfp46EjHurlfEl4eYb7g9PqPpU8s8OryLhwHB/0a7vrJQzmwCA3xvlV80vCWrM4adyME2Y6i1jDpwMsv6AIqIQv9+2M/u3Re6s2E+E1PhWBhcSGzFmrJu0rZoRDtwPJXCWkKFMQdy1yRTlbxRV5TT4tDLgwmj3V7ZBv87jch1pYoCIB9VYMMHcCzxXJtttAzTIi+qNBgJ+QIoB/Gvj3Lyqo1GJIIp3QUtyuJ6x4chxMsDG7CenWCw7RlGka1if+7ZTtgfoOYSRcxjaJFNW6EJoDpt/HH0nziArEUSDO46hwwIDAQAB",
"icons": {
"32": "icons/32.png",
"128": "icons/128.png"
},
"action": {
"default_title": "Jag Pass",
"default_popup": "popup.html",
"default_icon": {
"32": "icons/32.png",
"128": "icons/128.png"
}
},
"background": {
"service_worker": "background.js"
},
"permissions": ["nativeMessaging", "storage", "alarms", "webAuthenticationProxy"],
"host_permissions": ["http://*/*", "https://*/*"],
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*"],
"js": ["page.js"],
"run_at": "document_start",
"all_frames": true,
"world": "MAIN"
},
{
"matches": ["http://*/*", "https://*/*"],
"js": ["content.js"],
"run_at": "document_start",
"all_frames": true
}
],
"web_accessible_resources": [
{
"resources": ["icons/32.png"],
"matches": ["http://*/*", "https://*/*"]
}
],
"commands": {
"fill-tab": {
"suggested_key": { "default": "Ctrl+Shift+U" },
"description": "Fill the current tab"
}
}
}
background.js
const HOST = "com.jagjourney.jagpass";
const LOGIN_TTL_MS = 2 * 60 * 1000;
const RAPID_CAPTURE_MS = 2000;
const loginMemory = new Map();
const usernameMemory = new Map();
const frameMemory = new Map();
const loginQueues = new Map();
function currentTab() {
return new Promise((resolve, reject) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (!tabs || !tabs[0]) {
reject(new Error("no active tab"));
return;
}
resolve(tabs[0]);
});
});
}
function browserId() {
const ua = navigator.userAgent || "";
if (/Firefox\//i.test(ua)) return "firefox";
if (/Edg\//i.test(ua)) return "edge";
if (/Brave/i.test(ua) || navigator.brave) return "brave";
return "chrome";
}
function nativeOp(op, url, payload) {
return new Promise((resolve) => {
chrome.runtime.sendNativeMessage(
HOST,
{ op, url: url || "", browser: browserId(), payload: payload || {} },
(reply) => {
if (chrome.runtime.lastError) {
resolve({
ok: false,
running: false,
locked: false,
error: chrome.runtime.lastError.message,
matches: [],
});
return;
}
resolve(reply || { ok: false, matches: [], error: "empty host reply" });
},
);
});
}
function nativeFind(url) {
return nativeOp("find", url, {});
}
function urlFromSender(sender) {
if (sender && sender.url && /^https?:/i.test(sender.url)) return sender.url;
if (sender && sender.tab && sender.tab.url && /^https?:/i.test(sender.tab.url)) {
return sender.tab.url;
}
return "";
}
function loginKey(tabId) {
return `login-candidate-${tabId}`;
}
function usernameKey(tabId) {
return `login-username-${tabId}`;
}
function candidateAlarm(tabId) {
return `jagpass-login-candidate:${tabId}`;
}
function usernameAlarm(tabId) {
return `jagpass-login-username:${tabId}`;
}
function sessionArea() {
return chrome.storage && chrome.storage.session ? chrome.storage.session : null;
}
function setSession(key, value) {
const area = sessionArea();
if (!area) return Promise.resolve();
return new Promise((resolve) => area.set({ [key]: value }, resolve));
}
function getSession(key) {
const area = sessionArea();
if (!area) return Promise.resolve(null);
return new Promise((resolve) => {
area.get(key, (stored) => resolve((stored && stored[key]) || null));
});
}
function removeSession(key) {
const area = sessionArea();
if (!area) return Promise.resolve();
return new Promise((resolve) => area.remove(key, resolve));
}
function scheduleExpiry(name, when) {
if (chrome.alarms && chrome.alarms.create) chrome.alarms.create(name, { when });
}
function clearExpiry(name) {
if (chrome.alarms && chrome.alarms.clear) chrome.alarms.clear(name, () => {});
}
function storeCandidate(tabId, candidate) {
loginMemory.set(tabId, candidate);
scheduleExpiry(candidateAlarm(tabId), candidate.createdAt + LOGIN_TTL_MS);
return setSession(loginKey(tabId), candidate);
}
async function loadCandidate(tabId) {
const stored = await getSession(loginKey(tabId));
return stored || loginMemory.get(tabId) || null;
}
function removeCandidate(tabId) {
loginMemory.delete(tabId);
clearExpiry(candidateAlarm(tabId));
return removeSession(loginKey(tabId));
}
function storeUsername(tabId, context) {
usernameMemory.set(tabId, context);
scheduleExpiry(usernameAlarm(tabId), context.createdAt + LOGIN_TTL_MS);
return setSession(usernameKey(tabId), context);
}
async function loadUsername(tabId) {
const stored = await getSession(usernameKey(tabId));
return stored || usernameMemory.get(tabId) || null;
}
function removeUsername(tabId) {
usernameMemory.delete(tabId);
clearExpiry(usernameAlarm(tabId));
return removeSession(usernameKey(tabId));
}
function registrableHost(rawUrl) {
let host = "";
try {
host = new URL(rawUrl).hostname.toLowerCase().replace(/^\.+|\.+$/g, "");
} catch {
return "";
}
if (!host || /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || !host.includes(".")) return host;
const labels = host.split(".");
const last2 = labels.slice(-2).join(".");
const multi = new Set([
"co.uk", "org.uk", "ac.uk", "gov.uk", "co.jp", "ne.jp", "or.jp", "com.au",
"net.au", "org.au", "co.nz", "com.br", "com.mx", "co.in", "com.cn", "com.hk",
"co.za", "com.sg", "co.kr", "com.tw",
]);
return multi.has(last2) && labels.length >= 3 ? labels.slice(-3).join(".") : last2;
}
function foldUsername(value) {
return String(value || "").replace(/[A-Z]/g, (char) => char.toLowerCase());
}
function publicChoice(match) {
return {
id: match.credential_id || match.id || "",
credential_id: match.credential_id || match.id || "",
title: String(match.title || "").slice(0, 200),
username: String(match.username || "").slice(0, 320),
site_id: match.site_id || "",
site_label: String(match.site_label || "").slice(0, 200),
};
}
function publicSite(site) {
if (!site || typeof site !== "object") return null;
return {
id: site.id || "",
key: String(site.key || "").slice(0, 320),
label: String(site.label || "").slice(0, 200),
primary_site: String(site.primary_site || "").slice(0, 500),
aliases: Array.isArray(site.aliases) ? site.aliases.map((v) => String(v).slice(0, 320)) : [],
credential_count: Number(site.credential_count || 0),
};
}
function candidateMeta(candidate) {
const out = {
id: candidate.id,
title: candidate.title,
username: candidate.username,
action: ["updated", "choose"].includes(candidate.action) ? candidate.action : "created",
};
if (candidate.action === "choose") out.choices = (candidate.choices || []).map(publicChoice);
if (candidate.site) out.site = candidate.site;
return out;
}
function validCandidate(candidate) {
return Boolean(candidate && candidate.password && Date.now() - candidate.createdAt <= LOGIN_TTL_MS);
}
function validUsername(context) {
return Boolean(context && context.username && Date.now() - context.createdAt <= LOGIN_TTL_MS);
}
function matchingExact(candidate, matches) {
const username = foldUsername(candidate.username);
return (Array.isArray(matches) ? matches : []).filter((match) => {
if (!match || match.exact !== true) return false;
return !username || foldUsername(match.username) === username;
});
}
function classifyCapturedLogin(candidate, matches) {
const username = foldUsername(candidate.username);
const all = Array.isArray(matches) ? matches.filter(Boolean) : [];
const sameUsername = username
? all.filter((match) => foldUsername(match.username) === username)
: all.filter((match) => !foldUsername(match.username));
if (sameUsername.some((match) => match.password === candidate.password)) return "unchanged";
const exact = matchingExact(candidate, all);
if (!username && exact.length > 0) return "choose";
if (exact.length > 1) return "choose";
return exact.length === 1 ? "updated" : "created";
}
async function settleCandidate(tabId, candidate) {
const res = await nativeFind(candidate.url);
const action = res && res.ok && !res.locked
? classifyCapturedLogin(candidate, res.matches)
: "checking";
const current = await loadCandidate(tabId);
if (!validCandidate(current) || current.id !== candidate.id) return null;
if (action === "unchanged") {
await removeCandidate(tabId);
return null;
}
current.action = action;
current.site = publicSite(res && res.site);
current.choices = [];
current.credentialId = "";
if (action === "choose") {
current.choices = matchingExact(current, res.matches).map(publicChoice);
} else if (action === "updated") {
const exact = matchingExact(current, res.matches);
current.credentialId = exact[0] ? exact[0].credential_id || exact[0].id || "" : "";
}
await storeCandidate(tabId, current);
return current;
}
function notifyTopFrame(tabId, candidate, sourceFrameId) {
if (sourceFrameId == null || sourceFrameId === 0 || !chrome.tabs || !chrome.tabs.sendMessage) return;
chrome.tabs.sendMessage(
tabId,
{ type: "show-login-candidate", candidate: candidateMeta(candidate) },
{ frameId: 0 },
() => void chrome.runtime.lastError,
);
}
function queueLogin(tabId, work) {
const before = loginQueues.get(tabId) || Promise.resolve();
const next = before.catch(() => {}).then(work);
loginQueues.set(tabId, next);
next.finally(() => {
if (loginQueues.get(tabId) === next) loginQueues.delete(tabId);
});
return next;
}
async function handleLoginCandidate(tabId, page, payload, sourceFrameId) {
const password = typeof payload.password === "string" ? payload.password : "";
let username = String(payload.username || "").replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 320);
if (!username) {
const remembered = await loadUsername(tabId);
if (validUsername(remembered) && registrableHost(remembered.url) === registrableHost(page)) {
username = remembered.username;
}
}
const previous = await loadCandidate(tabId);
if (
validCandidate(previous)
&& Date.now() - previous.createdAt <= RAPID_CAPTURE_MS
&& registrableHost(previous.url) === registrableHost(page)
&& foldUsername(previous.username) === foldUsername(username)
&& previous.password === password
) {
const settled = previous.action === "checking" ? await settleCandidate(tabId, previous) : previous;
if (settled) notifyTopFrame(tabId, settled, sourceFrameId);
return settled;
}
const candidate = {
id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
url: page,
title: String(payload.title || "").replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200),
username,
password,
action: "checking",
choices: [],
credentialId: "",
site: null,
createdAt: Date.now(),
};
await storeCandidate(tabId, candidate);
const settled = await settleCandidate(tabId, candidate);
if (settled) notifyTopFrame(tabId, settled, sourceFrameId);
return settled;
}
async function activeTarget() {
const tab = await currentTab();
const remembered = tab.id == null ? null : frameMemory.get(tab.id);
if (remembered && Date.now() - remembered.at <= LOGIN_TTL_MS && /^https?:/i.test(remembered.url)) {
return { tab, frameId: remembered.frameId, url: remembered.url };
}
return { tab, frameId: 0, url: tab.url || "" };
}
chrome.runtime.onMessage.addListener((msg, sender, send) => {
if (!msg || !msg.type) return;
if (msg.type === "frame-focus") {
const tabId = sender && sender.tab && sender.tab.id;
const page = urlFromSender(sender);
if (tabId != null && page) {
frameMemory.set(tabId, { frameId: sender.frameId || 0, url: page, at: Date.now() });
}
send({ ok: true });
return;
}
if (msg.type === "find") {
const page = urlFromSender(sender);
const work = page
? Promise.resolve({ url: page, frameId: sender.frameId || 0 })
: activeTarget().then((target) => ({ url: target.url, frameId: target.frameId }));
work
.then(async (target) => ({ target, res: await nativeFind(target.url) }))
.then(({ target, res }) => send(Object.assign({}, res, { target_frame_id: target.frameId })))
.catch((err) => send({ ok: false, error: String(err), matches: [] }));
return true;
}
if (["open-app", "open-site-account", "open-new-login"].includes(msg.type)) {
const page = urlFromSender(sender);
const work = page ? Promise.resolve(page) : activeTarget().then((target) => target.url);
work
.then((url) => nativeOp(msg.type, url, msg.payload || {}))
.then((res) => send(res))
.catch((err) => send({ ok: false, error: String(err) }));
return true;
}
if (msg.type === "fill-active-frame") {
activeTarget()
.then((target) => {
const frameId = Number.isInteger(msg.payload && msg.payload.frame_id)
? msg.payload.frame_id
: target.frameId;
chrome.tabs.sendMessage(
target.tab.id,
{ type: "fill", match: msg.payload && msg.payload.match },
{ frameId },
() => {
const error = chrome.runtime.lastError;
send(error ? { ok: false, error: error.message } : { ok: true });
},
);
})
.catch((err) => send({ ok: false, error: String(err) }));
return true;
}
if (msg.type === "remember-login-username") {
const tabId = sender && sender.tab && sender.tab.id;
const page = urlFromSender(sender);
const username = String((msg.payload && msg.payload.username) || "")
.replace(/[\u0000-\u001f\u007f]/g, "")
.slice(0, 320);
if (tabId == null || !page || !username) {
send({ ok: false });
return;
}
storeUsername(tabId, { username, url: page, createdAt: Date.now() }).then(() => send({ ok: true }));
return true;
}
if (msg.type === "login-candidate") {
const tabId = sender && sender.tab && sender.tab.id;
const page = urlFromSender(sender);
const payload = msg.payload || {};
const password = typeof payload.password === "string" ? payload.password : "";
if (tabId == null || !page || !password || password.length > 4096) {
send({ ok: false, error: "Login was not captured." });
return;
}
queueLogin(tabId, () => handleLoginCandidate(tabId, page, payload, sender.frameId || 0))
.then((settled) => send({ ok: true, candidate: settled ? candidateMeta(settled) : null }))
.catch(() => send({ ok: false, error: "Login was not captured." }));
return true;
}
if (msg.type === "pending-login-candidate") {
const tabId = sender && sender.tab && sender.tab.id;
const page = urlFromSender(sender);
if (tabId == null || !page) {
send({ ok: true, candidate: null });
return;
}
loadCandidate(tabId).then(async (candidate) => {
if (!validCandidate(candidate)) {
await removeCandidate(tabId);
send({ ok: true, candidate: null });
return;
}
if (registrableHost(candidate.url) !== registrableHost(page)) {
await removeCandidate(tabId);
send({ ok: true, candidate: null });
return;
}
const settled = candidate.action === "checking" ? await settleCandidate(tabId, candidate) : candidate;
send({ ok: true, candidate: settled ? candidateMeta(settled) : null });
}).catch(() => send({ ok: false, error: "Login was not captured." }));
return true;
}
if (msg.type === "save-login-candidate") {
const tabId = sender && sender.tab && sender.tab.id;
if (tabId == null) {
send({ ok: false, error: "Login was not captured." });
return;
}
loadCandidate(tabId).then(async (loaded) => {
let candidate = loaded;
if (!validCandidate(candidate) || candidate.id !== (msg.payload && msg.payload.id)) {
await removeCandidate(tabId);
send({ ok: false, error: "This login has expired." });
return;
}
if (candidate.action === "checking") {
candidate = await settleCandidate(tabId, candidate);
if (!candidate) {
send({ ok: true, save: { action: "unchanged" } });
return;
}
}
const requestedId = String((msg.payload && msg.payload.credential_id) || "");
const selected = candidate.action === "choose"
? (candidate.choices || []).find((choice) => choice.credential_id === requestedId)
: null;
const forceNew = Boolean(msg.payload && msg.payload.force_new);
if (candidate.action === "choose" && !selected && !forceNew) {
send({ ok: false, selection_required: true, candidate: candidateMeta(candidate) });
return;
}
const suppliedUsername = String((msg.payload && msg.payload.username) || candidate.username)
.replace(/[\u0000-\u001f\u007f]/g, "")
.slice(0, 320);
const changedUsername = foldUsername(suppliedUsername) !== foldUsername(candidate.username);
const credentialId = selected
? selected.credential_id
: !changedUsername && candidate.action === "updated"
? candidate.credentialId
: "";
const title = String((msg.payload && msg.payload.title) || candidate.title)
.replace(/[\u0000-\u001f\u007f]/g, "")
.slice(0, 200);
const username = selected ? selected.username : suppliedUsername;
const savePayload = { title, username, password: candidate.password };
if (credentialId) savePayload.credential_id = credentialId;
if (forceNew || (candidate.action === "updated" && changedUsername)) savePayload.force_new = true;
const res = await nativeOp("save-login", candidate.url, savePayload);
if (res && res.ok && res.save) await removeCandidate(tabId);
send(res);
}).catch(() => send({ ok: false, error: "Could not save this login." }));
return true;
}
if (msg.type === "discard-login-candidate") {
const tabId = sender && sender.tab && sender.tab.id;
if (tabId == null) {
send({ ok: true });
return;
}
removeCandidate(tabId).then(() => send({ ok: true }));
return true;
}
if (msg.type === "passkey-create" || msg.type === "passkey-list" || msg.type === "passkey-assert") {
const page = urlFromSender(sender);
nativeOp(msg.type, page, msg.payload || {})
.then((res) => send(res))
.catch((err) => send({ ok: false, error: String(err) }));
return true;
}
});
if (chrome.alarms && chrome.alarms.onAlarm) {
chrome.alarms.onAlarm.addListener((alarm) => {
const candidate = /^jagpass-login-candidate:(\d+)$/.exec(alarm.name || "");
if (candidate) removeCandidate(Number(candidate[1]));
const username = /^jagpass-login-username:(\d+)$/.exec(alarm.name || "");
if (username) removeUsername(Number(username[1]));
});
}
if (chrome.tabs && chrome.tabs.onRemoved) {
chrome.tabs.onRemoved.addListener((tabId) => {
removeCandidate(tabId);
removeUsername(tabId);
frameMemory.delete(tabId);
loginQueues.delete(tabId);
});
}
if (chrome.tabs && chrome.tabs.onUpdated) {
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (!changeInfo.url || !/^https?:/i.test(changeInfo.url)) return;
frameMemory.set(tabId, { frameId: 0, url: changeInfo.url, at: Date.now() });
loadCandidate(tabId).then((candidate) => {
if (candidate && registrableHost(candidate.url) !== registrableHost(changeInfo.url)) {
removeCandidate(tabId);
}
});
loadUsername(tabId).then((context) => {
if (context && registrableHost(context.url) !== registrableHost(changeInfo.url)) {
removeUsername(tabId);
}
});
});
}
function credentialJson(data, kind) {
if (kind === "create") {
return {
type: "public-key",
id: data.id,
rawId: data.rawId || data.id,
authenticatorAttachment: "platform",
response: {
clientDataJSON: data.clientDataJSON,
attestationObject: data.attestationObject,
transports: ["internal", "hybrid"],
},
clientExtensionResults: {},
};
}
return {
type: "public-key",
id: data.id,
rawId: data.rawId || data.id,
authenticatorAttachment: "platform",
response: {
clientDataJSON: data.clientDataJSON,
authenticatorData: data.authenticatorData,
signature: data.signature,
userHandle: data.userHandle || null,
},
clientExtensionResults: {},
};
}
function askTabPasskey(op, options) {
return currentTab().then(
(tab) =>
new Promise((resolve) => {
if (tab.id == null) {
resolve({ error: { name: "NotAllowedError", message: "no tab" } });
return;
}
chrome.tabs.sendMessage(tab.id, { type: "passkey-proxy", op, options }, (res) => {
if (chrome.runtime.lastError) {
resolve({ error: { name: "NotAllowedError", message: chrome.runtime.lastError.message } });
return;
}
resolve(res || { error: { name: "NotAllowedError", message: "no reply" } });
});
}),
);
}
function attachPasskeyProxy() {
const api = chrome.webAuthenticationProxy;
if (!api || !api.attach) return;
api.attach().then(() => {}).catch(() => {});
if (api.onIsUvpaaRequest && !attachPasskeyProxy.bound) {
attachPasskeyProxy.bound = true;
api.onIsUvpaaRequest.addListener((req) => {
api.completeIsUvpaaRequest({ requestId: req.requestId, isUvpaa: true });
});
api.onCreateRequest.addListener((req) => {
let opts = {};
try {
opts = JSON.parse(req.requestDetailsJson || "{}");
} catch {
opts = {};
}
askTabPasskey("create", opts).then((res) => {
if (res && res.fallback) {
api.completeCreateRequest({
requestId: req.requestId,
error: { name: "NotAllowedError", message: "Use your other device" },
});
return;
}
if (res && res.result) {
api.completeCreateRequest({
requestId: req.requestId,
responseJson: JSON.stringify(credentialJson(res.result, "create")),
});
return;
}
api.completeCreateRequest({
requestId: req.requestId,
error: (res && res.error) || { name: "NotAllowedError", message: "cancelled" },
});
});
});
api.onGetRequest.addListener((req) => {
let opts = {};
try {
opts = JSON.parse(req.requestDetailsJson || "{}");
} catch {
opts = {};
}
askTabPasskey("get", opts).then((res) => {
if (res && res.fallback) {
api.completeGetRequest({
requestId: req.requestId,
error: { name: "NotAllowedError", message: "Use your other device" },
});
return;
}
if (res && res.result) {
api.completeGetRequest({
requestId: req.requestId,
responseJson: JSON.stringify(credentialJson(res.result, "get")),
});
return;
}
api.completeGetRequest({
requestId: req.requestId,
error: (res && res.error) || { name: "NotAllowedError", message: "cancelled" },
});
});
});
}
}
attachPasskeyProxy();
if (chrome.runtime.onStartup) chrome.runtime.onStartup.addListener(attachPasskeyProxy);
if (chrome.runtime.onInstalled) chrome.runtime.onInstalled.addListener(attachPasskeyProxy);
chrome.commands.onCommand.addListener((command) => {
if (command !== "fill-tab") return;
activeTarget().then((target) => {
nativeFind(target.url).then((res) => {
const matches = (res && res.matches) || [];
const match = matches.find((item) => item && item.exact) || matches[0];
if (match && target.tab.id != null) {
chrome.tabs.sendMessage(target.tab.id, { type: "fill", match }, { frameId: target.frameId });
}
});
});
});
content.js
function jagPassSkipSecretField(blob) {
return /search|query|captcha|otp|totp|one[- ]?time|2fa|mfa|recovery|backup.?code|cvv|cvc|card.?number|pin|ssn/.test(
String(blob || "").toLowerCase(),
);
}
function jagPassSelectPassword(fields) {
const candidates = (Array.isArray(fields) ? fields : []).filter(
(field) => field && field.value && !jagPassSkipSecretField(field.blob),
);
if (candidates.length === 0) return null;
const current = candidates.filter((field) => /current[-_ ]?password|old.?pass/.test(field.blob));
const confirmations = candidates.filter((field) => /confirm|repeat|retype/.test(field.blob));
const explicitNew = candidates.filter(
(field) => /new[-_ ]?password/.test(field.blob) && !confirmations.includes(field),
);
if (confirmations.length > 0) {
const primary = explicitNew[0] || candidates.find(
(field) => !current.includes(field) && !confirmations.includes(field),
);
if (!primary || confirmations.some((field) => field.value !== primary.value)) return null;
return primary;
}
if (explicitNew.length > 0) return explicitNew[0];
return current[0] || candidates[0];
}
function jagPassNextMenuIndex(key, current, count) {
if (count < 1) return -1;
if (key === "Home") return 0;
if (key === "End") return count - 1;
if (key === "ArrowDown") return current < 0 ? 0 : (current + 1) % count;
if (key === "ArrowUp") return current < 0 ? count - 1 : (current - 1 + count) % count;
return current;
}
function jagPassShouldCaptureSignal(signal) {
if (!signal || signal.isComposing) return false;
if (signal.kind === "submit") return signal.isTrusted === true || signal.recentTrustedActivation === true;
if (signal.kind === "keydown") {
return signal.isTrusted === true && signal.key === "Enter" && signal.hasPassword === true;
}
if (signal.kind !== "click") return false;
if (signal.isTrusted !== true) return false;
if (["submit", "image"].includes(String(signal.buttonType || "").toLowerCase())) return true;
return /log\s*in|sign\s*in|sign\s*up|register|create\s*account|submit|continue|next|change\s*password|update\s*password|reset\s*password/i.test(
String(signal.words || ""),
);
}
function jagPassRunCaptureSignal(signal, capture) {
if (!jagPassShouldCaptureSignal(signal)) return false;
capture();
return true;
}
function jagPassWalkInputs(rootNode, out, onShadow) {
if (!rootNode) return out;
const inputs = rootNode.querySelectorAll ? rootNode.querySelectorAll("input") : [];
for (const input of inputs) out.push(input);
const elements = rootNode.querySelectorAll ? rootNode.querySelectorAll("*") : [];
for (const element of elements) {
if (!element.shadowRoot) continue;
if (onShadow) onShadow(element.shadowRoot);
jagPassWalkInputs(element.shadowRoot, out, onShadow);
}
return out;
}
function jagPassFindPasswordScope(element, hasPassword, body, documentElement) {
if (!element || typeof hasPassword !== "function") return null;
if (element.form) return hasPassword(element.form) ? element.form : null;
const owner = typeof element.getRootNode === "function" ? element.getRootNode() : null;
if (owner && owner.host) return hasPassword(owner) ? owner : null;
let current = element.parentElement || null;
while (current && current !== body && current !== documentElement) {
if (hasPassword(current)) return current;
current = current.parentElement || null;
}
return null;
}
function jagPassFilterMatches(matches, filter) {
const query = String(filter || "").trim().toLowerCase();
if (!query) return [...(Array.isArray(matches) ? matches : [])];
return (Array.isArray(matches) ? matches : []).filter((match) =>
[match.username, match.title, match.site_label, match.rp_id, ...(match.flags || [])]
.join(" ")
.toLowerCase()
.includes(query),
);
}
function jagPassNeedsSearch(count) {
return Number(count) > 5;
}
function jagPassShouldWarmFind(isTopFrame) {
return isTopFrame === true;
}
function jagPassWarmFindIfNeeded(isTopFrame, find) {
if (!jagPassShouldWarmFind(isTopFrame)) return false;
find();
return true;
}
function jagPassOverlayPolicy() {
return { root: "none", menu: "auto", prompt: "auto", passkeyScrim: "auto" };
}
function jagPassReturnFocus(field) {
if (!field || !field.isConnected || typeof field.focus !== "function") return false;
field.focus();
return true;
}
function jagPassLogoActivationController(openMenu, schedule) {
let suppressClick = false;
const later = schedule || ((work) => setTimeout(work, 500));
function suppressOneClick() {
suppressClick = true;
later(() => { suppressClick = false; });
}
return {
activate(kind, key, button, repeat) {
if (kind === "mousedown") {
if (button != null && button !== 0) return false;
suppressOneClick();
openMenu();
return true;
}
if (kind === "keydown") {
if (repeat || (key !== "Enter" && key !== " ")) return false;
suppressOneClick();
openMenu();
return true;
}
if (kind === "click") {
if (suppressClick) {
suppressClick = false;
return false;
}
openMenu();
return true;
}
return false;
},
};
}
var __JAGPASS_FILL_BOOTED__;
if (!(typeof globalThis !== "undefined" && globalThis.__JAGPASS_CAPTURE_TEST_MODE__) && !__JAGPASS_FILL_BOOTED__) {
__JAGPASS_FILL_BOOTED__ = true;
(function () {
const ICON_SIZE = 18;
const SKIP =
/search|query|captcha|otp|totp|one[- ]?time|2fa|mfa|recovery|backup.?code|cvv|cvc|card.?number|pin|ssn/;
const USER_HINT =
/user|email|login|account|identifier|id$|uid|mail|signin|sign-in|username/;
const NEW_PASS = /new[-_ ]?password|confirm|repeat|retype/;
const tracked = new WeakSet();
const watchedRoots = new WeakSet();
const observedRoots = new WeakSet();
const icons = new Map();
let filling = false;
let cache = { url: "", at: 0, res: null };
let pickerField = null;
let scanTimer = 0;
let root = null;
let shadow = null;
let picker = null;
let pickerMode = "direct";
let pickerResult = null;
let returningPickerFocus = false;
let iconLayer = null;
let scrim = null;
let savePrompt = null;
let lastTrustedActivationAt = 0;
function iconUrl() {
try {
return chrome.runtime.getURL("icons/32.png");
} catch {
return "";
}
}
function visible(el) {
if (!el || el.disabled || el.readOnly) return false;
if (el.type === "hidden") return false;
const r = el.getBoundingClientRect();
if (r.width < 24 || r.height < 12) return false;
const s = getComputedStyle(el);
return s.display !== "none" && s.visibility !== "hidden" && s.opacity !== "0";
}
function blobOf(el) {
return [
el.type,
el.name,
el.id,
el.autocomplete,
el.placeholder,
el.getAttribute("aria-label"),
el.getAttribute("inputmode"),
]
.join(" ")
.toLowerCase();
}
function isPassword(el) {
return (
el instanceof HTMLInputElement
&& el.type === "password"
&& visible(el)
&& !jagPassSkipSecretField(blobOf(el))
);
}
function isUserField(el) {
if (!(el instanceof HTMLInputElement) || !visible(el) || isPassword(el)) return false;
const t = (el.type || "text").toLowerCase();
if (["button", "submit", "reset", "checkbox", "radio", "file", "hidden", "range", "color", "date"].includes(t)) {
return false;
}
const b = blobOf(el);
if (SKIP.test(b) && !USER_HINT.test(b)) return false;
if (t === "email" || el.autocomplete === "username" || el.autocomplete === "email") return true;
if (USER_HINT.test(b)) return true;
return false;
}
function isLoginField(el) {
return isPassword(el) || isUserField(el);
}
function walkInputs(rootNode, out) {
jagPassWalkInputs(rootNode, out, watchRoot);
}
function allInputs() {
const out = [];
walkInputs(document, out);
return out;
}
function formScope(el) {
return jagPassFindPasswordScope(
el,
(scope) => passwordsIn(scope).length > 0,
document.body,
document.documentElement,
);
}
function passwordsIn(scope) {
const inputs = [];
walkInputs(scope, inputs);
return inputs.filter(isPassword);
}
function usersIn(scope) {
const inputs = [];
walkInputs(scope, inputs);
return inputs.filter(isUserField);
}
function pickPassword(scope, around) {
if (around && isPassword(around)) return around;
const passwords = passwordsIn(scope);
const current = passwords.find((el) => /current-password|old.?pass/i.test(blobOf(el)));
if (current) return current;
const notNew = passwords.filter((el) => !NEW_PASS.test(blobOf(el)));
return notNew[0] || passwords[0] || null;
}
function pickUser(scope, around) {
if (around && isUserField(around)) return around;
const users = usersIn(scope);
if (users[0]) return users[0];
const inputs = [...scope.querySelectorAll("input")].filter(visible);
const pass = pickPassword(scope, around);
if (!pass) return null;
const idx = inputs.indexOf(pass);
for (let i = idx - 1; i >= 0; i -= 1) {
const el = inputs[i];
if (el.type === "text" || el.type === "email" || el.type === "tel") return el;
}
return null;
}
function capturePassword(scope) {
const fields = passwordsIn(scope).map((element) => ({
blob: blobOf(element),
element,
value: element.value,
}));
const selected = jagPassSelectPassword(fields);
return selected ? selected.element : null;
}
function captureLogin(scope) {
if (filling) return null;
const password = capturePassword(scope);
if (!password || !password.value) return null;
const username = pickUser(scope, password) || pickUser(document, password);
return {
title: (document.title || location.hostname || "Login").trim().slice(0, 200),
username: username ? username.value.trim().slice(0, 320) : "",
password: password.value,
};
}
function setValue(el, value) {
if (!el) return;
el.focus();
const proto = HTMLInputElement.prototype;
const desc = Object.getOwnPropertyDescriptor(proto, "value");
if (desc && desc.set) desc.set.call(el, value);
else el.value = value;
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
}
function fillMatch(match, around) {
filling = true;
const scope = formScope(around);
const user = (scope ? pickUser(scope, around) : null) || pickUser(document, around);
const pass = (scope ? pickPassword(scope, around) : null) || pickPassword(document, around);
if (around && isPassword(around)) {
setValue(around, match.password || "");
if (user && user !== around) setValue(user, match.username || "");
} else if (around && isUserField(around)) {
setValue(around, match.username || "");
if (pass) setValue(pass, match.password || "");
} else {
if (user) setValue(user, match.username || "");
if (pass) setValue(pass, match.password || "");
}
window.setTimeout(() => {
filling = false;
}, 250);
}
function ensureRoot() {
if (root && shadow) return;
const overlay = jagPassOverlayPolicy();
root = document.createElement("div");
root.setAttribute("data-jagpass-ui", "1");
root.style.cssText =
`all:initial;position:fixed;inset:0;pointer-events:${overlay.root};z-index:2147483646;`;
shadow = root.attachShadow({ mode: "closed" });
const style = document.createElement("style");
style.textContent = `
* { box-sizing: border-box; font-family: "Segoe UI", system-ui, sans-serif; }
[hidden] { display: none !important; }
.icon {
position: fixed; width: ${ICON_SIZE}px; height: ${ICON_SIZE}px; padding: 0;
border: 0; border-radius: 4px; cursor: pointer; pointer-events: auto;
background: #1a150c center / 16px 16px no-repeat;
box-shadow: 0 0 0 1px #c9a227, 0 2px 6px rgba(0,0,0,.35);
}
.icon:hover { transform: scale(1.06); }
.picker {
position: fixed; min-width: 280px; max-width: 420px; max-height: 360px;
overflow: hidden; pointer-events: ${overlay.menu}; background: #fff; color: #111;
border: 1px solid #d0d0d0; border-radius: 8px;
box-shadow: 0 12px 32px rgba(0,0,0,.28); z-index: 2;
display: flex; flex-direction: column;
}
.save-prompt {
position: fixed; right: 18px; bottom: 18px; width: min(360px, calc(100vw - 36px));
pointer-events: ${overlay.prompt}; background: #fff; color: #111; border: 1px solid #d0d0d0;
border-radius: 10px; box-shadow: 0 14px 38px rgba(0,0,0,.3); z-index: 4;
padding: 14px;
}
.save-prompt h2 { margin: 0 0 5px; font-size: 16px; }
.save-prompt p { margin: 0 0 10px; font-size: 12px; color: #555; }
.save-prompt label { display: block; margin: 8px 0 3px; font-size: 12px; color: #555; }
.save-prompt input {
width: 100%; border: 1px solid #c9c9c9; border-radius: 6px; padding: 7px 8px;
font-size: 13px; color: #111; background: #fff;
}
.save-prompt select {
width: 100%; border: 1px solid #c9c9c9; border-radius: 6px; padding: 7px 8px;
font-size: 13px; color: #111; background: #fff;
}
.save-prompt input:focus, .save-prompt select:focus { outline: 2px solid #c9a227; border-color: #c9a227; }
.save-status { min-height: 16px; margin-top: 8px !important; color: #8a5b00 !important; }
.head {
display: flex; align-items: center; justify-content: space-between;
gap: 8px; padding: 10px 12px; font-weight: 700; font-size: 14px;
border-bottom: 1px solid #ececec; flex: none;
}
.head img { width: 18px; height: 18px; border-radius: 4px; }
.x { border: 0; background: transparent; cursor: pointer; font-size: 16px; color: #666; }
.search { padding: 8px 12px; border-bottom: 1px solid #ececec; flex: none; }
.search input {
width: 100%; border: 1px solid #d0d0d0; border-radius: 6px;
padding: 7px 8px; font-size: 13px; color: #111; background: #fff;
}
.search input:focus { outline: 2px solid #c9a227; border-color: #c9a227; }
.list { overflow: auto; flex: 1; min-height: 0; }
.msg { padding: 12px; color: #555; font-size: 13px; }
.row {
display: flex; align-items: flex-start; gap: 10px; width: 100%;
padding: 10px 12px; border: 0; background: #fff; text-align: left;
cursor: pointer; border-bottom: 1px solid #f0f0f0;
}
.row:hover, .row:focus { background: #e8f0fe; outline: 0; }
.row img { width: 20px; height: 20px; border-radius: 4px; flex: none; }
.who { font-weight: 650; font-size: 13px; color: #111; }
.sub { font-size: 12px; color: #666; margin-top: 2px; }
.command { align-items: center; }
.command .chev { margin-left: auto; color: #666; font-weight: 700; }
.scrim {
position: fixed; inset: 0; background: rgba(0,0,0,.45);
pointer-events: ${overlay.passkeyScrim}; z-index: 3;
display: flex; align-items: center; justify-content: center;
}
.dlg {
width: min(420px, calc(100vw - 32px)); background: #fff; color: #111;
border-radius: 10px; box-shadow: 0 18px 48px rgba(0,0,0,.35);
padding: 18px 18px 14px; pointer-events: auto;
}
.dlg h2 { margin: 0 0 8px; font-size: 16px; }
.dlg p { margin: 0 0 12px; font-size: 13px; color: #444; }
.dlg label { display: block; font-size: 12px; color: #555; margin-bottom: 4px; }
.dlg input {
width: 100%; border: 1px solid #d0d0d0; border-radius: 6px;
padding: 8px; font-size: 13px; margin-bottom: 12px;
}
.actions { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
.actions button { border: 0; border-radius: 6px; padding: 8px 12px; cursor: pointer; font-weight: 650; }
.primary { background: #1a73e8; color: #fff; }
.ghost { background: #f1f1f1; color: #222; }
`;
shadow.append(style);
iconLayer = document.createElement("div");
picker = document.createElement("div");
picker.className = "picker";
picker.hidden = true;
picker.addEventListener("keydown", (ev) => {
if (ev.key === "Escape") {
ev.preventDefault();
ev.stopPropagation();
hidePicker(true);
return;
}
if (ev.key === "ArrowLeft" && pickerMode === "command-logins") {
ev.preventDefault();
ev.stopPropagation();
renderCommandMenu(pickerField, pickerResult);
return;
}
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(ev.key)) return;
const items = [...picker.querySelectorAll("[data-menu-item='1']")];
if (items.length === 0) return;
const current = items.indexOf(shadow.activeElement);
const next = jagPassNextMenuIndex(ev.key, current, items.length);
if (next >= 0) {
ev.preventDefault();
ev.stopPropagation();
items[next].focus();
}
});
scrim = document.createElement("div");
scrim.className = "scrim";
scrim.hidden = true;
savePrompt = document.createElement("div");
savePrompt.className = "save-prompt";
savePrompt.hidden = true;
shadow.append(iconLayer, picker, savePrompt, scrim);
(document.documentElement || document.body).append(root);
}
function hidePicker(returnFocus) {
if (!picker) return;
const field = pickerField;
picker.hidden = true;
picker.replaceChildren();
pickerField = null;
pickerResult = null;
pickerMode = "direct";
if (returnFocus) {
returningPickerFocus = true;
jagPassReturnFocus(field);
returningPickerFocus = false;
}
}
function focusFirstMenuItem() {
const first = picker && picker.querySelector("[data-menu-item='1']");
if (first) window.setTimeout(() => first.focus(), 0);
}
function placeIcon(btn, el) {
const r = el.getBoundingClientRect();
const top = r.top + (r.height - ICON_SIZE) / 2;
const left = r.right - ICON_SIZE - 6;
btn.style.top = `${Math.round(top)}px`;
btn.style.left = `${Math.round(left)}px`;
btn.hidden = r.width < 40 || r.height < 12;
}
function placePicker(el) {
const r = el.getBoundingClientRect();
const width = Math.max(280, Math.min(420, r.width));
let top = r.bottom + 6;
let left = r.left;
picker.style.width = `${width}px`;
picker.hidden = false;
const h = picker.offsetHeight || 220;
if (top + h > window.innerHeight - 8) top = Math.max(8, r.top - h - 6);
if (left + width > window.innerWidth - 8) left = Math.max(8, window.innerWidth - width - 8);
picker.style.top = `${Math.round(top)}px`;
picker.style.left = `${Math.round(left)}px`;
}
function flagText(match) {
const flags = match.flags || [];
const bits = [];
if (flags.includes("passkey")) bits.push("Passkey");
if (flags.includes("weak")) bits.push("Weak");
if (flags.includes("reused")) bits.push("Re-used");
if (!match.exact) bits.push("same domain");
return bits.join(", ");
}
function matchBlob(match) {
return [match.username, match.title, match.rp_id, (match.flags || []).join(" ")]
.join(" ")
.toLowerCase();
}
function native(type, payload) {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ type, payload }, (res) => {
resolve(
chrome.runtime.lastError
? { ok: false, error: chrome.runtime.lastError.message }
: res || { ok: false, error: "empty reply" },
);
});
});
}
function hideSavePrompt(discard) {
if (!savePrompt) return;
savePrompt.hidden = true;
savePrompt.replaceChildren();
if (discard) native("discard-login-candidate", {});
}
function showSavePrompt(candidate) {
if (!candidate || window.top !== window) return;
ensureRoot();
hidePicker();
savePrompt.replaceChildren();
const updating = candidate.action === "updated";
const choosing = candidate.action === "choose";
const heading = document.createElement("h2");
heading.textContent = choosing
? "Save or update login in Jag Pass?"
: updating
? "Update login in Jag Pass?"
: "Save login to Jag Pass?";
const detail = document.createElement("p");
detail.textContent = choosing
? "Choose the exact account to update, or save a separate credential. The password stays hidden."
: updating
? "This account is already saved with a different password. Review it, then choose Update."
: "Review the login, then choose Save. The password stays hidden.";
const titleLabel = document.createElement("label");
titleLabel.textContent = "Name";
const title = document.createElement("input");
title.type = "text";
title.value = candidate.title || location.hostname;
title.maxLength = 200;
const userLabel = document.createElement("label");
userLabel.textContent = "Username or email";
const username = document.createElement("input");
username.type = "text";
username.value = candidate.username || "";
username.maxLength = 320;
username.autocomplete = "off";
let decision = null;
if (choosing) {
const decisionLabel = document.createElement("label");
decisionLabel.textContent = "Save decision";
decision = document.createElement("select");
const create = document.createElement("option");
create.value = "";
create.textContent = "Save as a new credential";
decision.append(create);
for (const choice of candidate.choices || []) {
const option = document.createElement("option");
option.value = choice.credential_id || choice.id || "";
option.textContent = `Update ${choice.username || "No username"}${choice.title ? ` (${choice.title})` : ""}`;
decision.append(option);
}
savePrompt.append(heading, detail, titleLabel, title, userLabel, username, decisionLabel, decision);
} else {
savePrompt.append(heading, detail, titleLabel, title, userLabel, username);
}
const status = document.createElement("p");
status.className = "save-status";
const actions = document.createElement("div");
actions.className = "actions";
const cancel = document.createElement("button");
cancel.className = "ghost";
cancel.type = "button";
cancel.textContent = "Not now";
cancel.addEventListener("click", () => hideSavePrompt(true));
const save = document.createElement("button");
save.className = "primary";
save.type = "button";
save.textContent = updating ? "Update" : "Save";
if (decision) {
decision.addEventListener("change", () => {
const selected = (candidate.choices || []).find(
(choice) => (choice.credential_id || choice.id || "") === decision.value,
);
if (selected) username.value = selected.username || "";
save.textContent = selected ? "Update" : "Save";
});
username.addEventListener("input", () => {
const selected = (candidate.choices || []).find(
(choice) => (choice.credential_id || choice.id || "") === decision.value,
);
if (selected && String(selected.username || "").toLowerCase() !== username.value.trim().toLowerCase()) {
decision.value = "";
save.textContent = "Save";
}
});
} else if (updating) {
username.addEventListener("input", () => {
save.textContent = username.value.trim().toLowerCase() === (candidate.username || "").toLowerCase()
? "Update"
: "Save";
});
}
save.addEventListener("click", async () => {
save.disabled = true;
const selectedId = decision ? decision.value : "";
const doingUpdate = Boolean(selectedId) || (updating && save.textContent === "Update");
status.textContent = doingUpdate ? "Updating..." : "Saving...";
const payload = {
id: candidate.id,
title: title.value.trim(),
username: username.value.trim(),
};
if (selectedId) payload.credential_id = selectedId;
if (decision && !selectedId) payload.force_new = true;
const res = await native("save-login-candidate", payload);
save.disabled = false;
if (res && res.selection_required && res.candidate) {
showSavePrompt(res.candidate);
return;
}
if (res && res.locked) {
status.textContent = `Unlock Jag Pass on the desktop, then choose ${doingUpdate ? "Update" : "Save"} again.`;
return;
}
if (!res || !res.ok || !res.save) {
status.textContent = (res && res.error) || "Could not save this login.";
return;
}
const words = {
created: "Saved to Jag Pass.",
updated: "Password updated in Jag Pass.",
unchanged: "Already saved in Jag Pass.",
};
status.textContent = words[res.save.action] || "Saved to Jag Pass.";
window.setTimeout(() => hideSavePrompt(false), 900);
});
actions.append(cancel, save);
savePrompt.append(status, actions);
savePrompt.hidden = false;
}
function offerCapturedLogin(scope) {
const payload = captureLogin(scope);
if (!payload) return;
native("login-candidate", payload).then((res) => {
if (res && res.ok && res.candidate) showSavePrompt(res.candidate);
});
}
function replyPage(id, extra) {
window.postMessage(Object.assign({ source: "jagpass-content", id }, extra), location.origin);
}
function hidePasskey() {
if (!scrim) return;
scrim.hidden = true;
scrim.replaceChildren();
}
function dlgButtons(primaryLabel, onPrimary, onCancel, onOther) {
const actions = document.createElement("div");
actions.className = "actions";
const other = document.createElement("button");
other.className = "ghost";
other.type = "button";
other.textContent = "Use your other device";
other.addEventListener("click", onOther);
const cancel = document.createElement("button");
cancel.className = "ghost";
cancel.type = "button";
cancel.textContent = "Cancel";
cancel.addEventListener("click", onCancel);
const ok = document.createElement("button");
ok.className = "primary";
ok.type = "button";
ok.textContent = primaryLabel;
ok.addEventListener("click", onPrimary);
actions.append(other, cancel, ok);
return actions;
}
function findMatches(cb) {
const url = location.href;
const now = Date.now();
if (cache.res && cache.url === url && now - cache.at < 5000) {
cb(cache.res);
return;
}
chrome.runtime.sendMessage({ type: "find" }, (res) => {
const out = chrome.runtime.lastError
? { ok: false, error: chrome.runtime.lastError.message, matches: [] }
: res || { ok: false, matches: [] };
cache = { url, at: Date.now(), res: out };
cb(out);
});
}
function orderedMatches(matches) {
return [...(Array.isArray(matches) ? matches : [])].sort((a, b) => {
if (Boolean(a.exact) !== Boolean(b.exact)) return a.exact ? -1 : 1;
return [a.site_label, a.title, a.username].join("\u0000").localeCompare(
[b.site_label, b.title, b.username].join("\u0000"),
undefined,
{ sensitivity: "base" },
);
});
}
function pickerHead(label) {
const src = iconUrl();
const head = document.createElement("div");
head.className = "head";
const left = document.createElement("span");
left.style.cssText = "display:flex;align-items:center;gap:8px";
if (src) {
const mark = document.createElement("img");
mark.src = src;
mark.alt = "";
left.append(mark);
}
const title = document.createElement("span");
title.textContent = label;
left.append(title);
const close = document.createElement("button");
close.className = "x";
close.type = "button";
close.textContent = "x";
close.setAttribute("aria-label", "Close Jag Pass menu");
close.addEventListener("click", () => hidePicker(true));
head.append(left, close);
return head;
}
function pickerMessage(text) {
const msg = document.createElement("p");
msg.className = "msg";
msg.textContent = text;
return msg;
}
function renderLoginList(el, res, fromCommand) {
ensureRoot();
picker.replaceChildren();
pickerField = el;
pickerResult = res;
pickerMode = fromCommand ? "command-logins" : "direct";
picker.append(pickerHead(fromCommand ? "Log in as" : "Log in as:"));
if (!res || !res.ok) {
picker.append(pickerMessage(
res && res.error
? res.error
: "Jag Pass host is not installed. Open Jag Pass, Settings, Install browser host.",
));
placePicker(el);
return;
}
if (res.locked) {
picker.append(pickerMessage("Unlock Jag Pass on the desktop, then choose Log in as again."));
placePicker(el);
return;
}
const matches = orderedMatches(res.matches);
if (matches.length === 0) {
picker.append(pickerMessage("No login for this host."));
placePicker(el);
return;
}
const src = iconUrl();
const list = document.createElement("div");
list.className = "list";
function paint(filter) {
list.replaceChildren();
const shown = jagPassFilterMatches(matches, filter);
if (shown.length === 0) {
list.append(pickerMessage("No name matches that search."));
return;
}
for (const match of shown) {
const btn = document.createElement("button");
btn.className = "row";
btn.type = "button";
btn.dataset.menuItem = "1";
if (src) {
const img = document.createElement("img");
img.src = src;
img.alt = "";
btn.append(img);
}
const col = document.createElement("span");
const who = document.createElement("div");
who.className = "who";
who.textContent = match.username || match.title || "Login";
col.append(who);
const sub = document.createElement("div");
sub.className = "sub";
const extra = flagText(match);
const label = match.site_label || match.title || "";
sub.textContent = extra ? `${label}${label ? " · " : ""}${extra}` : label;
if (sub.textContent) col.append(sub);
btn.append(col);
btn.addEventListener("click", () => {
fillMatch(match, el);
hidePicker();
});
list.append(btn);
}
}
let searchInput = null;
if (jagPassNeedsSearch(matches.length)) {
const search = document.createElement("div");
search.className = "search";
searchInput = document.createElement("input");
searchInput.type = "search";
searchInput.placeholder = "Search name or username";
searchInput.autocomplete = "off";
searchInput.spellcheck = false;
searchInput.addEventListener("input", () => paint(searchInput.value));
search.append(searchInput);
picker.append(search);
}
paint("");
picker.append(list);
placePicker(el);
if (fromCommand) {
if (searchInput) window.setTimeout(() => searchInput.focus(), 0);
else focusFirstMenuItem();
}
}
function commandButton(label, subtext, onUse, submenu) {
const btn = document.createElement("button");
btn.className = "row command";
btn.type = "button";
btn.dataset.menuItem = "1";
const col = document.createElement("span");
const who = document.createElement("div");
who.className = "who";
who.textContent = label;
col.append(who);
if (subtext) {
const sub = document.createElement("div");
sub.className = "sub";
sub.textContent = subtext;
col.append(sub);
}
btn.append(col);
if (submenu) {
const chev = document.createElement("span");
chev.className = "chev";
chev.textContent = ">";
btn.append(chev);
}
btn.addEventListener("click", onUse);
return btn;
}
function renderCommandMenu(el, res) {
ensureRoot();
picker.replaceChildren();
pickerField = el;
pickerResult = res;
pickerMode = "commands";
const matches = orderedMatches((res && res.matches) || []);
const siteLabel = (res && res.site && res.site.label) || (matches[0] && matches[0].site_label) || location.hostname;
picker.append(pickerHead(siteLabel || "Jag Pass"));
const status = pickerMessage("");
status.hidden = true;
const showResult = (reply, verb) => {
if (reply && reply.ok && !reply.locked) {
hidePicker();
return;
}
status.hidden = false;
status.textContent = reply && reply.locked
? `Unlock Jag Pass on the desktop. ${verb} will resume after unlock.`
: (reply && reply.error) || "Jag Pass could not complete that action.";
placePicker(el);
};
picker.append(
commandButton(
"Log in as",
res && res.locked ? "Unlock Jag Pass to see credentials" : `${matches.length} credential${matches.length === 1 ? "" : "s"}`,
() => renderLoginList(el, res, true),
true,
),
commandButton("Add login...", `New credential for ${location.hostname}`, async () => {
showResult(await native("open-new-login", { origin: location.origin }), "Add login");
}),
commandButton("Edit account...", siteLabel, async () => {
const siteId = (res && res.site && res.site.id) || (matches[0] && matches[0].site_id) || "";
showResult(await native("open-site-account", { site_id: siteId }), "Edit account");
}),
commandButton("Open Jag Pass", "Show and focus the desktop app", async () => {
showResult(await native("open-app", {}), "Open Jag Pass");
}),
status,
);
placePicker(el);
focusFirstMenuItem();
}
function openPicker(el, mode) {
if (filling || !el) return;
ensureRoot();
findMatches((res) => {
if (mode === "commands") renderCommandMenu(el, res);
else renderLoginList(el, res, false);
});
}
function attach(el) {
if (tracked.has(el)) return;
tracked.add(el);
ensureRoot();
const btn = document.createElement("button");
btn.className = "icon";
btn.type = "button";
btn.title = "Jag Pass menu";
btn.setAttribute("aria-label", "Open Jag Pass menu");
const src = iconUrl();
if (src) btn.style.backgroundImage = `url("${src}")`;
const activation = jagPassLogoActivationController(() => openPicker(el, "commands"));
btn.addEventListener("mousedown", (ev) => {
if (!activation.activate("mousedown", "", ev.button)) return;
ev.preventDefault();
ev.stopPropagation();
});
btn.addEventListener("click", (ev) => {
ev.preventDefault();
ev.stopPropagation();
activation.activate("click", "", 0);
});
btn.addEventListener("keydown", (ev) => {
if (!activation.activate("keydown", ev.key, 0, ev.repeat)) return;
ev.preventDefault();
ev.stopPropagation();
});
iconLayer.append(btn);
icons.set(el, btn);
placeIcon(btn, el);
}
function scan() {
if (!document.documentElement) return;
ensureRoot();
const live = new Set();
for (const el of allInputs()) {
if (!isLoginField(el)) continue;
attach(el);
live.add(el);
}
for (const [el, btn] of icons) {
if (!live.has(el) || !el.isConnected) {
btn.remove();
icons.delete(el);
if (pickerField === el) hidePicker();
} else {
placeIcon(btn, el);
}
}
if (pickerField && !picker.hidden) placePicker(pickerField);
}
function scheduleScan() {
window.clearTimeout(scanTimer);
scanTimer = window.setTimeout(scan, 160);
}
function inputFromEvent(ev) {
const path = ev.composedPath ? ev.composedPath() : [ev.target];
return path.find((node) => node instanceof HTMLInputElement) || null;
}
function buttonFromEvent(ev) {
const path = ev.composedPath ? ev.composedPath() : [ev.target];
for (const node of path) {
if (!(node instanceof Element)) continue;
const button = node.closest("button,input[type='submit'],input[type='image']");
if (button) return button;
}
return null;
}
function onRootFocus(ev) {
const el = inputFromEvent(ev);
if (!el || filling || returningPickerFocus) return;
native("frame-focus", {});
if (!tracked.has(el) && isLoginField(el)) attach(el);
if (tracked.has(el)) openPicker(el, "direct");
}
function onRootInput(ev) {
const el = inputFromEvent(ev);
if (!el || filling || ev.isTrusted === false || !isUserField(el)) return;
const username = el.value.trim().slice(0, 320);
if (username) native("remember-login-username", { username });
}
function onRootMouseDown(ev) {
if (!picker || picker.hidden) return;
const path = ev.composedPath ? ev.composedPath() : [ev.target];
if (root && path.includes(root)) return;
const input = inputFromEvent(ev);
if (input && tracked.has(input)) return;
hidePicker();
}
function onRootKeyDown(ev) {
if (ev.key === "Escape") {
hidePicker(true);
if (scrim && !scrim.hidden) hidePasskey();
if (savePrompt && !savePrompt.hidden) hideSavePrompt(true);
return;
}
const input = inputFromEvent(ev);
if (!input || !isLoginField(input)) return;
const scope = formScope(input);
const signal = {
hasPassword: passwordsIn(scope).some((field) => field.value),
isComposing: ev.isComposing,
isTrusted: ev.isTrusted,
key: ev.key,
kind: "keydown",
};
if (jagPassShouldCaptureSignal(signal)) lastTrustedActivationAt = Date.now();
jagPassRunCaptureSignal(signal, () => offerCapturedLogin(scope));
}
function onRootSubmit(ev) {
const form = ev.target instanceof HTMLFormElement
? ev.target
: (ev.composedPath ? ev.composedPath().find((node) => node instanceof HTMLFormElement) : null);
jagPassRunCaptureSignal({
isTrusted: ev.isTrusted,
kind: "submit",
recentTrustedActivation: Date.now() - lastTrustedActivationAt <= 2000,
}, () => offerCapturedLogin(form || document));
}
function onRootClick(ev) {
const button = buttonFromEvent(ev);
if (!button) return;
const words = `${button.textContent || ""} ${button.value || ""} ${button.getAttribute("aria-label") || ""}`;
const type = String(button.type || "").toLowerCase();
const signal = { buttonType: type, isTrusted: ev.isTrusted, kind: "click", words };
if (jagPassShouldCaptureSignal(signal)) lastTrustedActivationAt = Date.now();
jagPassRunCaptureSignal(signal, () => offerCapturedLogin(formScope(button)));
}
function watchRoot(rootNode) {
if (!rootNode) return;
if (!watchedRoots.has(rootNode)) {
watchedRoots.add(rootNode);
rootNode.addEventListener("focusin", onRootFocus, true);
rootNode.addEventListener("input", onRootInput, true);
rootNode.addEventListener("mousedown", onRootMouseDown, true);
rootNode.addEventListener("keydown", onRootKeyDown, true);
rootNode.addEventListener("submit", onRootSubmit, true);
rootNode.addEventListener("click", onRootClick, true);
}
const target = rootNode instanceof Document ? rootNode.documentElement : rootNode;
if (target && !observedRoots.has(rootNode)) {
observedRoots.add(rootNode);
const observer = new MutationObserver(scheduleScan);
observer.observe(target, { childList: true, subtree: true });
}
}
function showDlg(node) {
ensureRoot();
hidePicker();
scrim.replaceChildren(node);
scrim.hidden = false;
}
function cancelPasskey(id, send) {
hidePasskey();
const extra = { error: { name: "NotAllowedError", message: "The operation was cancelled." } };
if (send) send(extra);
else replyPage(id, extra);
}
function otherDevice(id, send) {
hidePasskey();
const extra = { fallback: true };
if (send) send(extra);
else replyPage(id, extra);
}
function donePasskey(id, extra, send) {
if (send) send(extra);
else replyPage(id, extra);
}
function packedFromCreate(opts) {
const rp = opts.rp || {};
const user = opts.user || {};
const exclude = opts.excludeCredentials || opts.exclude || [];
return {
origin: opts.origin || location.origin,
rpId: rp.id || rp.name || opts.rpId || location.hostname,
rpName: rp.name || opts.rpName || "",
userName: user.name || opts.userName || "",
userDisplayName: user.displayName || opts.userDisplayName || "",
userId: user.id || opts.userId || "",
challenge: opts.challenge || "",
exclude: exclude.map((c) => (typeof c === "string" ? c : c.id)).filter(Boolean),
};
}
function packedFromGet(opts) {
const allow = opts.allowCredentials || opts.allow || [];
return {
origin: opts.origin || location.origin,
rpId: opts.rpId || (opts.rp && opts.rp.id) || location.hostname,
challenge: opts.challenge || "",
allow: allow.map((c) => (typeof c === "string" ? c : c.id)).filter(Boolean),
};
}
function showCreate(id, options, send) {
const box = document.createElement("div");
box.className = "dlg";
const h = document.createElement("h2");
h.textContent = "Add New Passkey";
const p = document.createElement("p");
p.textContent = `This passkey will be used for "${options.rpId || location.hostname}".`;
const label = document.createElement("label");
label.textContent = "Login:";
const input = document.createElement("input");
input.type = "text";
input.value = options.userName || options.userDisplayName || "";
input.autocomplete = "username";
const go = async () => {
const payload = {
origin: options.origin || location.origin,
rp_id: options.rpId,
rp_name: options.rpName || options.rpId,
user_name: input.value.trim() || options.userName || "",
user_display_name: options.userDisplayName || input.value.trim(),
user_id: options.userId || "",
challenge: options.challenge,
exclude: options.exclude || [],
};
const res = await native("passkey-create", payload);
if (res.locked) {
p.textContent = "Unlock Jag Pass on the desktop, then click Add Passkey again.";
return;
}
if (!res.ok || !res.credential) {
if ((res.error || "").includes("already")) {
donePasskey(id, { error: { name: "InvalidStateError", message: res.error } }, send);
hidePasskey();
return;
}
p.textContent = res.error || "Could not save this passkey.";
return;
}
hidePasskey();
donePasskey(id, { result: res.credential }, send);
};
box.append(
h,
p,
label,
input,
dlgButtons("Add Passkey", go, () => cancelPasskey(id, send), () => otherDevice(id, send)),
);
showDlg(box);
window.setTimeout(() => input.focus(), 0);
}
function pickPasskey(id, options, keys, send) {
const box = document.createElement("div");
box.className = "dlg";
const h = document.createElement("h2");
h.textContent = "Sign in with passkey";
const p = document.createElement("p");
p.textContent = options.rpId || location.hostname;
const search = document.createElement("div");
search.className = "search";
const input = document.createElement("input");
input.type = "search";
input.placeholder = "Search name or username";
search.append(input);
const list = document.createElement("div");
list.className = "list";
list.style.maxHeight = "220px";
async function useKey(item) {
const res = await native("passkey-assert", {
origin: options.origin || location.origin,
rp_id: options.rpId,
challenge: options.challenge,
credential_id: item.credential_id,
});
if (res.locked) {
p.textContent = "Unlock Jag Pass on the desktop, then pick the passkey again.";
return;
}
if (!res.ok || !res.assertion) {
p.textContent = res.error || "Could not use this passkey.";
return;
}
hidePasskey();
donePasskey(id, { result: res.assertion }, send);
}
function paint(filter) {
list.replaceChildren();
const shown = jagPassFilterMatches(keys, filter);
if (shown.length === 0) {
const msg = document.createElement("p");
msg.className = "msg";
msg.textContent = "No name matches that search.";
list.append(msg);
return;
}
for (const item of shown) {
const btn = document.createElement("button");
btn.className = "row";
btn.type = "button";
const col = document.createElement("span");
const who = document.createElement("div");
who.className = "who";
who.textContent = item.username || item.title || "Passkey";
const sub = document.createElement("div");
sub.className = "sub";
sub.textContent = item.title || item.rp_id || "";
col.append(who);
if (sub.textContent) col.append(sub);
btn.append(col);
btn.addEventListener("click", () => useKey(item));
list.append(btn);
}
}
input.addEventListener("input", () => paint(input.value));
paint("");
const actions = document.createElement("div");
actions.className = "actions";
const other = document.createElement("button");
other.className = "ghost";
other.type = "button";
other.textContent = "Use your other device";
other.addEventListener("click", () => otherDevice(id, send));
const cancel = document.createElement("button");
cancel.className = "ghost";
cancel.type = "button";
cancel.textContent = "Cancel";
cancel.addEventListener("click", () => cancelPasskey(id, send));
actions.append(other, cancel);
box.append(h, p);
if (jagPassNeedsSearch(keys.length)) box.append(search);
box.append(list, actions);
showDlg(box);
if (jagPassNeedsSearch(keys.length)) window.setTimeout(() => input.focus(), 0);
}
async function onPasskey(id, op, options, send) {
if (!options) {
donePasskey(id, { fallback: true }, send);
return;
}
if (op === "create") {
showCreate(id, options, send);
return;
}
const res = await native("passkey-list", {
rp_id: options.rpId,
allow: options.allow || [],
});
if (res.locked) {
const box = document.createElement("div");
box.className = "dlg";
const h = document.createElement("h2");
h.textContent = "Sign in with passkey";
const p = document.createElement("p");
p.textContent = "Unlock Jag Pass on the desktop, then try again.";
box.append(h, p, dlgButtons("Cancel", () => cancelPasskey(id, send), () => cancelPasskey(id, send), () => otherDevice(id, send)));
showDlg(box);
return;
}
const keys = (res && res.passkeys) || [];
if (!res.ok || keys.length === 0) {
donePasskey(id, { fallback: true }, send);
return;
}
if (keys.length === 1) {
const one = keys[0];
const asserted = await native("passkey-assert", {
origin: options.origin || location.origin,
rp_id: options.rpId,
challenge: options.challenge,
credential_id: one.credential_id,
});
if (asserted.ok && asserted.assertion) {
donePasskey(id, { result: asserted.assertion }, send);
return;
}
}
pickPasskey(id, options, keys, send);
}
window.addEventListener("message", (ev) => {
if (ev.source !== window || !ev.data || ev.data.source !== "jagpass-page") return;
onPasskey(ev.data.id, ev.data.op, ev.data.options);
});
chrome.runtime.onMessage.addListener((msg, _sender, send) => {
if (msg && msg.type === "fill" && msg.match) {
hidePicker();
fillMatch(msg.match, document.activeElement instanceof HTMLInputElement ? document.activeElement : null);
return;
}
if (msg && msg.type === "show-login-candidate" && msg.candidate && window.top === window) {
showSavePrompt(msg.candidate);
return;
}
if (msg && msg.type === "passkey-proxy") {
const packed =
msg.op === "create" ? packedFromCreate(msg.options || {}) : packedFromGet(msg.options || {});
onPasskey("proxy", msg.op, packed, send);
return true;
}
});
window.addEventListener("scroll", () => {
for (const [el, btn] of icons) placeIcon(btn, el);
if (pickerField && !picker.hidden) placePicker(pickerField);
}, true);
window.addEventListener("resize", scheduleScan);
watchRoot(document);
document.addEventListener("DOMContentLoaded", () => {
watchRoot(document);
scheduleScan();
}, { once: true });
scheduleScan();
if (jagPassWarmFindIfNeeded(window.top === window, () => findMatches(() => {}))) {
native("pending-login-candidate", {}).then((res) => {
if (res && res.ok && res.candidate) showSavePrompt(res.candidate);
});
}
})();
}
page.js
var __JAGPASS_PAGE__;
if (!__JAGPASS_PAGE__) {
__JAGPASS_PAGE__ = true;
(function () {
const origCreate = navigator.credentials && navigator.credentials.create
? navigator.credentials.create.bind(navigator.credentials)
: null;
const origGet = navigator.credentials && navigator.credentials.get
? navigator.credentials.get.bind(navigator.credentials)
: null;
if (!origCreate || !origGet) {
return;
}
let seq = 1;
const pending = new Map();
function b64url(buf) {
const bytes = buf instanceof ArrayBuffer ? new Uint8Array(buf) : new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
let bin = "";
for (let i = 0; i < bytes.length; i += 1) bin += String.fromCharCode(bytes[i]);
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
function fromB64(text) {
const pad = text.replace(/-/g, "+").replace(/_/g, "/");
const raw = atob(pad);
const out = new Uint8Array(raw.length);
for (let i = 0; i < raw.length; i += 1) out[i] = raw.charCodeAt(i);
return out.buffer;
}
function idsOf(list) {
if (!list || !list.length) return [];
const out = [];
for (const item of list) {
if (item && item.id) out.push(b64url(item.id));
}
return out;
}
function hasEs256(params) {
if (!params || !params.length) return true;
return params.some((p) => p && (p.alg === -7 || p.alg === "-7"));
}
function ask(op, options, signal) {
const id = "p" + seq;
seq += 1;
return new Promise((resolve, reject) => {
const timer = window.setTimeout(() => {
pending.delete(id);
reject(new DOMException("The operation either timed out or was not allowed.", "NotAllowedError"));
}, 120000);
pending.set(id, {
resolve: (value) => {
window.clearTimeout(timer);
resolve(value);
},
reject: (err) => {
window.clearTimeout(timer);
reject(err);
},
});
if (signal) {
if (signal.aborted) {
pending.delete(id);
window.clearTimeout(timer);
reject(new DOMException("Aborted", "AbortError"));
return;
}
signal.addEventListener("abort", () => {
const wait = pending.get(id);
pending.delete(id);
if (wait) wait.reject(new DOMException("Aborted", "AbortError"));
});
}
window.postMessage({ source: "jagpass-page", id, op, options }, location.origin);
});
}
window.addEventListener("message", (ev) => {
if (ev.source !== window || !ev.data || ev.data.source !== "jagpass-content") return;
const wait = pending.get(ev.data.id);
if (!wait) return;
pending.delete(ev.data.id);
if (ev.data.fallback) {
wait.resolve({ fallback: true });
return;
}
if (ev.data.error) {
wait.reject(new DOMException(ev.data.error.message || ev.data.error.name, ev.data.error.name || "NotAllowedError"));
return;
}
wait.resolve(ev.data.result);
});
function attachProto(obj, proto) {
try {
if (proto) Object.setPrototypeOf(obj, proto);
} catch {
/* page may freeze prototypes */
}
return obj;
}
function asCreate(data) {
const response = attachProto(
{
clientDataJSON: fromB64(data.clientDataJSON),
attestationObject: fromB64(data.attestationObject),
getAuthenticatorData: () => new Uint8Array(fromB64(data.authenticatorData)),
getPublicKey: () => new Uint8Array(fromB64(data.publicKey)),
getPublicKeyAlgorithm: () => data.publicKeyAlgorithm || -7,
getTransports: () => ["internal", "hybrid"],
},
window.AuthenticatorAttestationResponse && window.AuthenticatorAttestationResponse.prototype,
);
return attachProto(
{
id: data.id,
rawId: fromB64(data.rawId || data.id),
type: "public-key",
authenticatorAttachment: "platform",
response,
getClientExtensionResults: () => ({}),
},
window.PublicKeyCredential && window.PublicKeyCredential.prototype,
);
}
function asGet(data) {
const response = attachProto(
{
clientDataJSON: fromB64(data.clientDataJSON),
authenticatorData: fromB64(data.authenticatorData),
signature: fromB64(data.signature),
userHandle: data.userHandle ? fromB64(data.userHandle) : null,
},
window.AuthenticatorAssertionResponse && window.AuthenticatorAssertionResponse.prototype,
);
return attachProto(
{
id: data.id,
rawId: fromB64(data.rawId || data.id),
type: "public-key",
authenticatorAttachment: "platform",
response,
getClientExtensionResults: () => ({}),
},
window.PublicKeyCredential && window.PublicKeyCredential.prototype,
);
}
navigator.credentials.create = async function (options) {
if (!options || !options.publicKey || !hasEs256(options.publicKey.pubKeyCredParams)) {
return origCreate(options);
}
const pk = options.publicKey;
const packed = {
origin: location.origin,
rpId: (pk.rp && (pk.rp.id || pk.rp.name)) || location.hostname,
rpName: (pk.rp && pk.rp.name) || location.hostname,
userName: (pk.user && (pk.user.name || pk.user.displayName)) || "",
userDisplayName: (pk.user && (pk.user.displayName || pk.user.name)) || "",
userId: pk.user && pk.user.id ? b64url(pk.user.id) : "",
challenge: b64url(pk.challenge),
exclude: idsOf(pk.excludeCredentials),
};
try {
const reply = await ask("create", packed, options.signal || pk.signal);
if (reply && reply.fallback) return origCreate(options);
return asCreate(reply);
} catch (err) {
if (err && err.name === "AbortError") throw err;
throw err;
}
};
navigator.credentials.get = async function (options) {
if (!options || !options.publicKey) {
return origGet(options);
}
const pk = options.publicKey;
const packed = {
origin: location.origin,
rpId: pk.rpId || location.hostname,
challenge: b64url(pk.challenge),
allow: idsOf(pk.allowCredentials),
};
try {
const reply = await ask("get", packed, options.signal || pk.signal);
if (reply && reply.fallback) return origGet(options);
return asGet(reply);
} catch (err) {
if (err && err.name === "AbortError") throw err;
throw err;
}
};
if (window.PublicKeyCredential) {
window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = function () {
return Promise.resolve(true);
};
if (window.PublicKeyCredential.isConditionalMediationAvailable) {
window.PublicKeyCredential.isConditionalMediationAvailable = function () {
return Promise.resolve(true);
};
}
}
})();
}
popup.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
body { font: 13px/1.4 "Segoe UI", sans-serif; margin: 12px; width: 280px; background: #121212; color: #f3f3f3; }
button { width: 100%; margin: 4px 0; padding: 8px; border: 0; border-radius: 6px; background: #c9a227; color: #111; font-weight: 700; cursor: pointer; }
.ghost { background: #1b1b1b; color: #f3f3f3; border: 1px solid #2a2a2a; }
p { color: #9a9a9a; }
.match { text-align: left; }
.actions { border-top: 1px solid #2a2a2a; margin-top: 8px; padding-top: 6px; }
.action { text-align: left; }
input { width: 100%; margin: 6px 0 8px; padding: 7px 8px; border-radius: 6px; border: 1px solid #2a2a2a; background: #1b1b1b; color: #f3f3f3; }
</style>
</head>
<body>
<p id="status">Looking up this tab in Jag Pass.</p>
<div id="list"></div>
<script src="popup.js"></script>
</body>
</html>
popup.js
const status = document.getElementById("status");
const list = document.getElementById("list");
function show(message) {
status.textContent = message;
}
function send(type, payload) {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ type, payload: payload || {} }, (reply) => {
resolve(
chrome.runtime.lastError
? { ok: false, error: chrome.runtime.lastError.message }
: reply || { ok: false, error: "empty reply" },
);
});
});
}
function orderedMatches(matches) {
return [...(Array.isArray(matches) ? matches : [])].sort((a, b) => {
if (Boolean(a.exact) !== Boolean(b.exact)) return a.exact ? -1 : 1;
return [a.site_label, a.title, a.username].join("\u0000").localeCompare(
[b.site_label, b.title, b.username].join("\u0000"),
undefined,
{ sensitivity: "base" },
);
});
}
async function fill(match, frameId) {
const reply = await send("fill-active-frame", { match, frame_id: frameId });
if (!reply.ok) {
show(reply.error || "Could not fill the active login frame.");
return;
}
window.close();
}
function actionButton(label, use) {
const button = document.createElement("button");
button.type = "button";
button.className = "ghost action";
button.textContent = label;
button.addEventListener("click", use);
return button;
}
function renderActions(result, matches) {
const actions = document.createElement("div");
actions.className = "actions";
const run = async (type, payload, pendingText) => {
const reply = await send(type, payload);
if (reply.ok && !reply.locked) {
window.close();
return;
}
show(reply.locked ? `${pendingText} will resume after you unlock Jag Pass.` : reply.error || "Jag Pass could not complete that action.");
};
const siteId = (result.site && result.site.id) || (matches[0] && matches[0].site_id) || "";
actions.append(
actionButton("Add login...", () => run("open-new-login", {}, "Add login")),
actionButton("Edit account...", () => run("open-site-account", { site_id: siteId }, "Edit account")),
actionButton("Open Jag Pass", () => run("open-app", {}, "Open Jag Pass")),
);
list.after(actions);
}
send("find", {}).then((result) => {
if (!result || !result.ok) {
show(result && result.error ? result.error : "Jag Pass host is not installed. Open Jag Pass, Settings, Install browser host.");
return;
}
const matches = orderedMatches(result.matches);
const siteLabel = (result.site && result.site.label) || (matches[0] && matches[0].site_label) || "this site";
renderActions(result, matches);
if (result.locked) {
show("Unlock Jag Pass on the desktop. Add, Edit, or Open will resume after unlock.");
return;
}
if (matches.length === 0) {
show(`No login for ${siteLabel}.`);
return;
}
show(`${siteLabel}: ${matches.length} credential${matches.length === 1 ? "" : "s"}.`);
const paint = (filter) => {
list.replaceChildren();
const query = (filter || "").trim().toLowerCase();
const shown = query
? matches.filter((match) => `${match.username || ""} ${match.title || ""} ${match.site_label || ""}`.toLowerCase().includes(query))
: matches;
if (shown.length === 0) {
const empty = document.createElement("p");
empty.textContent = "No name matches that search.";
list.append(empty);
return;
}
for (const match of shown) {
const button = document.createElement("button");
button.className = "match";
button.type = "button";
const label = match.username || match.title || "Login";
const detail = match.site_label || match.title || "";
button.textContent = `${label}${detail && detail !== label ? ` - ${detail}` : ""}${match.exact ? "" : " - same domain"}`;
button.addEventListener("click", () => fill(match, result.target_frame_id || 0));
list.append(button);
}
};
if (matches.length > 5) {
const input = document.createElement("input");
input.type = "search";
input.placeholder = "Search name or username";
input.setAttribute("aria-label", "Search credentials");
input.addEventListener("input", () => paint(input.value));
status.after(input);
input.focus();
}
paint("");
});
Home · Privacy