Skip to content

Add Firebase-based cloud backup/restore for bookmark data#1

Open
rishnudk wants to merge 1 commit intomainfrom
codex/explain-project-details
Open

Add Firebase-based cloud backup/restore for bookmark data#1
rishnudk wants to merge 1 commit intomainfrom
codex/explain-project-details

Conversation

@rishnudk
Copy link
Owner

@rishnudk rishnudk commented Feb 12, 2026

Motivation

  • Provide an optional cloud backup/restore flow so users can persist their bookmarks across installs and devices using Firebase.
  • Use Firebase Auth + Firestore (REST APIs) to avoid adding heavy SDKs and to keep the extension lightweight and self-contained.

Description

  • Add a Firebase cloud panel to the new tab sidebar with inputs for Firebase Web API Key, Firebase Project ID, email/password auth fields and buttons for Sign Up, Sign In, Sign Out, Backup to Cloud and Restore from Cloud (newtab.html and style.css).
  • Implement REST-based Firebase integration in script.js including local state for firebaseConfig, firebaseAuth, and lastUpdatedAt, plus functions authWithFirebase, backupToCloud, restoreFromCloud, cloudDocumentUrl, and UI helpers (initializeCloudUI, setCloudStatus, persistCloudState).
  • Wire cloud flow into app lifecycle so loadAll() restores cloud state on load and saveAll() updates lastUpdatedAt, persists local state and triggers a background backup when signed in.
  • Add necessary host permissions for Firebase REST endpoints in manifest.json and document setup + usage in README.md (Firestore document path: bookmarkfolderUsers/{firebaseLocalUserId}).

Testing

  • Ran node --check script.js to validate JavaScript syntax and it completed successfully.
  • Validated manifest.json formatting with python -m json.tool manifest.json and it parsed successfully.
  • Served the UI with python -m http.server 4173 and captured a visual verification screenshot using Playwright showing the new Firebase panel (artifact produced).

Codex Task

Summary by CodeRabbit

  • New Features

    • Added optional Firebase Cloud Backup feature enabling bookmark synchronization across devices with email/password authentication
    • Integrated cloud panel in sidebar with backup, restore, and authentication controls
  • Documentation

    • Updated README with Firebase Cloud Backup setup workflow and configuration instructions

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

This PR adds a Firebase Cloud Backup feature to the bookmark manager extension. It introduces cloud storage integration via Firebase, including UI components for authentication and operations, REST API integration for backup and restore workflows, local state management for cloud data, and manifest permissions to access Firebase services.

Changes

Cohort / File(s) Summary
Documentation
README.md
Added Firebase Cloud Backup setup instructions, including Firebase project configuration, authentication setup, Firestore document path (bookmarkfolderUsers/{firebaseLocalUserId}), stored fields (data, updatedAt), and security notes.
Configuration
manifest.json
Added host_permissions for Firebase APIs (identitytoolkit.googleapis.com, firestore.googleapis.com); reformatted permissions array to multi-line layout.
UI Components
newtab.html, style.css
Added Firebase Backup cloud panel with inputs for API Key and Project ID, authentication fields, action buttons (Sign Up, Sign In, Sign Out, Backup to Cloud, Restore from Cloud), and cloud status display. Introduced corresponding CSS rules for cloud-panel styling.
Core Cloud Integration
script.js
Added global state for Firebase configuration and authentication; introduced 14 new functions for cloud UI initialization, state persistence, authentication workflow (sign up/in/out), backup, and restore operations; extended loadAll and saveAll to integrate cloud state management and trigger cloud sync operations; added timestamp-based conflict resolution for restore.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as Extension UI
    participant Script as script.js
    participant AuthAPI as Firebase Auth API
    participant FirestoreAPI as Firestore API

    User->>UI: Enter Firebase credentials & click Sign Up
    UI->>Script: authWithFirebase('signUp')
    Script->>AuthAPI: POST /accounts:signUp with credentials
    AuthAPI-->>Script: idToken, localId
    Script->>Script: Update firebaseAuthState
    Script->>Script: persistCloudState()
    Script-->>UI: setCloudStatus('Signed in')
    Script->>Script: restoreFromCloud()
    Script->>FirestoreAPI: GET Firestore document
    FirestoreAPI-->>Script: Cloud bookmarks data
    Script->>Script: Compare timestamps & merge
    Script->>Script: Apply to local state & re-render
    UI-->>User: Display restored bookmarks

    User->>UI: Make local changes & trigger saveAll
    Script->>Script: backupToCloud()
    Script->>Script: Assemble cloudPayload()
    Script->>FirestoreAPI: POST/PUT Firestore document with updated data
    FirestoreAPI-->>Script: Success response
    Script-->>UI: setCloudStatus('Backed up')
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Poem

🐰 Cloud storage takes flight,
Firebase shines bright,
Bookmarks backup with care,
Sync'd everywhere with flair,
Restore what you dare! ☁️✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding Firebase-based cloud backup/restore functionality for bookmark data, which aligns with all significant changes across README.md, manifest.json, newtab.html, script.js, and style.css.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/explain-project-details

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
script.js (1)

20-55: ⚠️ Potential issue | 🟠 Major

restoreFromCloud runs on every new-tab open, adding latency and Firestore reads.

Line 53 (await restoreFromCloud(false)) fires on every loadAll(), which runs whenever a new tab is opened. This means:

  1. A Firestore REST read on every new tab open (cost + latency).
  2. With an expired token, it will fail silently each time.
  3. The await blocks the initial load — the UI won't finish rendering until the network call completes or fails.

Consider making the restore non-blocking, or only auto-restoring on first install / sign-in rather than every page load.

🔧 Quick fix — make restore non-blocking
     renderFolders();
     populateFolderSelect();
     renderGoogleApps();
     initializeCloudUI();
 
-    await restoreFromCloud(false);
+    // Non-blocking: don't delay tab render for cloud fetch
+    restoreFromCloud(false);
   });
🤖 Fix all issues with AI agents
In `@newtab.html`:
- Around line 31-35: Add accessible labels for each input element
(firebaseApiKey, firebaseProjectId, authEmail, authPassword) by adding
corresponding <label> elements with for attributes matching each input id;
labels may be visually shown or use an sr-only CSS class for screen-reader-only
text so placeholders are not the only accessible hint for screen readers and
users who forget the placeholder once typing begins. Ensure label text clearly
describes the field (e.g., "Firebase API Key", "Firebase Project ID", "Email",
"Password") and that each label-input pair matches by id to maintain proper
associations.

In `@README.md`:
- Line 81: Update the README's "Enable Firestore Database" instruction to remove
the blanket "start in test mode" advice or prepend a prominent security warning
about test mode allowing unauthenticated reads/writes for 30 days, and add a
recommended production rules example for the bookmarkfolderUsers collection that
enforces request.auth != null and request.auth.uid == userId (place the full
rules snippet in a fenced code block and label it as production rules). Ensure
the warning appears immediately next to the Firestore setup step and include a
short note reminding developers to deploy rules before leaving development.

In `@script.js`:
- Around line 13-15: The firebaseAuthState object (idToken, localId, email) is
sensitive and must not be saved to chrome.storage.sync; update any
persistence/read logic that currently writes or reads firebaseAuthState to use
chrome.storage.local instead, while leaving firebaseConfigState (apiKey,
projectId) in chrome.storage.sync if desired; ensure idToken is never written to
sync storage and migrate/remove any existing synced tokens (use lastUpdatedAt to
coordinate migration/cleanup).
- Around line 42-46: The code sets lastUpdatedAt = res.lastUpdatedAt ||
Date.now(), which makes a fresh install appear newer than cloud backups and
prevents restore; change the default fallback to 0 instead (i.e., set
lastUpdatedAt = res.lastUpdatedAt ?? 0) so that when no stored timestamp exists
cloud data is preferred, and keep the chrome.storage.sync.set call (which writes
firebaseConfigState, firebaseAuthState, lastUpdatedAt) so the updated default is
persisted; check restoreFromCloud to rely on this 0 default for initial
restores.
- Around line 769-788: The restoreFromCloud flow assigns untrusted JSON into
bookmarks/folders/openFolders (in restoreFromCloud) and those values are later
rendered via innerHTML in renderFolders and populateFolderSelect; to fix, ensure
all user-controlled strings are escaped or inserted as text nodes before any DOM
insertion: add/use an escapeHTML utility (or prefer
element.textContent/createElement and appendChild) when rendering folder names,
bookmark names, descriptions and any interpolated fields, and validate/sanitize
b.icon before setting it as an image src; update renderFolders,
populateFolderSelect and any code that builds HTML from bookmarks/folders to use
the sanitizer or textContent approach instead of raw innerHTML.
- Around line 744-793: restoreFromCloud currently calls saveAll() which
immediately triggers backupToCloud and writes the restored data back to
Firestore; stop that by adding a way to save locally without invoking cloud
backup: modify saveAll to accept an optional flag (e.g., saveAll(skipCloud =
false)) or create a saveLocalOnly wrapper that performs the same local
persistence steps but does not call backupToCloud, update saveAll implementation
to call backupToCloud only when skipCloud is false, and change restoreFromCloud
to call saveAll(true) (or saveLocalOnly) after populating
bookmarks/folders/openFolders; ensure other callers of saveAll keep the default
behavior so normal saves still back up to cloud.
- Around line 674-699: The authWithFirebase function currently drops
data.refreshToken; modify it to persist refreshToken into firebaseAuthState
(e.g., firebaseAuthState.refreshToken) when setting idToken/localId/email and
call persistCloudState(); then implement a token refresh helper (e.g.,
refreshFirebaseIdToken) that POSTs to
https://securetoken.googleapis.com/v1/token with grant_type=refresh_token and
the stored refreshToken to obtain a new id_token and refresh_token, update
firebaseAuthState and persist; invoke this helper before cloud operations
(backupToCloud, restoreFromCloud) or on 401 responses from cloud endpoints to
automatically refresh tokens and retry the request, falling back to prompting
re-authentication if refresh fails.
🧹 Nitpick comments (5)
style.css (1)

730-752: Stylelint flags modern color-function notation, but the existing file consistently uses rgba() with decimal alphas.

The linter prefers rgb(255 255 255 / 4%) over rgba(255, 255, 255, 0.04). Since the entire file (~40+ occurrences) uses the legacy notation, this is a codebase-wide concern rather than something to fix only in the new lines. Consider a follow-up to migrate the whole file if you want to satisfy the linter.

newtab.html (1)

35-35: Consider adding autocomplete="new-password" on the password field.

Without this, the browser may offer to save/autofill credentials from the new-tab page, which could confuse users or leak the Firebase password into their browser's credential manager.

script.js (3)

61-65: saveAll is synchronous but triggers async backupToCloud — errors are silently swallowed.

backupToCloud returns a promise but it's not awaited (fire-and-forget). If the cloud backup fails, there's no indication to the user in the non-showMessage path. Additionally, rapid successive calls to saveAll (e.g., during drag-and-drop) will fire multiple concurrent backup requests with no debouncing.

Consider debouncing the cloud backup call (e.g., 2–3 seconds) to avoid excessive API calls during rapid edits.


707-709: cloudDocumentUrl does not encode path segments.

If firebaseConfigState.projectId or firebaseAuthState.localId ever contain special characters, the URL would be malformed. While Firebase-generated localId values are typically safe, defensively encoding the values is good practice.

🔧 Proposed fix
 function cloudDocumentUrl() {
-  return `https://firestore.googleapis.com/v1/projects/${firebaseConfigState.projectId}/databases/(default)/documents/bookmarkfolderUsers/${firebaseAuthState.localId}`;
+  return `https://firestore.googleapis.com/v1/projects/${encodeURIComponent(firebaseConfigState.projectId)}/databases/(default)/documents/bookmarkfolderUsers/${encodeURIComponent(firebaseAuthState.localId)}`;
 }

638-640: persistCloudState stores auth tokens but signOutFirebase doesn't clear the password field.

After signOutFirebase (line 701–705), the password <input> still retains the user's password in the DOM. Consider clearing the password field on sign-out to reduce credential exposure.

🔧 Proposed fix in signOutFirebase
 function signOutFirebase() {
   firebaseAuthState = { idToken: "", localId: "", email: "" };
   persistCloudState();
   updateCloudStatusFromState();
+  const pw = document.getElementById("authPassword");
+  if (pw) pw.value = "";
 }

Comment on lines +31 to +35
<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">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inputs lack <label> elements — accessibility concern.

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 sr-only labels for each input.

♿ 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<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">
<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">
🤖 Prompt for AI Agents
In `@newtab.html` around lines 31 - 35, Add accessible labels for each input
element (firebaseApiKey, firebaseProjectId, authEmail, authPassword) by adding
corresponding <label> elements with for attributes matching each input id;
labels may be visually shown or use an sr-only CSS class for screen-reader-only
text so placeholders are not the only accessible hint for screen readers and
users who forget the placeholder once typing begins. Ensure label text clearly
describes the field (e.g., "Firebase API Key", "Firebase Project ID", "Email",
"Password") and that each label-input pair matches by id to maintain proper
associations.


1. Create a Firebase project
2. Enable **Authentication → Email/Password**
3. Enable **Firestore Database** (start in test mode while developing)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

"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
-3. Enable **Firestore Database** (start in test mode while developing)
+3. Enable **Firestore Database** and set security rules (see below)

Then expand the security note at the end:

-> Security note: set Firestore security rules so users can only read/write their own doc.
+> **Security rules (required):** Apply the following Firestore rules so each user can only access their own document:
+>
+> ```
+> rules_version = '2';
+> service cloud.firestore {
+>   match /databases/{database}/documents {
+>     match /bookmarkfolderUsers/{userId} {
+>       allow read, write: if request.auth != null && request.auth.uid == userId;
+>     }
+>   }
+> }
+> ```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
3. Enable **Firestore Database** (start in test mode while developing)
3. Enable **Firestore Database** and set security rules (see below)
🤖 Prompt for AI Agents
In `@README.md` at line 81, Update the README's "Enable Firestore Database"
instruction to remove the blanket "start in test mode" advice or prepend a
prominent security warning about test mode allowing unauthenticated reads/writes
for 30 days, and add a recommended production rules example for the
bookmarkfolderUsers collection that enforces request.auth != null and
request.auth.uid == userId (place the full rules snippet in a fenced code block
and label it as production rules). Ensure the warning appears immediately next
to the Firestore setup step and include a short note reminding developers to
deploy rules before leaving development.

Comment on lines +13 to +15
let firebaseConfigState = { apiKey: "", projectId: "" };
let firebaseAuthState = { idToken: "", localId: "", email: "" };
let lastUpdatedAt = Date.now();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Firebase auth token stored in chrome.storage.sync — consider chrome.storage.local instead.

chrome.storage.sync replicates data across all signed-in Chrome instances. Storing idToken (a bearer credential) in sync storage means it will be synced to every device the user is signed into Chrome on. Use chrome.storage.local for sensitive auth state, or at minimum store only the non-sensitive config (apiKey, projectId) in sync and keep tokens local.

🤖 Prompt for AI Agents
In `@script.js` around lines 13 - 15, The firebaseAuthState object (idToken,
localId, email) is sensitive and must not be saved to chrome.storage.sync;
update any persistence/read logic that currently writes or reads
firebaseAuthState to use chrome.storage.local instead, while leaving
firebaseConfigState (apiKey, projectId) in chrome.storage.sync if desired;
ensure idToken is never written to sync storage and migrate/remove any existing
synced tokens (use lastUpdatedAt to coordinate migration/cleanup).

Comment on lines +42 to +46
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 });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

lastUpdatedAt defaults to Date.now() on first load, which may prevent cloud restore.

Line 43: if lastUpdatedAt is not yet stored, it defaults to the current time. When a user installs the extension on a new device and signs in, the freshly-initialized lastUpdatedAt will be "now", which is newer than any existing cloud backup. The timestamp comparison in restoreFromCloud (line 773) will then skip the restore, defeating the purpose.

Default to 0 instead so cloud data is always preferred on a fresh install.

🐛 Proposed fix
-    lastUpdatedAt = res.lastUpdatedAt || Date.now();
+    lastUpdatedAt = res.lastUpdatedAt || 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 });
firebaseConfigState = res.firebaseConfig || { apiKey: "", projectId: "" };
lastUpdatedAt = res.lastUpdatedAt || 0;
firebaseAuthState = res.firebaseAuth || { idToken: "", localId: "", email: "" };
chrome.storage.sync.set({ bookmarks, folders, openFolders, firebaseConfig: firebaseConfigState, firebaseAuth: firebaseAuthState, lastUpdatedAt });
🤖 Prompt for AI Agents
In `@script.js` around lines 42 - 46, The code sets lastUpdatedAt =
res.lastUpdatedAt || Date.now(), which makes a fresh install appear newer than
cloud backups and prevents restore; change the default fallback to 0 instead
(i.e., set lastUpdatedAt = res.lastUpdatedAt ?? 0) so that when no stored
timestamp exists cloud data is preferred, and keep the chrome.storage.sync.set
call (which writes firebaseConfigState, firebaseAuthState, lastUpdatedAt) so the
updated default is persisted; check restoreFromCloud to rely on this 0 default
for initial restores.

Comment on lines +674 to +699
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);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Firebase Identity Toolkit API refresh token endpoint documentation

💡 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:

  • HTTP method: POST
  • Endpoint: https://securetoken.googleapis.com/v1/token?key=[API_KEY]
  • Content-Type: application/x-www-form-urlencoded
  • Body fields:
    • grant_type=refresh_token (always)
    • refresh_token=[REFRESH_TOKEN] (the user’s Firebase refresh token)
  • Response includes: id_token, refresh_token, expires_in, token_type, user_id, project_id [1] (also documented for Identity Platform) [2]

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 2

Repository: 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.js

Repository: rishnudk/BookmarkFolder

Length of output: 2294


🌐 Web query:

Firebase ID token expiration duration hours

💡 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 exp can be at most 1 hour after iat.) [5]


🌐 Web query:

Firebase Identity Toolkit signupNewUser signin REST API response format refreshToken

💡 Result:

For Firebase Auth / Identity Toolkit REST (Identity Platform), the refreshToken is returned in the JSON response when you send returnSecureToken: true.

signupNewUser (aka accounts:signUp)

Endpoint: POST https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=API_KEY

Response JSON (success):

{
  "kind": "identitytoolkit#SignupNewUserResponse",
  "idToken": "",
  "email": "user@example.com",
  "refreshToken": "",
  "expiresIn": "3600",
  "localId": ""
}

(expiresIn is a string containing the number of seconds until the ID token expires.) [1][2]

signin (aka accounts:signInWithPassword)

Endpoint: POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=API_KEY

Response JSON (success):

{
  "kind": "identitytoolkit#VerifyPasswordResponse",
  "localId": "",
  "email": "user@example.com",
  "displayName": "",
  "idToken": "",
  "registered": true,
  "refreshToken": "",
  "expiresIn": "3600"
}
``` [2][3]

If you’re not seeing `refreshToken` in responses, it’s typically because `returnSecureToken` was omitted or set to `false`. [2]

**Sources:** [1] SignUpResponse schema, [2] “Using the REST API” (sample requests/responses), [3] `accounts.signInWithPassword` reference response schema.

---



</details>

**Store `refreshToken` and implement token refresh before cloud operations.**

`authWithFirebase` receives and discards the `refreshToken` returned by the Identity Toolkit API. Since ID tokens expire after 1 hour, all cloud operations (`backupToCloud`, `restoreFromCloud`) will fail with a 401 when `firebaseAuthState.idToken` becomes invalid. The current error handling will mask this as a generic "Backup failed" or "Restore failed" message without recovery.

Store `data.refreshToken` in `firebaseAuthState` and refresh the token using `https://securetoken.googleapis.com/v1/token` (with `grant_type=refresh_token`) before making cloud calls, or detect 401 responses and prompt re-authentication.

<details>
<summary>🤖 Prompt for AI Agents</summary>

In @script.js around lines 674 - 699, The authWithFirebase function currently
drops data.refreshToken; modify it to persist refreshToken into
firebaseAuthState (e.g., firebaseAuthState.refreshToken) when setting
idToken/localId/email and call persistCloudState(); then implement a token
refresh helper (e.g., refreshFirebaseIdToken) that POSTs to
https://securetoken.googleapis.com/v1/token with grant_type=refresh_token and
the stored refreshToken to obtain a new id_token and refresh_token, update
firebaseAuthState and persist; invoke this helper before cloud operations
(backupToCloud, restoreFromCloud) or on 401 responses from cloud endpoints to
automatically refresh tokens and retry the request, falling back to prompting
re-authentication if refresh fails.


</details>

<!-- fingerprinting:phantom:medusa:phoenix -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines +744 to +793
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();

if (showMessage) setCloudStatus("Restore complete.");
} catch (err) {
setCloudStatus(`Restore failed: ${err.message}`, true);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

restoreFromCloudsaveAllbackupToCloud creates a wasteful round-trip.

Line 786 calls saveAll(), which (line 64) immediately calls backupToCloud(false), pushing the just-restored data right back to Firestore. This is a redundant write on every restore and could also cause unexpected timestamp advancement.

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 restoreFromCloud:

-    saveAll();
+    saveAll({ skipCloudBackup: true });
🤖 Prompt for AI Agents
In `@script.js` around lines 744 - 793, restoreFromCloud currently calls saveAll()
which immediately triggers backupToCloud and writes the restored data back to
Firestore; stop that by adding a way to save locally without invoking cloud
backup: modify saveAll to accept an optional flag (e.g., saveAll(skipCloud =
false)) or create a saveLocalOnly wrapper that performs the same local
persistence steps but does not call backupToCloud, update saveAll implementation
to call backupToCloud only when skipCloud is false, and change restoreFromCloud
to call saveAll(true) (or saveLocalOnly) after populating
bookmarks/folders/openFolders; ensure other callers of saveAll keep the default
behavior so normal saves still back up to cloud.

Comment on lines +769 to +788
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();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

XSS risk: cloud-restored data is rendered via innerHTML without sanitization.

restoreFromCloud parses arbitrary JSON from Firestore (line 769) and assigns it directly to bookmarks and folders. These values are later interpolated into innerHTML (e.g., line 122: <span class="folder-name">${folder}</span> and line 217: <div class="tile-name">${b.name}</div>).

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 textContent/createElement for rendering.

🛡️ Minimal fix — escape HTML in rendered values

Add a utility:

function escapeHTML(str) {
  const div = document.createElement("div");
  div.textContent = str;
  return div.innerHTML;
}

Then use it in renderFolders:

-      <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 b.icon in the <img src=...> or validate it's a proper URL.

🤖 Prompt for AI Agents
In `@script.js` around lines 769 - 788, The restoreFromCloud flow assigns
untrusted JSON into bookmarks/folders/openFolders (in restoreFromCloud) and
those values are later rendered via innerHTML in renderFolders and
populateFolderSelect; to fix, ensure all user-controlled strings are escaped or
inserted as text nodes before any DOM insertion: add/use an escapeHTML utility
(or prefer element.textContent/createElement and appendChild) when rendering
folder names, bookmark names, descriptions and any interpolated fields, and
validate/sanitize b.icon before setting it as an image src; update
renderFolders, populateFolderSelect and any code that builds HTML from
bookmarks/folders to use the sanitizer or textContent approach instead of raw
innerHTML.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant