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.
- package/dist/background.js +95 -23
- package/dist/gulp/tasks/publish.js +39 -51
- package/dist/lib/auth-helpers.js +53 -29
- package/package.json +1 -1
package/dist/background.js
CHANGED
|
@@ -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 -
|
|
306
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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');
|
package/dist/lib/auth-helpers.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
* @
|
|
18
|
+
* @returns {Promise<string|null>} The ID token or null if not signed in
|
|
8
19
|
*/
|
|
9
|
-
async function
|
|
10
|
-
const {
|
|
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]
|
|
24
|
+
logger.log('[AUTH-SYNC] Requesting fresh ID token from background...');
|
|
20
25
|
|
|
21
|
-
//
|
|
22
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
40
|
+
logger.error('[AUTH-SYNC] Error getting ID token:', error.message);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
28
44
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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?.
|
|
50
|
-
logger.log('[AUTH-SYNC] Found existing auth state
|
|
51
|
-
|
|
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
|
-
//
|
|
88
|
-
if (newAuthState?.
|
|
89
|
-
|
|
111
|
+
// User signed in - request fresh ID token from background
|
|
112
|
+
if (newAuthState?.user) {
|
|
113
|
+
syncAuthFromBackground(context);
|
|
90
114
|
}
|
|
91
115
|
});
|
|
92
116
|
|