Fix cross-browser GitHub Gist sync when encryption is enabled

Two bugs prevented sync from working between different browsers
sharing the same GitHub token and encryption password:

1. pullFromGist failed immediately with "No Gist ID stored" on any
   browser that had not previously pushed, because ss_gist_id lives
   in browser.storage.local (isolated per browser). Fix: when no
   local Gist ID is found, search the authenticated user's Gists for
   one containing silent-send-sync.json and cache the result.

2. When the pulled data was encrypted with a different salt (each
   browser generates its own random salt on setup), pullFromGist
   returned needsAuth:true but the UI never set
   window.__ssPendingSyncImport, so the auth prompt fell through to
   reverifyWithPassword instead of authenticateForSync, and the pull
   was never retried after the user entered their password. Fix: set
   window.__ssPendingSyncImport before showing the auth prompt for
   both Gist pull and custom-URL pull, matching how sync-code import
   already handled this flow.

https://claude.ai/code/session_01QJnEnLfbXKR5FSCQ3Qfs53
This commit is contained in:
Claude
2026-03-31 20:55:12 +00:00
parent 7a128b9f10
commit 3420299f51
2 changed files with 68 additions and 4 deletions
+32 -2
View File
@@ -752,12 +752,42 @@ const SilentSendSync = {
} }
}, },
/**
* Search the authenticated user's Gists for one containing silent-send-sync.json.
* Returns the Gist ID string, or null if not found.
*/
async _findSyncGistId(token) {
try {
let page = 1;
while (page <= 5) {
const resp = await fetch(`https://api.github.com/gists?per_page=100&page=${page}`, {
headers: { Authorization: `token ${token}` },
});
if (!resp.ok) return null;
const gists = await resp.json();
if (!gists.length) break;
for (const gist of gists) {
if (gist.files?.['silent-send-sync.json']) return gist.id;
}
if (gists.length < 100) break;
page++;
}
return null;
} catch { return null; }
},
async pullFromGist(token) { async pullFromGist(token) {
if (!token) return { success: false, reason: 'No GitHub token provided.' }; if (!token) return { success: false, reason: 'No GitHub token provided.' };
try { try {
const stored = await api.storage.local.get('ss_gist_id'); const stored = await api.storage.local.get('ss_gist_id');
const gistId = stored.ss_gist_id; let gistId = stored.ss_gist_id;
if (!gistId) return { success: false, reason: 'No Gist ID stored. Push first.' };
if (!gistId) {
// No locally-stored Gist ID — search the account for one
gistId = await this._findSyncGistId(token);
if (!gistId) return { success: false, reason: 'No sync Gist found. Push from the source browser first.' };
await api.storage.local.set({ ss_gist_id: gistId });
}
const resp = await fetch(`https://api.github.com/gists/${gistId}`, { const resp = await fetch(`https://api.github.com/gists/${gistId}`, {
headers: { Authorization: `token ${token}` }, headers: { Authorization: `token ${token}` },
+36 -2
View File
@@ -189,7 +189,24 @@ document.addEventListener('DOMContentLoaded', async () => {
const r = await SilentSendSync.pullFromGist(token); const r = await SilentSendSync.pullFromGist(token);
if (r.needsAuth) { if (r.needsAuth) {
setGistSyncStatus('Authentication required to decrypt.', 'warn'); setGistSyncStatus('Authentication required to decrypt.', 'warn');
showSyncAuthPrompt(); window.__ssPendingSyncImport = async () => {
const r2 = await SilentSendSync.pullFromGist(token);
if (r2.needsAuth) {
setGistSyncStatus('Authentication failed — wrong password?', 'error');
} else if (!r2.success) {
setGistSyncStatus('Pull failed: ' + r2.reason, 'error');
} else if (r2.imported) {
setGistSyncStatus(`Pulled (${r2.time}). Refreshing…`, 'ok');
mappings = await Storage.getMappings();
settings = await Storage.getSettings();
renderMappings();
renderDomains();
renderLog();
} else {
setGistSyncStatus('Already up to date.', 'ok');
}
};
showSyncAuthPrompt('decrypt');
} else if (!r.success) { } else if (!r.success) {
setGistSyncStatus('Pull failed: ' + r.reason, 'error'); setGistSyncStatus('Pull failed: ' + r.reason, 'error');
} else if (r.imported) { } else if (r.imported) {
@@ -229,7 +246,24 @@ document.addEventListener('DOMContentLoaded', async () => {
const r = await SilentSendSync.pullFromUrl({ url, headers }); const r = await SilentSendSync.pullFromUrl({ url, headers });
if (r.needsAuth) { if (r.needsAuth) {
setUrlSyncStatus('Authentication required to decrypt.', 'warn'); setUrlSyncStatus('Authentication required to decrypt.', 'warn');
showSyncAuthPrompt(); window.__ssPendingSyncImport = async () => {
const r2 = await SilentSendSync.pullFromUrl({ url, headers });
if (r2.needsAuth) {
setUrlSyncStatus('Authentication failed — wrong password?', 'error');
} else if (!r2.success) {
setUrlSyncStatus('Pull failed: ' + r2.reason, 'error');
} else if (r2.imported) {
setUrlSyncStatus(`Pulled (${r2.time}). Refreshing…`, 'ok');
mappings = await Storage.getMappings();
settings = await Storage.getSettings();
renderMappings();
renderDomains();
renderLog();
} else {
setUrlSyncStatus('Already up to date.', 'ok');
}
};
showSyncAuthPrompt('decrypt');
} else if (!r.success) { } else if (!r.success) {
setUrlSyncStatus('Pull failed: ' + r.reason, 'error'); setUrlSyncStatus('Pull failed: ' + r.reason, 'error');
} else if (r.imported) { } else if (r.imported) {