browser-extension-manager 1.3.15 → 1.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -354,11 +354,7 @@ Provides cross-context auth synchronization and reusable auth UI event handlers.
354
354
 
355
355
  ### Cross-Context Auth Architecture
356
356
 
357
- **Background.js is the source of truth** for authentication. Browser extensions have multiple isolated JavaScript contexts (background, popup, options, pages, sidepanel) - each runs its own Firebase instance. BEM solves this by:
358
-
359
- 1. Background.js monitors for auth tokens via `tabs.onUpdated`
360
- 2. When detected, background signs in Firebase and saves to `chrome.storage`
361
- 3. Other contexts listen to storage changes and sync their Firebase instances
357
+ **Background.js is the source of truth** for authentication. Browser extensions have multiple isolated JavaScript contexts (background, popup, options, pages, sidepanel) - each runs its own Firebase instance. BEM syncs them via messaging (no storage).
362
358
 
363
359
  **Sign-in Flow:**
364
360
  ```
@@ -366,29 +362,36 @@ User clicks .auth-signin-btn
366
362
  → openAuthPage() opens https://{authDomain}/token?authSourceTabId=123
367
363
  → Website authenticates, redirects to /token?authToken=xxx
368
364
  → background.js tabs.onUpdated detects authDomain URL with authToken param
369
- → background.js calls handleAuthToken():
370
- signInWithCustomToken(auth, token)
371
- Saves {token, user, timestamp} to storage key 'bxm:authState'
372
- Closes the /token tab
373
- → Reactivates the original tab (authSourceTabId)
374
- → Other contexts detect storage change via setupAuthStorageListener()
375
- Each context calls signInWithStoredToken() to sign in their Firebase
365
+ → background.js signs in with signInWithCustomToken()
366
+ background.js broadcasts token to all open contexts via postMessage()
367
+ Closes the /token tab, reactivates original tab
368
+ Open contexts receive broadcast and sign in with the token
369
+ ```
370
+
371
+ **Context Load Flow:**
372
+ ```
373
+ Context loads (popup, page, options, sidepanel)
374
+ → Web Manager initializes, waits for auth to settle via auth.listen({once: true})
375
+ → Sends bxm:syncAuth message to background with local UID
376
+ → Background compares UIDs:
377
+ → Same UID (including both null) → already in sync, no action
378
+ → Different UID → background fetches fresh custom token from server, sends to context
379
+ → Background signed out, context signed in → tells context to sign out
376
380
  ```
377
381
 
378
382
  **Sign-out Flow:**
379
383
  ```
380
384
  User clicks .auth-signout-btn
381
- → Web Manager signs out Firebase locally
382
- setupAuthStorageListener() detects WM auth state change (user=null)
383
- Clears 'bxm:authState' from storage
384
- → background.js setupAuthStorageListener() detects storage cleared
385
- background.js signs out its Firebase instance
386
- → Other contexts detect storage cleared and sign out
385
+ → Web Manager signs out that context's Firebase
386
+ setupSignOutListener() detects sign-out, sends bxm:signOut to background
387
+ background.js signs out its Firebase
388
+ → background.js broadcasts bxm:signOut to all other contexts
389
+ All contexts sign out
387
390
  ```
388
391
 
389
392
  **Key Implementation Details:**
390
393
 
391
- 1. **Storage area**: BEM normalizes storage to `sync` (if available) or `local`. Auth listeners should NOT check `areaName` - just check for the `bxm:authState` key directly.
394
+ 1. **No storage**: Auth state is NOT stored in `chrome.storage`. Firebase persists sessions in IndexedDB per context. Web Manager handles UI bindings.
392
395
 
393
396
  2. **Firebase in service workers**: Static ES6 imports are required. Dynamic `import()` fails with webpack chunking in service workers.
394
397
 
@@ -397,7 +400,9 @@ User clicks .auth-signout-btn
397
400
  4. **Required permission**: `tabs` permission needed for `tabs.onUpdated` listener.
398
401
 
399
402
  **Functions:**
400
- - `setupAuthStorageListener(context)` - Listens for auth state changes from background.js AND monitors WM auth state to clear storage on sign-out
403
+ - `syncWithBackground(context)` - Compares context's UID with background's UID on load, syncs if different
404
+ - `setupAuthBroadcastListener(context)` - Listens for sign-in/sign-out broadcasts from background
405
+ - `setupSignOutListener(context)` - Notifies background when context signs out
401
406
  - `setupAuthEventListeners(context)` - Sets up delegated click handlers for auth buttons
402
407
  - `openAuthPage(context, options)` - Opens auth page on the website with authSourceTabId for tab restoration
403
408
 
@@ -432,21 +437,6 @@ Add these classes to your HTML elements to enable automatic auth handling:
432
437
  - `data-wm-bind="@text auth.user.email"` - Display user's email
433
438
  - `data-wm-bind="@attr src auth.user.photoURL"` - Set avatar image src
434
439
 
435
- **Storage Schema (`bxm:authState`):**
436
- ```javascript
437
- {
438
- token: "firebase-custom-token", // Used by other contexts to sign in
439
- user: {
440
- uid: "...",
441
- email: "...",
442
- displayName: "...",
443
- photoURL: "...",
444
- emailVerified: true
445
- },
446
- timestamp: 1234567890
447
- }
448
- ```
449
-
450
440
  **Logger:** [src/lib/logger.js](src/lib/logger.js)
451
441
  - Full logging utility
452
442
  - [src/lib/logger-lite.js](src/lib/logger-lite.js) for lightweight contexts
@@ -808,8 +798,8 @@ Manager.initialize().then(() => {
808
798
  - [src/cli.js](src/cli.js) - CLI implementation
809
799
 
810
800
  **Auth System (Cross-Context):**
811
- - [src/background.js](src/background.js) - Source of truth for auth; `setupAuthTokenListener()`, `setupAuthStorageListener()`, `handleAuthToken()`
812
- - [src/lib/auth-helpers.js](src/lib/auth-helpers.js) - `setupAuthStorageListener()`, `openAuthPage()`, `setupAuthEventListeners()` for non-background contexts
801
+ - [src/background.js](src/background.js) - Source of truth; `handleSyncAuth()`, `handleSignOut()`, `broadcastAuthToken()`
802
+ - [src/lib/auth-helpers.js](src/lib/auth-helpers.js) - `syncWithBackground()`, `setupAuthBroadcastListener()`, `setupSignOutListener()`
813
803
 
814
804
  **CSS Framework:**
815
805
  - [src/assets/css/browser-extension-manager.scss](src/assets/css/browser-extension-manager.scss) - Main entry
package/README.md CHANGED
@@ -88,112 +88,43 @@ Only stores with configured credentials will be published to.
88
88
 
89
89
  ## 🔐 Authentication
90
90
 
91
- BEM provides built-in authentication support that syncs across all extension contexts (popup, options, pages, sidepanel, background).
91
+ BEM provides built-in authentication that syncs across all extension contexts (popup, options, pages, sidepanel, background).
92
92
 
93
- ### Auth Architecture Overview
93
+ ### How It Works
94
94
 
95
- **Background.js is the source of truth** for authentication state. When a user signs in via the website, the auth token flows through background.js to all other contexts via `chrome.storage`.
95
+ **Background.js is the source of truth.** Auth syncs via messaging (no storage).
96
96
 
97
- ```
98
- ┌─────────────────────────────────────────────────────────────────────────┐
99
- │ SIGN-IN FLOW │
100
- ├─────────────────────────────────────────────────────────────────────────┤
101
- │ 1. User clicks .auth-signin-btn in any context │
102
- │ 2. Extension opens https://{authDomain}/token?authSourceTabId=123 │
103
- │ 3. Website authenticates user, redirects to /token?authToken=xxx │
104
- │ 4. Background.js detects URL via tabs.onUpdated listener │
105
- │ 5. Background signs in Firebase with custom token │
106
- │ 6. Background saves auth state to chrome.storage (bxm:authState) │
107
- │ 7. Background closes /token tab and reactivates original tab │
108
- │ 8. Other contexts detect storage change and sign in their Firebase │
109
- └─────────────────────────────────────────────────────────────────────────┘
110
-
111
- ┌─────────────────────────────────────────────────────────────────────────┐
112
- │ SIGN-OUT FLOW │
113
- ├─────────────────────────────────────────────────────────────────────────┤
114
- │ 1. User clicks .auth-signout-btn in any context │
115
- │ 2. Web Manager signs out Firebase locally │
116
- │ 3. Auth helper detects WM auth change, clears bxm:authState storage │
117
- │ 4. Background.js detects storage cleared, signs out its Firebase │
118
- │ 5. Other contexts detect storage change and sign out │
119
- └─────────────────────────────────────────────────────────────────────────┘
120
- ```
121
-
122
- ### Required Configuration
123
-
124
- Add `authDomain` to your Firebase config in `config/browser-extension-manager.json`:
125
-
126
- ```json
127
- {
128
- "firebaseConfig": {
129
- "apiKey": "...",
130
- "authDomain": "your-app.firebaseapp.com",
131
- "projectId": "..."
132
- }
133
- }
134
- ```
97
+ - **Sign-in**: User clicks `.auth-signin-btn` → opens `/token` page on website → website authenticates and redirects with token → background.js signs in and broadcasts to all open contexts
98
+ - **Context load**: Each context compares its UID with background's UID on load; syncs if different
99
+ - **Sign-out**: User clicks `.auth-signout-btn` → context signs out → notifies background → background broadcasts sign-out to all contexts
135
100
 
136
- ### Required Permission
101
+ ### Required Setup
137
102
 
138
- Add the `tabs` permission to your `src/manifest.json`:
139
-
140
- ```json
141
- {
142
- "permissions": ["tabs"]
143
- }
144
- ```
145
-
146
- This is required for background.js to monitor tab URL changes and detect auth tokens.
103
+ 1. Add `authDomain` to your Firebase config in `config/browser-extension-manager.json`
104
+ 2. Add `tabs` permission to `src/manifest.json` (for URL monitoring)
147
105
 
148
106
  ### Auth Button Classes
149
107
 
150
- Add these classes to your HTML elements to enable automatic auth handling:
151
-
152
- | Class | Description | Action |
153
- |-------|-------------|--------|
154
- | `.auth-signin-btn` | Sign in button | Opens `/token` page on website |
155
- | `.auth-signout-btn` | Sign out button | Signs out via Web Manager (which triggers storage sync) |
156
- | `.auth-account-btn` | Account button | Opens `/account` page on website |
108
+ | Class | Action |
109
+ |-------|--------|
110
+ | `.auth-signin-btn` | Opens `/token` page on website |
111
+ | `.auth-signout-btn` | Signs out via Web Manager |
112
+ | `.auth-account-btn` | Opens `/account` page on website |
157
113
 
158
114
  ### Example
159
115
  ```html
160
- <!-- Sign In Button (shown when logged out) -->
161
- <button class="btn auth-signin-btn" data-wm-bind="@show !auth.user">
162
- Sign In
163
- </button>
116
+ <button class="btn auth-signin-btn" data-wm-bind="@show !auth.user">Sign In</button>
164
117
 
165
- <!-- Account Section (shown when logged in) -->
166
118
  <div data-wm-bind="@show auth.user" hidden>
167
119
  <span data-wm-bind="@text auth.user.displayName">User</span>
168
- <a class="auth-account-btn" href="#">Account</a>
169
120
  <button class="auth-signout-btn">Sign Out</button>
170
121
  </div>
171
122
  ```
172
123
 
173
124
  ### Reactive Bindings
174
- - `data-wm-bind="@show auth.user"` - Show when logged in
175
- - `data-wm-bind="@show !auth.user"` - Show when logged out
176
- - `data-wm-bind="@text auth.user.displayName"` - Display user's name
177
- - `data-wm-bind="@text auth.user.email"` - Display user's email
178
- - `data-wm-bind="@attr src auth.user.photoURL"` - Set avatar image src
179
-
180
- ### Storage Key
181
-
182
- Auth state is stored in `chrome.storage` under the key `bxm:authState`:
183
-
184
- ```javascript
185
- {
186
- token: "firebase-custom-token",
187
- user: {
188
- uid: "...",
189
- email: "...",
190
- displayName: "...",
191
- photoURL: "...",
192
- emailVerified: true
193
- },
194
- timestamp: 1234567890
195
- }
196
- ```
125
+ - `@show auth.user` / `@show !auth.user` - Show/hide based on auth state
126
+ - `@text auth.user.displayName` / `@text auth.user.email` - Display user info
127
+ - `@attr src auth.user.photoURL` - Set avatar image
197
128
 
198
129
  <!-- ## ⛳️ Flags
199
130
  * `--test=false` - Coming soon
@@ -65,8 +65,8 @@ class Manager {
65
65
  // Setup auth token listener (for cross-runtime auth)
66
66
  this.setupAuthTokenListener();
67
67
 
68
- // Setup auth storage listener (detect sign-out from pages)
69
- this.setupAuthStorageListener();
68
+ // Initialize Firebase auth on startup (restores persisted session if any)
69
+ this.initializeAuth();
70
70
 
71
71
  // Setup livereload
72
72
  this.setupLiveReload();
@@ -120,10 +120,147 @@ class Manager {
120
120
  }
121
121
  });
122
122
 
123
+ // Listen for runtime messages (from popup, options, pages, etc.)
124
+ this.extension.runtime.onMessage.addListener((message, _sender, sendResponse) => {
125
+ // Handle auth sync requests - contexts ask background for auth state on load
126
+ if (message.command === 'bxm:syncAuth') {
127
+ this.handleSyncAuth(message, sendResponse);
128
+ return true; // Keep channel open for async response
129
+ }
130
+
131
+ // Handle sign-out requests from contexts
132
+ if (message.command === 'bxm:signOut') {
133
+ this.handleSignOut(sendResponse);
134
+ return true; // Keep channel open for async response
135
+ }
136
+ });
137
+
123
138
  // Log
124
139
  this.logger.log('Set up message handlers');
125
140
  }
126
141
 
142
+ // Handle auth sync request from other contexts (popup, page, options, sidepanel)
143
+ // Compares context's UID with background's UID and provides fresh token only if different
144
+ async handleSyncAuth(message, sendResponse) {
145
+ try {
146
+ const contextUid = message.contextUid || null; // UID from asking context (or null)
147
+
148
+ // Get or initialize Firebase auth
149
+ const auth = this.getFirebaseAuth();
150
+ const bgUser = auth.currentUser;
151
+ const bgUid = bgUser?.uid || null;
152
+
153
+ this.logger.log('[AUTH] syncAuth: Comparing UIDs - context:', contextUid, 'background:', bgUid);
154
+
155
+ // Already in sync (both null, or same UID)
156
+ if (contextUid === bgUid) {
157
+ this.logger.log('[AUTH] syncAuth: Already in sync');
158
+ sendResponse({ needsSync: false });
159
+ return;
160
+ }
161
+
162
+ // Context is signed in but background is not → context should sign out
163
+ if (!bgUser && contextUid) {
164
+ this.logger.log('[AUTH] syncAuth: Background signed out, telling context to sign out');
165
+ sendResponse({ needsSync: true, signOut: true });
166
+ return;
167
+ }
168
+
169
+ // Background is signed in, context is not (or different user) → provide token
170
+ this.logger.log('[AUTH] syncAuth: Fetching fresh custom token for context...', bgUser.email);
171
+
172
+ // Get API URL from config
173
+ const apiUrl = this.config?.web_manager?.api?.url || 'https://api.itwcreativeworks.com';
174
+
175
+ // Get fresh ID token for authorization
176
+ const idToken = await bgUser.getIdToken(true);
177
+
178
+ // Fetch fresh custom token from server
179
+ const response = await fetch(`${apiUrl}/backend-manager`, {
180
+ method: 'POST',
181
+ headers: {
182
+ 'Content-Type': 'application/json',
183
+ 'Authorization': `Bearer ${idToken}`,
184
+ },
185
+ body: JSON.stringify({
186
+ command: 'user:create-custom-token',
187
+ payload: {},
188
+ }),
189
+ });
190
+
191
+ // Check response
192
+ if (!response.ok) {
193
+ throw new Error(`Server responded with ${response.status}`);
194
+ }
195
+
196
+ // Parse response
197
+ const data = await response.json();
198
+
199
+ // Check for token in response
200
+ if (!data.response?.token) {
201
+ throw new Error('No token in server response');
202
+ }
203
+
204
+ this.logger.log('[AUTH] syncAuth: Got fresh custom token, sending to context');
205
+
206
+ // Send user info and fresh custom token
207
+ sendResponse({
208
+ needsSync: true,
209
+ customToken: data.response.token,
210
+ user: {
211
+ uid: bgUser.uid,
212
+ email: bgUser.email,
213
+ displayName: bgUser.displayName,
214
+ photoURL: bgUser.photoURL,
215
+ emailVerified: bgUser.emailVerified,
216
+ },
217
+ });
218
+
219
+ } catch (error) {
220
+ this.logger.error('[AUTH] syncAuth error:', error.message);
221
+ sendResponse({ needsSync: false, error: error.message });
222
+ }
223
+ }
224
+
225
+ // Handle sign-out request from a context
226
+ // Signs out background's Firebase and broadcasts to all other contexts
227
+ async handleSignOut(sendResponse) {
228
+ try {
229
+ this.logger.log('[AUTH] handleSignOut: Signing out background Firebase...');
230
+
231
+ // Sign out background's Firebase
232
+ if (this.libraries.firebaseAuth?.currentUser) {
233
+ await this.libraries.firebaseAuth.signOut();
234
+ }
235
+
236
+ // Broadcast to all contexts
237
+ await this.broadcastSignOut();
238
+
239
+ this.logger.log('[AUTH] handleSignOut: Complete');
240
+ sendResponse({ success: true });
241
+ } catch (error) {
242
+ this.logger.error('[AUTH] handleSignOut error:', error.message);
243
+ sendResponse({ success: false, error: error.message });
244
+ }
245
+ }
246
+
247
+ // Broadcast sign-out to all open extension contexts
248
+ async broadcastSignOut() {
249
+ try {
250
+ const clients = await self.clients.matchAll({ type: 'all' });
251
+
252
+ this.logger.log(`[AUTH] Broadcasting sign-out to ${clients.length} clients...`);
253
+
254
+ for (const client of clients) {
255
+ client.postMessage({ command: 'bxm:signOut' });
256
+ }
257
+
258
+ this.logger.log('[AUTH] Sign-out broadcast complete');
259
+ } catch (error) {
260
+ this.logger.error('[AUTH] Error broadcasting sign-out:', error.message);
261
+ }
262
+ }
263
+
127
264
  // Initialize Firebase
128
265
  initializeFirebase() {
129
266
  // Get Firebase config
@@ -246,24 +383,26 @@ class Manager {
246
383
  });
247
384
  }
248
385
 
249
- // Setup auth storage listener (detect sign-out from pages)
250
- setupAuthStorageListener() {
251
- this.extension.storage.onChanged.addListener((changes) => {
252
- const authChange = changes['bxm:authState'];
253
- if (!authChange) {
254
- return;
255
- }
256
-
257
- this.logger.log('[AUTH] Storage auth state changed:', authChange.newValue ? 'signed in' : 'signed out');
386
+ // Initialize Firebase auth on startup
387
+ // Firebase Auth persists sessions in IndexedDB - we just need to initialize it
388
+ initializeAuth() {
389
+ // Get Firebase config
390
+ const firebaseConfig = this.config?.firebase?.app?.config;
391
+ if (!firebaseConfig) {
392
+ this.logger.log('[AUTH] Firebase config not available, skipping auth initialization');
393
+ return;
394
+ }
258
395
 
259
- // If storage was cleared (sign-out from a page) and we have Firebase initialized, sign out
260
- if (!authChange.newValue && this.libraries.firebaseAuth) {
261
- this.logger.log('[AUTH] Signing out background Firebase...');
262
- this.libraries.firebaseAuth.signOut();
263
- }
264
- });
396
+ // Initialize Firebase auth - it will auto-restore from IndexedDB if session exists
397
+ this.logger.log('[AUTH] Initializing Firebase Auth (will restore persisted session if any)...');
398
+ const auth = this.getFirebaseAuth();
265
399
 
266
- this.logger.log('[AUTH] Auth storage listener set up');
400
+ // Check if already signed in (Firebase restored from IndexedDB)
401
+ if (auth.currentUser) {
402
+ this.logger.log('[AUTH] Firebase restored session from persistence:', auth.currentUser.email);
403
+ } else {
404
+ this.logger.log('[AUTH] No persisted Firebase session found');
405
+ }
267
406
  }
268
407
 
269
408
  // Get or initialize Firebase auth (reuse existing instance)
@@ -298,39 +437,13 @@ class Manager {
298
437
  }
299
438
 
300
439
  // Handle Firebase auth state changes (source of truth for all contexts)
301
- async handleAuthStateChange(user) {
440
+ // No storage operations - Web Manager handles auth state internally
441
+ handleAuthStateChange(user) {
302
442
  this.logger.log('[AUTH] Auth state changed:', user?.email || 'signed out');
303
-
304
- if (user) {
305
- // User is signed in - get current stored state to preserve token
306
- const result = await new Promise(resolve =>
307
- this.extension.storage.local.get('bxm:authState', resolve)
308
- );
309
- const currentState = result['bxm:authState'] || {};
310
-
311
- // Update auth state with current user info
312
- const authState = {
313
- token: currentState.token, // Preserve existing token
314
- user: {
315
- uid: user.uid,
316
- email: user.email,
317
- displayName: user.displayName,
318
- photoURL: user.photoURL,
319
- emailVerified: user.emailVerified,
320
- },
321
- timestamp: Date.now(),
322
- };
323
-
324
- await this.extension.storage.local.set({ 'bxm:authState': authState });
325
- this.logger.log('[AUTH] Auth state synced to storage');
326
- } else {
327
- // User is signed out - clear storage
328
- await this.extension.storage.local.remove('bxm:authState');
329
- this.logger.log('[AUTH] Auth state cleared from storage');
330
- }
443
+ // Nothing else to do - contexts sync via messages, WM handles UI
331
444
  }
332
445
 
333
- // Handle auth token from website
446
+ // Handle auth token from website (custom token from /token page)
334
447
  async handleAuthToken(token, tabId, authSourceTabId = null) {
335
448
  try {
336
449
  // Log
@@ -347,19 +460,12 @@ class Manager {
347
460
  // Log
348
461
  this.logger.log('[AUTH] Signed in successfully:', user.email);
349
462
 
350
- // Save token to storage (user state will be synced by onAuthStateChanged)
351
- const result = await new Promise(resolve =>
352
- this.extension.storage.local.get('bxm:authState', resolve)
353
- );
354
- const currentState = result['bxm:authState'] || {};
463
+ // Broadcast token to all open extension contexts so they can sign in immediately
464
+ // Token is NOT stored - it expires in 1 hour and is only needed for initial sign-in
465
+ this.broadcastAuthToken(token);
355
466
 
356
- await this.extension.storage.local.set({
357
- 'bxm:authState': {
358
- ...currentState,
359
- token: token,
360
- timestamp: Date.now(),
361
- }
362
- });
467
+ // Note: onAuthStateChanged will fire and store user info (without token) in storage
468
+ // This allows UI to show auth state, but token is never persisted
363
469
 
364
470
  // Close the auth tab
365
471
  await this.extension.tabs.remove(tabId);
@@ -381,6 +487,29 @@ class Manager {
381
487
  }
382
488
  }
383
489
 
490
+ // Broadcast auth token to all open extension contexts
491
+ // Used during initial sign-in to immediately sync all open popups, pages, etc.
492
+ async broadcastAuthToken(token) {
493
+ try {
494
+ // Get all clients (extension pages, popups, etc.)
495
+ const clients = await self.clients.matchAll({ type: 'all' });
496
+
497
+ this.logger.log(`[AUTH] Broadcasting token to ${clients.length} clients...`);
498
+
499
+ // Send token to each client
500
+ for (const client of clients) {
501
+ client.postMessage({
502
+ command: 'bxm:signInWithToken',
503
+ token: token,
504
+ });
505
+ }
506
+
507
+ this.logger.log('[AUTH] Token broadcast complete');
508
+ } catch (error) {
509
+ this.logger.error('[AUTH] Error broadcasting token:', error.message);
510
+ }
511
+ }
512
+
384
513
  // Setup livereload
385
514
  setupLiveReload() {
386
515
  // Quit if not in dev mode
@@ -1,97 +1,136 @@
1
1
  // Auth helpers for cross-context auth sync in browser extensions
2
2
  // Used by popup.js, options.js, sidepanel.js, page.js
3
+ //
4
+ // Architecture:
5
+ // - Background.js is the SOURCE OF TRUTH for authentication
6
+ // - On context load, contexts wait for WM auth to settle, then ask background if in sync
7
+ // - If out of sync, background provides a fresh custom token (fetched from server)
8
+ // - No BEM-specific storage - Web Manager handles auth state internally
3
9
 
4
10
  /**
5
- * Sign in with custom token from storage
6
- * @param {Object} context - The manager instance
7
- * @param {Object} authState - Auth state with token
11
+ * Sync auth state with background.js on context load
12
+ * Waits for WM auth to settle, then asks background if in sync
13
+ * @param {Object} context - The manager instance (must have extension, webManager, logger)
8
14
  */
9
- async function signInWithStoredToken(context, authState) {
10
- const { webManager, logger } = context;
11
-
12
- // Skip if no token
13
- if (!authState?.token) {
14
- logger.log('[AUTH-SYNC] No token in auth state, skipping sign in');
15
- return;
16
- }
15
+ export async function syncWithBackground(context) {
16
+ const { extension, webManager, logger } = context;
17
17
 
18
18
  try {
19
- logger.log('[AUTH-SYNC] Signing in with stored token...');
20
-
21
- // Sign in using webManager's auth which initializes Firebase
22
- await webManager.auth().signInWithCustomToken(authState.token);
19
+ // Wait for WM auth state to settle FIRST (prevents race conditions)
20
+ const localState = await new Promise(resolve => {
21
+ webManager.auth().listen({ once: true }, resolve);
22
+ });
23
+
24
+ const localUid = localState.user?.uid || null;
25
+ logger.log('[AUTH-SYNC] Local auth state settled, UID:', localUid);
26
+
27
+ // Ask background for auth state comparison
28
+ const response = await new Promise((resolve) => {
29
+ extension.runtime.sendMessage(
30
+ { command: 'bxm:syncAuth', contextUid: localUid },
31
+ (res) => {
32
+ if (extension.runtime.lastError) {
33
+ logger.log('[AUTH-SYNC] Background not ready:', extension.runtime.lastError.message);
34
+ resolve({ needsSync: false });
35
+ return;
36
+ }
37
+ resolve(res || { needsSync: false });
38
+ }
39
+ );
40
+ });
41
+
42
+ // Already in sync
43
+ if (!response.needsSync) {
44
+ logger.log('[AUTH-SYNC] Already in sync with background');
45
+ return;
46
+ }
23
47
 
24
- logger.log('[AUTH-SYNC] Signed in successfully:', authState.user?.email);
25
- } catch (error) {
26
- // Token may have expired, clear it
27
- logger.error('[AUTH-SYNC] Error signing in with token:', error.message);
48
+ // Need to sign out (background is signed out, context is signed in)
49
+ if (response.signOut) {
50
+ logger.log('[AUTH-SYNC] Background signed out, signing out context...');
51
+ await webManager.auth().signOut();
52
+ return;
53
+ }
28
54
 
29
- // If token is invalid/expired, clear the auth state
30
- if (error.code === 'auth/invalid-custom-token' || error.code === 'auth/custom-token-expired') {
31
- logger.log('[AUTH-SYNC] Token expired, clearing auth state');
32
- context.extension.storage.local.remove('bxm:authState');
55
+ // Need to sign in with token
56
+ if (response.customToken) {
57
+ logger.log('[AUTH-SYNC] Syncing with background...', response.user?.email);
58
+ await webManager.auth().signInWithCustomToken(response.customToken);
59
+ logger.log('[AUTH-SYNC] Synced successfully');
33
60
  }
61
+
62
+ } catch (error) {
63
+ logger.error('[AUTH-SYNC] Error syncing with background:', error.message);
34
64
  }
35
65
  }
36
66
 
37
67
  /**
38
- * Set up storage listener for cross-context auth sync
39
- * Listens for auth state changes from background.js and syncs Firebase auth
68
+ * Set up listener for auth token broadcasts from background.js
69
+ * Handles both sign-in broadcasts and sign-out broadcasts
40
70
  * @param {Object} context - The manager instance (must have extension, webManager, logger)
41
71
  */
42
- export function setupAuthStorageListener(context) {
43
- const { extension, webManager, logger } = context;
44
-
45
- // Check existing auth state on load and sign in
46
- extension.storage.local.get('bxm:authState', (result) => {
47
- const authState = result['bxm:authState'];
72
+ export function setupAuthBroadcastListener(context) {
73
+ const { webManager, logger } = context;
48
74
 
49
- if (authState?.token) {
50
- logger.log('[AUTH-SYNC] Found existing auth state, signing in...', authState.user?.email);
51
- signInWithStoredToken(context, authState);
75
+ // Listen for messages from service worker (background.js)
76
+ navigator.serviceWorker?.addEventListener('message', async (event) => {
77
+ const { command, token } = event.data || {};
78
+
79
+ // Handle sign-in broadcast
80
+ if (command === 'bxm:signInWithToken' && token) {
81
+ logger.log('[AUTH-BROADCAST] Received sign-in broadcast');
82
+ try {
83
+ await webManager.auth().signInWithCustomToken(token);
84
+ logger.log('[AUTH-BROADCAST] Signed in via broadcast');
85
+ } catch (error) {
86
+ logger.error('[AUTH-BROADCAST] Error signing in:', error.message);
87
+ }
88
+ return;
52
89
  }
53
- });
54
90
 
55
- // Listen for WM auth state changes and sync to storage
56
- // When user signs out via WM, clear storage so background.js knows
57
- webManager.auth().listen((state) => {
58
- if (!state.user) {
59
- // User signed out - clear storage so all contexts sync
60
- logger.log('[AUTH-SYNC] WM auth signed out, clearing storage...');
61
- extension.storage.local.remove('bxm:authState');
91
+ // Handle sign-out broadcast
92
+ if (command === 'bxm:signOut') {
93
+ // Skip if already signed out (prevents loops)
94
+ if (!webManager.auth().getUser()) {
95
+ logger.log('[AUTH-BROADCAST] Already signed out, ignoring broadcast');
96
+ return;
97
+ }
98
+ logger.log('[AUTH-BROADCAST] Received sign-out broadcast');
99
+ try {
100
+ await webManager.auth().signOut();
101
+ logger.log('[AUTH-BROADCAST] Signed out via broadcast');
102
+ } catch (error) {
103
+ logger.error('[AUTH-BROADCAST] Error signing out:', error.message);
104
+ }
62
105
  }
63
106
  });
64
107
 
65
- // Listen for storage changes from background.js
66
- // Note: BEM normalizes storage to sync or local, so we listen to all areas
67
- extension.storage.onChanged.addListener((changes) => {
68
- // Check for auth state change
69
- const authChange = changes['bxm:authState'];
70
- if (!authChange) {
71
- return;
72
- }
108
+ logger.log('[AUTH-BROADCAST] Broadcast listener set up');
109
+ }
110
+
111
+ /**
112
+ * Set up listener to notify background when user signs out from this context
113
+ * @param {Object} context - The manager instance (must have extension, webManager, logger)
114
+ */
115
+ export function setupSignOutListener(context) {
116
+ const { extension, webManager, logger } = context;
73
117
 
74
- // Log
75
- logger.log('[AUTH-SYNC] Auth state changed in storage:', authChange);
118
+ // Track previous user to detect sign-out
119
+ let previousUid = null;
76
120
 
77
- // Get the new auth state
78
- const newAuthState = authChange.newValue;
121
+ webManager.auth().listen((state) => {
122
+ const currentUid = state.user?.uid || null;
79
123
 
80
- // If auth state was cleared (signed out)
81
- if (!newAuthState) {
82
- logger.log('[AUTH-SYNC] Auth state cleared, signing out...');
83
- webManager.auth().signOut();
84
- return;
124
+ // Detect sign-out (had user, now don't)
125
+ if (previousUid && !currentUid) {
126
+ logger.log('[AUTH-SYNC] Detected sign-out, notifying background...');
127
+ extension.runtime.sendMessage({ command: 'bxm:signOut' });
85
128
  }
86
129
 
87
- // Sign in with the new token
88
- if (newAuthState?.token) {
89
- signInWithStoredToken(context, newAuthState);
90
- }
130
+ previousUid = currentUid;
91
131
  });
92
132
 
93
- // Log
94
- logger.log('Auth storage listener set up');
133
+ logger.log('[AUTH-SYNC] Sign-out listener set up');
95
134
  }
96
135
 
97
136
  /**
@@ -176,8 +215,7 @@ export function setupAuthEventListeners(context) {
176
215
  });
177
216
 
178
217
  // Note: .auth-signout-btn is handled by web-manager's auth module
179
- // BEM's storage listener will detect the sign-out via onAuthStateChanged in background.js
180
- // If background hasn't initialized Firebase yet, stale storage is cleared on next auth attempt
218
+ // setupSignOutListener detects sign-out and notifies background
181
219
 
182
220
  // Log
183
221
  context.logger.log('Auth event listeners set up');
package/dist/options.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Manager as WebManager } from 'web-manager';
3
3
  import extension from './lib/extension.js';
4
4
  import LoggerLite from './lib/logger-lite.js';
5
- import { setupAuthStorageListener, setupAuthEventListeners, openAuthPage as openAuthPageHelper } from './lib/auth-helpers.js';
5
+ import { syncWithBackground, setupAuthBroadcastListener, setupSignOutListener, setupAuthEventListeners, openAuthPage as openAuthPageHelper } from './lib/auth-helpers.js';
6
6
 
7
7
  // Import theme (exposes Bootstrap to window.bootstrap)
8
8
  import '__theme__/_theme.js';
@@ -35,8 +35,14 @@ class Manager {
35
35
  this.logger.log('Auth state changed:', state);
36
36
  });
37
37
 
38
- // Set up storage listener for cross-context auth sync
39
- setupAuthStorageListener(this);
38
+ // Sync auth with background.js (waits for WM auth to settle first)
39
+ await syncWithBackground(this);
40
+
41
+ // Set up broadcast listener for sign-in/sign-out from background
42
+ setupAuthBroadcastListener(this);
43
+
44
+ // Set up sign-out listener to notify background when user signs out
45
+ setupSignOutListener(this);
40
46
 
41
47
  // Set up auth event listeners (sign in, account buttons)
42
48
  setupAuthEventListeners(this);
package/dist/page.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Manager as WebManager } from 'web-manager';
3
3
  import extension from './lib/extension.js';
4
4
  import LoggerLite from './lib/logger-lite.js';
5
- import { setupAuthStorageListener, setupAuthEventListeners, openAuthPage as openAuthPageHelper } from './lib/auth-helpers.js';
5
+ import { syncWithBackground, setupAuthBroadcastListener, setupSignOutListener, setupAuthEventListeners, openAuthPage as openAuthPageHelper } from './lib/auth-helpers.js';
6
6
 
7
7
  // Import theme (exposes Bootstrap to window.bootstrap)
8
8
  import '__theme__/_theme.js';
@@ -35,8 +35,14 @@ class Manager {
35
35
  this.logger.log('Auth state changed:', state);
36
36
  });
37
37
 
38
- // Set up storage listener for cross-context auth sync
39
- setupAuthStorageListener(this);
38
+ // Sync auth with background.js (waits for WM auth to settle first)
39
+ await syncWithBackground(this);
40
+
41
+ // Set up broadcast listener for sign-in/sign-out from background
42
+ setupAuthBroadcastListener(this);
43
+
44
+ // Set up sign-out listener to notify background when user signs out
45
+ setupSignOutListener(this);
40
46
 
41
47
  // Set up auth event listeners (sign in, account buttons)
42
48
  setupAuthEventListeners(this);
package/dist/popup.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Manager as WebManager } from 'web-manager';
3
3
  import extension from './lib/extension.js';
4
4
  import LoggerLite from './lib/logger-lite.js';
5
- import { setupAuthStorageListener, setupAuthEventListeners, openAuthPage as openAuthPageHelper } from './lib/auth-helpers.js';
5
+ import { syncWithBackground, setupAuthBroadcastListener, setupSignOutListener, setupAuthEventListeners, openAuthPage as openAuthPageHelper } from './lib/auth-helpers.js';
6
6
 
7
7
  // Import theme (exposes Bootstrap to window.bootstrap)
8
8
  import '__theme__/_theme.js';
@@ -35,8 +35,14 @@ class Manager {
35
35
  this.logger.log('Auth state changed:', state);
36
36
  });
37
37
 
38
- // Set up storage listener for cross-context auth sync
39
- setupAuthStorageListener(this);
38
+ // Sync auth with background.js (waits for WM auth to settle first)
39
+ await syncWithBackground(this);
40
+
41
+ // Set up broadcast listener for sign-in/sign-out from background
42
+ setupAuthBroadcastListener(this);
43
+
44
+ // Set up sign-out listener to notify background when user signs out
45
+ setupSignOutListener(this);
40
46
 
41
47
  // Set up auth event listeners (sign in, account buttons)
42
48
  setupAuthEventListeners(this);
package/dist/sidepanel.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { Manager as WebManager } from 'web-manager';
3
3
  import extension from './lib/extension.js';
4
4
  import LoggerLite from './lib/logger-lite.js';
5
- import { setupAuthStorageListener, setupAuthEventListeners, openAuthPage as openAuthPageHelper } from './lib/auth-helpers.js';
5
+ import { syncWithBackground, setupAuthBroadcastListener, setupSignOutListener, setupAuthEventListeners, openAuthPage as openAuthPageHelper } from './lib/auth-helpers.js';
6
6
 
7
7
  // Import theme (exposes Bootstrap to window.bootstrap)
8
8
  import '__theme__/_theme.js';
@@ -35,8 +35,14 @@ class Manager {
35
35
  this.logger.log('Auth state changed:', state);
36
36
  });
37
37
 
38
- // Set up storage listener for cross-context auth sync
39
- setupAuthStorageListener(this);
38
+ // Sync auth with background.js (waits for WM auth to settle first)
39
+ await syncWithBackground(this);
40
+
41
+ // Set up broadcast listener for sign-in/sign-out from background
42
+ setupAuthBroadcastListener(this);
43
+
44
+ // Set up sign-out listener to notify background when user signs out
45
+ setupSignOutListener(this);
40
46
 
41
47
  // Set up auth event listeners (sign in, account buttons)
42
48
  setupAuthEventListeners(this);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-extension-manager",
3
- "version": "1.3.15",
3
+ "version": "1.3.17",
4
4
  "description": "Browser Extension Manager dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {