Add browser save and update workflow
This commit is contained in:
@@ -191,6 +191,96 @@ function cloneTarget(target) {
|
||||
return target && typeof target === "object" ? { ...target } : null;
|
||||
}
|
||||
|
||||
function cloneSavePlan(plan) {
|
||||
if (!plan || typeof plan !== "object") {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
mode: plan.mode === "update" ? "update" : "save",
|
||||
entryId: typeof plan.entryId === "string" ? plan.entryId : "",
|
||||
title: typeof plan.title === "string" ? plan.title : "",
|
||||
path: Array.isArray(plan.path) ? [...plan.path] : [],
|
||||
username: typeof plan.username === "string" ? plan.username : "",
|
||||
password: typeof plan.password === "string" ? plan.password : "",
|
||||
url: typeof plan.url === "string" ? plan.url : ""
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeObservedCredential(observed) {
|
||||
if (!observed || typeof observed !== "object") {
|
||||
return null;
|
||||
}
|
||||
const password = typeof observed.password === "string" ? observed.password.trim() : "";
|
||||
const url = typeof observed.url === "string" ? observed.url.trim() : "";
|
||||
if (!password || !url) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
title: typeof observed.title === "string" ? observed.title.trim() : "",
|
||||
username: typeof observed.username === "string" ? observed.username.trim() : "",
|
||||
password,
|
||||
url
|
||||
};
|
||||
}
|
||||
|
||||
function matchHost(rawURL) {
|
||||
if (typeof rawURL !== "string") {
|
||||
return "";
|
||||
}
|
||||
const trimmed = rawURL.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return new URL(trimmed).hostname.toLowerCase();
|
||||
} catch (_error) {
|
||||
return trimmed.replace(/^https?:\/\//i, "").replace(/\/.*$/, "").toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
function defaultObservedTitle(observed) {
|
||||
if (observed?.title) {
|
||||
return observed.title;
|
||||
}
|
||||
return matchHost(observed?.url) || "Browser Login";
|
||||
}
|
||||
|
||||
function savePlanForObservedLogin(observed, matches) {
|
||||
const normalized = normalizeObservedCredential(observed);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const targetHost = matchHost(normalized.url);
|
||||
const exact = Array.isArray(matches) ? matches.find((match) =>
|
||||
typeof match?.id === "string" &&
|
||||
String(match?.username || "").trim().toLowerCase() === normalized.username.toLowerCase() &&
|
||||
matchHost(match?.url || "") === targetHost
|
||||
) : null;
|
||||
if (exact) {
|
||||
return {
|
||||
mode: "update",
|
||||
entryId: exact.id,
|
||||
title: exact.title || defaultObservedTitle(normalized),
|
||||
path: Array.isArray(exact.path) ? [...exact.path] : [],
|
||||
username: normalized.username,
|
||||
password: normalized.password,
|
||||
url: normalized.url
|
||||
};
|
||||
}
|
||||
const fallbackPath = Array.isArray(matches) && matches.length > 0 && Array.isArray(matches[0]?.path)
|
||||
? [...matches[0].path]
|
||||
: [];
|
||||
return {
|
||||
mode: "save",
|
||||
entryId: "",
|
||||
title: defaultObservedTitle(normalized),
|
||||
path: fallbackPath,
|
||||
username: normalized.username,
|
||||
password: normalized.password,
|
||||
url: normalized.url
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePageState(state) {
|
||||
return {
|
||||
tabId: Number.isInteger(state?.tabId) ? state.tabId : null,
|
||||
@@ -207,6 +297,7 @@ function normalizePageState(state) {
|
||||
pendingEntryId: typeof state?.pendingEntryId === "string" ? state.pendingEntryId : "",
|
||||
pendingTarget: cloneTarget(state?.pendingTarget),
|
||||
pendingMessage: typeof state?.pendingMessage === "string" ? state.pendingMessage : "",
|
||||
pendingSave: cloneSavePlan(state?.pendingSave),
|
||||
lastFilledEntryId: typeof state?.lastFilledEntryId === "string" ? state.lastFilledEntryId : "",
|
||||
updatedAt: Number.isFinite(state?.updatedAt) ? state.updatedAt : 0
|
||||
};
|
||||
@@ -228,6 +319,7 @@ function defaultPageState(tabId, pageUrl) {
|
||||
pendingEntryId: "",
|
||||
pendingTarget: null,
|
||||
pendingMessage: "",
|
||||
pendingSave: null,
|
||||
lastFilledEntryId: "",
|
||||
updatedAt: 0
|
||||
});
|
||||
@@ -347,6 +439,12 @@ function actionPresentationForState(state) {
|
||||
badgeText = "!";
|
||||
color = "#9f5f0e";
|
||||
title = approvalHintForState(state) || "KeePassGO approval needed for this page";
|
||||
} else if (state.pendingSave) {
|
||||
badgeText = "S";
|
||||
color = "#255f4a";
|
||||
title = state.pendingSave.mode === "update"
|
||||
? `KeePassGO can update ${state.pendingSave.title || "this login"}`
|
||||
: "KeePassGO can save the submitted login";
|
||||
} else if (!state.configured) {
|
||||
title = "Configure KeePassGO Browser in extension settings";
|
||||
} else if (!state.success) {
|
||||
@@ -663,12 +761,64 @@ async function refreshActivePage(options = {}) {
|
||||
return refreshPageState(page.tabId, page.url, options);
|
||||
}
|
||||
|
||||
async function saveObservedLogin(tabId, selectedMatch = null) {
|
||||
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 : "";
|
||||
let state = await getPageState(tabId, pageUrl);
|
||||
const pendingSave = cloneSavePlan(state.pendingSave);
|
||||
if (!pendingSave) {
|
||||
throw new Error("There is no pending login to save.");
|
||||
}
|
||||
const request = {
|
||||
action: "save-login",
|
||||
title: pendingSave.title,
|
||||
username: pendingSave.username,
|
||||
password: pendingSave.password,
|
||||
url: pendingSave.url
|
||||
};
|
||||
if (selectedMatch && typeof selectedMatch === "object") {
|
||||
if (pendingSave.mode === "update" && typeof selectedMatch.id === "string" && selectedMatch.id.trim()) {
|
||||
request.entryId = selectedMatch.id.trim();
|
||||
request.title = String(selectedMatch.title || pendingSave.title || "").trim();
|
||||
} else if (Array.isArray(selectedMatch.path) && selectedMatch.path.length > 0) {
|
||||
request.path = [...selectedMatch.path];
|
||||
}
|
||||
} else if (pendingSave.mode === "update" && pendingSave.entryId) {
|
||||
request.entryId = pendingSave.entryId;
|
||||
} else if (pendingSave.path.length > 0) {
|
||||
request.path = [...pendingSave.path];
|
||||
}
|
||||
const settings = await loadSettings();
|
||||
if (!settings.bearerToken) {
|
||||
throw new Error("API token is not configured.");
|
||||
}
|
||||
const response = await connectNative({
|
||||
...request,
|
||||
bearerToken: settings.bearerToken
|
||||
});
|
||||
if (!response?.success) {
|
||||
throw new Error(response?.error || "KeePassGO did not save the submitted login.");
|
||||
}
|
||||
state = await setPageState(tabId, {
|
||||
...state,
|
||||
pendingSave: null,
|
||||
error: "",
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
await refreshPageState(tabId, pageUrl, { force: true });
|
||||
return { state };
|
||||
}
|
||||
|
||||
const backgroundTestExports = {
|
||||
normalizePageState,
|
||||
actionPresentationForState,
|
||||
shouldReuseMatches,
|
||||
shouldContinueWatchingState,
|
||||
tokenPendingApprovalCount,
|
||||
savePlanForObservedLogin,
|
||||
defaultSettings
|
||||
};
|
||||
|
||||
@@ -723,6 +873,33 @@ if (isNodeTestEnv) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "keepassgo-observed-login":
|
||||
if (Number.isInteger(sender?.tab?.id)) {
|
||||
const targetState = await getPageState(sender.tab.id, sender.tab.url || "");
|
||||
const nextSave = savePlanForObservedLogin(message.observed, targetState.matches);
|
||||
sendResponse(await setPageState(sender.tab.id, {
|
||||
...targetState,
|
||||
pendingSave: nextSave,
|
||||
updatedAt: Date.now()
|
||||
}));
|
||||
return;
|
||||
}
|
||||
sendResponse({ success: false, error: "No active tab is available." });
|
||||
return;
|
||||
case "keepassgo-save-login": {
|
||||
const targetTabID = Number.isInteger(message?.tabId)
|
||||
? message.tabId
|
||||
: (Number.isInteger(sender?.tab?.id) ? sender.tab.id : (await activePageContext()).tabId);
|
||||
const selectedMatch = message?.selectedMatch && typeof message.selectedMatch === "object"
|
||||
? {
|
||||
id: String(message.selectedMatch.id || "").trim(),
|
||||
title: String(message.selectedMatch.title || "").trim(),
|
||||
path: Array.isArray(message.selectedMatch.path) ? message.selectedMatch.path : []
|
||||
}
|
||||
: null;
|
||||
sendResponse({ success: true, ...(await saveObservedLogin(targetTabID, selectedMatch)) });
|
||||
return;
|
||||
}
|
||||
case "keepassgo-page-ready":
|
||||
if (Number.isInteger(sender?.tab?.id)) {
|
||||
sendResponse(await refreshPageState(sender.tab.id, sender.tab.url, {
|
||||
|
||||
Reference in New Issue
Block a user