diff --git a/manifest.firefox.json b/manifest.firefox.json index ed6b379..c8bac70 100644 --- a/manifest.firefox.json +++ b/manifest.firefox.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "0.9.33", + "version": "0.9.34", "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 f95f476..c6a8c15 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Silent Send", - "version": "0.9.33", + "version": "0.9.34", "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 70a70a5..25c703f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "silent-send", - "version": "0.9.33", + "version": "0.9.34", "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 b8d5a4b..f0f35a0 100644 --- a/src/background/service-worker.js +++ b/src/background/service-worker.js @@ -472,6 +472,20 @@ api.alarms.onAlarm.addListener(async (alarm) => { const result = await SilentSendSync.performAutoSync(); if (result.pulled) { console.log('[Silent Send] Auto-sync pulled new data'); + // Broadcast decrypted data to all open content scripts so + // substitution works immediately without a page reload. + const mappings = await Storage.getMappings(); + const identity = await Storage.getIdentity(); + const settings = await Storage.getSettings(); + const allPatterns = [...BUILTIN_URL_PATTERNS]; + const customDomains = settings.customDomains || []; + for (const domain of customDomains) allPatterns.push(domain + '/*'); + for (const urlPattern of allPatterns) { + const tabs = await api.tabs.query({ url: urlPattern }).catch(() => []); + for (const tab of tabs) { + api.tabs.sendMessage(tab.id, { type: 'vault:unlocked', mappings, identity, settings }).catch(() => {}); + } + } } if (result.error) { console.warn('[Silent Send] Auto-sync error:', result.error); diff --git a/src/lib/storage.js b/src/lib/storage.js index 23b4122..ea9a7ef 100644 --- a/src/lib/storage.js +++ b/src/lib/storage.js @@ -226,7 +226,19 @@ const Storage = { }, async saveProfiles(profiles) { - await this._writeSecure(KEYS.IDENTITY, { profiles }, { ss_lastModified: Date.now() }); + // Only advance ss_lastModified if at least one profile contains real PII. + // Creating the default empty profile on a fresh install should not count + // as "this browser has data" — that would make the pull timestamp check + // think remote Gist data is stale and skip the import. + const hasRealData = (profiles || []).some(p => + (p.names || []).some(n => n.real?.trim()) || + (p.emails || []).some(e => e.real?.trim()) || + (p.usernames || []).some(u => u.real?.trim()) || + (p.phones || []).some(ph => ph.real?.trim()) || + p.catchAllEmail?.trim() + ); + const extras = hasRealData ? { ss_lastModified: Date.now() } : {}; + await this._writeSecure(KEYS.IDENTITY, { profiles }, extras); }, async addProfile(name) { diff --git a/src/lib/sync.js b/src/lib/sync.js index c5f8792..2a62e64 100644 --- a/src/lib/sync.js +++ b/src/lib/sync.js @@ -709,6 +709,12 @@ const SilentSendSync = { try { const data = await this._getAllData(); + // Don't push if this browser has no saved data — it would overwrite + // real data on the Gist with an empty payload. + if (data.lastModified === 0) { + return { success: false, reason: 'Nothing to push — no data has been saved on this browser yet. Pull first.' }; + } + // Encrypt if enabled const encResult = await this._encryptForSync(data); if (encResult.needsAuth) { @@ -745,7 +751,8 @@ const SilentSendSync = { } const json = await resp.json(); - await api.storage.local.set({ ss_gist_id: json.id }); + const now = Date.now(); + await api.storage.local.set({ ss_gist_id: json.id, ss_last_push_time: now, ss_last_push_source: 'gist' }); return { success: true, gistId: json.id }; } catch (e) { return { success: false, reason: e.message }; @@ -804,9 +811,14 @@ const SilentSendSync = { const rawResp = await fetch(file.raw_url); let data = JSON.parse(await rawResp.text()); - // Check if new data exists before requiring auth + // Check if new data exists before requiring auth. + // remoteMod === 0 means the Gist was clobbered by an empty push — treat + // as stale rather than skipping so the user sees a useful error. const local = await this._getAllData(); - const remoteMod = data._ssEncrypted ? data.lastModified : data.lastModified; + const remoteMod = data.lastModified || 0; + if (remoteMod === 0) { + return { success: false, reason: 'Gist contains no data (timestamp is 0). Push from the source browser first.' }; + } if (remoteMod <= (local.lastModified || 0)) { return { success: true, imported: false }; } @@ -839,6 +851,10 @@ const SilentSendSync = { try { const data = await this._getAllData(); + if (data.lastModified === 0) { + return { success: false, reason: 'Nothing to push — no data has been saved on this browser yet. Pull first.' }; + } + const encResult = await this._encryptForSync(data); if (encResult.needsAuth) { return { success: false, needsAuth: true, reason: 'Authentication required.' }; @@ -1032,9 +1048,9 @@ const SilentSendSync = { const pullResult = await this.pullFromGist(config.gistToken); if (pullResult.success && pullResult.imported) pulled = true; - // Push if local data changed since last push + // Push if local data changed since last push (skip if no real data yet) const local = await this._getAllData(); - if (!config.lastPush || local.lastModified > config.lastPush) { + if (local.lastModified > 0 && (!config.lastPush || local.lastModified > config.lastPush)) { const pushResult = await this.pushToGist(config.gistToken); if (pushResult.success) { pushed = true; @@ -1047,9 +1063,9 @@ const SilentSendSync = { const pullResult = await this.pullFromUrl({ url: config.url, headers }); if (pullResult.success && pullResult.imported) pulled = true; - // Push if local data changed since last push + // Push if local data changed since last push (skip if no real data yet) const local = await this._getAllData(); - if (!config.lastPush || local.lastModified > config.lastPush) { + if (local.lastModified > 0 && (!config.lastPush || local.lastModified > config.lastPush)) { const pushResult = await this.pushToUrl({ url: config.url, method: config.httpMethod || 'PUT', diff --git a/src/options/options.js b/src/options/options.js index 9123539..313aedf 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -161,11 +161,17 @@ 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', 'ss_last_pull_time', 'ss_last_pull_source']); + const stored = await api.storage.local.get([ + 'ss_gist_id', 'ss_last_pull_time', 'ss_last_pull_source', + 'ss_last_push_time', 'ss_last_push_source', + ]); if (stored.ss_gist_id) { let status = `Gist ID: ${stored.ss_gist_id.slice(0, 12)}…`; + if (stored.ss_last_push_time && stored.ss_last_push_source === 'gist') { + status += ` · Pushed: ${new Date(stored.ss_last_push_time).toLocaleString()}`; + } if (stored.ss_last_pull_time && stored.ss_last_pull_source === 'gist') { - status += ` · Last pulled: ${new Date(stored.ss_last_pull_time).toLocaleString()}`; + status += ` · Pulled: ${new Date(stored.ss_last_pull_time).toLocaleString()}`; } setGistSyncStatus(status, 'ok'); }