Files
waggle-os/apps/browser-ext/background.js
Oleg Maslov b20b138fe4 moving
2026-09-02 10:14:22 +02:00

153 lines
5.2 KiB
JavaScript

// Waggle Companion background service worker — the only extension process that
// talks to the loopback sidecar. The popup supplies a one-time code; only the
// resulting scoped credential is persisted.
const SIDECAR = 'http://127.0.0.1:3333';
const PAIRING_REQUIRED = 'Browser Companion not paired. Generate a one-time code in Waggle Settings.';
async function readJson(response) {
try {
return await response.json();
} catch {
return null;
}
}
function authErrorMessage(status, body) {
const code = body?.code;
if (code === 'EXTENSION_NOT_ALLOWLISTED') {
return 'Browser Companion not allowlisted. Add the extension ID in Waggle, restart Waggle, and try again.';
}
if (code === 'PAIRING_CODE_INVALID') {
return 'Invalid or expired pairing code. Generate a new one-time code in Waggle Settings.';
}
if (status === 401 && (code === 'INVALID_TOKEN' || code === 'MISSING_TOKEN' || !code)) {
return PAIRING_REQUIRED;
}
return body?.error || `HTTP ${status}`;
}
async function removeLegacyToken() {
await chrome.storage.local.remove('sessionToken');
}
async function getAuthHeaders() {
try {
const { companionToken, sessionToken } = await chrome.storage.local.get([
'companionToken',
'sessionToken',
]);
if (sessionToken) await removeLegacyToken();
if (!companionToken) return { headers: {}, error: PAIRING_REQUIRED };
return { headers: { Authorization: `Bearer ${companionToken}` } };
} catch (err) {
return { headers: {}, error: String(err) };
}
}
async function pairWithCode(rawCode) {
const code = typeof rawCode === 'string' ? rawCode.trim().toUpperCase() : '';
if (!/^[A-HJ-NP-Z2-9]{8}$/.test(code)) {
return { ok: false, error: 'Enter the 8-character code shown in Waggle Settings.' };
}
try {
const response = await fetch(`${SIDECAR}/api/browser-ext/pair`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Waggle-Extension-Id': chrome.runtime.id,
},
body: JSON.stringify({ code }),
});
const data = await readJson(response);
if (!response.ok || typeof data?.token !== 'string') {
return { ok: false, error: authErrorMessage(response.status, data) };
}
await chrome.storage.local.set({ companionToken: data.token });
await removeLegacyToken();
return { ok: true };
} catch (err) {
return { ok: false, error: String(err) };
}
}
async function health() {
try {
const auth = await getAuthHeaders();
if (auth.error) return { ok: false, error: auth.error };
const response = await fetch(`${SIDECAR}/api/browser-ext/health`, {
method: 'GET',
headers: { Accept: 'application/json', ...auth.headers },
});
const data = await readJson(response);
if (response.status === 401) {
await chrome.storage.local.remove('companionToken');
return { ok: false, error: PAIRING_REQUIRED };
}
if (!response.ok) return { ok: false, error: authErrorMessage(response.status, data) };
return data;
} catch (err) {
return { ok: false, error: String(err) };
}
}
async function saveMemory(payload) {
try {
const auth = await getAuthHeaders();
if (auth.error) return { saved: false, error: auth.error };
const response = await fetch(`${SIDECAR}/api/memory/frames`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...auth.headers },
body: JSON.stringify({
content: payload.content,
source: payload.source || 'import',
importance: payload.importance || 'normal',
}),
});
const data = await readJson(response);
if (response.status === 401) {
await chrome.storage.local.remove('companionToken');
return { saved: false, error: PAIRING_REQUIRED };
}
if (!response.ok) return { saved: false, error: authErrorMessage(response.status, data) };
return {
saved: data?.saved ?? true,
duplicate: data?.duplicate ?? false,
frameId: data?.frameId,
};
} catch (err) {
return { saved: false, error: String(err) };
}
}
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
(async () => {
if (message?.type === 'health') sendResponse(await health());
else if (message?.type === 'pair') sendResponse(await pairWithCode(message.code));
else if (message?.type === 'save-memory') sendResponse(await saveMemory(message));
else sendResponse({ error: 'unknown message type' });
})();
return true;
});
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'waggle-save-selection',
title: 'Save to Waggle memory',
contexts: ['selection'],
});
});
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId !== 'waggle-save-selection' || !info.selectionText) return;
const result = await saveMemory({
content: `Selection from ${tab?.title || tab?.url}\n\n${info.selectionText}`,
source: 'import',
importance: 'normal',
});
await chrome.action.setBadgeText({ text: result.saved ? '✓' : '!' });
await chrome.action.setBadgeBackgroundColor({ color: result.saved ? '#10b981' : '#ef4444' });
setTimeout(() => chrome.action.setBadgeText({ text: '' }), 2500);
});