Add browser extension gRPC bridge
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
const ext = globalThis.browser ?? globalThis.chrome;
|
||||
const nativeHost = "org.keepassgo.browser";
|
||||
const defaultSettings = {
|
||||
grpcAddress: "127.0.0.1:47777",
|
||||
bearerToken: ""
|
||||
};
|
||||
|
||||
function storageGet(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) {
|
||||
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 tabsQuery(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 tabsSendMessage(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) {
|
||||
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 || "").trim()
|
||||
};
|
||||
}
|
||||
|
||||
async function activePageContext() {
|
||||
const [tab] = await tabsQuery({ active: true, currentWindow: true });
|
||||
return {
|
||||
tabId: tab?.id ?? null,
|
||||
url: typeof tab?.url === "string" ? tab.url : ""
|
||||
};
|
||||
}
|
||||
|
||||
async function statusForPage() {
|
||||
const settings = await loadSettings();
|
||||
const page = await activePageContext();
|
||||
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,
|
||||
configured: true,
|
||||
status: status.status ?? null,
|
||||
pageUrl: page.url,
|
||||
matches: [],
|
||||
error: status.error ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
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 ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
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 fillResponse = await tabsSendMessage(page.tabId, {
|
||||
type: "keepassgo-fill-credential",
|
||||
credential: response.credential
|
||||
});
|
||||
if (!fillResponse?.ok) {
|
||||
throw new Error(fillResponse?.error || "The current page could not be filled.");
|
||||
}
|
||||
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) });
|
||||
});
|
||||
return true;
|
||||
});
|
||||
Reference in New Issue
Block a user