@omega.js/client 0.1.0
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/LICENSE +98 -0
- package/README.md +874 -0
- package/dist/index.js +999 -0
- package/dist/modules/analytics.js +584 -0
- package/dist/modules/auth.js +469 -0
- package/dist/modules/bindings.js +319 -0
- package/dist/modules/device.js +282 -0
- package/dist/modules/dom.js +96 -0
- package/dist/modules/features.js +30 -0
- package/dist/modules/firestore.js +313 -0
- package/dist/modules/form-manager.js +1577 -0
- package/dist/modules/icon-core.js +226 -0
- package/dist/modules/icon-renderer.js +149 -0
- package/dist/modules/live-page.js +235 -0
- package/dist/modules/logger.js +36 -0
- package/dist/modules/motion.js +853 -0
- package/dist/modules/notifications.js +433 -0
- package/dist/modules/path-prefix.js +22 -0
- package/dist/modules/request.js +223 -0
- package/dist/modules/sentry.js +108 -0
- package/dist/modules/service-worker.js +237 -0
- package/dist/modules/storage.js +133 -0
- package/dist/modules/triggers.js +117 -0
- package/dist/modules/utilities.js +479 -0
- package/dist/modules/vert-document.js +354 -0
- package/dist/modules/verts.js +1133 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/analytics/adapters/ga4.js +26 -0
- package/dist/vendor/analytics/adapters/meta.js +26 -0
- package/dist/vendor/analytics/adapters/resolve.js +130 -0
- package/dist/vendor/analytics/adapters/tiktok.js +27 -0
- package/dist/vendor/analytics/catalog.js +908 -0
- package/dist/vendor/analytics/consent.js +49 -0
- package/dist/vendor/analytics/core.js +141 -0
- package/dist/vendor/analytics/identity.js +136 -0
- package/dist/vendor/analytics/index.js +170 -0
- package/dist/vendor/analytics/logger.js +40 -0
- package/dist/vendor/analytics/transports/browser.js +110 -0
- package/dist/vendor/monitoring/browser.js +207 -0
- package/dist/vendor/monitoring/core.js +180 -0
- package/dist/vendor/monitoring/logger.js +39 -0
- package/docs/architecture.md +59 -0
- package/docs/bindings.md +235 -0
- package/docs/build-system.md +32 -0
- package/docs/cdp-debugging.md +29 -0
- package/docs/code-patterns.md +96 -0
- package/docs/common-tasks.md +36 -0
- package/docs/dependencies.md +19 -0
- package/docs/index.md +159 -0
- package/docs/modules.md +180 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/testing.md +9 -0
- package/package.json +65 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
// The account schema + subscription derivation live in @omega.js/account — the
|
|
2
|
+
// single source of truth shared with @omega.js/backend, so a doc resolved here is
|
|
3
|
+
// byte-identical to one resolved by the backend. No generators are injected:
|
|
4
|
+
// $uuid/$randomId/$apiKey fields resolve to null (real values always come from
|
|
5
|
+
// the backend-written doc).
|
|
6
|
+
import { resolveAccount, resolveSubscription } from '../vendor/account/index.js';
|
|
7
|
+
import { resolveFeatures } from '../vendor/account/features.js';
|
|
8
|
+
import { registerTrigger } from './triggers.js';
|
|
9
|
+
import { createLogger } from './logger.js';
|
|
10
|
+
|
|
11
|
+
const logger = createLogger('auth');
|
|
12
|
+
|
|
13
|
+
// The auth codes the session probe refuses to read as a verdict on the session
|
|
14
|
+
// ([#798](https://github.com/Omega-JS-Stack/omega/issues/798)): the connection,
|
|
15
|
+
// a throttle, and the Auth server failing to answer at all. They all clear on
|
|
16
|
+
// their own, and signing a user out over one loses a session that never died.
|
|
17
|
+
// Every OTHER `auth/*` code is a definite verdict, so it signs out.
|
|
18
|
+
const TRANSIENT_PROBE_CODES = new Set([
|
|
19
|
+
'auth/network-request-failed',
|
|
20
|
+
'auth/too-many-requests',
|
|
21
|
+
'auth/internal-error',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
class Auth {
|
|
25
|
+
constructor(manager) {
|
|
26
|
+
this.manager = manager;
|
|
27
|
+
this._authStateCallbacks = [];
|
|
28
|
+
this._hasProcessedStateChange = false;
|
|
29
|
+
|
|
30
|
+
// Bumped by every auth state change so emissions stay strictly ordered: a
|
|
31
|
+
// signed-in emission awaits its account fetch while a signed-out one fires
|
|
32
|
+
// instantly, so a slow fetch would otherwise deliver a STALE signed-in
|
|
33
|
+
// state after a newer signed-out one (#196).
|
|
34
|
+
this._stateGeneration = 0;
|
|
35
|
+
|
|
36
|
+
// The one probe in flight, or null (#798; see probeSession)
|
|
37
|
+
this._sessionProbe = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if user is authenticated
|
|
41
|
+
isAuthenticated() {
|
|
42
|
+
return !!this.getUser();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Get current user
|
|
46
|
+
getUser() {
|
|
47
|
+
const user = this.manager.firebaseAuth?.currentUser;
|
|
48
|
+
if (!user) return null;
|
|
49
|
+
|
|
50
|
+
// Get displayName and photoURL from providerData if not set on main user
|
|
51
|
+
let displayName = user.displayName;
|
|
52
|
+
let photoURL = user.photoURL;
|
|
53
|
+
|
|
54
|
+
// If no displayName or photoURL, check providerData
|
|
55
|
+
if ((!displayName || !photoURL) && user.providerData && user.providerData.length > 0) {
|
|
56
|
+
for (const provider of user.providerData) {
|
|
57
|
+
if (!displayName && provider.displayName) {
|
|
58
|
+
displayName = provider.displayName;
|
|
59
|
+
}
|
|
60
|
+
if (!photoURL && provider.photoURL) {
|
|
61
|
+
photoURL = provider.photoURL;
|
|
62
|
+
}
|
|
63
|
+
// Stop if we found both
|
|
64
|
+
if (displayName && photoURL) break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If still no displayName, use email or fallback
|
|
69
|
+
if (!displayName) {
|
|
70
|
+
displayName = user.email ? user.email.split('@')[0] : 'User';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If still no photoURL, use a default avatar service
|
|
74
|
+
if (!photoURL) {
|
|
75
|
+
// Use ui-avatars.com which generates avatars from initials
|
|
76
|
+
const name = displayName || user.email.split('@')[0] || 'ME';
|
|
77
|
+
const initials = name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
|
78
|
+
photoURL = `https://ui-avatars.com/api/?name=${encodeURIComponent(initials)}&size=200&background=random&color=000`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
uid: user.uid,
|
|
83
|
+
email: user.email,
|
|
84
|
+
displayName: displayName,
|
|
85
|
+
photoURL: photoURL,
|
|
86
|
+
emailVerified: user.emailVerified,
|
|
87
|
+
metadata: user.metadata,
|
|
88
|
+
providerData: user.providerData,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Listen for auth state changes (waits for settled state before first callback)
|
|
93
|
+
listen(options = {}, callback) {
|
|
94
|
+
// Handle overloaded signatures - if first param is a function, it's the callback
|
|
95
|
+
if (typeof options === 'function') {
|
|
96
|
+
callback = options;
|
|
97
|
+
options = {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// If Firebase can't boot, call callback immediately with null. Same
|
|
101
|
+
// condition as initialize(): a projectId-only blob resolves (URL
|
|
102
|
+
// derivation) but never registers onAuthStateChanged, so _authReady
|
|
103
|
+
// would never settle and listeners would hang forever.
|
|
104
|
+
if (!this.manager._resolveFirebaseConfig()?.apiKey) {
|
|
105
|
+
callback({
|
|
106
|
+
user: null,
|
|
107
|
+
account: resolveAccount({}),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return () => {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Build auth state and call the provided callback.
|
|
114
|
+
// Returns true when it delivered, false when a newer state superseded it.
|
|
115
|
+
const run = async (user) => {
|
|
116
|
+
const generation = this._stateGeneration;
|
|
117
|
+
const state = { user: this.getUser() };
|
|
118
|
+
|
|
119
|
+
// Fetch account data if the user is logged in and Firestore is available
|
|
120
|
+
// (every failure but a denied read is captured inside _getAccountData and
|
|
121
|
+
// degrades to null)
|
|
122
|
+
if (user && this.manager.firebaseFirestore) {
|
|
123
|
+
try {
|
|
124
|
+
state.account = await this._getAccountData(user.uid);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// The one failure _getAccountData rethrows: rules denied the read.
|
|
127
|
+
// Consumers branch on THIS flag and never on an empty account — a doc
|
|
128
|
+
// that is not written yet is the normal state right after signup
|
|
129
|
+
// ([#700](https://github.com/Omega-JS-Stack/omega/issues/700)). The
|
|
130
|
+
// account still resolves to the empty shape below, so nothing
|
|
131
|
+
// downstream has to null-check.
|
|
132
|
+
logger.warn('Account read denied — flagging the state as denied:', error.message);
|
|
133
|
+
state.accountDenied = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A newer auth state change owns the truth now — delivering this one
|
|
138
|
+
// would hand consumers a stale user out of order. Drop it entirely; the
|
|
139
|
+
// newer run updates the bindings, the storage and the callback.
|
|
140
|
+
if (generation !== this._stateGeneration) {
|
|
141
|
+
logger.warn('Dropping a superseded auth state emission — a newer state change owns the truth');
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Ensure account is always a resolved object
|
|
146
|
+
state.account = state.account || resolveAccount({}, { user: { uid: user?.uid } });
|
|
147
|
+
|
|
148
|
+
// Derive resolved subscription state for bindings and consumers
|
|
149
|
+
state.resolved = this.resolveSubscription(state.account);
|
|
150
|
+
|
|
151
|
+
// Update bindings and storage once per auth state change
|
|
152
|
+
if (!this._hasProcessedStateChange) {
|
|
153
|
+
this.manager.bindings().update({
|
|
154
|
+
auth: state,
|
|
155
|
+
usage: this._resolveUsage(state),
|
|
156
|
+
});
|
|
157
|
+
this.manager.storage().set('auth', state);
|
|
158
|
+
|
|
159
|
+
this._hasProcessedStateChange = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
callback(state);
|
|
163
|
+
|
|
164
|
+
return true;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Once listeners: wait for auth to settle, fire once, done.
|
|
168
|
+
// A superseded run must be RE-DELIVERED here: a once listener holds no
|
|
169
|
+
// subscription, so nothing would ever re-issue it and every awaiting caller
|
|
170
|
+
// (checkout boot, the extension auth helpers) would hang forever. Each retry
|
|
171
|
+
// re-reads the current user, so the loop settles as soon as auth stops
|
|
172
|
+
// changing. The persistent path below needs no loop — it IS subscribed, so
|
|
173
|
+
// the superseding state change re-issues through _authStateCallbacks.
|
|
174
|
+
if (options.once) {
|
|
175
|
+
this.manager._authReady.then(async () => {
|
|
176
|
+
let delivered = false;
|
|
177
|
+
|
|
178
|
+
while (!delivered) {
|
|
179
|
+
delivered = await run(this.manager.firebaseAuth?.currentUser || null);
|
|
180
|
+
|
|
181
|
+
if (!delivered) {
|
|
182
|
+
logger.warn('Re-running a superseded once listener with the newest auth state');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return () => {};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Persistent listeners: subscribe to all auth state changes (initial + future)
|
|
191
|
+
// If auth already settled, fire the first callback via the promise to catch up
|
|
192
|
+
const unsubscribe = this._subscribe(run);
|
|
193
|
+
|
|
194
|
+
if (this.manager._firebaseAuthInitialized) {
|
|
195
|
+
this.manager._authReady.then(() => {
|
|
196
|
+
run(this.manager.firebaseAuth?.currentUser || null);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return unsubscribe;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Subscribe to ongoing auth state changes (sign-in, sign-out after initial settle)
|
|
204
|
+
_subscribe(callback) {
|
|
205
|
+
this._authStateCallbacks.push(callback);
|
|
206
|
+
|
|
207
|
+
return () => {
|
|
208
|
+
const index = this._authStateCallbacks.indexOf(callback);
|
|
209
|
+
if (index > -1) {
|
|
210
|
+
this._authStateCallbacks.splice(index, 1);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Called by Manager when Firebase auth state changes
|
|
216
|
+
_handleAuthStateChange(user) {
|
|
217
|
+
// Supersede any in-flight emission before starting this one
|
|
218
|
+
this._stateGeneration++;
|
|
219
|
+
|
|
220
|
+
// Reset state processing flag for new auth state
|
|
221
|
+
this._hasProcessedStateChange = false;
|
|
222
|
+
|
|
223
|
+
// Call all persistent listener callbacks
|
|
224
|
+
this._authStateCallbacks.forEach(callback => {
|
|
225
|
+
try {
|
|
226
|
+
callback(user);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error('Auth state callback error:', error);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Resolves calculated subscription fields that require derivation logic
|
|
234
|
+
// (shared @omega.js/account implementation — same math as the backend).
|
|
235
|
+
// Returns: { plan, active, trialing, cancelling, everPaid }
|
|
236
|
+
// Falls back to the stored auth state when no account is passed.
|
|
237
|
+
resolveSubscription(account) {
|
|
238
|
+
return resolveSubscription(account || this.manager.storage().get('auth', {})?.account);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Resolve usage bindings from account data + the EFFECTIVE limits of the
|
|
242
|
+
// resolved plan ([#647](https://github.com/Omega-JS-Stack/omega/issues/647)).
|
|
243
|
+
// Returns, per counted feature:
|
|
244
|
+
// { monthly, daily, total, limit, left, day: { limit, used, left }, override }
|
|
245
|
+
//
|
|
246
|
+
// Both halves are config: the FEATURES CATALOG (`config.features`) says what
|
|
247
|
+
// a feature is and whether it is counted, and the product's `features` map
|
|
248
|
+
// (`config.payment.products[].features`) says what this tier promises. The
|
|
249
|
+
// arithmetic — the per-user override winning over the plan's number, the day
|
|
250
|
+
// share of a month limit — is @omega.js/account's, the same module the
|
|
251
|
+
// backend's `consume` gate reads, so a bar can never draw a limit the gate
|
|
252
|
+
// would not enforce.
|
|
253
|
+
_resolveUsage(state) {
|
|
254
|
+
const accountUsage = state.account?.usage || {};
|
|
255
|
+
const productId = state.resolved?.plan || 'basic';
|
|
256
|
+
const products = this.manager.config.payment?.products || [];
|
|
257
|
+
const product = products.find(p => p.id === productId) || {};
|
|
258
|
+
const catalog = this.manager.config.features || {};
|
|
259
|
+
|
|
260
|
+
const usage = {};
|
|
261
|
+
|
|
262
|
+
for (const resolved of resolveFeatures({ catalog, product, account: state.account })) {
|
|
263
|
+
if (!resolved.counted) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
usage[resolved.id] = {
|
|
268
|
+
...(accountUsage[resolved.id] || {}),
|
|
269
|
+
limit: resolved.limit,
|
|
270
|
+
left: resolved.left,
|
|
271
|
+
override: resolved.override,
|
|
272
|
+
day: resolved.day,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// A counter the account carries that the catalog no longer defines still
|
|
277
|
+
// rides the bindings — a page that reads it must not blank out mid-release
|
|
278
|
+
for (const key of Object.keys(accountUsage)) {
|
|
279
|
+
if (key !== 'overrides' && !usage[key]) {
|
|
280
|
+
usage[key] = { ...accountUsage[key], limit: 0 };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return usage;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Get ID token for the current user
|
|
288
|
+
async getIdToken(forceRefresh = false) {
|
|
289
|
+
try {
|
|
290
|
+
const user = this.manager.firebaseAuth.currentUser;
|
|
291
|
+
|
|
292
|
+
const { getIdToken } = await import('firebase/auth');
|
|
293
|
+
return await getIdToken(user, forceRefresh);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error('Get ID token error:', error);
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Ask the Auth SERVER whether this session is still good, at a moment of
|
|
301
|
+
// doubt ([#798](https://github.com/Omega-JS-Stack/omega/issues/798)). Firebase
|
|
302
|
+
// itself only asks at page load and at the hourly refresh, so a revoked,
|
|
303
|
+
// disabled or deleted account keeps an open tab signed in until a reload,
|
|
304
|
+
// and a dev backend restart leaves the tab on a session the emulator no
|
|
305
|
+
// longer has. The probe is a FORCED token refresh, which exchanges the
|
|
306
|
+
// refresh token with the Auth server; it never asks our backend, so dev and
|
|
307
|
+
// production run the same code.
|
|
308
|
+
//
|
|
309
|
+
// Resolves 'signed-out' | 'alive' | 'gone' | 'unknown', and never rejects on
|
|
310
|
+
// the classification itself: callers fire it and move on.
|
|
311
|
+
probeSession() {
|
|
312
|
+
if (!this.isAuthenticated()) {
|
|
313
|
+
return Promise.resolve('signed-out');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// One probe in flight per instance: focus, online and a 401 arrive
|
|
317
|
+
// together all the time, and they are all asking the same question.
|
|
318
|
+
if (this._sessionProbe) {
|
|
319
|
+
return this._sessionProbe;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
this._sessionProbe = this._runSessionProbe().finally(() => {
|
|
323
|
+
this._sessionProbe = null;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return this._sessionProbe;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async _runSessionProbe() {
|
|
330
|
+
try {
|
|
331
|
+
await this.getIdToken(true);
|
|
332
|
+
return 'alive';
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const code = error.code || '';
|
|
335
|
+
|
|
336
|
+
// An auth error carrying a verdict means the session is gone: expired,
|
|
337
|
+
// revoked, disabled, deleted. Sign out: the onAuthStateChanged emission
|
|
338
|
+
// is what drives every surface's policy listener.
|
|
339
|
+
if (code.startsWith('auth/') && !TRANSIENT_PROBE_CODES.has(code)) {
|
|
340
|
+
logger.warn(`Session is gone (${code}); signing out`);
|
|
341
|
+
await this.signOut();
|
|
342
|
+
return 'gone';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// A bad connection, a throttle and a failing Auth server never sign
|
|
346
|
+
// anyone out, and neither does the "Backend starting" window: keep the
|
|
347
|
+
// user and say nothing louder.
|
|
348
|
+
logger.log(`Session probe inconclusive (${code || error.message}); keeping the user signed in`);
|
|
349
|
+
return 'unknown';
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Sign in with custom token
|
|
354
|
+
async signInWithCustomToken(token) {
|
|
355
|
+
try {
|
|
356
|
+
if (!this.manager.firebaseAuth) {
|
|
357
|
+
throw new Error('Firebase Auth is not initialized');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const { signInWithCustomToken } = await import('firebase/auth');
|
|
361
|
+
const userCredential = await signInWithCustomToken(this.manager.firebaseAuth, token);
|
|
362
|
+
return userCredential.user;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error('Sign in with custom token error:', error);
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Sign in with email and password
|
|
370
|
+
async signInWithEmailAndPassword(email, password) {
|
|
371
|
+
try {
|
|
372
|
+
if (!this.manager.firebaseAuth) {
|
|
373
|
+
throw new Error('Firebase Auth is not initialized');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const { signInWithEmailAndPassword } = await import('firebase/auth');
|
|
377
|
+
const userCredential = await signInWithEmailAndPassword(this.manager.firebaseAuth, email, password);
|
|
378
|
+
return userCredential.user;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error('Sign in with email and password error:', error);
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Sign out the current user
|
|
386
|
+
async signOut() {
|
|
387
|
+
try {
|
|
388
|
+
const { signOut } = await import('firebase/auth');
|
|
389
|
+
await signOut(this.manager.firebaseAuth);
|
|
390
|
+
return true;
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error('Sign out error:', error);
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Get account data from Firestore
|
|
398
|
+
async _getAccountData(uid) {
|
|
399
|
+
try {
|
|
400
|
+
if (!this.manager.firebaseFirestore) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const { doc, getDoc } = await import('firebase/firestore');
|
|
405
|
+
|
|
406
|
+
const accountDoc = doc(this.manager.firebaseFirestore, 'users', uid);
|
|
407
|
+
const snapshot = await getDoc(accountDoc);
|
|
408
|
+
|
|
409
|
+
// Get current Firebase user to pass uid and email to resolver
|
|
410
|
+
const firebaseUser = this.manager.firebaseAuth?.currentUser || { uid };
|
|
411
|
+
|
|
412
|
+
if (snapshot.exists()) {
|
|
413
|
+
// Resolve the account data to ensure proper structure and defaults
|
|
414
|
+
const rawData = snapshot.data();
|
|
415
|
+
const resolvedAccount = resolveAccount(rawData, { user: firebaseUser });
|
|
416
|
+
return resolvedAccount;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// If no account exists, return resolved empty object for consistent structure
|
|
420
|
+
return resolveAccount({}, { user: firebaseUser });
|
|
421
|
+
} catch (error) {
|
|
422
|
+
// Capture here — every failure passes through this catch, so monitoring
|
|
423
|
+
// sees them all: the degrade-to-null path below never surfaces to callers,
|
|
424
|
+
// and the permission-denied rethrow is captured before it throws.
|
|
425
|
+
console.error('Get account data error:', error);
|
|
426
|
+
this.manager.sentry().captureException(new Error('Failed to get account data', { cause: error }));
|
|
427
|
+
|
|
428
|
+
// Rules refused the read: a REAL failure, and the caller has to be able to
|
|
429
|
+
// tell it apart from a doc that simply is not written yet — that one
|
|
430
|
+
// resolves to an empty account above and is normal
|
|
431
|
+
// ([#700](https://github.com/Omega-JS-Stack/omega/issues/700)). Every
|
|
432
|
+
// other failure keeps degrading to null.
|
|
433
|
+
if (error?.code === 'permission-denied') {
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Register the GENERIC auth triggers on the shared click-trigger registry
|
|
442
|
+
// (#16). `omega-signout` is the one cross-surface trigger — web, desktop and
|
|
443
|
+
// extension all get it from here; surface-specific ones (the extension's
|
|
444
|
+
// `omega-signin`) are registered by that surface.
|
|
445
|
+
setupEventListeners() {
|
|
446
|
+
registerTrigger('signout', async () => {
|
|
447
|
+
try {
|
|
448
|
+
// Show confirmation
|
|
449
|
+
if (!confirm('Are you sure you want to sign out?')) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Sign out
|
|
454
|
+
await this.signOut();
|
|
455
|
+
|
|
456
|
+
// Show success notification
|
|
457
|
+
this.manager.utilities().showNotification('Successfully signed out.', 'success');
|
|
458
|
+
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.error('Sign out error:', error);
|
|
461
|
+
// Show error notification if utilities are available
|
|
462
|
+
this.manager.utilities().showNotification('Failed to sign out. Please try again.', 'danger');
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export default Auth;
|