-
Notifications
You must be signed in to change notification settings - Fork 0
Add Firebase-based cloud backup/restore for bookmark data #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -24,6 +24,29 @@ <h1>Bookmarks</h1> | |||||||||||||||||||||||||||||
| <div class="sidebar"> | ||||||||||||||||||||||||||||||
| <h2>Google Apps</h2> | ||||||||||||||||||||||||||||||
| <div id="googleApps" class="apps"></div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <div class="cloud-panel"> | ||||||||||||||||||||||||||||||
| <h3>Firebase Backup</h3> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <input id="firebaseApiKey" type="text" placeholder="Firebase Web API Key"> | ||||||||||||||||||||||||||||||
| <input id="firebaseProjectId" type="text" placeholder="Firebase Project ID"> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <input id="authEmail" type="email" placeholder="Email"> | ||||||||||||||||||||||||||||||
| <input id="authPassword" type="password" placeholder="Password"> | ||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inputs lack Screen readers may not announce placeholder-only inputs correctly, and placeholders disappear once the user starts typing, making it hard to recall what each field expects. Add visible or ♿ Proposed fix (example for one input)+ <label for="firebaseApiKey">Firebase Web API Key</label>
<input id="firebaseApiKey" type="text" placeholder="Firebase Web API Key">
+ <label for="firebaseProjectId">Firebase Project ID</label>
<input id="firebaseProjectId" type="text" placeholder="Firebase Project ID">
+ <label for="authEmail">Email</label>
<input id="authEmail" type="email" placeholder="Email">
+ <label for="authPassword">Password</label>
<input id="authPassword" type="password" placeholder="Password">📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <div class="cloud-actions"> | ||||||||||||||||||||||||||||||
| <button id="signUpBtn" class="small-btn">Sign Up</button> | ||||||||||||||||||||||||||||||
| <button id="signInBtn" class="small-btn">Sign In</button> | ||||||||||||||||||||||||||||||
| <button id="signOutBtn" class="small-btn">Sign Out</button> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <div class="cloud-actions"> | ||||||||||||||||||||||||||||||
| <button id="backupNowBtn" class="small-btn">Backup to Cloud</button> | ||||||||||||||||||||||||||||||
| <button id="restoreNowBtn" class="small-btn">Restore from Cloud</button> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <p id="cloudStatus" class="cloud-status">Not connected</p> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| </div> <!-- END CONTAINER --> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,12 +10,16 @@ let openFolders = {}; | |||||||||||||||||||||
| let dragSourceTile = null; | ||||||||||||||||||||||
| let dragPlaceholder = null; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let firebaseConfigState = { apiKey: "", projectId: "" }; | ||||||||||||||||||||||
| let firebaseAuthState = { idToken: "", localId: "", email: "" }; | ||||||||||||||||||||||
| let lastUpdatedAt = Date.now(); | ||||||||||||||||||||||
|
Comment on lines
+13
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Firebase auth token stored in
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // --------------------------------------------------------- | ||||||||||||||||||||||
| // LOAD EVERYTHING | ||||||||||||||||||||||
| // --------------------------------------------------------- | ||||||||||||||||||||||
| function loadAll() { | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| chrome.storage.sync.get(["bookmarks", "folders", "openFolders"], res => { | ||||||||||||||||||||||
| chrome.storage.sync.get(["bookmarks", "folders", "openFolders", "firebaseConfig", "firebaseAuth", "lastUpdatedAt"], async res => { | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const raw = res.bookmarks || []; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -35,11 +39,18 @@ function loadAll() { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| chrome.storage.sync.set({ bookmarks, folders, openFolders }); | ||||||||||||||||||||||
| firebaseConfigState = res.firebaseConfig || { apiKey: "", projectId: "" }; | ||||||||||||||||||||||
| lastUpdatedAt = res.lastUpdatedAt || Date.now(); | ||||||||||||||||||||||
| firebaseAuthState = res.firebaseAuth || { idToken: "", localId: "", email: "" }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| chrome.storage.sync.set({ bookmarks, folders, openFolders, firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState, lastUpdatedAt }); | ||||||||||||||||||||||
|
Comment on lines
+42
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 43: if Default to 🐛 Proposed fix- lastUpdatedAt = res.lastUpdatedAt || Date.now();
+ lastUpdatedAt = res.lastUpdatedAt || 0;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| renderFolders(); | ||||||||||||||||||||||
| populateFolderSelect(); | ||||||||||||||||||||||
| renderGoogleApps(); | ||||||||||||||||||||||
| initializeCloudUI(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| await restoreFromCloud(false); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| loadAll(); | ||||||||||||||||||||||
|
|
@@ -48,7 +59,9 @@ loadAll(); | |||||||||||||||||||||
| // SAVE ALL | ||||||||||||||||||||||
| // --------------------------------------------------------- | ||||||||||||||||||||||
| function saveAll() { | ||||||||||||||||||||||
| chrome.storage.sync.set({ bookmarks, folders, openFolders }); | ||||||||||||||||||||||
| lastUpdatedAt = Date.now(); | ||||||||||||||||||||||
| chrome.storage.sync.set({ bookmarks, folders, openFolders, firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState, lastUpdatedAt }); | ||||||||||||||||||||||
| backupToCloud(false); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // --------------------------------------------------------- | ||||||||||||||||||||||
|
|
@@ -584,3 +597,198 @@ function renderGoogleApps() { | |||||||||||||||||||||
| div.appendChild(item); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // --------------------------------------------------------- | ||||||||||||||||||||||
| // FIREBASE CLOUD BACKUP (REST API) | ||||||||||||||||||||||
| // --------------------------------------------------------- | ||||||||||||||||||||||
| function initializeCloudUI() { | ||||||||||||||||||||||
| const apiKeyInput = document.getElementById("firebaseApiKey"); | ||||||||||||||||||||||
| const projectIdInput = document.getElementById("firebaseProjectId"); | ||||||||||||||||||||||
| const emailInput = document.getElementById("authEmail"); | ||||||||||||||||||||||
| const passwordInput = document.getElementById("authPassword"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (!apiKeyInput || !projectIdInput) return; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| apiKeyInput.value = firebaseConfigState.apiKey || ""; | ||||||||||||||||||||||
| projectIdInput.value = firebaseConfigState.projectId || ""; | ||||||||||||||||||||||
| emailInput.value = firebaseAuthState.email || ""; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| apiKeyInput.addEventListener("change", () => { | ||||||||||||||||||||||
| firebaseConfigState.apiKey = apiKeyInput.value.trim(); | ||||||||||||||||||||||
| persistCloudState(); | ||||||||||||||||||||||
| setCloudStatus("Firebase config updated."); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| projectIdInput.addEventListener("change", () => { | ||||||||||||||||||||||
| firebaseConfigState.projectId = projectIdInput.value.trim(); | ||||||||||||||||||||||
| persistCloudState(); | ||||||||||||||||||||||
| setCloudStatus("Firebase config updated."); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| document.getElementById("signUpBtn")?.addEventListener("click", () => authWithFirebase("signUp")); | ||||||||||||||||||||||
| document.getElementById("signInBtn")?.addEventListener("click", () => authWithFirebase("signInWithPassword")); | ||||||||||||||||||||||
| document.getElementById("signOutBtn")?.addEventListener("click", signOutFirebase); | ||||||||||||||||||||||
| document.getElementById("backupNowBtn")?.addEventListener("click", () => backupToCloud(true)); | ||||||||||||||||||||||
| document.getElementById("restoreNowBtn")?.addEventListener("click", () => restoreFromCloud(true)); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| updateCloudStatusFromState(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function persistCloudState() { | ||||||||||||||||||||||
| chrome.storage.sync.set({ firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function setCloudStatus(message, isError = false) { | ||||||||||||||||||||||
| const status = document.getElementById("cloudStatus"); | ||||||||||||||||||||||
| if (!status) return; | ||||||||||||||||||||||
| status.textContent = message; | ||||||||||||||||||||||
| status.style.color = isError ? "#fca5a5" : "#93c5fd"; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function updateCloudStatusFromState() { | ||||||||||||||||||||||
| if (firebaseAuthState.email) { | ||||||||||||||||||||||
| setCloudStatus(`Connected as ${firebaseAuthState.email}`); | ||||||||||||||||||||||
| } else { | ||||||||||||||||||||||
| setCloudStatus("Not connected"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function cloudConfigReady() { | ||||||||||||||||||||||
| return Boolean(firebaseConfigState.apiKey && firebaseConfigState.projectId); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function cloudAuthReady() { | ||||||||||||||||||||||
| return Boolean(firebaseAuthState.idToken && firebaseAuthState.localId); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function cloudPayload() { | ||||||||||||||||||||||
| return { | ||||||||||||||||||||||
| bookmarks, | ||||||||||||||||||||||
| folders, | ||||||||||||||||||||||
| openFolders, | ||||||||||||||||||||||
| updatedAt: lastUpdatedAt | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async function authWithFirebase(mode) { | ||||||||||||||||||||||
| const email = document.getElementById("authEmail")?.value.trim(); | ||||||||||||||||||||||
| const password = document.getElementById("authPassword")?.value.trim(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (!cloudConfigReady()) return setCloudStatus("Add Firebase API Key + Project ID first.", true); | ||||||||||||||||||||||
| if (!email || !password) return setCloudStatus("Enter email and password.", true); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| const endpoint = `https://identitytoolkit.googleapis.com/v1/accounts:${mode}?key=${firebaseConfigState.apiKey}`; | ||||||||||||||||||||||
| const res = await fetch(endpoint, { | ||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||
| headers: { "Content-Type": "application/json" }, | ||||||||||||||||||||||
| body: JSON.stringify({ email, password, returnSecureToken: true }) | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const data = await res.json(); | ||||||||||||||||||||||
| if (!res.ok) throw new Error(data.error?.message || "Authentication failed"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| firebaseAuthState = { idToken: data.idToken, localId: data.localId, email: data.email || email }; | ||||||||||||||||||||||
| persistCloudState(); | ||||||||||||||||||||||
| updateCloudStatusFromState(); | ||||||||||||||||||||||
| await restoreFromCloud(false); | ||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||
| setCloudStatus(`Auth failed: ${err.message}`, true); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+674
to
+699
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Firebase (Identity Toolkit / Firebase Auth) refresh-token exchange is documented under “Exchange a refresh token for an ID token” and uses the Secure Token API:
Example (from the docs): curl 'https://securetoken.googleapis.com/v1/token?key=[API_KEY]' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=refresh_token&refresh_token=[REFRESH_TOKEN]'Sources: Firebase Auth REST API docs [1], Google Cloud Identity Platform REST docs [2]. 🏁 Script executed: # Search for refreshToken in the codebase
rg "refreshToken" -i --type js
# Check the firebaseAuthState structure and its usage
rg "firebaseAuthState" --type js -A 3 -B 1
# Look for token-related code around the auth function
rg "idToken|token" script.js -i -B 2 -A 2Repository: rishnudk/BookmarkFolder Length of output: 4641 🏁 Script executed: # Check for 401 error handling in cloud operations
rg "401|unauthorized|token.*expire|Bearer" script.js -i -B 2 -A 2
# Look at the full backupToCloud and restoreFromCloud functions
rg "async function (backupToCloud|restoreFromCloud)" -A 30 script.jsRepository: rishnudk/BookmarkFolder Length of output: 2294 🌐 Web query:
💡 Result: Firebase Authentication ID tokens expire after 1 hour (≈ 3600 seconds) — i.e., 1 hour. The client SDKs automatically refresh them (using the refresh token) about every hour. [1], [2] (If you meant custom tokens you mint server-side: their 🌐 Web query:
💡 Result: For Firebase Auth / Identity Toolkit REST (Identity Platform), the
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function signOutFirebase() { | ||||||||||||||||||||||
| firebaseAuthState = { idToken: "", localId: "", email: "" }; | ||||||||||||||||||||||
| persistCloudState(); | ||||||||||||||||||||||
| updateCloudStatusFromState(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function cloudDocumentUrl() { | ||||||||||||||||||||||
| return `https://firestore.googleapis.com/v1/projects/${firebaseConfigState.projectId}/databases/(default)/documents/bookmarkfolderUsers/${firebaseAuthState.localId}`; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async function backupToCloud(showMessage = true) { | ||||||||||||||||||||||
| if (!cloudConfigReady() || !cloudAuthReady()) { | ||||||||||||||||||||||
| if (showMessage) setCloudStatus("Cloud backup skipped: sign in first.", true); | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| const payload = cloudPayload(); | ||||||||||||||||||||||
| const body = { | ||||||||||||||||||||||
| fields: { | ||||||||||||||||||||||
| data: { stringValue: JSON.stringify(payload) }, | ||||||||||||||||||||||
| updatedAt: { integerValue: String(payload.updatedAt) } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const res = await fetch(cloudDocumentUrl(), { | ||||||||||||||||||||||
| method: "PATCH", | ||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||
| "Content-Type": "application/json", | ||||||||||||||||||||||
| Authorization: `Bearer ${firebaseAuthState.idToken}` | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| body: JSON.stringify(body) | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const data = await res.json(); | ||||||||||||||||||||||
| if (!res.ok) throw new Error(data.error?.message || "Backup failed"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (showMessage) setCloudStatus("Backup complete."); | ||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||
| setCloudStatus(`Backup failed: ${err.message}`, true); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| async function restoreFromCloud(showMessage = true) { | ||||||||||||||||||||||
| if (!cloudConfigReady() || !cloudAuthReady()) { | ||||||||||||||||||||||
| if (showMessage) setCloudStatus("Cloud restore skipped: sign in first.", true); | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| const res = await fetch(cloudDocumentUrl(), { | ||||||||||||||||||||||
| headers: { Authorization: `Bearer ${firebaseAuthState.idToken}` } | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (res.status === 404) { | ||||||||||||||||||||||
| if (showMessage) setCloudStatus("No cloud backup found yet."); | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const data = await res.json(); | ||||||||||||||||||||||
| if (!res.ok) throw new Error(data.error?.message || "Restore failed"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const encoded = data.fields?.data?.stringValue; | ||||||||||||||||||||||
| if (!encoded) { | ||||||||||||||||||||||
| if (showMessage) setCloudStatus("Cloud document is empty.", true); | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const cloud = JSON.parse(encoded); | ||||||||||||||||||||||
| const localUpdatedAt = Number(lastUpdatedAt || 0); | ||||||||||||||||||||||
| const cloudUpdatedAt = Number(cloud.updatedAt || 0); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (cloudUpdatedAt < localUpdatedAt) { | ||||||||||||||||||||||
| if (showMessage) setCloudStatus("Local data is newer than cloud."); | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| bookmarks = Array.isArray(cloud.bookmarks) ? cloud.bookmarks : []; | ||||||||||||||||||||||
| folders = Array.isArray(cloud.folders) && cloud.folders.length ? cloud.folders : ["Default"]; | ||||||||||||||||||||||
| openFolders = cloud.openFolders && typeof cloud.openFolders === "object" ? cloud.openFolders : {}; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| folders.forEach(f => { | ||||||||||||||||||||||
| if (openFolders[f] === undefined) openFolders[f] = f === "Default"; | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| saveAll(); | ||||||||||||||||||||||
| renderFolders(); | ||||||||||||||||||||||
| populateFolderSelect(); | ||||||||||||||||||||||
|
Comment on lines
+769
to
+788
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. XSS risk: cloud-restored data is rendered via
A compromised or tampered Firestore document could inject malicious HTML/JS into the page. Sanitize or escape all user-controlled strings before DOM insertion, or switch to 🛡️ Minimal fix — escape HTML in rendered valuesAdd a utility: function escapeHTML(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}Then use it in - <span class="folder-name">${folder}</span>
+ <span class="folder-name">${escapeHTML(folder)}</span>- <div class="tile-name">${b.name}</div>
+ <div class="tile-name">${escapeHTML(b.name)}</div>Also escape 🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (showMessage) setCloudStatus("Restore complete."); | ||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||
| setCloudStatus(`Restore failed: ${err.message}`, true); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+744
to
+793
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Line 786 calls Consider splitting the persistence concern: save to local storage without triggering cloud backup after a restore. 🔧 Proposed approach — add a flag to skip cloud backup-function saveAll() {
+function saveAll({ skipCloudBackup = false } = {}) {
lastUpdatedAt = Date.now();
chrome.storage.sync.set({ bookmarks, folders, openFolders, firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState, lastUpdatedAt });
- backupToCloud(false);
+ if (!skipCloudBackup) backupToCloud(false);
}Then in - saveAll();
+ saveAll({ skipCloudBackup: true });🤖 Prompt for AI Agents |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Start in test mode" advice is a security risk.
Firestore test mode allows unauthenticated reads and writes to all data for 30 days. Users who follow this advice and forget to update rules will expose all stored bookmark data (including any data from other users). At minimum, add a warning and provide recommended production security rules.
📝 Suggested documentation improvement
Then expand the security note at the end:
📝 Committable suggestion
🤖 Prompt for AI Agents