Complete browser extension gRPC flow
This commit is contained in:
+637
-82
@@ -1,11 +1,22 @@
|
||||
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;
|
||||
@@ -19,6 +30,9 @@ function storageGet(keys) {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -31,7 +45,74 @@ function storageSet(value) {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -44,7 +125,26 @@ function tabsQuery(query) {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -58,6 +158,9 @@ function tabsSendMessage(tabId, message) {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -74,122 +177,574 @@ async function loadSettings() {
|
||||
const stored = await storageGet(["grpcAddress", "bearerToken"]);
|
||||
return {
|
||||
grpcAddress: (stored.grpcAddress || defaultSettings.grpcAddress).trim(),
|
||||
bearerToken: (stored.bearerToken || "").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: tab?.id ?? null,
|
||||
tabId: Number.isInteger(tab?.id) ? tab.id : null,
|
||||
url: typeof tab?.url === "string" ? tab.url : ""
|
||||
};
|
||||
}
|
||||
|
||||
async function statusForPage() {
|
||||
const settings = await loadSettings();
|
||||
const page = await activePageContext();
|
||||
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,
|
||||
pageUrl: page.url,
|
||||
matches: [],
|
||||
error: "Set an API token in extension settings."
|
||||
};
|
||||
}
|
||||
|
||||
const status = await connectNative({
|
||||
action: "status",
|
||||
grpcAddress: settings.grpcAddress,
|
||||
bearerToken: settings.bearerToken
|
||||
});
|
||||
if (!status.success || status.status?.locked || !page.url.startsWith("http")) {
|
||||
return {
|
||||
success: status.success,
|
||||
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: status.status ?? null,
|
||||
pageUrl: page.url,
|
||||
matches: [],
|
||||
error: status.error ?? ""
|
||||
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 : ""
|
||||
};
|
||||
}
|
||||
|
||||
const matches = await connectNative({
|
||||
action: "find-logins",
|
||||
grpcAddress: settings.grpcAddress,
|
||||
bearerToken: settings.bearerToken,
|
||||
url: page.url
|
||||
});
|
||||
return {
|
||||
success: matches.success,
|
||||
configured: true,
|
||||
status: matches.status ?? status.status ?? null,
|
||||
pageUrl: page.url,
|
||||
matches: matches.matches ?? [],
|
||||
error: matches.error ?? ""
|
||||
};
|
||||
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(entryId) {
|
||||
const settings = await loadSettings();
|
||||
const page = await activePageContext();
|
||||
if (!settings.bearerToken) {
|
||||
throw new Error("API token is not configured.");
|
||||
}
|
||||
if (page.tabId == null) {
|
||||
async function fillLogin(tabId, entryId) {
|
||||
if (!Number.isInteger(tabId)) {
|
||||
throw new Error("No active tab is available.");
|
||||
}
|
||||
|
||||
const response = await connectNative({
|
||||
action: "get-login",
|
||||
grpcAddress: settings.grpcAddress,
|
||||
bearerToken: settings.bearerToken,
|
||||
entryId,
|
||||
url: page.url
|
||||
});
|
||||
if (!response.success || !response.credential) {
|
||||
throw new Error(response.error || "KeePassGO did not return a credential.");
|
||||
const tab = await tabsGet(tabId);
|
||||
const pageUrl = typeof tab?.url === "string" ? tab.url : "";
|
||||
if (!supportsPageStateURL(pageUrl)) {
|
||||
throw new Error("This page cannot be filled.");
|
||||
}
|
||||
|
||||
const fillResponse = await tabsSendMessage(page.tabId, {
|
||||
type: "keepassgo-fill-credential",
|
||||
credential: response.credential
|
||||
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()
|
||||
});
|
||||
if (!fillResponse?.ok) {
|
||||
throw new Error(fillResponse?.error || "The current page could not be filled.");
|
||||
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;
|
||||
}
|
||||
return {
|
||||
credential: response.credential,
|
||||
pageUrl: page.url
|
||||
};
|
||||
}
|
||||
|
||||
ext.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
(async () => {
|
||||
switch (message?.type) {
|
||||
case "keepassgo-popup-state":
|
||||
sendResponse(await statusForPage());
|
||||
return;
|
||||
case "keepassgo-fill-entry":
|
||||
sendResponse({ success: true, ...(await fillLogin(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()
|
||||
});
|
||||
sendResponse({ success: true });
|
||||
return;
|
||||
default:
|
||||
sendResponse({ success: false, error: `Unsupported message ${message?.type || ""}`.trim() });
|
||||
}
|
||||
})().catch((error) => {
|
||||
sendResponse({ success: false, error: error instanceof Error ? error.message : String(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;
|
||||
});
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user