@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,108 @@
1
+ /**
2
+ * The client runtime's error reporting — a HOST of `@omega.js/monitoring`,
3
+ * never a second copy of its policy (#380).
4
+ *
5
+ * What lives here is what only the client can know: when to load the SDK, which
6
+ * release tag this page is (`brand.id@version`), where the signed-in user
7
+ * comes from (the auth storage keys), and the public surface every consumer
8
+ * calls — `omega.sentry().captureException(err)`.
9
+ *
10
+ * Everything else — the send gate, the @omega.js-bundle filter, the PII scrub,
11
+ * the integrations — is the package's, shared with the backend and the desktop
12
+ * main process so no surface can drift.
13
+ */
14
+
15
+ import monitoring from '../vendor/monitoring/browser.js';
16
+ import monitoringCore from '../vendor/monitoring/core.js';
17
+ import { createLogger } from './logger.js';
18
+
19
+ const logger = createLogger('sentry');
20
+
21
+ class mod {
22
+ constructor(manager) {
23
+ this.manager = manager;
24
+ this.initialized = false;
25
+ this.Sentry = null;
26
+ this.config = null;
27
+ }
28
+
29
+ /**
30
+ * Initialize Sentry error tracking
31
+ * @param {Object} config - the resolved Sentry settings for this surface
32
+ * (the build maps `monitoring.providers.sentry` into `config.sentry.config`)
33
+ * @returns {Promise} Resolves when initialization is complete
34
+ */
35
+ init(config = {}) {
36
+ // Dynamically imported to keep the SDK out of the initial chunk — and
37
+ // never reached at all when the config carries no DSN (index.js gates on
38
+ // `config.sentry.enabled`, which the build maps from
39
+ // `monitoring.providers.sentry.dsn`).
40
+ return import('@sentry/browser')
41
+ .then((sdk) => {
42
+ this.Sentry = sdk;
43
+
44
+ // Expose globally so page code and devtools can reach the same SDK
45
+ if (typeof window !== 'undefined') {
46
+ window.Sentry = sdk;
47
+ }
48
+
49
+ this.config = monitoring.buildInitOptions({
50
+ Sentry: sdk,
51
+ config,
52
+ release: monitoringCore.releaseTag({
53
+ id: this.manager.config.brand?.id,
54
+ // The host blob's app version when it carries one (@omega.js/extension
55
+ // bakes it, and it is the same key device stats read) — the build stamp
56
+ // is the fallback for a surface that ships no version yet.
57
+ version: this.manager.config.version || this.manager.config.buildTime,
58
+ }),
59
+ environment: this.manager.config.environment,
60
+ isDevelopment: () => this.manager.isDevelopment(),
61
+ getUser: () => {
62
+ const storage = this.manager.storage();
63
+ return {
64
+ uid: storage.get('auth.user.uid', ''),
65
+ email: storage.get('auth.user.email', ''),
66
+ };
67
+ },
68
+ });
69
+
70
+ sdk.init(this.config);
71
+ this.initialized = true;
72
+
73
+ return { initialized: true };
74
+ })
75
+ .catch((error) => {
76
+ logger.error('Failed to initialize:', error);
77
+ throw error;
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Capture an exception and send to Sentry
83
+ * Safe to call even if Sentry is not initialized
84
+ * @param {Error} error - The error to capture
85
+ * @param {Object} captureContext - Additional context for the error
86
+ * @returns {string|null} Event ID if successful, null otherwise
87
+ */
88
+ captureException(error, captureContext) {
89
+ // Log the error
90
+ logger.error('Capturing exception:', error);
91
+
92
+ // Safe to call - won't throw if not initialized
93
+ if (!this.initialized) {
94
+ logger.log('Not initialized, skipping capture');
95
+ return null;
96
+ }
97
+
98
+ // Call Sentry to capture the exception
99
+ try {
100
+ return this.Sentry.captureException(error, captureContext);
101
+ } catch (captureError) {
102
+ logger.error('Failed to capture exception:', captureError);
103
+ return null;
104
+ }
105
+ }
106
+ }
107
+
108
+ export default mod;
@@ -0,0 +1,237 @@
1
+ import { createLogger } from './logger.js';
2
+ import { pathPrefix } from './path-prefix.js';
3
+
4
+ const logger = createLogger('service-worker');
5
+
6
+ class ServiceWorker {
7
+ constructor(manager) {
8
+ this.manager = manager;
9
+ this._registration = null;
10
+ this._messageHandlers = new Map();
11
+ }
12
+
13
+ // Check if service workers are supported
14
+ isSupported() {
15
+ return 'serviceWorker' in navigator;
16
+ }
17
+
18
+ // Return promise that resolves when service worker is ready
19
+ async ready() {
20
+ if (!this.isSupported()) {
21
+ throw new Error('Service Workers not supported');
22
+ }
23
+
24
+ // If already registered and active
25
+ if (this._registration?.active) {
26
+ return this._registration;
27
+ }
28
+
29
+ // Wait for service worker to be ready
30
+ const registration = await navigator.serviceWorker.ready;
31
+ return registration;
32
+ }
33
+
34
+ // Register service worker
35
+ async register(options = {}) {
36
+ try {
37
+ if (!this.isSupported()) {
38
+ console.warn('Service Workers are not supported');
39
+ return null;
40
+ }
41
+
42
+ // Mount the script and the scope under the page's base path (#360): a
43
+ // worker served at /<prefix>/service-worker.js can only claim
44
+ // /<prefix>/. The prefix rides the script URL as a query param because
45
+ // the worker has no document to read the stamp from — it reads it back
46
+ // off self.location.search. No prefix leaves both untouched.
47
+ const prefix = pathPrefix();
48
+ const configuredPath = options.path || this.manager.config.serviceWorker?.config?.path || '/service-worker.js';
49
+ const swPath = prefix && configuredPath.startsWith('/') && !configuredPath.startsWith('//')
50
+ ? `${prefix}${configuredPath}`
51
+ : configuredPath;
52
+ const swUrl = prefix ? `${swPath}?omega-path-prefix=${encodeURIComponent(prefix)}` : swPath;
53
+ const scope = options.scope || `${prefix}/`;
54
+
55
+ // Build config object to pass to service worker
56
+ const config = {
57
+ brand: this.manager.config.brand?.id,
58
+ environment: this.manager.config.environment,
59
+ buildTime: this.manager.config.buildTime,
60
+ firebase: this.manager._resolveFirebaseConfig()
61
+ };
62
+
63
+ // Register service worker
64
+ const registration = await navigator.serviceWorker.register(swUrl, {
65
+ scope,
66
+ updateViaCache: 'none'
67
+ });
68
+
69
+ // Store registration
70
+ this._registration = registration;
71
+ this.manager.state.serviceWorker = registration;
72
+
73
+ // Wait for service worker to be ready and send config
74
+ await navigator.serviceWorker.ready;
75
+
76
+ // Send config to active service worker
77
+ // Removed due to issues init'ing firebase asynchronously in SW (now config is fetched directly in SW)
78
+ // if (registration.active) {
79
+ // try {
80
+ // this.postMessage({
81
+ // command: 'update-config',
82
+ // payload: config
83
+ // });
84
+ // } catch (error) {
85
+ // console.warn('Could not send config to service worker:', error);
86
+ // }
87
+ // }
88
+
89
+ // Resolve with registration
90
+ return registration;
91
+ } catch (error) {
92
+ console.error('Service Worker registration failed:', error);
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ // Unregister every service worker claiming this origin. Dev-loop hygiene:
98
+ // a previous project's service worker on the same localhost port would
99
+ // otherwise keep controlling pages and serving its stale caches.
100
+ async unregisterAll() {
101
+ if (!this.isSupported()) {
102
+ return 0;
103
+ }
104
+
105
+ try {
106
+ const registrations = await navigator.serviceWorker.getRegistrations();
107
+
108
+ await Promise.all(registrations.map((registration) => registration.unregister()));
109
+
110
+ if (registrations.length > 0) {
111
+ logger.log(`Unregistered ${registrations.length} service worker(s) claiming this origin`);
112
+ }
113
+
114
+ this._registration = null;
115
+
116
+ return registrations.length;
117
+ } catch (error) {
118
+ logger.warn('Failed to unregister service workers:', error);
119
+ return 0;
120
+ }
121
+ }
122
+
123
+ // Get current registration
124
+ getRegistration() {
125
+ return this._registration;
126
+ }
127
+
128
+ // Post message to service worker
129
+ postMessage(message, options = {}) {
130
+ return new Promise((resolve, reject) => {
131
+ // Check support
132
+ if (!this.isSupported()) {
133
+ return reject(new Error('Service Workers not supported'));
134
+ }
135
+
136
+ // Get active service worker
137
+ const controller = this._registration?.active || navigator.serviceWorker.controller;
138
+
139
+ if (!controller) {
140
+ return reject(new Error('No active service worker'));
141
+ }
142
+
143
+ // Create message channel for two-way communication
144
+ const messageChannel = new MessageChannel();
145
+ const timeout = options.timeout || 5000;
146
+ let timeoutId;
147
+
148
+ // Set up timeout to prevent hanging
149
+ if (timeout > 0) {
150
+ timeoutId = setTimeout(() => {
151
+ messageChannel.port1.close();
152
+ reject(new Error('Service worker message timeout'));
153
+ }, timeout);
154
+ }
155
+
156
+ // Listen for response from service worker
157
+ messageChannel.port1.onmessage = (event) => {
158
+ clearTimeout(timeoutId);
159
+
160
+ if (event.data.error) {
161
+ reject(new Error(event.data.error));
162
+ } else {
163
+ resolve(event.data);
164
+ }
165
+ };
166
+
167
+ // Send message with port for reply
168
+ controller.postMessage(message, [messageChannel.port2]);
169
+ });
170
+ }
171
+
172
+ // Listen for messages from service worker
173
+ onMessage(type, handler) {
174
+ if (!this.isSupported()) {
175
+ return () => {};
176
+ }
177
+
178
+ // Store handler
179
+ if (!this._messageHandlers.has(type)) {
180
+ this._messageHandlers.set(type, new Set());
181
+ }
182
+ this._messageHandlers.get(type).add(handler);
183
+
184
+ // Set up global message listener if not already done
185
+ if (this._messageHandlers.size === 1) {
186
+ navigator.serviceWorker.addEventListener('message', this._handleMessage.bind(this));
187
+ }
188
+
189
+ // Return unsubscribe function
190
+ return () => {
191
+ const handlers = this._messageHandlers.get(type);
192
+ if (handlers) {
193
+ handlers.delete(handler);
194
+ if (handlers.size === 0) {
195
+ this._messageHandlers.delete(type);
196
+ }
197
+ }
198
+ };
199
+ }
200
+
201
+ // Get service worker state
202
+ getState() {
203
+ if (!this._registration) {
204
+ return 'none';
205
+ }
206
+
207
+ if (this._registration.installing) {
208
+ return 'installing';
209
+ } else if (this._registration.waiting) {
210
+ return 'waiting';
211
+ } else if (this._registration.active) {
212
+ return 'active';
213
+ }
214
+
215
+ return 'unknown';
216
+ }
217
+
218
+ // Private: Handle incoming messages
219
+ _handleMessage(event) {
220
+ const { type, ...data } = event.data || {};
221
+
222
+ if (!type) return;
223
+
224
+ const handlers = this._messageHandlers.get(type);
225
+ if (handlers) {
226
+ handlers.forEach(handler => {
227
+ try {
228
+ handler(data, event);
229
+ } catch (error) {
230
+ console.error('Message handler error:', error);
231
+ }
232
+ });
233
+ }
234
+ }
235
+ }
236
+
237
+ export default ServiceWorker;
@@ -0,0 +1,133 @@
1
+ import lodash from 'lodash';
2
+ const { get: _get, set: _set } = lodash;
3
+
4
+ class Storage {
5
+ constructor() {
6
+ this.storageKey = '_manager';
7
+ this.pseudoStorage = {};
8
+ }
9
+
10
+ get(path, defaultValue) {
11
+ let usableStorage;
12
+
13
+ // Try to parse the localStorage object
14
+ try {
15
+ usableStorage = JSON.parse(window.localStorage.getItem(this.storageKey) || '{}');
16
+ } catch (e) {
17
+ usableStorage = this.pseudoStorage;
18
+ }
19
+
20
+ // If there's no path, return the entire storage object
21
+ if (!path) {
22
+ return usableStorage || defaultValue;
23
+ }
24
+
25
+ // Return the value at the path
26
+ return _get(usableStorage, path, defaultValue);
27
+ }
28
+
29
+ set(path, value) {
30
+ let usableStorage;
31
+
32
+ // Try to get the current storage
33
+ try {
34
+ usableStorage = this.get();
35
+ } catch (e) {
36
+ usableStorage = this.pseudoStorage;
37
+ }
38
+
39
+ // If there's no path, replace the entire storage object
40
+ if (!path) {
41
+ usableStorage = value || {};
42
+ } else {
43
+ // Set the value at the path
44
+ _set(usableStorage, path, value);
45
+ }
46
+
47
+ // Try to set the localStorage object
48
+ try {
49
+ window.localStorage.setItem(this.storageKey, JSON.stringify(usableStorage));
50
+ } catch (e) {
51
+ this.pseudoStorage = usableStorage;
52
+ }
53
+
54
+ return usableStorage;
55
+ }
56
+
57
+ remove(path) {
58
+ if (!path) {
59
+ this.clear();
60
+ } else {
61
+ this.set(path, undefined);
62
+ }
63
+ }
64
+
65
+ clear() {
66
+ try {
67
+ window.localStorage.setItem(this.storageKey, '{}');
68
+ } catch (e) {
69
+ this.pseudoStorage = {};
70
+ }
71
+ }
72
+
73
+ // Session storage methods
74
+ session = {
75
+ get: (path, defaultValue) => {
76
+ let usableStorage;
77
+
78
+ try {
79
+ usableStorage = JSON.parse(window.sessionStorage.getItem(this.storageKey) || '{}');
80
+ } catch (e) {
81
+ return defaultValue;
82
+ }
83
+
84
+ if (!path) {
85
+ return usableStorage || defaultValue;
86
+ }
87
+
88
+ return _get(usableStorage, path, defaultValue);
89
+ },
90
+
91
+ set: (path, value) => {
92
+ let usableStorage;
93
+
94
+ try {
95
+ usableStorage = this.session.get();
96
+ } catch (e) {
97
+ usableStorage = {};
98
+ }
99
+
100
+ if (!path) {
101
+ usableStorage = value || {};
102
+ } else {
103
+ _set(usableStorage, path, value);
104
+ }
105
+
106
+ try {
107
+ window.sessionStorage.setItem(this.storageKey, JSON.stringify(usableStorage));
108
+ } catch (e) {
109
+ // Silent fail
110
+ }
111
+
112
+ return usableStorage;
113
+ },
114
+
115
+ remove: (path) => {
116
+ if (!path) {
117
+ this.session.clear();
118
+ } else {
119
+ this.session.set(path, undefined);
120
+ }
121
+ },
122
+
123
+ clear: () => {
124
+ try {
125
+ window.sessionStorage.setItem(this.storageKey, '{}');
126
+ } catch (e) {
127
+ // Silent fail
128
+ }
129
+ }
130
+ };
131
+ }
132
+
133
+ export default Storage;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * triggers — the ONE click-trigger registry every OMEGA surface shares
3
+ * ([#16](https://github.com/Omega-JS-Stack/omega/issues/16)).
4
+ *
5
+ * A trigger is markup wiring: a class on an element means "clicking this runs
6
+ * that action", with no per-page JS. Before this module every surface rolled
7
+ * its own delegated `document` click listener with its own naming convention
8
+ * (`.auth-signout-btn` in client, `.auth-signin-btn` in extension,
9
+ * `.uj-password-toggle` in web, twice). Now there is exactly ONE listener and
10
+ * exactly one naming rule:
11
+ *
12
+ * registerTrigger('signout', handler) → the class is `omega-signout`
13
+ *
14
+ * The class is ALWAYS `omega-<name>` — callers never spell it, so it can never
15
+ * drift. A click anywhere inside a trigger element counts (closest()), which is
16
+ * what makes icon-only and label-wrapped buttons work.
17
+ *
18
+ * Who registers what: the client registers the GENERIC actions (sign-out), and
19
+ * each surface registers its own (extension: sign-in opens its auth page; web:
20
+ * the password eye). Registration is what arms the listener, so a surface can
21
+ * register before or after `omega.initialize()` — order never matters.
22
+ *
23
+ * Sibling of `motion` and `icon-renderer`: transport-free, DOM-only, and inert
24
+ * where there is no document (desktop main, the extension service worker).
25
+ */
26
+
27
+ import { createLogger } from './logger.js';
28
+
29
+ const logger = createLogger('triggers');
30
+
31
+ // The one prefix. A trigger named `signout` is the class `omega-signout`.
32
+ const PREFIX = 'omega-';
33
+
34
+ // name → handler. Module-level because the registry IS the singleton: one
35
+ // document, one listener, one table.
36
+ const handlers = new Map();
37
+
38
+ // True once the ONE delegated listener is attached.
39
+ let listening = false;
40
+
41
+ /**
42
+ * Attach the single delegated listener, once.
43
+ */
44
+ function ensureListener() {
45
+ if (listening || typeof document === 'undefined') {
46
+ return;
47
+ }
48
+
49
+ listening = true;
50
+ document.addEventListener('click', handleClick);
51
+ }
52
+
53
+ /**
54
+ * The one delegated handler: find the innermost trigger element the click
55
+ * happened inside, then run every trigger that element carries.
56
+ * @param {Event} event
57
+ */
58
+ function handleClick(event) {
59
+ if (!handlers.size) {
60
+ return;
61
+ }
62
+
63
+ // ONE closest() call over the union selector — the INNERMOST trigger wins,
64
+ // so nesting a trigger inside a trigger is deterministic.
65
+ const selector = [...handlers.keys()].map((name) => `.${PREFIX}${name}`).join(',');
66
+ const element = event.target?.closest?.(selector);
67
+
68
+ if (!element) {
69
+ return;
70
+ }
71
+
72
+ // A trigger class means the framework owns this click: no default navigation,
73
+ // no page-level handler behind it. Both legacy auth listeners did exactly
74
+ // this, and the markup contract is now the same everywhere.
75
+ event.preventDefault();
76
+ event.stopPropagation();
77
+
78
+ for (const [name, handler] of handlers) {
79
+ if (!element.classList.contains(`${PREFIX}${name}`)) {
80
+ continue;
81
+ }
82
+
83
+ // A throwing trigger is logged and never allowed to swallow the others.
84
+ // Async handlers (sign-out awaits Firebase) hand their failure back the
85
+ // same way.
86
+ try {
87
+ const result = handler(event, element);
88
+ if (typeof result?.catch === 'function') {
89
+ result.catch((error) => logger.error(`Trigger "${name}" failed:`, error));
90
+ }
91
+ } catch (error) {
92
+ logger.error(`Trigger "${name}" failed:`, error);
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Register a click trigger. The class it answers to is always `omega-<name>`.
99
+ * Re-registering the same name REPLACES the handler (with a warning) — there is
100
+ * no stacking, so a hot reload or a double boot can never double-fire.
101
+ * @param {string} name - trigger name, e.g. 'signout'
102
+ * @param {Function} handler - called with (event, element)
103
+ */
104
+ export function registerTrigger(name, handler) {
105
+ if (typeof handler !== 'function') {
106
+ throw new Error(`registerTrigger("${name}") needs a handler function`);
107
+ }
108
+
109
+ if (handlers.has(name)) {
110
+ logger.warn(`Trigger "${name}" re-registered — the new handler replaces the old one`);
111
+ }
112
+
113
+ handlers.set(name, handler);
114
+ ensureListener();
115
+ }
116
+
117
+ export default registerTrigger;