diff --git a/VIBE/CHAT_LOG.md b/VIBE/CHAT_LOG.md index 7221178..0d9ca70 100644 --- a/VIBE/CHAT_LOG.md +++ b/VIBE/CHAT_LOG.md @@ -1,5 +1,11 @@ # Chat Log +### User +Implement SA-006 permission changes: remove `` and unnecessary `tabs`, request exact access to the configured self-hosted backend, and keep page capture behind `activeTab`. + +### Assistant outcome +Updated the Firefox extension manifest to use `activeTab` and `storage` only, with Firefox-compatible optional HTTP/HTTPS host permissions. The options login page now normalizes the backend to an origin and requests only that origin during the user-initiated login. The popup checks that exact grant before session validation or link submission, so it does not perform backend calls without permission. + ## 2026-08-24 ### User diff --git a/VIBE/PROMPTS.md b/VIBE/PROMPTS.md index b98e905..3aa52a0 100644 --- a/VIBE/PROMPTS.md +++ b/VIBE/PROMPTS.md @@ -181,6 +181,7 @@ 161. The popup still shows the sign-in block even though the authenticated session text is displayed; show the block only when signed out. 162. The authenticated session text and sign-in block are still shown together. 163. Use the VIBE directory to log interactions. +164. Implement SA-006: remove broad Firefox extension host access and unnecessary tabs permission, request exact configured backend origin access, and keep page capture behind activeTab. ## Future entries diff --git a/XPI/unsigned/LinkLog-0.1.0.xpi b/XPI/unsigned/LinkLog-0.1.0.xpi index 648866e..69c179e 100644 Binary files a/XPI/unsigned/LinkLog-0.1.0.xpi and b/XPI/unsigned/LinkLog-0.1.0.xpi differ diff --git a/webextension/manifest.json b/webextension/manifest.json index e2d42b5..3e9f104 100644 --- a/webextension/manifest.json +++ b/webextension/manifest.json @@ -6,11 +6,11 @@ "default_locale": "en-US", "permissions": [ "activeTab", - "storage", - "tabs" + "storage" ], - "host_permissions": [ - "" + "optional_permissions": [ + "http://*/*", + "https://*/*" ], "action": { "default_title": "__MSG_extensionName__", diff --git a/webextension/options.js b/webextension/options.js index 12480df..0948442 100644 --- a/webextension/options.js +++ b/webextension/options.js @@ -13,6 +13,34 @@ const signOutButton = document.getElementById('sign-out'); const t = window.linklogI18n; +function normalizeBackendOrigin(backendUrl) { + try { + const url = new URL(backendUrl); + if (!['http:', 'https:'].includes(url.protocol)) { + return null; + } + return url.origin; + } catch (error) { + return null; + } +} + +async function hasBackendPermission(backendUrl) { + const origin = normalizeBackendOrigin(backendUrl); + if (!origin) return false; + return browser.permissions.contains({origins: [`${origin}/*`]}); +} + +async function requestBackendPermission(backendUrl) { + const origin = normalizeBackendOrigin(backendUrl); + if (!origin) return false; + try { + return await browser.permissions.request({origins: [`${origin}/*`]}); + } catch (error) { + return false; + } +} + function setStatus(message, isError = false) { statusEl.textContent = message; statusEl.classList.remove('hidden'); @@ -25,7 +53,7 @@ async function loadSettings() { backendUrlInput.value = settings.backendUrl || ''; emailInput.value = settings.email || ''; - if (settings.accessToken && settings.backendUrl) { + if (settings.accessToken && settings.backendUrl && await hasBackendPermission(settings.backendUrl)) { try { const response = await fetch( `${settings.backendUrl}/api/auth/me`, @@ -61,7 +89,7 @@ async function clearSession() { async function saveSettingsAndLogin(event) { event.preventDefault(); - const backendUrl = backendUrlInput.value.trim(); + const backendUrl = normalizeBackendOrigin(backendUrlInput.value.trim()); const email = emailInput.value.trim(); const password = passwordInput.value; const otp = otpInput.value.trim(); @@ -71,6 +99,11 @@ async function saveSettingsAndLogin(event) { return; } + if (!(await requestBackendPermission(backendUrl))) { + setStatus(t('unableToLogIn'), true); + return; + } + try { const response = await fetch(`${backendUrl}/api/auth/login`, { method: 'POST', @@ -104,8 +137,9 @@ async function saveSettingsAndLogin(event) { async function signOut() { const settings = await browser.storage.local.get(['accessToken']); - if (settings.accessToken) { - await fetch(`${backendUrlInput.value.trim()}/api/auth/logout`, { + const backendUrl = normalizeBackendOrigin(backendUrlInput.value.trim()); + if (settings.accessToken && await hasBackendPermission(backendUrl)) { + await fetch(`${backendUrl}/api/auth/logout`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: settings.accessToken }), diff --git a/webextension/popup.js b/webextension/popup.js index 3a538a4..97761ac 100644 --- a/webextension/popup.js +++ b/webextension/popup.js @@ -16,6 +16,24 @@ const authSession = document.getElementById('auth-session'); const t = window.linklogI18n; +function normalizeBackendOrigin(backendUrl) { + try { + const url = new URL(backendUrl); + if (!['http:', 'https:'].includes(url.protocol)) { + return null; + } + return url.origin; + } catch (error) { + return null; + } +} + +async function hasBackendPermission(backendUrl) { + const origin = normalizeBackendOrigin(backendUrl); + if (!origin) return false; + return browser.permissions.contains({origins: [`${origin}/*`]}); +} + function setStatus(message, isError = false) { statusEl.textContent = message; statusEl.classList.remove('hidden'); @@ -33,7 +51,7 @@ async function getSettings() { } async function validateSession(settings) { - if (!settings.backendUrl || !settings.accessToken) return null; + if (!settings.backendUrl || !settings.accessToken || !(await hasBackendPermission(settings.backendUrl))) return null; try { const response = await fetch(`${settings.backendUrl}/api/auth/me`, { headers: {'Authorization': `Bearer ${settings.accessToken}`}, @@ -157,7 +175,7 @@ async function populateCurrentTab() { async function checkExistingLink() { const settings = await getSettings(); - if (!settings.backendUrl || !settings.accessToken || !titleInput.value || !urlInput.value) return; + if (!settings.backendUrl || !settings.accessToken || !(await hasBackendPermission(settings.backendUrl)) || !titleInput.value || !urlInput.value) return; try { const response = await fetch(`${settings.backendUrl}/api/links/check?${new URLSearchParams({ title: titleInput.value, @@ -181,7 +199,7 @@ async function handleSubmit(event) { const token = settings.accessToken; const backendUrl = settings.backendUrl; - if (!token || !backendUrl) { + if (!token || !backendUrl || !(await hasBackendPermission(backendUrl))) { setStatus(t('configureAndLogIn'), true); browser.runtime.openOptionsPage(); return;