751 lines
22 KiB
JavaScript
751 lines
22 KiB
JavaScript
const ext = globalThis.browser ?? globalThis.chrome;
|
|
const nativeHost = "com.keepassgo.browser";
|
|
const isNodeTestEnv = typeof module !== "undefined" && module.exports;
|
|
const usePromiseAPI = typeof globalThis.browser !== "undefined";
|
|
const defaultSettings = {
|
|
grpcAddress: "",
|
|
bearerToken: ""
|
|
};
|
|
const pageStatePrefix = "keepassgo-page-state:";
|
|
const matchCacheTTL = 30 * 1000;
|
|
const pendingPollMillis = 1500;
|
|
const pageStates = new Map();
|
|
const refreshJobs = new Map();
|
|
const pendingPollers = new Map();
|
|
|
|
function storageGet(keys) {
|
|
if (usePromiseAPI) {
|
|
return ext.storage.local.get(keys);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
ext.storage.local.get(keys, (value) => {
|
|
const error = ext.runtime.lastError;
|
|
if (error) {
|
|
reject(new Error(error.message));
|
|
return;
|
|
}
|
|
resolve(value);
|
|
});
|
|
});
|
|
}
|
|
|
|
function storageSet(value) {
|
|
if (usePromiseAPI) {
|
|
return ext.storage.local.set(value);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
ext.storage.local.set(value, () => {
|
|
const error = ext.runtime.lastError;
|
|
if (error) {
|
|
reject(new Error(error.message));
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
function sessionArea() {
|
|
return ext.storage?.session ?? null;
|
|
}
|
|
|
|
function sessionStorageGet(keys) {
|
|
const area = sessionArea();
|
|
if (!area) {
|
|
return Promise.resolve({});
|
|
}
|
|
if (usePromiseAPI) {
|
|
return area.get(keys).then((value) => value || {});
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
area.get(keys, (value) => {
|
|
const error = ext.runtime.lastError;
|
|
if (error) {
|
|
reject(new Error(error.message));
|
|
return;
|
|
}
|
|
resolve(value || {});
|
|
});
|
|
});
|
|
}
|
|
|
|
function sessionStorageSet(value) {
|
|
const area = sessionArea();
|
|
if (!area) {
|
|
return Promise.resolve();
|
|
}
|
|
if (usePromiseAPI) {
|
|
return area.set(value);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
area.set(value, () => {
|
|
const error = ext.runtime.lastError;
|
|
if (error) {
|
|
reject(new Error(error.message));
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
function sessionStorageRemove(keys) {
|
|
const area = sessionArea();
|
|
if (!area) {
|
|
return Promise.resolve();
|
|
}
|
|
if (usePromiseAPI) {
|
|
return area.remove(keys);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
area.remove(keys, () => {
|
|
const error = ext.runtime.lastError;
|
|
if (error) {
|
|
reject(new Error(error.message));
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
function tabsQuery(query) {
|
|
if (usePromiseAPI) {
|
|
return ext.tabs.query(query);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
ext.tabs.query(query, (tabs) => {
|
|
const error = ext.runtime.lastError;
|
|
if (error) {
|
|
reject(new Error(error.message));
|
|
return;
|
|
}
|
|
resolve(tabs);
|
|
});
|
|
});
|
|
}
|
|
|
|
function tabsGet(tabId) {
|
|
if (usePromiseAPI) {
|
|
return ext.tabs.get(tabId);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
ext.tabs.get(tabId, (tab) => {
|
|
const error = ext.runtime.lastError;
|
|
if (error) {
|
|
reject(new Error(error.message));
|
|
return;
|
|
}
|
|
resolve(tab);
|
|
});
|
|
});
|
|
}
|
|
|
|
function tabsSendMessage(tabId, message) {
|
|
if (usePromiseAPI) {
|
|
return ext.tabs.sendMessage(tabId, message);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
ext.tabs.sendMessage(tabId, message, (response) => {
|
|
const error = ext.runtime.lastError;
|
|
if (error) {
|
|
reject(new Error(error.message));
|
|
return;
|
|
}
|
|
resolve(response);
|
|
});
|
|
});
|
|
}
|
|
|
|
function connectNative(message) {
|
|
if (usePromiseAPI) {
|
|
return ext.runtime.sendNativeMessage(nativeHost, message);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
ext.runtime.sendNativeMessage(nativeHost, message, (response) => {
|
|
const error = ext.runtime.lastError;
|
|
if (error) {
|
|
reject(new Error(error.message));
|
|
return;
|
|
}
|
|
resolve(response);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function loadSettings() {
|
|
const stored = await storageGet(["grpcAddress", "bearerToken"]);
|
|
return {
|
|
grpcAddress: (stored.grpcAddress || defaultSettings.grpcAddress).trim(),
|
|
bearerToken: (stored.bearerToken || defaultSettings.bearerToken).trim()
|
|
};
|
|
}
|
|
|
|
function supportsPageStateURL(rawURL) {
|
|
return typeof rawURL === "string" && /^https?:\/\//i.test(rawURL);
|
|
}
|
|
|
|
function pageStateKey(tabId) {
|
|
return `${pageStatePrefix}${String(tabId)}`;
|
|
}
|
|
|
|
function cloneTarget(target) {
|
|
return target && typeof target === "object" ? { ...target } : null;
|
|
}
|
|
|
|
function normalizePageState(state) {
|
|
return {
|
|
tabId: Number.isInteger(state?.tabId) ? state.tabId : null,
|
|
pageUrl: typeof state?.pageUrl === "string" ? state.pageUrl : "",
|
|
configured: Boolean(state?.configured),
|
|
success: state?.success !== false,
|
|
status: state?.status ?? null,
|
|
matches: Array.isArray(state?.matches) ? state.matches : [],
|
|
error: typeof state?.error === "string" ? state.error : "",
|
|
pageHasLoginForm: Boolean(state?.pageHasLoginForm),
|
|
signature: typeof state?.signature === "string" ? state.signature : "",
|
|
focusTarget: cloneTarget(state?.focusTarget),
|
|
pendingFill: Boolean(state?.pendingFill),
|
|
pendingEntryId: typeof state?.pendingEntryId === "string" ? state.pendingEntryId : "",
|
|
pendingTarget: cloneTarget(state?.pendingTarget),
|
|
pendingMessage: typeof state?.pendingMessage === "string" ? state.pendingMessage : "",
|
|
lastFilledEntryId: typeof state?.lastFilledEntryId === "string" ? state.lastFilledEntryId : "",
|
|
updatedAt: Number.isFinite(state?.updatedAt) ? state.updatedAt : 0
|
|
};
|
|
}
|
|
|
|
function defaultPageState(tabId, pageUrl) {
|
|
return normalizePageState({
|
|
tabId,
|
|
pageUrl,
|
|
configured: true,
|
|
success: true,
|
|
status: null,
|
|
matches: [],
|
|
error: "",
|
|
pageHasLoginForm: false,
|
|
signature: "",
|
|
focusTarget: null,
|
|
pendingFill: false,
|
|
pendingEntryId: "",
|
|
pendingTarget: null,
|
|
pendingMessage: "",
|
|
lastFilledEntryId: "",
|
|
updatedAt: 0
|
|
});
|
|
}
|
|
|
|
async function getPageState(tabId, pageUrl) {
|
|
if (!Number.isInteger(tabId)) {
|
|
return defaultPageState(null, pageUrl || "");
|
|
}
|
|
const existing = pageStates.get(tabId);
|
|
if (existing && (!pageUrl || existing.pageUrl === pageUrl)) {
|
|
return normalizePageState(existing);
|
|
}
|
|
const stored = await sessionStorageGet(pageStateKey(tabId));
|
|
const state = normalizePageState(stored[pageStateKey(tabId)] || defaultPageState(tabId, pageUrl || ""));
|
|
if (pageUrl && state.pageUrl !== pageUrl) {
|
|
return defaultPageState(tabId, pageUrl);
|
|
}
|
|
pageStates.set(tabId, state);
|
|
return state;
|
|
}
|
|
|
|
async function setPageState(tabId, nextState) {
|
|
const state = normalizePageState({ ...nextState, tabId });
|
|
if (!Number.isInteger(tabId)) {
|
|
return state;
|
|
}
|
|
pageStates.set(tabId, state);
|
|
await sessionStorageSet({ [pageStateKey(tabId)]: state });
|
|
await updateActionState(tabId, state);
|
|
await notifyContentState(tabId, state);
|
|
return state;
|
|
}
|
|
|
|
function clearPendingPoll(tabId) {
|
|
const timer = pendingPollers.get(tabId);
|
|
if (timer !== undefined) {
|
|
clearTimeout(timer);
|
|
pendingPollers.delete(tabId);
|
|
}
|
|
}
|
|
|
|
async function clearPageState(tabId) {
|
|
if (!Number.isInteger(tabId)) {
|
|
return;
|
|
}
|
|
pageStates.delete(tabId);
|
|
refreshJobs.delete(tabId);
|
|
clearPendingPoll(tabId);
|
|
await sessionStorageRemove(pageStateKey(tabId));
|
|
await clearActionState(tabId);
|
|
}
|
|
|
|
function describeError(error) {
|
|
return error instanceof Error ? error.message : String(error || "Unknown error");
|
|
}
|
|
|
|
function approvalHintForState(state) {
|
|
if (!state.pendingFill) {
|
|
return "";
|
|
}
|
|
return state.pendingMessage || "Approve or deny the fill request in KeePassGO.";
|
|
}
|
|
|
|
function schedulePendingPoll(tabId, pageUrl) {
|
|
if (!Number.isInteger(tabId)) {
|
|
return;
|
|
}
|
|
clearPendingPoll(tabId);
|
|
const timer = setTimeout(() => {
|
|
pendingPollers.delete(tabId);
|
|
void refreshPageState(tabId, pageUrl, { force: true }).catch(() => null);
|
|
}, pendingPollMillis);
|
|
pendingPollers.set(tabId, timer);
|
|
}
|
|
|
|
async function notifyContentState(tabId, state) {
|
|
if (!Number.isInteger(tabId)) {
|
|
return;
|
|
}
|
|
try {
|
|
await tabsSendMessage(tabId, {
|
|
type: "keepassgo-page-state",
|
|
state
|
|
});
|
|
} catch (_error) {
|
|
// Ignore pages without a ready content script.
|
|
}
|
|
}
|
|
|
|
async function clearActionState(tabId) {
|
|
if (!Number.isInteger(tabId) || !ext.action) {
|
|
return;
|
|
}
|
|
await Promise.allSettled([
|
|
ext.action.setBadgeText({ tabId, text: "" }),
|
|
ext.action.setTitle({ tabId, title: "KeePassGO Browser" })
|
|
]);
|
|
}
|
|
|
|
function actionPresentationForState(state) {
|
|
let badgeText = "";
|
|
let title = "KeePassGO Browser";
|
|
let color = "#255f4a";
|
|
|
|
if (state.pendingFill) {
|
|
badgeText = "!";
|
|
color = "#9f5f0e";
|
|
title = approvalHintForState(state) || "KeePassGO approval needed for this page";
|
|
} else if (!state.configured) {
|
|
title = "Configure KeePassGO Browser in extension settings";
|
|
} else if (!state.success) {
|
|
badgeText = "!";
|
|
color = "#9f2f2f";
|
|
title = state.error || "KeePassGO is unavailable for this page";
|
|
} else if (state.status?.locked) {
|
|
title = "Unlock KeePassGO to fill this page";
|
|
} else if (state.pageHasLoginForm && state.matches.length > 0) {
|
|
badgeText = String(Math.min(state.matches.length, 9));
|
|
title = `KeePassGO found ${state.matches.length} matching entr${state.matches.length === 1 ? "y" : "ies"} on this page`;
|
|
} else if (state.pageHasLoginForm) {
|
|
title = "KeePassGO found no matching entries on this page";
|
|
}
|
|
|
|
return { badgeText, title, color };
|
|
}
|
|
|
|
async function updateActionState(tabId, state) {
|
|
if (!Number.isInteger(tabId) || !ext.action) {
|
|
return;
|
|
}
|
|
const presentation = actionPresentationForState(state);
|
|
await Promise.allSettled([
|
|
ext.action.setBadgeText({ tabId, text: presentation.badgeText }),
|
|
ext.action.setBadgeBackgroundColor({ tabId, color: presentation.color }),
|
|
ext.action.setTitle({ tabId, title: presentation.title })
|
|
]);
|
|
}
|
|
|
|
async function activePageContext() {
|
|
const [tab] = await tabsQuery({ active: true, currentWindow: true });
|
|
return {
|
|
tabId: Number.isInteger(tab?.id) ? tab.id : null,
|
|
url: typeof tab?.url === "string" ? tab.url : ""
|
|
};
|
|
}
|
|
|
|
async function scanTabForLoginForm(tabId) {
|
|
if (!Number.isInteger(tabId)) {
|
|
return { pageHasLoginForm: false, focusTarget: null, signature: "" };
|
|
}
|
|
try {
|
|
const response = await tabsSendMessage(tabId, { type: "keepassgo-page-scan" });
|
|
return {
|
|
pageHasLoginForm: Boolean(response?.pageHasLoginForm),
|
|
focusTarget: cloneTarget(response?.focusTarget),
|
|
signature: typeof response?.signature === "string" ? response.signature : ""
|
|
};
|
|
} catch (_error) {
|
|
return { pageHasLoginForm: false, focusTarget: null, signature: "" };
|
|
}
|
|
}
|
|
|
|
function shouldReuseMatches(state, force) {
|
|
if (force || state.pendingFill) {
|
|
return false;
|
|
}
|
|
if (!state.pageHasLoginForm || !Array.isArray(state.matches)) {
|
|
return false;
|
|
}
|
|
return Date.now() - (state.updatedAt || 0) < matchCacheTTL;
|
|
}
|
|
|
|
function tokenPendingApprovalCount(status) {
|
|
return Number(status?.tokenPendingApprovalCount || 0);
|
|
}
|
|
|
|
async function fetchStatus(settings) {
|
|
if (!settings.bearerToken) {
|
|
return {
|
|
success: false,
|
|
configured: false,
|
|
status: null,
|
|
error: "Set an API token in extension settings."
|
|
};
|
|
}
|
|
const status = await connectNative({
|
|
action: "status",
|
|
grpcAddress: settings.grpcAddress,
|
|
bearerToken: settings.bearerToken
|
|
});
|
|
return {
|
|
success: Boolean(status?.success),
|
|
configured: true,
|
|
status: status?.status ?? null,
|
|
error: status?.error ?? ""
|
|
};
|
|
}
|
|
|
|
async function refreshPageState(tabId, pageUrl, options = {}) {
|
|
if (!Number.isInteger(tabId)) {
|
|
return defaultPageState(null, pageUrl || "");
|
|
}
|
|
const force = Boolean(options.force);
|
|
const existingJob = refreshJobs.get(tabId);
|
|
if (existingJob && !force) {
|
|
return existingJob;
|
|
}
|
|
|
|
const job = (async () => {
|
|
let resolvedURL = typeof pageUrl === "string" ? pageUrl : "";
|
|
if (!supportsPageStateURL(resolvedURL)) {
|
|
const tab = await tabsGet(tabId).catch(() => null);
|
|
resolvedURL = typeof tab?.url === "string" ? tab.url : resolvedURL;
|
|
}
|
|
if (!supportsPageStateURL(resolvedURL)) {
|
|
await clearPageState(tabId);
|
|
return defaultPageState(tabId, resolvedURL || "");
|
|
}
|
|
|
|
let state = await getPageState(tabId, resolvedURL);
|
|
if (state.pageUrl !== resolvedURL) {
|
|
state = defaultPageState(tabId, resolvedURL);
|
|
}
|
|
|
|
const scan = typeof options.pageHasLoginForm === "boolean"
|
|
? {
|
|
pageHasLoginForm: options.pageHasLoginForm,
|
|
focusTarget: cloneTarget(options.focusTarget) || state.focusTarget,
|
|
signature: typeof options.signature === "string" ? options.signature : state.signature
|
|
}
|
|
: await scanTabForLoginForm(tabId);
|
|
|
|
state = {
|
|
...state,
|
|
pageUrl: resolvedURL,
|
|
pageHasLoginForm: scan.pageHasLoginForm,
|
|
focusTarget: cloneTarget(scan.focusTarget) || state.focusTarget,
|
|
signature: typeof scan.signature === "string" ? scan.signature : state.signature
|
|
};
|
|
|
|
const settings = await loadSettings();
|
|
const statusInfo = await fetchStatus(settings).catch((error) => ({
|
|
success: false,
|
|
configured: true,
|
|
status: null,
|
|
error: describeError(error)
|
|
}));
|
|
|
|
state = {
|
|
...state,
|
|
configured: statusInfo.configured,
|
|
success: statusInfo.success,
|
|
status: statusInfo.status,
|
|
pendingFill: state.pendingFill || tokenPendingApprovalCount(statusInfo.status) > 0,
|
|
pendingMessage: tokenPendingApprovalCount(statusInfo.status) > 0
|
|
? approvalHintForState(state) || "Approve or deny the browser fill request in KeePassGO."
|
|
: "",
|
|
error: statusInfo.error
|
|
};
|
|
|
|
if (!statusInfo.configured || !statusInfo.success || statusInfo.status?.locked || !state.pageHasLoginForm) {
|
|
state.matches = [];
|
|
state.updatedAt = Date.now();
|
|
const saved = await setPageState(tabId, state);
|
|
if (saved.pendingFill) {
|
|
schedulePendingPoll(tabId, resolvedURL);
|
|
} else {
|
|
clearPendingPoll(tabId);
|
|
}
|
|
return saved;
|
|
}
|
|
|
|
if (shouldReuseMatches(state, force)) {
|
|
const saved = await setPageState(tabId, state);
|
|
if (saved.pendingFill) {
|
|
schedulePendingPoll(tabId, resolvedURL);
|
|
} else {
|
|
clearPendingPoll(tabId);
|
|
}
|
|
return saved;
|
|
}
|
|
|
|
const matches = await connectNative({
|
|
action: "find-logins",
|
|
grpcAddress: settings.grpcAddress,
|
|
bearerToken: settings.bearerToken,
|
|
url: resolvedURL
|
|
});
|
|
|
|
state = {
|
|
...state,
|
|
success: Boolean(matches?.success),
|
|
status: matches?.status ?? state.status,
|
|
pendingFill: state.pendingFill || tokenPendingApprovalCount(matches?.status ?? state.status) > 0,
|
|
pendingMessage: tokenPendingApprovalCount(matches?.status ?? state.status) > 0
|
|
? approvalHintForState(state) || "Approve or deny the browser fill request in KeePassGO."
|
|
: "",
|
|
matches: Array.isArray(matches?.matches) ? matches.matches : [],
|
|
error: matches?.error ?? "",
|
|
updatedAt: Date.now()
|
|
};
|
|
const saved = await setPageState(tabId, state);
|
|
if (saved.pendingFill) {
|
|
schedulePendingPoll(tabId, resolvedURL);
|
|
} else {
|
|
clearPendingPoll(tabId);
|
|
}
|
|
return saved;
|
|
})().finally(() => {
|
|
if (refreshJobs.get(tabId) === job) {
|
|
refreshJobs.delete(tabId);
|
|
}
|
|
});
|
|
|
|
refreshJobs.set(tabId, job);
|
|
return job;
|
|
}
|
|
|
|
async function statusForPage(options = {}) {
|
|
let page = await activePageContext();
|
|
if (Number.isInteger(options.tabId)) {
|
|
const tab = await tabsGet(options.tabId).catch(() => null);
|
|
page = {
|
|
tabId: options.tabId,
|
|
url: typeof tab?.url === "string" ? tab.url : ""
|
|
};
|
|
}
|
|
if (page.tabId == null) {
|
|
return defaultPageState(null, page.url);
|
|
}
|
|
if (!options.force) {
|
|
const cached = await getPageState(page.tabId, page.url);
|
|
if (cached.pageUrl === page.url && cached.updatedAt && !cached.pendingFill) {
|
|
return cached;
|
|
}
|
|
}
|
|
return refreshPageState(page.tabId, page.url, options);
|
|
}
|
|
|
|
async function fillLogin(tabId, entryId) {
|
|
if (!Number.isInteger(tabId)) {
|
|
throw new Error("No active tab is available.");
|
|
}
|
|
const tab = await tabsGet(tabId);
|
|
const pageUrl = typeof tab?.url === "string" ? tab.url : "";
|
|
if (!supportsPageStateURL(pageUrl)) {
|
|
throw new Error("This page cannot be filled.");
|
|
}
|
|
|
|
let state = await getPageState(tabId, pageUrl);
|
|
state = await setPageState(tabId, {
|
|
...state,
|
|
pageUrl,
|
|
pendingFill: true,
|
|
pendingEntryId: String(entryId || "").trim(),
|
|
pendingTarget: cloneTarget(state.focusTarget),
|
|
pendingMessage: "Approve or deny the browser fill request in KeePassGO.",
|
|
error: "",
|
|
updatedAt: Date.now()
|
|
});
|
|
schedulePendingPoll(tabId, pageUrl);
|
|
|
|
try {
|
|
const settings = await loadSettings();
|
|
if (!settings.bearerToken) {
|
|
throw new Error("API token is not configured.");
|
|
}
|
|
|
|
const response = await connectNative({
|
|
action: "get-login",
|
|
grpcAddress: settings.grpcAddress,
|
|
bearerToken: settings.bearerToken,
|
|
entryId,
|
|
url: pageUrl
|
|
});
|
|
if (!response?.success || !response.credential) {
|
|
throw new Error(response?.error || "KeePassGO did not return a credential.");
|
|
}
|
|
|
|
const fillResponse = await tabsSendMessage(tabId, {
|
|
type: "keepassgo-fill-credential",
|
|
credential: response.credential,
|
|
target: state.pendingTarget
|
|
});
|
|
if (!fillResponse?.ok) {
|
|
throw new Error(fillResponse?.error || "The current page could not be filled.");
|
|
}
|
|
|
|
state = await setPageState(tabId, {
|
|
...state,
|
|
pendingFill: false,
|
|
pendingEntryId: "",
|
|
pendingTarget: null,
|
|
pendingMessage: "",
|
|
lastFilledEntryId: String(entryId || "").trim(),
|
|
error: "",
|
|
updatedAt: Date.now()
|
|
});
|
|
clearPendingPoll(tabId);
|
|
return {
|
|
credential: response.credential,
|
|
pageUrl,
|
|
state
|
|
};
|
|
} catch (error) {
|
|
state = await setPageState(tabId, {
|
|
...state,
|
|
pendingFill: false,
|
|
pendingEntryId: "",
|
|
pendingTarget: null,
|
|
pendingMessage: "",
|
|
error: describeError(error),
|
|
updatedAt: Date.now()
|
|
});
|
|
clearPendingPoll(tabId);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function refreshActivePage(options = {}) {
|
|
const page = await activePageContext();
|
|
if (page.tabId == null) {
|
|
return defaultPageState(null, page.url);
|
|
}
|
|
return refreshPageState(page.tabId, page.url, options);
|
|
}
|
|
|
|
const backgroundTestExports = {
|
|
normalizePageState,
|
|
actionPresentationForState,
|
|
shouldReuseMatches,
|
|
tokenPendingApprovalCount,
|
|
defaultSettings
|
|
};
|
|
|
|
if (isNodeTestEnv) {
|
|
module.exports = backgroundTestExports;
|
|
} else {
|
|
ext.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
(async () => {
|
|
switch (message?.type) {
|
|
case "keepassgo-popup-state":
|
|
sendResponse(await statusForPage({
|
|
force: Boolean(message.force),
|
|
tabId: Number.isInteger(message?.tabId) ? message.tabId : null
|
|
}));
|
|
return;
|
|
case "keepassgo-fill-entry": {
|
|
const targetTabID = Number.isInteger(message?.tabId)
|
|
? message.tabId
|
|
: (Number.isInteger(sender?.tab?.id) ? sender.tab.id : (await activePageContext()).tabId);
|
|
if (Number.isInteger(targetTabID) && message.target) {
|
|
const targetState = await getPageState(targetTabID, "");
|
|
await setPageState(targetTabID, {
|
|
...targetState,
|
|
focusTarget: cloneTarget(message.target)
|
|
});
|
|
}
|
|
sendResponse({ success: true, ...(await fillLogin(targetTabID, message.entryId)) });
|
|
return;
|
|
}
|
|
case "keepassgo-load-settings":
|
|
sendResponse({ success: true, settings: await loadSettings() });
|
|
return;
|
|
case "keepassgo-save-settings":
|
|
await storageSet({
|
|
grpcAddress: String(message.settings?.grpcAddress || defaultSettings.grpcAddress).trim(),
|
|
bearerToken: String(message.settings?.bearerToken || "").trim()
|
|
});
|
|
await refreshActivePage({ force: true }).catch(() => null);
|
|
sendResponse({ success: true });
|
|
return;
|
|
case "keepassgo-page-ready":
|
|
if (Number.isInteger(sender?.tab?.id)) {
|
|
sendResponse(await refreshPageState(sender.tab.id, sender.tab.url, {
|
|
force: Boolean(message.force),
|
|
pageHasLoginForm: Boolean(message.pageHasLoginForm),
|
|
focusTarget: cloneTarget(message.focusTarget),
|
|
signature: typeof message.signature === "string" ? message.signature : ""
|
|
}));
|
|
return;
|
|
}
|
|
sendResponse(defaultPageState(null, ""));
|
|
return;
|
|
case "keepassgo-refresh-page-state":
|
|
if (Number.isInteger(sender?.tab?.id)) {
|
|
sendResponse(await refreshPageState(sender.tab.id, sender.tab.url, { force: true }));
|
|
return;
|
|
}
|
|
sendResponse(defaultPageState(null, ""));
|
|
return;
|
|
default:
|
|
sendResponse({ success: false, error: `Unsupported message ${message?.type || ""}`.trim() });
|
|
}
|
|
})().catch((error) => {
|
|
sendResponse({ success: false, error: describeError(error) });
|
|
});
|
|
return true;
|
|
});
|
|
|
|
ext.tabs?.onActivated?.addListener(({ tabId }) => {
|
|
void refreshPageState(tabId, "", { force: false }).catch(() => null);
|
|
});
|
|
|
|
ext.tabs?.onUpdated?.addListener((tabId, changeInfo, tab) => {
|
|
if (typeof changeInfo.url === "string") {
|
|
void clearPageState(tabId).catch(() => null);
|
|
}
|
|
if (changeInfo.status === "complete") {
|
|
void refreshPageState(tabId, tab?.url || changeInfo.url || "", { force: false }).catch(() => null);
|
|
}
|
|
});
|
|
|
|
ext.tabs?.onRemoved?.addListener((tabId) => {
|
|
void clearPageState(tabId).catch(() => null);
|
|
});
|
|
}
|