browser-extension-manager 1.3.14 → 1.3.16

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.
@@ -68,6 +68,9 @@ class Manager {
68
68
  // Setup auth storage listener (detect sign-out from pages)
69
69
  this.setupAuthStorageListener();
70
70
 
71
+ // Restore auth state from storage on startup
72
+ this.restoreAuthState();
73
+
71
74
  // Setup livereload
72
75
  this.setupLiveReload();
73
76
 
@@ -120,10 +123,42 @@ class Manager {
120
123
  }
121
124
  });
122
125
 
126
+ // Listen for runtime messages (from popup, options, pages, etc.)
127
+ 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);
132
+ return true; // Keep channel open for async response
133
+ }
134
+ });
135
+
123
136
  // Log
124
137
  this.logger.log('Set up message handlers');
125
138
  }
126
139
 
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) {
143
+ 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' });
148
+ return;
149
+ }
150
+
151
+ // Get fresh ID token (Firebase auto-refreshes if needed)
152
+ const idToken = await this.libraries.firebaseAuth.currentUser.getIdToken(true);
153
+
154
+ this.logger.log('[AUTH] getIdToken: Providing fresh ID token');
155
+ sendResponse({ success: true, idToken: idToken });
156
+ } catch (error) {
157
+ this.logger.error('[AUTH] getIdToken error:', error.message);
158
+ sendResponse({ success: false, error: error.message });
159
+ }
160
+ }
161
+
127
162
  // Initialize Firebase
128
163
  initializeFirebase() {
129
164
  // Get Firebase config
@@ -266,6 +301,58 @@ class Manager {
266
301
  this.logger.log('[AUTH] Auth storage listener set up');
267
302
  }
268
303
 
304
+ // Restore auth state on startup
305
+ // 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();
337
+
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');
343
+
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);
353
+ }
354
+ }
355
+
269
356
  // Get or initialize Firebase auth (reuse existing instance)
270
357
  getFirebaseAuth() {
271
358
  // Return existing instance if available
@@ -298,19 +385,14 @@ class Manager {
298
385
  }
299
386
 
300
387
  // Handle Firebase auth state changes (source of truth for all contexts)
388
+ // Syncs user info to storage (NOT tokens - those are requested via messaging)
301
389
  async handleAuthStateChange(user) {
302
390
  this.logger.log('[AUTH] Auth state changed:', user?.email || 'signed out');
303
391
 
304
392
  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
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
312
395
  const authState = {
313
- token: currentState.token, // Preserve existing token
314
396
  user: {
315
397
  uid: user.uid,
316
398
  email: user.email,
@@ -322,7 +404,7 @@ class Manager {
322
404
  };
323
405
 
324
406
  await this.extension.storage.local.set({ 'bxm:authState': authState });
325
- this.logger.log('[AUTH] Auth state synced to storage');
407
+ this.logger.log('[AUTH] Auth state synced to storage (user info only)');
326
408
  } else {
327
409
  // User is signed out - clear storage
328
410
  await this.extension.storage.local.remove('bxm:authState');
@@ -330,7 +412,7 @@ class Manager {
330
412
  }
331
413
  }
332
414
 
333
- // Handle auth token from website
415
+ // Handle auth token from website (custom token from /token page)
334
416
  async handleAuthToken(token, tabId, authSourceTabId = null) {
335
417
  try {
336
418
  // Log
@@ -340,6 +422,8 @@ class Manager {
340
422
  const auth = this.getFirebaseAuth();
341
423
 
342
424
  // 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
343
427
  this.logger.log('[AUTH] Calling signInWithCustomToken...');
344
428
  const userCredential = await signInWithCustomToken(auth, token);
345
429
  const user = userCredential.user;
@@ -347,19 +431,7 @@ class Manager {
347
431
  // Log
348
432
  this.logger.log('[AUTH] Signed in successfully:', user.email);
349
433
 
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'] || {};
355
-
356
- await this.extension.storage.local.set({
357
- 'bxm:authState': {
358
- ...currentState,
359
- token: token,
360
- timestamp: Date.now(),
361
- }
362
- });
434
+ // Note: onAuthStateChanged will sync user info to storage automatically
363
435
 
364
436
  // Close the auth tab
365
437
  await this.extension.tabs.remove(tabId);
@@ -320,42 +320,23 @@ async function publishToEdge() {
320
320
  'X-ClientID': clientId,
321
321
  };
322
322
 
323
- // Step 1: Try to submit first to check if there's a pending submission
324
- // This is faster than uploading first, since upload always succeeds
325
- logger.log('[edge] Checking for pending submissions...');
323
+ // Helper to parse Edge API response (handles empty bodies)
324
+ async function parseEdgeResponse(response, label) {
325
+ const text = await response.text();
326
+ logger.log(`[edge] ${label} - Status: ${response.status}, Body: ${text || '(empty)'}`);
326
327
 
327
- const publishUrl = `https://api.addons.microsoftedge.microsoft.com/v1/products/${productId}/submissions`;
328
- const checkResponse = await fetch(publishUrl, {
329
- method: 'POST',
330
- headers: {
331
- ...edgeHeaders,
332
- 'Content-Type': 'application/json',
333
- },
334
- body: JSON.stringify({
335
- notes: `Check for pending submission`,
336
- }),
337
- });
338
-
339
- const checkData = await checkResponse.json().catch(() => null);
340
-
341
- // Log the response for debugging
342
- logger.log(`[edge] Submission check response: ${JSON.stringify(checkData)}`);
343
-
344
- // Check if there's a pending submission blocking us
345
- if (checkData && checkData.status === 'Failed') {
346
- if (checkData.errorCode === 'InProgressSubmission') {
347
- throw new Error('Extension already has a pending submission in review. Wait for it to complete before publishing again.');
348
- }
349
- if (checkData.errorCode === 'UnpublishInProgress') {
350
- throw new Error('Extension is being unpublished. Wait for unpublish to complete before publishing.');
328
+ if (!text) {
329
+ return { status: response.status, data: null };
351
330
  }
352
- // If it failed for another reason (like no draft package), that's expected - continue to upload
353
- if (checkData.errorCode !== 'NoDraftPackage' && checkData.errorCode !== 'NoPackageToPublish') {
354
- logger.log(`[edge] Check failed with: ${checkData.errorCode} - ${checkData.message}`);
331
+
332
+ try {
333
+ return { status: response.status, data: JSON.parse(text) };
334
+ } catch (e) {
335
+ return { status: response.status, data: text };
355
336
  }
356
337
  }
357
338
 
358
- // Step 2: Upload the package
339
+ // Step 1: Upload the package first
359
340
  logger.log('[edge] Uploading to Microsoft Edge Add-ons...');
360
341
 
361
342
  const zipBuffer = jetpack.read(PATHS.chromium.zip, 'buffer');
@@ -370,17 +351,16 @@ async function publishToEdge() {
370
351
  body: zipBuffer,
371
352
  });
372
353
 
354
+ const upload = await parseEdgeResponse(uploadResponse, 'Upload response');
355
+
373
356
  if (!uploadResponse.ok) {
374
- const errorText = await uploadResponse.text();
375
- throw new Error(`Edge upload error: ${uploadResponse.status} - ${errorText}`);
357
+ throw new Error(`Edge upload error: ${upload.status} - ${JSON.stringify(upload.data)}`);
376
358
  }
377
359
 
378
- const uploadData = await uploadResponse.json().catch(() => null);
379
- logger.log(`[edge] Upload response: ${JSON.stringify(uploadData)}`);
380
-
381
360
  logger.log('[edge] Package uploaded, submitting for review...');
382
361
 
383
- // Step 3: Submit for review
362
+ // Step 2: Submit for review - this is where we'll get InProgressSubmission if there's a pending review
363
+ const publishUrl = `https://api.addons.microsoftedge.microsoft.com/v1/products/${productId}/submissions`;
384
364
  const publishResponse = await fetch(publishUrl, {
385
365
  method: 'POST',
386
366
  headers: {
@@ -392,25 +372,33 @@ async function publishToEdge() {
392
372
  }),
393
373
  });
394
374
 
395
- const publishData = await publishResponse.json().catch(() => null);
396
-
397
- // Log the full response
398
- logger.log(`[edge] Publish response: ${JSON.stringify(publishData)}`);
375
+ const publish = await parseEdgeResponse(publishResponse, 'Publish response');
399
376
 
400
- // Check for HTTP errors
377
+ // Check for HTTP errors (4xx, 5xx)
401
378
  if (!publishResponse.ok) {
402
- throw new Error(`Edge publish error: ${publishResponse.status} - ${JSON.stringify(publishData)}`);
403
- }
404
-
405
- // Check for API-level failures (HTTP 200 but status: "Failed")
406
- if (publishData && publishData.status === 'Failed') {
407
- if (publishData.errorCode === 'InProgressSubmission') {
379
+ // Check if it's a 409 Conflict or similar indicating in-progress submission
380
+ if (publish.status === 409) {
408
381
  throw new Error('Extension already has a pending submission in review. Wait for it to complete before publishing again.');
409
382
  }
410
- if (publishData.errorCode === 'UnpublishInProgress') {
411
- throw new Error('Extension is being unpublished. Wait for unpublish to complete before publishing.');
383
+ throw new Error(`Edge publish error: ${publish.status} - ${JSON.stringify(publish.data)}`);
384
+ }
385
+
386
+ // Check for API-level failures (HTTP 200/202 but status: "Failed" in body)
387
+ if (publish.data && typeof publish.data === 'object') {
388
+ if (publish.data.status === 'Failed') {
389
+ if (publish.data.errorCode === 'InProgressSubmission') {
390
+ throw new Error('Extension already has a pending submission in review. Wait for it to complete before publishing again.');
391
+ }
392
+ if (publish.data.errorCode === 'UnpublishInProgress') {
393
+ throw new Error('Extension is being unpublished. Wait for unpublish to complete before publishing.');
394
+ }
395
+ throw new Error(`Edge publish failed: ${publish.data.message || publish.data.errorCode || 'Unknown error'}`);
412
396
  }
413
- throw new Error(`Edge publish failed: ${publishData.message || publishData.errorCode || 'Unknown error'}`);
397
+ }
398
+
399
+ // HTTP 202 Accepted means submission was queued successfully
400
+ if (publish.status === 202) {
401
+ logger.log('[edge] Submission accepted and queued for review');
414
402
  }
415
403
 
416
404
  logger.log('[edge] Upload complete');
@@ -1,36 +1,59 @@
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 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
3
13
 
4
14
  /**
5
- * Sign in with custom token from storage
15
+ * Request fresh ID token from background.js
16
+ * The ID token can be used for authenticated API calls
6
17
  * @param {Object} context - The manager instance
7
- * @param {Object} authState - Auth state with token
18
+ * @returns {Promise<string|null>} The ID token or null if not signed in
8
19
  */
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
- }
20
+ export async function getIdToken(context) {
21
+ const { extension, logger } = context;
17
22
 
18
23
  try {
19
- logger.log('[AUTH-SYNC] Signing in with stored token...');
24
+ logger.log('[AUTH-SYNC] Requesting fresh ID token from background...');
20
25
 
21
- // Sign in using webManager's auth which initializes Firebase
22
- await webManager.auth().signInWithCustomToken(authState.token);
26
+ // Request fresh ID token from background.js
27
+ const response = await new Promise((resolve) => {
28
+ extension.runtime.sendMessage({ command: 'bxm:getIdToken' }, resolve);
29
+ });
23
30
 
24
- logger.log('[AUTH-SYNC] Signed in successfully:', authState.user?.email);
31
+ // Check response
32
+ if (!response?.success) {
33
+ logger.log('[AUTH-SYNC] Failed to get ID token:', response?.error || 'Unknown error');
34
+ return null;
35
+ }
36
+
37
+ logger.log('[AUTH-SYNC] Got fresh ID token from background');
38
+ return response.idToken;
25
39
  } catch (error) {
26
- // Token may have expired, clear it
27
- logger.error('[AUTH-SYNC] Error signing in with token:', error.message);
40
+ logger.error('[AUTH-SYNC] Error getting ID token:', error.message);
41
+ return null;
42
+ }
43
+ }
28
44
 
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');
33
- }
45
+ /**
46
+ * Sync auth state from background.js
47
+ * Fetches fresh ID token and stores for API calls
48
+ * @param {Object} context - The manager instance
49
+ */
50
+ async function syncAuthFromBackground(context) {
51
+ const idToken = await getIdToken(context);
52
+
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');
34
57
  }
35
58
  }
36
59
 
@@ -42,13 +65,14 @@ async function signInWithStoredToken(context, authState) {
42
65
  export function setupAuthStorageListener(context) {
43
66
  const { extension, webManager, logger } = context;
44
67
 
45
- // Check existing auth state on load and sign in
68
+ // Check existing auth state on load
46
69
  extension.storage.local.get('bxm:authState', (result) => {
47
70
  const authState = result['bxm:authState'];
48
71
 
49
- if (authState?.token) {
50
- logger.log('[AUTH-SYNC] Found existing auth state, signing in...', authState.user?.email);
51
- signInWithStoredToken(context, authState);
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);
52
76
  }
53
77
  });
54
78
 
@@ -63,7 +87,6 @@ export function setupAuthStorageListener(context) {
63
87
  });
64
88
 
65
89
  // Listen for storage changes from background.js
66
- // Note: BEM normalizes storage to sync or local, so we listen to all areas
67
90
  extension.storage.onChanged.addListener((changes) => {
68
91
  // Check for auth state change
69
92
  const authChange = changes['bxm:authState'];
@@ -80,13 +103,14 @@ export function setupAuthStorageListener(context) {
80
103
  // If auth state was cleared (signed out)
81
104
  if (!newAuthState) {
82
105
  logger.log('[AUTH-SYNC] Auth state cleared, signing out...');
106
+ context._idToken = null;
83
107
  webManager.auth().signOut();
84
108
  return;
85
109
  }
86
110
 
87
- // Sign in with the new token
88
- if (newAuthState?.token) {
89
- signInWithStoredToken(context, newAuthState);
111
+ // User signed in - request fresh ID token from background
112
+ if (newAuthState?.user) {
113
+ syncAuthFromBackground(context);
90
114
  }
91
115
  });
92
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-extension-manager",
3
- "version": "1.3.14",
3
+ "version": "1.3.16",
4
4
  "description": "Browser Extension Manager dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {