browser-extension-manager 1.3.16 → 1.3.18

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/CHANGELOG.md CHANGED
@@ -15,6 +15,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
17
  ---
18
+ ## [1.3.17] - 2025-12-23
19
+ ### Changed
20
+ - Auth system now uses messaging instead of `chrome.storage` for cross-context sync
21
+ - Background.js is the source of truth; contexts sync via `bxm:syncAuth` message on load
22
+ - Fresh custom tokens fetched from server only when context UID differs from background UID
23
+
24
+ ### Fixed
25
+ - Auth failures caused by expired custom tokens (tokens no longer stored, fetched fresh when needed)
26
+
18
27
  ## [1.3.0] - 2025-12-16
19
28
 
20
29
  ### Added
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,11 +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();
70
-
71
- // Restore auth state from storage on startup
72
- this.restoreAuthState();
68
+ // Initialize Firebase auth on startup (restores persisted session if any)
69
+ this.initializeAuth();
73
70
 
74
71
  // Setup livereload
75
72
  this.setupLiveReload();
@@ -125,10 +122,15 @@ class Manager {
125
122
 
126
123
  // Listen for runtime messages (from popup, options, pages, etc.)
127
124
  this.extension.runtime.onMessage.addListener((message, _sender, sendResponse) => {
128
- // Handle auth ID token requests from other contexts
129
- // Other contexts use storage listener to know WHEN to request, this just provides the token
130
- if (message.command === 'bxm:getIdToken') {
131
- this.handleGetIdToken(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);
132
134
  return true; // Keep channel open for async response
133
135
  }
134
136
  });
@@ -137,28 +139,128 @@ class Manager {
137
139
  this.logger.log('Set up message handlers');
138
140
  }
139
141
 
140
- // Handle ID token request from other contexts
141
- // Called when other contexts detect auth state in storage and need a fresh token to sign in
142
- async handleGetIdToken(sendResponse) {
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) {
143
145
  try {
144
- // Check if Firebase auth is initialized and user is signed in
145
- if (!this.libraries.firebaseAuth?.currentUser) {
146
- this.logger.log('[AUTH] getIdToken: No user signed in');
147
- sendResponse({ success: false, error: 'No user signed in' });
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 });
148
166
  return;
149
167
  }
150
168
 
151
- // Get fresh ID token (Firebase auto-refreshes if needed)
152
- const idToken = await this.libraries.firebaseAuth.currentUser.getIdToken(true);
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();
153
238
 
154
- this.logger.log('[AUTH] getIdToken: Providing fresh ID token');
155
- sendResponse({ success: true, idToken: idToken });
239
+ this.logger.log('[AUTH] handleSignOut: Complete');
240
+ sendResponse({ success: true });
156
241
  } catch (error) {
157
- this.logger.error('[AUTH] getIdToken error:', error.message);
242
+ this.logger.error('[AUTH] handleSignOut error:', error.message);
158
243
  sendResponse({ success: false, error: error.message });
159
244
  }
160
245
  }
161
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
+
162
264
  // Initialize Firebase
163
265
  initializeFirebase() {
164
266
  // Get Firebase config
@@ -281,75 +383,25 @@ class Manager {
281
383
  });
282
384
  }
283
385
 
284
- // Setup auth storage listener (detect sign-out from pages)
285
- setupAuthStorageListener() {
286
- this.extension.storage.onChanged.addListener((changes) => {
287
- const authChange = changes['bxm:authState'];
288
- if (!authChange) {
289
- return;
290
- }
291
-
292
- this.logger.log('[AUTH] Storage auth state changed:', authChange.newValue ? 'signed in' : 'signed out');
293
-
294
- // If storage was cleared (sign-out from a page) and we have Firebase initialized, sign out
295
- if (!authChange.newValue && this.libraries.firebaseAuth) {
296
- this.logger.log('[AUTH] Signing out background Firebase...');
297
- this.libraries.firebaseAuth.signOut();
298
- }
299
- });
300
-
301
- this.logger.log('[AUTH] Auth storage listener set up');
302
- }
303
-
304
- // Restore auth state on startup
386
+ // Initialize Firebase auth on startup
305
387
  // Firebase Auth persists sessions in IndexedDB - we just need to initialize it
306
- // and onAuthStateChanged will fire if there's a persisted session
307
- async restoreAuthState() {
308
- try {
309
- // Check for existing auth state in storage (for logging purposes)
310
- const result = await new Promise(resolve =>
311
- this.extension.storage.local.get('bxm:authState', resolve)
312
- );
313
- const authState = result['bxm:authState'];
314
-
315
- // Log existing storage state
316
- if (authState) {
317
- this.logger.log('[AUTH] Found existing auth state in storage on startup:', {
318
- user: authState.user?.email || 'unknown',
319
- timestamp: authState.timestamp,
320
- age: authState.timestamp ? `${Math.round((Date.now() - authState.timestamp) / 1000 / 60)} minutes ago` : 'unknown',
321
- });
322
- } else {
323
- this.logger.log('[AUTH] No existing auth state found in storage on startup');
324
- }
325
-
326
- // Get Firebase config
327
- const firebaseConfig = this.config?.firebase?.app?.config;
328
- if (!firebaseConfig) {
329
- this.logger.log('[AUTH] Firebase config not available, skipping auth restore');
330
- return;
331
- }
332
-
333
- // Initialize Firebase auth - it will auto-restore from IndexedDB if session exists
334
- // onAuthStateChanged (set up in getFirebaseAuth) will fire and sync to storage
335
- this.logger.log('[AUTH] Initializing Firebase Auth (will restore persisted session if any)...');
336
- const auth = this.getFirebaseAuth();
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
+ }
337
395
 
338
- // Check if already signed in (Firebase restored from IndexedDB)
339
- if (auth.currentUser) {
340
- this.logger.log('[AUTH] Firebase restored session from persistence:', auth.currentUser.email);
341
- } else {
342
- this.logger.log('[AUTH] No persisted Firebase session found');
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();
343
399
 
344
- // If storage has auth state but Firebase doesn't, storage is stale - clear it
345
- if (authState) {
346
- this.logger.log('[AUTH] Clearing stale auth state from storage');
347
- await this.extension.storage.local.remove('bxm:authState');
348
- }
349
- }
350
-
351
- } catch (error) {
352
- this.logger.error('[AUTH] Error restoring auth state:', error.message);
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');
353
405
  }
354
406
  }
355
407
 
@@ -385,31 +437,10 @@ class Manager {
385
437
  }
386
438
 
387
439
  // Handle Firebase auth state changes (source of truth for all contexts)
388
- // Syncs user info to storage (NOT tokens - those are requested via messaging)
389
- async handleAuthStateChange(user) {
440
+ // No storage operations - Web Manager handles auth state internally
441
+ handleAuthStateChange(user) {
390
442
  this.logger.log('[AUTH] Auth state changed:', user?.email || 'signed out');
391
-
392
- if (user) {
393
- // User is signed in - store user info only (no tokens, they expire)
394
- // Other contexts will request fresh ID tokens via bxm:getIdToken message
395
- const authState = {
396
- user: {
397
- uid: user.uid,
398
- email: user.email,
399
- displayName: user.displayName,
400
- photoURL: user.photoURL,
401
- emailVerified: user.emailVerified,
402
- },
403
- timestamp: Date.now(),
404
- };
405
-
406
- await this.extension.storage.local.set({ 'bxm:authState': authState });
407
- this.logger.log('[AUTH] Auth state synced to storage (user info only)');
408
- } else {
409
- // User is signed out - clear storage
410
- await this.extension.storage.local.remove('bxm:authState');
411
- this.logger.log('[AUTH] Auth state cleared from storage');
412
- }
443
+ // Nothing else to do - contexts sync via messages, WM handles UI
413
444
  }
414
445
 
415
446
  // Handle auth token from website (custom token from /token page)
@@ -422,8 +453,6 @@ class Manager {
422
453
  const auth = this.getFirebaseAuth();
423
454
 
424
455
  // Sign in with custom token
425
- // Firebase Auth will persist the session - we don't store the custom token (it expires in 1 hour)
426
- // Other contexts will request fresh ID tokens via bxm:getIdToken message
427
456
  this.logger.log('[AUTH] Calling signInWithCustomToken...');
428
457
  const userCredential = await signInWithCustomToken(auth, token);
429
458
  const user = userCredential.user;
@@ -431,7 +460,12 @@ class Manager {
431
460
  // Log
432
461
  this.logger.log('[AUTH] Signed in successfully:', user.email);
433
462
 
434
- // Note: onAuthStateChanged will sync user info to storage automatically
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);
466
+
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
435
469
 
436
470
  // Close the auth tab
437
471
  await this.extension.tabs.remove(tabId);
@@ -453,6 +487,29 @@ class Manager {
453
487
  }
454
488
  }
455
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
+
456
513
  // Setup livereload
457
514
  setupLiveReload() {
458
515
  // Quit if not in dev mode
@@ -2,120 +2,135 @@
2
2
  // Used by popup.js, options.js, sidepanel.js, page.js
3
3
  //
4
4
  // Architecture:
5
- // - Background.js is the source of truth for Firebase Auth (persists in IndexedDB)
6
- // - Storage (bxm:authState): Contains user info only, used for UI updates and signaling auth changes
7
- // - Messages (bxm:getIdToken): Request fresh ID token from background.js when needed
8
- //
9
- // Flow:
10
- // 1. Storage listener detects auth state change (user signed in/out)
11
- // 2. If signed in, request fresh ID token from background.js via message
12
- // 3. Sign in local Firebase with the ID token using signInWithCustomToken workaround
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
13
9
 
14
10
  /**
15
- * Request fresh ID token from background.js
16
- * The ID token can be used for authenticated API calls
17
- * @param {Object} context - The manager instance
18
- * @returns {Promise<string|null>} The ID token or null if not signed in
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)
19
14
  */
20
- export async function getIdToken(context) {
21
- const { extension, logger } = context;
15
+ export async function syncWithBackground(context) {
16
+ const { extension, webManager, logger } = context;
22
17
 
23
18
  try {
24
- logger.log('[AUTH-SYNC] Requesting fresh ID token from background...');
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);
25
26
 
26
- // Request fresh ID token from background.js
27
+ // Ask background for auth state comparison
27
28
  const response = await new Promise((resolve) => {
28
- extension.runtime.sendMessage({ command: 'bxm:getIdToken' }, 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
+ );
29
40
  });
30
41
 
31
- // Check response
32
- if (!response?.success) {
33
- logger.log('[AUTH-SYNC] Failed to get ID token:', response?.error || 'Unknown error');
34
- return null;
42
+ // Already in sync
43
+ if (!response.needsSync) {
44
+ logger.log('[AUTH-SYNC] Already in sync with background');
45
+ return;
46
+ }
47
+
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
+ }
54
+
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');
35
60
  }
36
61
 
37
- logger.log('[AUTH-SYNC] Got fresh ID token from background');
38
- return response.idToken;
39
62
  } catch (error) {
40
- logger.error('[AUTH-SYNC] Error getting ID token:', error.message);
41
- return null;
63
+ logger.error('[AUTH-SYNC] Error syncing with background:', error.message);
42
64
  }
43
65
  }
44
66
 
45
67
  /**
46
- * Sync auth state from background.js
47
- * Fetches fresh ID token and stores for API calls
48
- * @param {Object} context - The manager instance
68
+ * Set up listener for auth token broadcasts from background.js
69
+ * Handles both sign-in broadcasts and sign-out broadcasts
70
+ * @param {Object} context - The manager instance (must have extension, webManager, logger)
49
71
  */
50
- async function syncAuthFromBackground(context) {
51
- const idToken = await getIdToken(context);
72
+ export function setupAuthBroadcastListener(context) {
73
+ const { webManager, logger } = context;
74
+
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;
89
+ }
52
90
 
53
- if (idToken) {
54
- // Store on context for API calls
55
- context._idToken = idToken;
56
- context.logger.log('[AUTH-SYNC] ID token synced and stored');
57
- }
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
+ }
105
+ }
106
+ });
107
+
108
+ logger.log('[AUTH-BROADCAST] Broadcast listener set up');
58
109
  }
59
110
 
60
111
  /**
61
- * Set up storage listener for cross-context auth sync
62
- * Listens for auth state changes from background.js and syncs Firebase auth
112
+ * Set up listener to notify background when user signs out from this context
63
113
  * @param {Object} context - The manager instance (must have extension, webManager, logger)
64
114
  */
65
- export function setupAuthStorageListener(context) {
115
+ export function setupSignOutListener(context) {
66
116
  const { extension, webManager, logger } = context;
67
117
 
68
- // Check existing auth state on load
69
- extension.storage.local.get('bxm:authState', (result) => {
70
- const authState = result['bxm:authState'];
71
-
72
- if (authState?.user) {
73
- logger.log('[AUTH-SYNC] Found existing auth state on load:', authState.user?.email);
74
- // Request fresh ID token from background for any API calls
75
- syncAuthFromBackground(context);
76
- }
77
- });
118
+ // Track previous user to detect sign-out
119
+ let previousUid = null;
78
120
 
79
- // Listen for WM auth state changes and sync to storage
80
- // When user signs out via WM, clear storage so background.js knows
81
121
  webManager.auth().listen((state) => {
82
- if (!state.user) {
83
- // User signed out - clear storage so all contexts sync
84
- logger.log('[AUTH-SYNC] WM auth signed out, clearing storage...');
85
- extension.storage.local.remove('bxm:authState');
86
- }
87
- });
88
-
89
- // Listen for storage changes from background.js
90
- extension.storage.onChanged.addListener((changes) => {
91
- // Check for auth state change
92
- const authChange = changes['bxm:authState'];
93
- if (!authChange) {
94
- return;
95
- }
96
-
97
- // Log
98
- logger.log('[AUTH-SYNC] Auth state changed in storage:', authChange);
99
-
100
- // Get the new auth state
101
- const newAuthState = authChange.newValue;
122
+ const currentUid = state.user?.uid || null;
102
123
 
103
- // If auth state was cleared (signed out)
104
- if (!newAuthState) {
105
- logger.log('[AUTH-SYNC] Auth state cleared, signing out...');
106
- context._idToken = null;
107
- webManager.auth().signOut();
108
- 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' });
109
128
  }
110
129
 
111
- // User signed in - request fresh ID token from background
112
- if (newAuthState?.user) {
113
- syncAuthFromBackground(context);
114
- }
130
+ previousUid = currentUid;
115
131
  });
116
132
 
117
- // Log
118
- logger.log('Auth storage listener set up');
133
+ logger.log('[AUTH-SYNC] Sign-out listener set up');
119
134
  }
120
135
 
121
136
  /**
@@ -186,22 +201,8 @@ export function setupAuthEventListeners(context) {
186
201
  openAuthPage(context);
187
202
  });
188
203
 
189
- // Account button (.auth-account-btn) - opens account page on website
190
- document.addEventListener('click', (event) => {
191
- const $accountBtn = event.target.closest('.auth-account-btn');
192
- if (!$accountBtn) {
193
- return;
194
- }
195
-
196
- event.preventDefault();
197
- event.stopPropagation();
198
-
199
- openAuthPage(context, { path: '/account' });
200
- });
201
-
202
204
  // Note: .auth-signout-btn is handled by web-manager's auth module
203
- // BEM's storage listener will detect the sign-out via onAuthStateChanged in background.js
204
- // If background hasn't initialized Firebase yet, stale storage is cleared on next auth attempt
205
+ // setupSignOutListener detects sign-out and notifies background
205
206
 
206
207
  // Log
207
208
  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.16",
3
+ "version": "1.3.18",
4
4
  "description": "Browser Extension Manager dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {