diff --git a/manifest.firefox.json b/manifest.firefox.json index 15b8772..ed6b379 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "0.9.32", + "version": "0.9.33", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "browser_specific_settings": { "gecko": { diff --git a/manifest.json b/manifest.json index 1d4c40c..f95f476 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "0.9.32", + "version": "0.9.33", "description": "Intercepts personal info and substitutes it with user-defined replacements before sending to AI services.", "permissions": [ "storage", diff --git a/package.json b/package.json index 2eb1169..70a70a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silent-send", - "version": "0.9.32", + "version": "0.9.33", "private": true, "license": "MIT", "description": "Browser extension that substitutes personal data before sending to AI services", diff --git a/src/background/service-worker.js b/src/background/service-worker.js index f7c0963..b8d5a4b 100644 --- a/src/background/service-worker.js +++ b/src/background/service-worker.js @@ -184,6 +184,23 @@ const messageHandlers = { sendResponse({ locked }); }, + // Content script requests decrypted data on page load when it sees + // encrypted storage but the key is already cached (e.g. after a sync import). + async 'vault:request-unlock'(_message, sender) { + const locked = await Storage.isLocked(); + if (locked) return; // key not available — user must unlock via popup + + const mappings = await Storage.getMappings(); + const identity = await Storage.getIdentity(); + const settings = await Storage.getSettings(); + api.tabs.sendMessage(sender.tab.id, { + type: 'vault:unlocked', + mappings, + identity, + settings, + }).catch(() => {}); + }, + async 'vault:unlocked'() { // User unlocked the vault — clear the LOCK badge and refresh icon api.action.setBadgeText({ text: '' }); diff --git a/src/content/injector.js b/src/content/injector.js index 03c9dc7..9ec3f47 100644 --- a/src/content/injector.js +++ b/src/content/injector.js @@ -60,13 +60,19 @@ const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']); const settings = result.ss_settings || { enabled: true }; - // Check if data is encrypted (locked) — pass empty config + // Check if data is encrypted — pass empty config and request decryption // The background will send decrypted data via vault:unlocked when ready const isLocked = result.ss_mappings?._ssLocalEncrypted || result.ss_identity?._ssLocalEncrypted; const mappings = isLocked ? [] : (result.ss_mappings || []); const identityData = isLocked ? {} : (result.ss_identity || {}); + // If data is encrypted but the vault key may already be cached + // (e.g. after a sync import), ask the background to push decrypted data + if (isLocked) { + api.runtime.sendMessage({ type: 'vault:request-unlock' }).catch(() => {}); + } + // Merge active profiles into a flat identity object for the content script const identity = mergeProfiles(identityData); diff --git a/src/lib/sync.js b/src/lib/sync.js index 9785714..c5f8792 100644 --- a/src/lib/sync.js +++ b/src/lib/sync.js @@ -901,7 +901,7 @@ const SilentSendSync = { const result = await api.storage.local.get('ss_lastModified'); return { version: '1', - lastModified: result.ss_lastModified || Date.now(), + lastModified: result.ss_lastModified || 0, identity: identity || {}, mappings: mappings || [], settings: settings || {}, @@ -924,9 +924,12 @@ const SilentSendSync = { } // Metadata stays plaintext + const now = Date.now(); await api.storage.local.set({ ss_lastModified: data.lastModified, - ss_sync_notification: { source, time: Date.now() }, + ss_sync_notification: { source, time: now }, + ss_last_pull_time: now, + ss_last_pull_source: source, }); }, diff --git a/src/options/options.js b/src/options/options.js index 78d0d4d..9123539 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -161,9 +161,13 @@ document.addEventListener('DOMContentLoaded', async () => { // --- GitHub Gist sync --- // Restore saved token (session only — never persisted to storage) { - const stored = await api.storage.local.get('ss_gist_id'); + const stored = await api.storage.local.get(['ss_gist_id', 'ss_last_pull_time', 'ss_last_pull_source']); if (stored.ss_gist_id) { - setGistSyncStatus(`Gist ID: ${stored.ss_gist_id.slice(0, 12)}…`, 'ok'); + let status = `Gist ID: ${stored.ss_gist_id.slice(0, 12)}…`; + if (stored.ss_last_pull_time && stored.ss_last_pull_source === 'gist') { + status += ` · Last pulled: ${new Date(stored.ss_last_pull_time).toLocaleString()}`; + } + setGistSyncStatus(status, 'ok'); } } @@ -197,6 +201,7 @@ document.addEventListener('DOMContentLoaded', async () => { setGistSyncStatus('Pull failed: ' + r2.reason, 'error'); } else if (r2.imported) { setGistSyncStatus(`Pulled (${r2.time}). Refreshing…`, 'ok'); + api.runtime.sendMessage({ type: 'vault:unlocked' }).catch(() => {}); mappings = await Storage.getMappings(); settings = await Storage.getSettings(); renderMappings(); @@ -211,6 +216,7 @@ document.addEventListener('DOMContentLoaded', async () => { setGistSyncStatus('Pull failed: ' + r.reason, 'error'); } else if (r.imported) { setGistSyncStatus(`Pulled (${r.time}). Refreshing…`, 'ok'); + api.runtime.sendMessage({ type: 'vault:unlocked' }).catch(() => {}); mappings = await Storage.getMappings(); settings = await Storage.getSettings(); renderMappings(); @@ -254,6 +260,7 @@ document.addEventListener('DOMContentLoaded', async () => { setUrlSyncStatus('Pull failed: ' + r2.reason, 'error'); } else if (r2.imported) { setUrlSyncStatus(`Pulled (${r2.time}). Refreshing…`, 'ok'); + api.runtime.sendMessage({ type: 'vault:unlocked' }).catch(() => {}); mappings = await Storage.getMappings(); settings = await Storage.getSettings(); renderMappings(); @@ -268,6 +275,7 @@ document.addEventListener('DOMContentLoaded', async () => { setUrlSyncStatus('Pull failed: ' + r.reason, 'error'); } else if (r.imported) { setUrlSyncStatus(`Pulled (${r.time}). Refreshing…`, 'ok'); + api.runtime.sendMessage({ type: 'vault:unlocked' }).catch(() => {}); mappings = await Storage.getMappings(); settings = await Storage.getSettings(); renderMappings();