browser-extension-manager 1.2.3 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +104 -0
- package/README.md +109 -0
- package/dist/assets/themes/classy/css/base/_utilities.scss +0 -51
- package/dist/assets/themes/classy/css/components/_text.scss +11 -0
- package/dist/background.js +220 -9
- package/dist/defaults/_.env +4 -0
- package/dist/defaults/config/browser-extension-manager.json +9 -11
- package/dist/gulp/main.js +4 -0
- package/dist/gulp/tasks/audit.js +1 -1
- package/dist/gulp/tasks/defaults.js +8 -1
- package/dist/gulp/tasks/distribute.js +6 -1
- package/dist/gulp/tasks/package.js +6 -2
- package/dist/gulp/tasks/publish.js +1 -0
- package/dist/gulp/tasks/webpack.js +8 -6
- package/dist/lib/auth-helpers.js +184 -0
- package/dist/lib/logger.js +1 -1
- package/dist/options.js +17 -0
- package/dist/page.js +17 -0
- package/dist/popup.js +17 -0
- package/dist/sidepanel.js +17 -0
- package/firebase-debug.log +280 -0
- package/package.json +9 -5
package/CLAUDE.md
CHANGED
|
@@ -323,6 +323,105 @@ Manager.initialize().then(() => {
|
|
|
323
323
|
**Storage normalization:**
|
|
324
324
|
The wrapper automatically resolves `storage` to `storage.sync` if available, falling back to `storage.local`.
|
|
325
325
|
|
|
326
|
+
**Auth Helpers:** [src/lib/auth-helpers.js](src/lib/auth-helpers.js)
|
|
327
|
+
|
|
328
|
+
Provides cross-context auth synchronization and reusable auth UI event handlers.
|
|
329
|
+
|
|
330
|
+
### Cross-Context Auth Architecture
|
|
331
|
+
|
|
332
|
+
**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:
|
|
333
|
+
|
|
334
|
+
1. Background.js monitors for auth tokens via `tabs.onUpdated`
|
|
335
|
+
2. When detected, background signs in Firebase and saves to `chrome.storage`
|
|
336
|
+
3. Other contexts listen to storage changes and sync their Firebase instances
|
|
337
|
+
|
|
338
|
+
**Sign-in Flow:**
|
|
339
|
+
```
|
|
340
|
+
User clicks .auth-signin-btn
|
|
341
|
+
→ openAuthPage() opens https://{authDomain}/token?authSourceTabId=123
|
|
342
|
+
→ Website authenticates, redirects to /token?authToken=xxx
|
|
343
|
+
→ background.js tabs.onUpdated detects authDomain URL with authToken param
|
|
344
|
+
→ background.js calls handleAuthToken():
|
|
345
|
+
→ signInWithCustomToken(auth, token)
|
|
346
|
+
→ Saves {token, user, timestamp} to storage key 'bxm:authState'
|
|
347
|
+
→ Closes the /token tab
|
|
348
|
+
→ Reactivates the original tab (authSourceTabId)
|
|
349
|
+
→ Other contexts detect storage change via setupAuthStorageListener()
|
|
350
|
+
→ Each context calls signInWithStoredToken() to sign in their Firebase
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Sign-out Flow:**
|
|
354
|
+
```
|
|
355
|
+
User clicks .auth-signout-btn
|
|
356
|
+
→ Web Manager signs out Firebase locally
|
|
357
|
+
→ setupAuthStorageListener() detects WM auth state change (user=null)
|
|
358
|
+
→ Clears 'bxm:authState' from storage
|
|
359
|
+
→ background.js setupAuthStorageListener() detects storage cleared
|
|
360
|
+
→ background.js signs out its Firebase instance
|
|
361
|
+
→ Other contexts detect storage cleared and sign out
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Key Implementation Details:**
|
|
365
|
+
|
|
366
|
+
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.
|
|
367
|
+
|
|
368
|
+
2. **Firebase in service workers**: Static ES6 imports are required. Dynamic `import()` fails with webpack chunking in service workers.
|
|
369
|
+
|
|
370
|
+
3. **Config path**: `authDomain` is at `config.firebase.app.config.authDomain` (loaded via BEM_BUILD_JSON).
|
|
371
|
+
|
|
372
|
+
4. **Required permission**: `tabs` permission needed for `tabs.onUpdated` listener.
|
|
373
|
+
|
|
374
|
+
**Functions:**
|
|
375
|
+
- `setupAuthStorageListener(context)` - Listens for auth state changes from background.js AND monitors WM auth state to clear storage on sign-out
|
|
376
|
+
- `setupAuthEventListeners(context)` - Sets up delegated click handlers for auth buttons
|
|
377
|
+
- `openAuthPage(context, options)` - Opens auth page on the website with authSourceTabId for tab restoration
|
|
378
|
+
|
|
379
|
+
**Auth Button CSS Classes:**
|
|
380
|
+
|
|
381
|
+
Add these classes to your HTML elements to enable automatic auth handling:
|
|
382
|
+
|
|
383
|
+
| Class | Description | Action |
|
|
384
|
+
|-------|-------------|--------|
|
|
385
|
+
| `.auth-signin-btn` | Sign in button | Opens `/token` page on website (authDomain) |
|
|
386
|
+
| `.auth-signout-btn` | Sign out button | Handled by Web Manager (triggers storage sync via auth listener) |
|
|
387
|
+
| `.auth-account-btn` | Account button | Opens `/account` page on website |
|
|
388
|
+
|
|
389
|
+
**Example usage:**
|
|
390
|
+
```html
|
|
391
|
+
<!-- Sign In Button (shown when logged out) -->
|
|
392
|
+
<button class="btn auth-signin-btn" data-wm-bind="@show !auth.user">
|
|
393
|
+
Sign In
|
|
394
|
+
</button>
|
|
395
|
+
|
|
396
|
+
<!-- Account Dropdown (shown when logged in) -->
|
|
397
|
+
<div data-wm-bind="@show auth.user" hidden>
|
|
398
|
+
<a class="auth-account-btn" href="#">Account</a>
|
|
399
|
+
<button class="auth-signout-btn">Sign Out</button>
|
|
400
|
+
</div>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Reactive bindings:**
|
|
404
|
+
- `data-wm-bind="@show auth.user"` - Show when logged in
|
|
405
|
+
- `data-wm-bind="@show !auth.user"` - Show when logged out
|
|
406
|
+
- `data-wm-bind="@text auth.user.displayName"` - Display user's name
|
|
407
|
+
- `data-wm-bind="@text auth.user.email"` - Display user's email
|
|
408
|
+
- `data-wm-bind="@attr src auth.user.photoURL"` - Set avatar image src
|
|
409
|
+
|
|
410
|
+
**Storage Schema (`bxm:authState`):**
|
|
411
|
+
```javascript
|
|
412
|
+
{
|
|
413
|
+
token: "firebase-custom-token", // Used by other contexts to sign in
|
|
414
|
+
user: {
|
|
415
|
+
uid: "...",
|
|
416
|
+
email: "...",
|
|
417
|
+
displayName: "...",
|
|
418
|
+
photoURL: "...",
|
|
419
|
+
emailVerified: true
|
|
420
|
+
},
|
|
421
|
+
timestamp: 1234567890
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
326
425
|
**Logger:** [src/lib/logger.js](src/lib/logger.js)
|
|
327
426
|
- Full logging utility
|
|
328
427
|
- [src/lib/logger-lite.js](src/lib/logger-lite.js) for lightweight contexts
|
|
@@ -679,9 +778,14 @@ Manager.initialize().then(() => {
|
|
|
679
778
|
|
|
680
779
|
**Utilities:**
|
|
681
780
|
- [src/lib/extension.js](src/lib/extension.js) - Cross-browser API wrapper
|
|
781
|
+
- [src/lib/auth-helpers.js](src/lib/auth-helpers.js) - Cross-context auth sync (see Auth Architecture section above)
|
|
682
782
|
- [src/lib/logger.js](src/lib/logger.js) - Logging utility
|
|
683
783
|
- [src/cli.js](src/cli.js) - CLI implementation
|
|
684
784
|
|
|
785
|
+
**Auth System (Cross-Context):**
|
|
786
|
+
- [src/background.js](src/background.js) - Source of truth for auth; `setupAuthTokenListener()`, `setupAuthStorageListener()`, `handleAuthToken()`
|
|
787
|
+
- [src/lib/auth-helpers.js](src/lib/auth-helpers.js) - `setupAuthStorageListener()`, `openAuthPage()`, `setupAuthEventListeners()` for non-background contexts
|
|
788
|
+
|
|
685
789
|
**CSS Framework:**
|
|
686
790
|
- [src/assets/css/browser-extension-manager.scss](src/assets/css/browser-extension-manager.scss) - Main entry
|
|
687
791
|
- [src/assets/css/core/](src/assets/css/core/) - Core styles
|
package/README.md
CHANGED
|
@@ -86,6 +86,115 @@ EDGE_API_KEY="your-api-key"
|
|
|
86
86
|
|
|
87
87
|
Only stores with configured credentials will be published to.
|
|
88
88
|
|
|
89
|
+
## 🔐 Authentication
|
|
90
|
+
|
|
91
|
+
BEM provides built-in authentication support that syncs across all extension contexts (popup, options, pages, sidepanel, background).
|
|
92
|
+
|
|
93
|
+
### Auth Architecture Overview
|
|
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`.
|
|
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
|
+
```
|
|
135
|
+
|
|
136
|
+
### Required Permission
|
|
137
|
+
|
|
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.
|
|
147
|
+
|
|
148
|
+
### Auth Button Classes
|
|
149
|
+
|
|
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 |
|
|
157
|
+
|
|
158
|
+
### Example
|
|
159
|
+
```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>
|
|
164
|
+
|
|
165
|
+
<!-- Account Section (shown when logged in) -->
|
|
166
|
+
<div data-wm-bind="@show auth.user" hidden>
|
|
167
|
+
<span data-wm-bind="@text auth.user.displayName">User</span>
|
|
168
|
+
<a class="auth-account-btn" href="#">Account</a>
|
|
169
|
+
<button class="auth-signout-btn">Sign Out</button>
|
|
170
|
+
</div>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 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
|
+
```
|
|
197
|
+
|
|
89
198
|
<!-- ## ⛳️ Flags
|
|
90
199
|
* `--test=false` - Coming soon
|
|
91
200
|
```bash
|
|
@@ -1,57 +1,6 @@
|
|
|
1
1
|
// Classy Utility Classes
|
|
2
2
|
// Custom helper classes and theme-specific utilities
|
|
3
3
|
|
|
4
|
-
// ============================================
|
|
5
|
-
// Section Spacing
|
|
6
|
-
// ============================================
|
|
7
|
-
|
|
8
|
-
// Section Spacing
|
|
9
|
-
// .section {
|
|
10
|
-
// padding: $classy-spacing-4xl 0;
|
|
11
|
-
|
|
12
|
-
// &.section-sm {
|
|
13
|
-
// padding: $classy-spacing-2xl 0;
|
|
14
|
-
// }
|
|
15
|
-
|
|
16
|
-
// &.section-lg {
|
|
17
|
-
// padding: 6rem 0;
|
|
18
|
-
// }
|
|
19
|
-
|
|
20
|
-
// &.section-xl {
|
|
21
|
-
// padding: 8rem 0;
|
|
22
|
-
// }
|
|
23
|
-
// }
|
|
24
|
-
|
|
25
|
-
// Hero Sections
|
|
26
|
-
// .hero {
|
|
27
|
-
// padding: 6rem 0;
|
|
28
|
-
// position: relative;
|
|
29
|
-
// overflow: hidden;
|
|
30
|
-
|
|
31
|
-
// &.hero-gradient {
|
|
32
|
-
// background: $classy-gradient-primary;
|
|
33
|
-
// }
|
|
34
|
-
|
|
35
|
-
// &.hero-pattern {
|
|
36
|
-
// &::before {
|
|
37
|
-
// content: "";
|
|
38
|
-
// position: absolute;
|
|
39
|
-
// top: 0;
|
|
40
|
-
// left: 0;
|
|
41
|
-
// right: 0;
|
|
42
|
-
// bottom: 0;
|
|
43
|
-
// background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%235B47FB' fill-opacity='0.1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
|
44
|
-
// opacity: 0.1;
|
|
45
|
-
// z-index: 0;
|
|
46
|
-
// }
|
|
47
|
-
// }
|
|
48
|
-
|
|
49
|
-
// .hero-content {
|
|
50
|
-
// position: relative;
|
|
51
|
-
// z-index: 1;
|
|
52
|
-
// }
|
|
53
|
-
// }
|
|
54
|
-
|
|
55
4
|
// ============================================
|
|
56
5
|
// Shadow Utilities
|
|
57
6
|
// ============================================
|
|
@@ -39,3 +39,14 @@
|
|
|
39
39
|
[data-bs-theme="dark"] .text-adaptive {
|
|
40
40
|
@extend .text-dark;
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
// ============================================
|
|
44
|
+
// Text Decoration Utilities
|
|
45
|
+
// ============================================
|
|
46
|
+
.text-decoration-dotted {
|
|
47
|
+
text-decoration-style: dotted !important;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.text-decoration-dashed {
|
|
51
|
+
text-decoration-style: dashed !important;
|
|
52
|
+
}
|
package/dist/background.js
CHANGED
|
@@ -2,23 +2,22 @@
|
|
|
2
2
|
import extension from './lib/extension.js';
|
|
3
3
|
import LoggerLite from './lib/logger-lite.js';
|
|
4
4
|
|
|
5
|
+
// Firebase (static imports - dynamic import() doesn't work in service workers with webpack chunking)
|
|
6
|
+
import { initializeApp, getApp } from 'firebase/app';
|
|
7
|
+
import { getAuth, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth';
|
|
8
|
+
|
|
5
9
|
// Variables
|
|
6
10
|
const serviceWorker = self;
|
|
7
11
|
|
|
8
12
|
// Import build config at the top level (synchronous)
|
|
9
13
|
importScripts('/build.js');
|
|
10
14
|
|
|
11
|
-
// ⚠️⚠️⚠️ CRITICAL: Setup global listeners BEFORE
|
|
15
|
+
// ⚠️⚠️⚠️ CRITICAL: Setup global listeners BEFORE any async operations ⚠️⚠️⚠️
|
|
12
16
|
// https://stackoverflow.com/questions/78270541/cant-catch-fcm-notificationclick-event-in-service-worker-using-firebase-messa
|
|
17
|
+
// Note: ES6 static imports above are fine - they're hoisted and bundled by webpack.
|
|
18
|
+
// The issue is with importScripts() calls which must be synchronous at top-level.
|
|
13
19
|
setupGlobalHandlers();
|
|
14
20
|
|
|
15
|
-
// Import Firebase libraries at the top level (before any async operations)
|
|
16
|
-
// ⚠️ importScripts MUST be called at top-level (synchronously) - it cannot be called inside functions or after async operations
|
|
17
|
-
// importScripts(
|
|
18
|
-
// 'https://www.gstatic.com/firebasejs/%%% firebaseVersion %%%/firebase-app-compat.js',
|
|
19
|
-
// 'https://www.gstatic.com/firebasejs/%%% firebaseVersion %%%/firebase-messaging-compat.js',
|
|
20
|
-
// );
|
|
21
|
-
|
|
22
21
|
// Class
|
|
23
22
|
class Manager {
|
|
24
23
|
constructor() {
|
|
@@ -36,7 +35,8 @@ class Manager {
|
|
|
36
35
|
this.app = this.config?.app?.id || 'extension';
|
|
37
36
|
this.environment = this.config?.bem?.environment || 'production';
|
|
38
37
|
this.libraries = {
|
|
39
|
-
firebase:
|
|
38
|
+
firebase: null,
|
|
39
|
+
firebaseAuth: null,
|
|
40
40
|
messaging: false,
|
|
41
41
|
promoServer: false,
|
|
42
42
|
};
|
|
@@ -62,6 +62,12 @@ class Manager {
|
|
|
62
62
|
// Initialize Firebase
|
|
63
63
|
this.initializeFirebase();
|
|
64
64
|
|
|
65
|
+
// Setup auth token listener (for cross-runtime auth)
|
|
66
|
+
this.setupAuthTokenListener();
|
|
67
|
+
|
|
68
|
+
// Setup auth storage listener (detect sign-out from pages)
|
|
69
|
+
this.setupAuthStorageListener();
|
|
70
|
+
|
|
65
71
|
// Setup livereload
|
|
66
72
|
this.setupLiveReload();
|
|
67
73
|
|
|
@@ -170,6 +176,211 @@ class Manager {
|
|
|
170
176
|
.catch(error => this.logger.error('Failed to cache resources:', error));
|
|
171
177
|
}
|
|
172
178
|
|
|
179
|
+
// Setup auth token listener (monitors tabs for auth tokens from website)
|
|
180
|
+
setupAuthTokenListener() {
|
|
181
|
+
// DEBUG: Log the full config to see what we have
|
|
182
|
+
console.log('[AUTH] setupAuthTokenListener called');
|
|
183
|
+
console.log('[AUTH] this.config:', this.config);
|
|
184
|
+
console.log('[AUTH] BEM_BUILD_JSON:', serviceWorker.BEM_BUILD_JSON);
|
|
185
|
+
|
|
186
|
+
// Get auth domain from config
|
|
187
|
+
// Structure is: this.config.firebase.app.config.authDomain
|
|
188
|
+
const authDomain = this.config?.firebase?.app?.config?.authDomain;
|
|
189
|
+
|
|
190
|
+
// Log config for debugging
|
|
191
|
+
this.logger.log('[AUTH] Config paths:', {
|
|
192
|
+
firebase_path: this.config?.firebase?.app?.config?.authDomain,
|
|
193
|
+
resolved: authDomain,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Skip if no auth domain configured
|
|
197
|
+
if (!authDomain) {
|
|
198
|
+
this.logger.log('[AUTH] No authDomain configured, skipping auth token listener');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Log
|
|
203
|
+
this.logger.log(`[AUTH] Setting up auth token listener for domain: ${authDomain}`);
|
|
204
|
+
|
|
205
|
+
// Listen for tab URL changes
|
|
206
|
+
this.extension.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
207
|
+
// Only process when URL changes and is complete
|
|
208
|
+
if (changeInfo.status !== 'complete' || !tab.url) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Parse the URL
|
|
213
|
+
let tabUrl;
|
|
214
|
+
try {
|
|
215
|
+
tabUrl = new URL(tab.url);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Log every tab update for auth domain matching
|
|
221
|
+
this.logger.log(`[AUTH] Tab updated: ${tabUrl.hostname} (looking for: ${authDomain})`);
|
|
222
|
+
|
|
223
|
+
// Check if this is our auth domain
|
|
224
|
+
if (tabUrl.hostname !== authDomain) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Log - we found our domain
|
|
229
|
+
this.logger.log(`[AUTH] Auth domain matched! Checking for authToken param...`);
|
|
230
|
+
|
|
231
|
+
// Check for authToken param
|
|
232
|
+
const authToken = tabUrl.searchParams.get('authToken');
|
|
233
|
+
if (!authToken) {
|
|
234
|
+
this.logger.log(`[AUTH] No authToken param found in URL: ${tabUrl.href}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Get source tab ID to restore after auth
|
|
239
|
+
const authSourceTabId = tabUrl.searchParams.get('authSourceTabId');
|
|
240
|
+
|
|
241
|
+
// Log
|
|
242
|
+
this.logger.log('[AUTH] Auth token detected in tab:', tabId);
|
|
243
|
+
|
|
244
|
+
// Handle the auth token
|
|
245
|
+
this.handleAuthToken(authToken, tabId, authSourceTabId ? parseInt(authSourceTabId, 10) : null);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Setup auth storage listener (detect sign-out from pages)
|
|
250
|
+
setupAuthStorageListener() {
|
|
251
|
+
this.extension.storage.onChanged.addListener((changes) => {
|
|
252
|
+
const authChange = changes['bxm:authState'];
|
|
253
|
+
if (!authChange) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.logger.log('[AUTH] Storage auth state changed:', authChange.newValue ? 'signed in' : 'signed out');
|
|
258
|
+
|
|
259
|
+
// If storage was cleared (sign-out from a page) and we have Firebase initialized, sign out
|
|
260
|
+
if (!authChange.newValue && this.libraries.firebaseAuth) {
|
|
261
|
+
this.logger.log('[AUTH] Signing out background Firebase...');
|
|
262
|
+
this.libraries.firebaseAuth.signOut();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
this.logger.log('[AUTH] Auth storage listener set up');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Get or initialize Firebase auth (reuse existing instance)
|
|
270
|
+
getFirebaseAuth() {
|
|
271
|
+
// Return existing instance if available
|
|
272
|
+
if (this.libraries.firebaseAuth) {
|
|
273
|
+
return this.libraries.firebaseAuth;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Get Firebase config
|
|
277
|
+
const firebaseConfig = this.config?.firebase?.app?.config;
|
|
278
|
+
if (!firebaseConfig) {
|
|
279
|
+
throw new Error('Firebase config not available');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Try to get existing app or create new one
|
|
283
|
+
try {
|
|
284
|
+
this.libraries.firebase = getApp('bxm-auth');
|
|
285
|
+
} catch (e) {
|
|
286
|
+
this.libraries.firebase = initializeApp(firebaseConfig, 'bxm-auth');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Get auth and set up state listener (only once)
|
|
290
|
+
this.libraries.firebaseAuth = getAuth(this.libraries.firebase);
|
|
291
|
+
|
|
292
|
+
// Set up auth state change listener (background is source of truth)
|
|
293
|
+
onAuthStateChanged(this.libraries.firebaseAuth, (user) => {
|
|
294
|
+
this.handleAuthStateChange(user);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return this.libraries.firebaseAuth;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Handle Firebase auth state changes (source of truth for all contexts)
|
|
301
|
+
async handleAuthStateChange(user) {
|
|
302
|
+
this.logger.log('[AUTH] Auth state changed:', user?.email || 'signed out');
|
|
303
|
+
|
|
304
|
+
if (user) {
|
|
305
|
+
// User is signed in - get current stored state to preserve token
|
|
306
|
+
const result = await new Promise(resolve =>
|
|
307
|
+
this.extension.storage.get('bxm:authState', resolve)
|
|
308
|
+
);
|
|
309
|
+
const currentState = result['bxm:authState'] || {};
|
|
310
|
+
|
|
311
|
+
// Update auth state with current user info
|
|
312
|
+
const authState = {
|
|
313
|
+
token: currentState.token, // Preserve existing token
|
|
314
|
+
user: {
|
|
315
|
+
uid: user.uid,
|
|
316
|
+
email: user.email,
|
|
317
|
+
displayName: user.displayName,
|
|
318
|
+
photoURL: user.photoURL,
|
|
319
|
+
emailVerified: user.emailVerified,
|
|
320
|
+
},
|
|
321
|
+
timestamp: Date.now(),
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
await this.extension.storage.set({ 'bxm:authState': authState });
|
|
325
|
+
this.logger.log('[AUTH] Auth state synced to storage');
|
|
326
|
+
} else {
|
|
327
|
+
// User is signed out - clear storage
|
|
328
|
+
await this.extension.storage.remove('bxm:authState');
|
|
329
|
+
this.logger.log('[AUTH] Auth state cleared from storage');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Handle auth token from website
|
|
334
|
+
async handleAuthToken(token, tabId, authSourceTabId = null) {
|
|
335
|
+
try {
|
|
336
|
+
// Log
|
|
337
|
+
this.logger.log('[AUTH] Processing auth token...');
|
|
338
|
+
|
|
339
|
+
// Get or initialize Firebase auth
|
|
340
|
+
const auth = this.getFirebaseAuth();
|
|
341
|
+
|
|
342
|
+
// Sign in with custom token
|
|
343
|
+
this.logger.log('[AUTH] Calling signInWithCustomToken...');
|
|
344
|
+
const userCredential = await signInWithCustomToken(auth, token);
|
|
345
|
+
const user = userCredential.user;
|
|
346
|
+
|
|
347
|
+
// Log
|
|
348
|
+
this.logger.log('[AUTH] Signed in successfully:', user.email);
|
|
349
|
+
|
|
350
|
+
// Save token to storage (user state will be synced by onAuthStateChanged)
|
|
351
|
+
const result = await new Promise(resolve =>
|
|
352
|
+
this.extension.storage.get('bxm:authState', resolve)
|
|
353
|
+
);
|
|
354
|
+
const currentState = result['bxm:authState'] || {};
|
|
355
|
+
|
|
356
|
+
await this.extension.storage.set({
|
|
357
|
+
'bxm:authState': {
|
|
358
|
+
...currentState,
|
|
359
|
+
token: token,
|
|
360
|
+
timestamp: Date.now(),
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Close the auth tab
|
|
365
|
+
await this.extension.tabs.remove(tabId);
|
|
366
|
+
this.logger.log('[AUTH] Auth tab closed');
|
|
367
|
+
|
|
368
|
+
// Reactivate the source tab if provided
|
|
369
|
+
if (authSourceTabId) {
|
|
370
|
+
try {
|
|
371
|
+
await this.extension.tabs.update(authSourceTabId, { active: true });
|
|
372
|
+
this.logger.log('[AUTH] Restored source tab:', authSourceTabId);
|
|
373
|
+
} catch (e) {
|
|
374
|
+
// Tab may have been closed, ignore
|
|
375
|
+
this.logger.log('[AUTH] Could not restore source tab (may be closed):', authSourceTabId);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
} catch (error) {
|
|
380
|
+
this.logger.error('[AUTH] Error handling auth token:', error);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
173
384
|
// Setup livereload
|
|
174
385
|
setupLiveReload() {
|
|
175
386
|
// Quit if not in dev mode
|
package/dist/defaults/_.env
CHANGED
|
@@ -18,5 +18,9 @@ EDGE_PRODUCT_ID="your-product-id"
|
|
|
18
18
|
EDGE_CLIENT_ID="your-client-id"
|
|
19
19
|
EDGE_API_KEY="your-api-key"
|
|
20
20
|
|
|
21
|
+
# Opera Add-ons
|
|
22
|
+
# NOTE: Opera does not have a publishing API. Extensions must be submitted manually at:
|
|
23
|
+
# https://addons.opera.com/developer/
|
|
24
|
+
|
|
21
25
|
# ========== Custom Values ==========
|
|
22
26
|
# ...
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
app: {
|
|
3
|
-
id: 'ultimate-jekyll',
|
|
4
|
-
},
|
|
5
2
|
theme: {
|
|
6
3
|
id: 'classy',
|
|
7
4
|
},
|
|
8
5
|
brand: {
|
|
6
|
+
id: 'ultimate-jekyll',
|
|
9
7
|
name: 'Ultimate Jekyll',
|
|
10
8
|
url: 'https://ultimate-jekyll.itwcreativeworks.com',
|
|
11
9
|
contact: {
|
|
@@ -25,13 +23,13 @@
|
|
|
25
23
|
secret: '',
|
|
26
24
|
},
|
|
27
25
|
firebaseConfig: {
|
|
28
|
-
apiKey: '
|
|
29
|
-
authDomain: '
|
|
30
|
-
databaseURL: 'https://
|
|
31
|
-
projectId: '
|
|
32
|
-
storageBucket: '
|
|
33
|
-
messagingSenderId: '
|
|
34
|
-
appId: '1:
|
|
35
|
-
measurementId: 'G-
|
|
26
|
+
apiKey: 'AIzaSyD1MvM-EQC_eVWqaTe5ImWALwhBr7iXgh8',
|
|
27
|
+
authDomain: 'universal-auth.itwcreativeworks.com',
|
|
28
|
+
databaseURL: 'https://itwcw-universal-auth.firebaseio.com',
|
|
29
|
+
projectId: 'itwcw-universal-auth',
|
|
30
|
+
storageBucket: 'itwcw-universal-auth.appspot.com',
|
|
31
|
+
messagingSenderId: '195162582358',
|
|
32
|
+
appId: '1:195162582358:web:6f9320c516d9ab4838baf7',
|
|
33
|
+
measurementId: 'G-W08HGRW4S',
|
|
36
34
|
},
|
|
37
35
|
}
|
package/dist/gulp/main.js
CHANGED
|
@@ -9,6 +9,10 @@ const glob = require('glob').globSync;
|
|
|
9
9
|
// Load package
|
|
10
10
|
const package = Manager.getPackage('main');
|
|
11
11
|
const project = Manager.getPackage('project');
|
|
12
|
+
const projectRoot = Manager.getRootPath('project');
|
|
13
|
+
|
|
14
|
+
// Load .env file from project root
|
|
15
|
+
require('dotenv').config({ path: path.join(projectRoot, '.env') });
|
|
12
16
|
|
|
13
17
|
// Log
|
|
14
18
|
logger.log('Starting...', argv);
|
package/dist/gulp/tasks/audit.js
CHANGED
|
@@ -4,7 +4,7 @@ const logger = Manager.logger('audit');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const jetpack = require('fs-jetpack');
|
|
6
6
|
const { series } = require('gulp');
|
|
7
|
-
const chalk = require('chalk');
|
|
7
|
+
const chalk = require('chalk').default;
|
|
8
8
|
|
|
9
9
|
// Load package
|
|
10
10
|
const package = Manager.getPackage('main');
|
|
@@ -18,6 +18,9 @@ const config = Manager.getConfig('project');
|
|
|
18
18
|
const rootPathPackage = Manager.getRootPath('main');
|
|
19
19
|
const rootPathProject = Manager.getRootPath('project');
|
|
20
20
|
|
|
21
|
+
// Constants
|
|
22
|
+
const LOUD = process.env.BXM_LOUD_LOGS === 'true';
|
|
23
|
+
|
|
21
24
|
// Get clean versions
|
|
22
25
|
// const cleanVersions = { versions: Manager.getCleanVersions()};
|
|
23
26
|
const cleanVersions = { versions: package.engines };
|
|
@@ -434,7 +437,11 @@ function customTransform() {
|
|
|
434
437
|
|
|
435
438
|
// Skip if instructed
|
|
436
439
|
if (options.skip || (!options.overwrite && exists && !options.merge && !options.mergeLines)) {
|
|
437
|
-
|
|
440
|
+
// Log if loud is enabled
|
|
441
|
+
if (LOUD) {
|
|
442
|
+
logger.log(`Skipping file: ${relativePath}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
438
445
|
return callback();
|
|
439
446
|
}
|
|
440
447
|
|