browser-extension-manager 1.2.3 → 1.2.5

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.
@@ -15,6 +15,9 @@ const config = Manager.getConfig('project');
15
15
  const rootPathPackage = Manager.getRootPath('main');
16
16
  const rootPathProject = Manager.getRootPath('project');
17
17
 
18
+ // Constants
19
+ const LOUD = process.env.BXM_LOUD_LOGS === 'true';
20
+
18
21
  // Glob
19
22
  const input = [
20
23
  // Files to include
@@ -108,7 +111,9 @@ function customTransform() {
108
111
  const relativePath = path.relative(file.base, file.path).replace(/\\/g, '/');
109
112
 
110
113
  // Log
111
- logger.log(`Processing file: ${relativePath}`);
114
+ if (LOUD) {
115
+ logger.log(`Processing file: ${relativePath}`);
116
+ }
112
117
 
113
118
  // Change path if it starts with 'pages/'
114
119
  // if (relativePath.startsWith('pages/')) {
@@ -58,6 +58,7 @@ async function generateBuildJs(outputDir) {
58
58
  },
59
59
  config: {
60
60
  // Core metadata
61
+ runtime: 'browser-extension',
61
62
  version: project.version,
62
63
  environment: Manager.getEnvironment(),
63
64
  buildTime: Date.now(),
@@ -97,8 +98,11 @@ async function generateBuildJs(outputDir) {
97
98
  refreshNewVersion: { enabled: true, config: {} },
98
99
  serviceWorker: { enabled: false, config: {} },
99
100
 
100
- // Analytics
101
- google_analytics: config.google_analytics || {},
101
+ // Tracking
102
+ tracking: {
103
+ 'google-analytics': config.google_analytics?.id || '',
104
+ 'google-analytics-secret': config.google_analytics?.secret || '',
105
+ },
102
106
 
103
107
  // Theme config
104
108
  theme: config.theme || {},
@@ -282,7 +286,12 @@ async function packageZip() {
282
286
 
283
287
  // Create packed extension (.zip)
284
288
  if (Manager.isBuildMode()) {
285
- await execute(`zip -r ${zipPath} ${inputDir}`);
289
+ // Remove existing zip if it exists
290
+ jetpack.remove(zipPath);
291
+
292
+ // Zip contents of raw directory (not the directory itself)
293
+ // This ensures manifest.json is at the root of the zip
294
+ await execute(`cd ${inputDir} && zip -r ../../${zipPath} .`);
286
295
  logger.log(`Zipped package created at ${zipPath}`);
287
296
  } else {
288
297
  logger.log(`Skipping zip (not in build mode)`);
@@ -12,19 +12,36 @@ const project = Manager.getPackage('project');
12
12
  // Paths
13
13
  const zipPath = path.join(process.cwd(), 'packaged', 'extension.zip');
14
14
 
15
+ // Helper to check if a credential is valid (not empty or placeholder)
16
+ function isValidCredential(value) {
17
+ return value && !value.startsWith('your-');
18
+ }
19
+
15
20
  // Store configurations - all credentials come from .env file
16
21
  const STORES = {
17
22
  chrome: {
18
23
  name: 'Chrome Web Store',
19
- enabled: () => !!(process.env.CHROME_EXTENSION_ID && process.env.CHROME_CLIENT_ID),
24
+ submitUrl: 'https://chrome.google.com/webstore/devconsole',
25
+ apiUrl: 'https://developer.chrome.com/docs/webstore/using_webstore_api/',
26
+ enabled: () => isValidCredential(process.env.CHROME_EXTENSION_ID) && isValidCredential(process.env.CHROME_CLIENT_ID),
20
27
  },
21
28
  firefox: {
22
29
  name: 'Firefox Add-ons',
23
- enabled: () => !!(process.env.FIREFOX_API_KEY && process.env.FIREFOX_API_SECRET),
30
+ submitUrl: 'https://addons.mozilla.org/en-US/developers/addon/submit/distribution',
31
+ apiUrl: 'https://addons.mozilla.org/developers/addon/api/key/',
32
+ enabled: () => isValidCredential(process.env.FIREFOX_API_KEY) && isValidCredential(process.env.FIREFOX_API_SECRET),
24
33
  },
25
34
  edge: {
26
35
  name: 'Microsoft Edge Add-ons',
27
- enabled: () => !!(process.env.EDGE_PRODUCT_ID && process.env.EDGE_CLIENT_ID),
36
+ submitUrl: 'https://partner.microsoft.com/dashboard/microsoftedge/',
37
+ apiUrl: 'https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/api/using-addons-api',
38
+ enabled: () => isValidCredential(process.env.EDGE_PRODUCT_ID) && isValidCredential(process.env.EDGE_CLIENT_ID),
39
+ },
40
+ opera: {
41
+ name: 'Opera Add-ons',
42
+ submitUrl: 'https://addons.opera.com/developer/',
43
+ apiUrl: null,
44
+ enabled: () => false,
28
45
  },
29
46
  };
30
47
 
@@ -50,20 +67,30 @@ async function publish(complete) {
50
67
 
51
68
  // Get enabled stores
52
69
  const enabledStores = Object.entries(STORES)
53
- .filter(([key, store]) => store.enabled())
70
+ .filter(([, store]) => store.enabled())
54
71
  .map(([key]) => key);
55
72
 
73
+ // If no stores are configured, error and show all store info
56
74
  if (enabledStores.length === 0) {
57
- logger.warn('No stores configured for publishing. Add credentials to .env file');
58
- logger.log('Required environment variables:');
59
- logger.log(' Chrome: CHROME_EXTENSION_ID, CHROME_CLIENT_ID, CHROME_CLIENT_SECRET, CHROME_REFRESH_TOKEN');
60
- logger.log(' Firefox: FIREFOX_EXTENSION_ID, FIREFOX_API_KEY, FIREFOX_API_SECRET');
61
- logger.log(' Edge: EDGE_PRODUCT_ID, EDGE_CLIENT_ID, EDGE_API_KEY');
62
- return complete();
75
+ logger.error('No stores configured for publishing. Add credentials to .env file');
76
+ logger.log('');
77
+ logger.log('Store URLs and API documentation:');
78
+ Object.entries(STORES).forEach(([, store]) => {
79
+ logger.log(` ${store.name}:`);
80
+ logger.log(` Submit: ${store.submitUrl}`);
81
+ logger.log(` API: ${store.apiUrl || 'N/A (manual submission only)'}`);
82
+ });
83
+ throw new Error('No stores configured for publishing');
63
84
  }
64
85
 
65
86
  logger.log(`Publishing to: ${enabledStores.join(', ')}`);
66
87
 
88
+ // Track results
89
+ const results = {
90
+ success: [],
91
+ failed: [],
92
+ };
93
+
67
94
  // Run publish tasks in parallel
68
95
  const publishTasks = enabledStores.map(async (store) => {
69
96
  try {
@@ -79,14 +106,37 @@ async function publish(complete) {
79
106
  break;
80
107
  }
81
108
  logger.log(`[${store}] Published successfully`);
109
+ results.success.push(store);
82
110
  } catch (e) {
83
111
  logger.error(`[${store}] Publish failed: ${e.message}`);
112
+ results.failed.push(store);
84
113
  }
85
114
  });
86
115
 
87
116
  await Promise.all(publishTasks);
88
117
 
118
+ // Log completion and show all store URLs
119
+ logger.log('');
120
+ logger.log('Store URLs:');
121
+ Object.entries(STORES).forEach(([key, store]) => {
122
+ let status = '○ Manual';
123
+ if (results.success.includes(key)) {
124
+ status = '✓ Published';
125
+ } else if (results.failed.includes(key)) {
126
+ status = '✗ Failed';
127
+ }
128
+ logger.log(` ${store.name}: ${status}`);
129
+ logger.log(` Submit: ${store.submitUrl}`);
130
+ logger.log(` API: ${store.apiUrl || 'N/A (manual submission only)'}`);
131
+ });
132
+
133
+ // Throw error if any failed
134
+ if (results.failed.length > 0) {
135
+ throw new Error(`Publish failed for: ${results.failed.join(', ')}`);
136
+ }
137
+
89
138
  // Log
139
+ logger.log('');
90
140
  logger.log('Publish finished!');
91
141
 
92
142
  // Complete
@@ -49,12 +49,11 @@ const watchInput = [
49
49
  `${rootPathPackage}/dist/assets/themes/**/*.js`,
50
50
  'src/assets/themes/**/*.js',
51
51
 
52
- // Component files - watch for changes to trigger recompile
53
- `${rootPathPackage}/src/popup.js`,
54
- `${rootPathPackage}/src/options.js`,
55
- `${rootPathPackage}/src/page.js`,
56
- `${rootPathPackage}/src/sidepanel.js`,
57
- `${rootPathPackage}/src/index.js`,
52
+ // All project assets js - watch for changes but don't compile as entry points
53
+ 'src/assets/js/**/*.js',
54
+
55
+ // All BEM package src files - watch for changes (includes background.js, popup.js, etc.)
56
+ `${rootPathPackage}/src/**/*.js`,
58
57
 
59
58
  // So we can watch for changes while we're developing web-manager
60
59
  `${rootPathPackage}/../web-manager/src`,
@@ -108,6 +107,9 @@ function getSettings() {
108
107
  },
109
108
  // Add module resolution paths
110
109
  modules: [
110
+ // Local web-manager's node_modules (for when we're using "web-manager": "file:../web-manager")
111
+ path.resolve(rootPathPackage, '../web-manager/node_modules'),
112
+
111
113
  // Package's node_modules
112
114
  path.resolve(rootPathPackage, 'node_modules'),
113
115
 
@@ -0,0 +1,184 @@
1
+ // Auth helpers for cross-context auth sync in browser extensions
2
+ // Used by popup.js, options.js, sidepanel.js, page.js
3
+
4
+ /**
5
+ * Sign in with custom token from storage
6
+ * @param {Object} context - The manager instance
7
+ * @param {Object} authState - Auth state with token
8
+ */
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
+ }
17
+
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);
23
+
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);
28
+
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.remove('bxm:authState');
33
+ }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Set up storage listener for cross-context auth sync
39
+ * Listens for auth state changes from background.js and syncs Firebase auth
40
+ * @param {Object} context - The manager instance (must have extension, webManager, logger)
41
+ */
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.get('bxm:authState', (result) => {
47
+ const authState = result['bxm:authState'];
48
+
49
+ if (authState?.token) {
50
+ logger.log('[AUTH-SYNC] Found existing auth state, signing in...', authState.user?.email);
51
+ signInWithStoredToken(context, authState);
52
+ }
53
+ });
54
+
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.remove('bxm:authState');
62
+ }
63
+ });
64
+
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
+ }
73
+
74
+ // Log
75
+ logger.log('[AUTH-SYNC] Auth state changed in storage:', authChange);
76
+
77
+ // Get the new auth state
78
+ const newAuthState = authChange.newValue;
79
+
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;
85
+ }
86
+
87
+ // Sign in with the new token
88
+ if (newAuthState?.token) {
89
+ signInWithStoredToken(context, newAuthState);
90
+ }
91
+ });
92
+
93
+ // Log
94
+ logger.log('Auth storage listener set up');
95
+ }
96
+
97
+ /**
98
+ * Open auth page in new tab (for signing in via website)
99
+ * @param {Object} context - The manager instance (must have extension, webManager, logger)
100
+ * @param {Object} options - Options object
101
+ * @param {string} options.path - Path to open (default: '/token')
102
+ * @param {string} options.authReturnUrl - Return URL for electron/deep links
103
+ */
104
+ export function openAuthPage(context, options = {}) {
105
+ const { extension, webManager, logger } = context;
106
+
107
+ // Get auth domain from config
108
+ const authDomain = webManager.config?.firebase?.app?.config?.authDomain;
109
+
110
+ if (!authDomain) {
111
+ logger.error('No authDomain configured');
112
+ return;
113
+ }
114
+
115
+ // Build the URL
116
+ const path = options.path || '/token';
117
+ const authUrl = new URL(path, `https://${authDomain}`);
118
+
119
+ // Add return URL if provided (for electron/deep links)
120
+ if (options.authReturnUrl) {
121
+ authUrl.searchParams.set('authReturnUrl', options.authReturnUrl);
122
+ }
123
+
124
+ // Log
125
+ logger.log('Opening auth page:', authUrl.toString());
126
+
127
+ // Get current active tab so we can restore it after auth
128
+ extension.tabs.query({ active: true, currentWindow: true }, (tabs) => {
129
+ const authSourceTabId = tabs[0]?.id;
130
+
131
+ // Add source tab ID to URL so background can restore it
132
+ if (authSourceTabId) {
133
+ authUrl.searchParams.set('authSourceTabId', authSourceTabId);
134
+ }
135
+
136
+ // Open in new tab
137
+ extension.tabs.create({ url: authUrl.toString() });
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Set up DOM event listeners for auth buttons (sign in, account)
143
+ * Uses event delegation so it works with dynamically rendered content
144
+ * @param {Object} context - The manager instance (must have extension, webManager, logger)
145
+ */
146
+ export function setupAuthEventListeners(context) {
147
+ // Only set up once DOM is ready
148
+ if (typeof document === 'undefined') {
149
+ return;
150
+ }
151
+
152
+ // Sign in button (.auth-signin-btn) - opens auth page
153
+ document.addEventListener('click', (event) => {
154
+ const $signInBtn = event.target.closest('.auth-signin-btn');
155
+ if (!$signInBtn) {
156
+ return;
157
+ }
158
+
159
+ event.preventDefault();
160
+ event.stopPropagation();
161
+
162
+ openAuthPage(context);
163
+ });
164
+
165
+ // Account button (.auth-account-btn) - opens account page on website
166
+ document.addEventListener('click', (event) => {
167
+ const $accountBtn = event.target.closest('.auth-account-btn');
168
+ if (!$accountBtn) {
169
+ return;
170
+ }
171
+
172
+ event.preventDefault();
173
+ event.stopPropagation();
174
+
175
+ openAuthPage(context, { path: '/account' });
176
+ });
177
+
178
+ // 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
181
+
182
+ // Log
183
+ context.logger.log('Auth event listeners set up');
184
+ }
@@ -1,5 +1,5 @@
1
1
  // Libraries
2
- const chalk = require('chalk');
2
+ const chalk = require('chalk').default;
3
3
 
4
4
  // Logger class
5
5
  function Logger(name) {
package/dist/options.js CHANGED
@@ -2,6 +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
6
 
6
7
  // Import theme (exposes Bootstrap to window.bootstrap)
7
8
  import '__theme__/_theme.js';
@@ -29,12 +30,28 @@ class Manager {
29
30
  // Initialize
30
31
  await this.webManager.initialize(configuration);
31
32
 
33
+ // Set up auth state listener (updates bindings with user/account state)
34
+ this.webManager.auth().listen((state) => {
35
+ this.logger.log('Auth state changed:', state);
36
+ });
37
+
38
+ // Set up storage listener for cross-context auth sync
39
+ setupAuthStorageListener(this);
40
+
41
+ // Set up auth event listeners (sign in, account buttons)
42
+ setupAuthEventListeners(this);
43
+
32
44
  // Log
33
45
  this.logger.log('Initialized!', this);
34
46
 
35
47
  // Return manager instance
36
48
  return this;
37
49
  }
50
+
51
+ // Open auth page in new tab (for signing in via website)
52
+ openAuthPage(options = {}) {
53
+ openAuthPageHelper(this, options);
54
+ }
38
55
  }
39
56
 
40
57
  // Export
package/dist/page.js CHANGED
@@ -2,6 +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
6
 
6
7
  // Import theme (exposes Bootstrap to window.bootstrap)
7
8
  import '__theme__/_theme.js';
@@ -29,12 +30,28 @@ class Manager {
29
30
  // Initialize
30
31
  await this.webManager.initialize(configuration);
31
32
 
33
+ // Set up auth state listener (updates bindings with user/account state)
34
+ this.webManager.auth().listen((state) => {
35
+ this.logger.log('Auth state changed:', state);
36
+ });
37
+
38
+ // Set up storage listener for cross-context auth sync
39
+ setupAuthStorageListener(this);
40
+
41
+ // Set up auth event listeners (sign in, account buttons)
42
+ setupAuthEventListeners(this);
43
+
32
44
  // Log
33
45
  this.logger.log('Initialized!', this);
34
46
 
35
47
  // Return manager instance
36
48
  return this;
37
49
  }
50
+
51
+ // Open auth page in new tab (for signing in via website)
52
+ openAuthPage(options = {}) {
53
+ openAuthPageHelper(this, options);
54
+ }
38
55
  }
39
56
 
40
57
  // Export
package/dist/popup.js CHANGED
@@ -2,6 +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
6
 
6
7
  // Import theme (exposes Bootstrap to window.bootstrap)
7
8
  import '__theme__/_theme.js';
@@ -29,12 +30,28 @@ class Manager {
29
30
  // Initialize
30
31
  await this.webManager.initialize(configuration);
31
32
 
33
+ // Set up auth state listener (updates bindings with user/account state)
34
+ this.webManager.auth().listen((state) => {
35
+ this.logger.log('Auth state changed:', state);
36
+ });
37
+
38
+ // Set up storage listener for cross-context auth sync
39
+ setupAuthStorageListener(this);
40
+
41
+ // Set up auth event listeners (sign in, account buttons)
42
+ setupAuthEventListeners(this);
43
+
32
44
  // Log
33
45
  this.logger.log('Initialized!', this);
34
46
 
35
47
  // Return manager instance
36
48
  return this;
37
49
  }
50
+
51
+ // Open auth page in new tab (for signing in via website)
52
+ openAuthPage(options = {}) {
53
+ openAuthPageHelper(this, options);
54
+ }
38
55
  }
39
56
 
40
57
  // Export
package/dist/sidepanel.js CHANGED
@@ -2,6 +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
6
 
6
7
  // Import theme (exposes Bootstrap to window.bootstrap)
7
8
  import '__theme__/_theme.js';
@@ -29,12 +30,28 @@ class Manager {
29
30
  // Initialize
30
31
  await this.webManager.initialize(configuration);
31
32
 
33
+ // Set up auth state listener (updates bindings with user/account state)
34
+ this.webManager.auth().listen((state) => {
35
+ this.logger.log('Auth state changed:', state);
36
+ });
37
+
38
+ // Set up storage listener for cross-context auth sync
39
+ setupAuthStorageListener(this);
40
+
41
+ // Set up auth event listeners (sign in, account buttons)
42
+ setupAuthEventListeners(this);
43
+
32
44
  // Log
33
45
  this.logger.log('Initialized!', this);
34
46
 
35
47
  // Return manager instance
36
48
  return this;
37
49
  }
50
+
51
+ // Open auth page in new tab (for signing in via website)
52
+ openAuthPage(options = {}) {
53
+ openAuthPageHelper(this, options);
54
+ }
38
55
  }
39
56
 
40
57
  // Export