@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.
Files changed (72) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +874 -0
  3. package/dist/index.js +999 -0
  4. package/dist/modules/analytics.js +584 -0
  5. package/dist/modules/auth.js +469 -0
  6. package/dist/modules/bindings.js +319 -0
  7. package/dist/modules/device.js +282 -0
  8. package/dist/modules/dom.js +96 -0
  9. package/dist/modules/features.js +30 -0
  10. package/dist/modules/firestore.js +313 -0
  11. package/dist/modules/form-manager.js +1577 -0
  12. package/dist/modules/icon-core.js +226 -0
  13. package/dist/modules/icon-renderer.js +149 -0
  14. package/dist/modules/live-page.js +235 -0
  15. package/dist/modules/logger.js +36 -0
  16. package/dist/modules/motion.js +853 -0
  17. package/dist/modules/notifications.js +433 -0
  18. package/dist/modules/path-prefix.js +22 -0
  19. package/dist/modules/request.js +223 -0
  20. package/dist/modules/sentry.js +108 -0
  21. package/dist/modules/service-worker.js +237 -0
  22. package/dist/modules/storage.js +133 -0
  23. package/dist/modules/triggers.js +117 -0
  24. package/dist/modules/utilities.js +479 -0
  25. package/dist/modules/vert-document.js +354 -0
  26. package/dist/modules/verts.js +1133 -0
  27. package/dist/vendor/account/engine.js +182 -0
  28. package/dist/vendor/account/features.js +220 -0
  29. package/dist/vendor/account/index.js +53 -0
  30. package/dist/vendor/account/schema.js +272 -0
  31. package/dist/vendor/account/subscription.js +38 -0
  32. package/dist/vendor/analytics/adapters/ga4.js +26 -0
  33. package/dist/vendor/analytics/adapters/meta.js +26 -0
  34. package/dist/vendor/analytics/adapters/resolve.js +130 -0
  35. package/dist/vendor/analytics/adapters/tiktok.js +27 -0
  36. package/dist/vendor/analytics/catalog.js +908 -0
  37. package/dist/vendor/analytics/consent.js +49 -0
  38. package/dist/vendor/analytics/core.js +141 -0
  39. package/dist/vendor/analytics/identity.js +136 -0
  40. package/dist/vendor/analytics/index.js +170 -0
  41. package/dist/vendor/analytics/logger.js +40 -0
  42. package/dist/vendor/analytics/transports/browser.js +110 -0
  43. package/dist/vendor/monitoring/browser.js +207 -0
  44. package/dist/vendor/monitoring/core.js +180 -0
  45. package/dist/vendor/monitoring/logger.js +39 -0
  46. package/docs/architecture.md +59 -0
  47. package/docs/bindings.md +235 -0
  48. package/docs/build-system.md +32 -0
  49. package/docs/cdp-debugging.md +29 -0
  50. package/docs/code-patterns.md +96 -0
  51. package/docs/common-tasks.md +36 -0
  52. package/docs/dependencies.md +19 -0
  53. package/docs/index.md +159 -0
  54. package/docs/modules.md +180 -0
  55. package/docs/shared/agent-docs.md +89 -0
  56. package/docs/shared/analytics.md +612 -0
  57. package/docs/shared/brands.md +51 -0
  58. package/docs/shared/breaking-changes.md +497 -0
  59. package/docs/shared/config.md +1387 -0
  60. package/docs/shared/deploys.md +215 -0
  61. package/docs/shared/icons.md +201 -0
  62. package/docs/shared/local-dev.md +147 -0
  63. package/docs/shared/logging.md +202 -0
  64. package/docs/shared/monitoring.md +153 -0
  65. package/docs/shared/publishing.md +183 -0
  66. package/docs/shared/rulings.md +34 -0
  67. package/docs/shared/testing.md +147 -0
  68. package/docs/shared/theming.md +604 -0
  69. package/docs/shared/translation.md +291 -0
  70. package/docs/shared/updates.md +61 -0
  71. package/docs/testing.md +9 -0
  72. 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;