@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,207 @@
1
+ /**
2
+ * browser — the web-bundle entry (#380). @omega.js/client's sentry module is
3
+ * its only host, which puts one policy on every browser surface the client
4
+ * ships into (@omega.js/web pages and @omega.js/extension views alike).
5
+ *
6
+ * The doctrine this file enforces (Ian, 2026-08-20): client-side, ONLY errors
7
+ * from our own framework code report. A web page shares its global with
8
+ * everything — user-land inline scripts, ad and chat widgets, browser
9
+ * extensions injecting into the DOM — and none of that is ours to answer for.
10
+ * So the send gate is a stack-frame check against our bundle URLs, on top of
11
+ * the environment gates (development, Lighthouse, automated browsers) the
12
+ * client already ran.
13
+ *
14
+ * No `process` and no `require` of an SDK: this file is BUNDLED into a page.
15
+ * The host imports @sentry/browser itself (dynamically, to keep it out of the
16
+ * initial chunk) and passes the module in.
17
+ */
18
+
19
+ const { DEFAULTS, normalizeUser, createBundleFilter } = require('./core.js');
20
+ const { createLogger } = require('./logger.js');
21
+
22
+ const logger = createLogger('browser');
23
+
24
+ /** Lighthouse audits every error it can provoke — none of it is a real user's. */
25
+ function isLighthouse() {
26
+ try {
27
+ return typeof navigator !== 'undefined' && navigator.userAgent?.includes('Lighthouse');
28
+ } catch (e) {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ /** Selenium/Puppeteer/Playwright — an e2e run's errors are the run's, not production's. */
34
+ function isAutomatedBrowser() {
35
+ try {
36
+ return typeof navigator !== 'undefined' && navigator.webdriver === true;
37
+ } catch (e) {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * The url params that ARE a credential (#661): `?authPrivateKey` is a durable
44
+ * key and `?authCustomToken` signs in whoever holds it. A module constant, not
45
+ * config — a page cannot be allowed to opt its own credentials back into an
46
+ * event. Not exhaustive by design: only params the framework's own auth lanes
47
+ * put in an address bar.
48
+ */
49
+ const SENSITIVE_AUTH_PARAMS = ['authPrivateKey', 'authCustomToken'];
50
+
51
+ // Any absolute url parses against a base too, so ONE parse serves both shapes;
52
+ // the host below only ever names it back out of a relative input.
53
+ const SCRUB_BASE = 'https://scrub.invalid';
54
+
55
+ /**
56
+ * Drop the credential params from a url string. The SDK reads the address bar
57
+ * in two places we cannot reach from a catch block — the navigation breadcrumb
58
+ * it records around `history.replaceState`, and the request url `httpContext`
59
+ * attaches at capture time — so the scrub belongs on this seam, once, rather
60
+ * than in every lane that handles a key.
61
+ *
62
+ * A url carrying none of them comes back untouched (never re-serialized), and a
63
+ * same-origin breadcrumb url stays relative, the way the SDK recorded it.
64
+ *
65
+ * @param {string} url - an absolute or root-relative url
66
+ * @returns {string} the url without the credential params
67
+ */
68
+ function scrubAuthParams(url) {
69
+ if (typeof url !== 'string' || !SENSITIVE_AUTH_PARAMS.some((param) => url.includes(param))) {
70
+ return url;
71
+ }
72
+
73
+ let parsed;
74
+ let relative = false;
75
+
76
+ try {
77
+ parsed = new URL(url);
78
+ } catch (e) {
79
+ relative = true;
80
+ }
81
+
82
+ if (relative) {
83
+ try {
84
+ parsed = new URL(url, SCRUB_BASE);
85
+ } catch (e) {
86
+ // Not a url at all — better a param name in a breadcrumb than a mangled one.
87
+ return url;
88
+ }
89
+ }
90
+
91
+ SENSITIVE_AUTH_PARAMS.forEach((param) => parsed.searchParams.delete(param));
92
+
93
+ return relative ? `${parsed.pathname}${parsed.search}${parsed.hash}` : parsed.toString();
94
+ }
95
+
96
+ /**
97
+ * Build the @sentry/browser init options: integrations plus the one beforeSend
98
+ * that decides what leaves the page.
99
+ *
100
+ * @param {object} params
101
+ * @param {object} params.Sentry - the imported @sentry/browser module
102
+ * @param {object} [params.config] - the resolved Sentry settings for this surface
103
+ * (the build maps `monitoring.providers.sentry` into the client's `sentry.config`)
104
+ * @param {string} [params.release] - the host's release tag (core.releaseTag)
105
+ * @param {string} [params.environment]
106
+ * @param {() => boolean} [params.isDevelopment] - the host's dev signal, read per event
107
+ * @param {() => object} [params.getUser] - the signed-in user, read per event
108
+ * @param {() => object} [params.getTags] - extra tags, read per event
109
+ * @returns {object} the options object to hand Sentry.init()
110
+ */
111
+ function buildInitOptions(params) {
112
+ const { Sentry, release, environment, isDevelopment, getUser, getTags } = params || {};
113
+
114
+ const options = { ...DEFAULTS, ...((params && params.config) || {}) };
115
+
116
+ const isFrameworkEvent = createBundleFilter(options.bundlePatterns);
117
+
118
+ // Session-hours baseline: the page's own clock starts when reporting boots.
119
+ const startTime = Date.now();
120
+
121
+ const integrations = [];
122
+ if (typeof Sentry.browserTracingIntegration === 'function') {
123
+ integrations.push(Sentry.browserTracingIntegration());
124
+ }
125
+ // Replay costs bandwidth and captures the DOM — strictly opt-in via sample rates.
126
+ const hasReplays = (options.replaysSessionSampleRate > 0) || (options.replaysOnErrorSampleRate > 0);
127
+ if (hasReplays && typeof Sentry.replayIntegration === 'function') {
128
+ integrations.push(Sentry.replayIntegration({ maskAllText: false, blockAllMedia: false }));
129
+ }
130
+
131
+ return {
132
+ dsn: options.dsn,
133
+ release,
134
+ environment: environment || options.environment || 'production',
135
+ sampleRate: options.sampleRate,
136
+ tracesSampleRate: options.tracesSampleRate,
137
+ ...(options.replaysSessionSampleRate > 0 ? { replaysSessionSampleRate: options.replaysSessionSampleRate } : {}),
138
+ ...(options.replaysOnErrorSampleRate > 0 ? { replaysOnErrorSampleRate: options.replaysOnErrorSampleRate } : {}),
139
+ integrations,
140
+
141
+ beforeBreadcrumb(breadcrumb) {
142
+ // The SDK patches history.replaceState, so the very call that STRIPS a key
143
+ // from the address bar records the pre-strip url as `data.from` — and the
144
+ // breadcrumb then rides with every later event on that page load (#661).
145
+ if (breadcrumb && breadcrumb.category === 'navigation' && breadcrumb.data) {
146
+ ['from', 'to'].forEach((key) => {
147
+ if (typeof breadcrumb.data[key] === 'string') {
148
+ breadcrumb.data[key] = scrubAuthParams(breadcrumb.data[key]);
149
+ }
150
+ });
151
+ }
152
+
153
+ return breadcrumb;
154
+ },
155
+
156
+ beforeSend(event, hint) {
157
+ // The console line lands FIRST, before any gate: a dropped event is still
158
+ // an error the developer wants to see in devtools.
159
+ logger.error('Caught error:', {
160
+ message: event.message || (event.exception?.values?.[0]?.value) || 'Unknown error',
161
+ level: event.level,
162
+ hint,
163
+ });
164
+
165
+ if (isDevelopment && isDevelopment()) {
166
+ logger.log('Development mode — not sending');
167
+ return null;
168
+ }
169
+ if (isLighthouse()) {
170
+ logger.log('Lighthouse detected — not sending');
171
+ return null;
172
+ }
173
+ if (isAutomatedBrowser()) {
174
+ logger.log('Automated browser detected — not sending');
175
+ return null;
176
+ }
177
+ if (!isFrameworkEvent(event)) {
178
+ logger.log('Not from an @omega.js bundle — not sending');
179
+ return null;
180
+ }
181
+
182
+ event.tags = {
183
+ ...event.tags,
184
+ 'process.type': 'browser',
185
+ 'usage.session.hours': ((Date.now() - startTime) / (1000 * 3600)).toFixed(2),
186
+ ...(getTags ? getTags() : {}),
187
+ };
188
+
189
+ // The other half of the #661 scrub: httpContext attaches the address bar
190
+ // as the request url when the event is CAPTURED, key and all.
191
+ if (typeof event.request?.url === 'string') {
192
+ event.request.url = scrubAuthParams(event.request.url);
193
+ }
194
+
195
+ // PII: the uid is the join key to the account and rides; the email is
196
+ // scrubbed unless `monitoring.providers.sentry.scrubEmail: false` opts in.
197
+ const user = normalizeUser(getUser ? getUser() : null, options);
198
+ if (user) {
199
+ event.user = { ...event.user, ...user };
200
+ }
201
+
202
+ return event;
203
+ },
204
+ };
205
+ }
206
+
207
+ module.exports = { buildInitOptions, isLighthouse, isAutomatedBrowser, scrubAuthParams };
@@ -0,0 +1,180 @@
1
+ /**
2
+ * core — the ONE place error-reporting policy lives (#380).
3
+ *
4
+ * Pure functions, zero runtime assumptions: no SDK, no `process`, no DOM. The
5
+ * browser bundle imports this file, so an env read here would be a
6
+ * ReferenceError on a web page (esbuild defines only the NODE_ENV key) — every
7
+ * environment signal is passed IN as a gate. `env.js` is the Node/Electron side
8
+ * of that seam.
9
+ *
10
+ * Policy it owns, once for every target:
11
+ * - config resolution from omega.json5's `monitoring` role section — the
12
+ * settings live under `monitoring.providers.sentry` (#425) and DSN presence
13
+ * is the enable signal (no separate `enabled` flag — matches
14
+ * @omega.js/backend convention: a config block's credentials are its switch)
15
+ * - the release tag, from the host's own version identity
16
+ * - user normalization with the email SCRUBBED BY DEFAULT (the uid stays —
17
+ * it is the join key to the account, and it is not PII on its own)
18
+ * - the client-side framework-bundle filter: browser events report ONLY when
19
+ * they come from OUR bundles. User-land page scripts, browser extensions
20
+ * injecting into the page, and cross-origin "Script error." noise never do.
21
+ *
22
+ * The SDK is never loaded from here. Nothing in this file can turn reporting
23
+ * ON either: it can only say whether a host SHOULD boot one.
24
+ */
25
+
26
+ const DEFAULTS = {
27
+ dsn: '',
28
+ environment: null, // null = the host's gate decides (production / development)
29
+ sampleRate: 1, // error events kept, 0..1 — the sampling knob (Sentry-native)
30
+ tracesSampleRate: 0.1,
31
+ attachScreenshot: false,
32
+ scrubEmail: true, // PII off by default — set false to opt IN to emails
33
+ // Browser only: the URL fragments that identify OUR bundles. Both @omega.js/web
34
+ // and @omega.js/extension serve every framework bundle (the client runtime
35
+ // included) out of `/assets/js/`, so one default covers both surfaces.
36
+ bundlePatterns: ['/assets/js/'],
37
+ };
38
+
39
+ // A resolved-but-off result. `options` still comes back so a caller can log what
40
+ // it read.
41
+ function disabled(options, reason) {
42
+ return { shouldEnable: false, options, reason };
43
+ }
44
+
45
+ /**
46
+ * The Sentry settings inside a `monitoring` role section (#425). The role level
47
+ * carries only `enabled`; every SDK-facing key hangs off the provider, so this
48
+ * is the one place that knows the nesting. `{ sentry: false }` (deliberately
49
+ * disabled) and a missing block both resolve to nothing — no dsn, no boot.
50
+ *
51
+ * @param {object} [section] - the `monitoring` config block (target-resolved)
52
+ * @returns {object} the provider's settings, or {} when there are none
53
+ */
54
+ function providerOptions(section) {
55
+ const settings = section && section.providers && section.providers.sentry;
56
+ return settings && typeof settings === 'object' ? settings : {};
57
+ }
58
+
59
+ /**
60
+ * Resolve the runtime config and decide whether reporting should boot.
61
+ *
62
+ * Reads omega.json5's `monitoring` role section: the SDK-facing keys come from
63
+ * `monitoring.providers.sentry`, so the resolved options feed init directly.
64
+ *
65
+ * @param {object} section - the `monitoring` config block (target-resolved)
66
+ * @param {object} [gates] - the host's environment signals
67
+ * @param {boolean} [gates.killed] - a kill switch tripped (env var, test runner)
68
+ * @param {string} [gates.killedReason] - what tripped it, for the log line
69
+ * @param {boolean} [gates.isProduction] - this run ships telemetry
70
+ * @param {boolean} [gates.allowInDev] - report anyway outside production
71
+ * @returns {{ shouldEnable: boolean, options: object, reason: string|null }}
72
+ */
73
+ function resolveConfig(section, gates) {
74
+ gates = gates || {};
75
+
76
+ const options = { ...DEFAULTS, ...providerOptions(section) };
77
+
78
+ if (gates.killed) {
79
+ return disabled(options, gates.killedReason || 'disabled by the host');
80
+ }
81
+ if (!options.dsn) {
82
+ return disabled(options, 'no dsn set');
83
+ }
84
+ if (!gates.isProduction && !gates.allowInDev) {
85
+ return disabled(options, 'not a production run (set OMEGA_SENTRY_FORCE=true to override)');
86
+ }
87
+
88
+ if (!options.environment) {
89
+ options.environment = gates.isProduction ? 'production' : 'development';
90
+ }
91
+
92
+ return { shouldEnable: true, options, reason: null };
93
+ }
94
+
95
+ /**
96
+ * Normalize a @omega.js/client / firebase / admin user into the minimal shape
97
+ * the SDK wants. The email is PII: it rides ONLY when the host explicitly opts
98
+ * in with `monitoring.providers.sentry.scrubEmail: false`.
99
+ *
100
+ * @param {object} user - a user-ish object carrying uid/id and maybe email
101
+ * @param {object} [options] - the resolved monitoring options
102
+ * @returns {object|null} { id, email? } — null when there is nothing safe to send
103
+ */
104
+ function normalizeUser(user, options) {
105
+ if (!user) return null;
106
+
107
+ const scrubEmail = (options || {}).scrubEmail !== false;
108
+ const out = {};
109
+
110
+ if (user.uid) out.id = user.uid;
111
+ else if (user.id) out.id = user.id;
112
+ if (user.email && !scrubEmail) out.email = user.email;
113
+
114
+ return Object.keys(out).length === 0 ? null : out;
115
+ }
116
+
117
+ /**
118
+ * Build the release tag from the host's own version identity. ONE format on
119
+ * every target (Ian, 2026-08-20): `<brand.id>@<version>` — a Sentry release is
120
+ * comparable across the backend, the desktop app and the browser bundles only
121
+ * when they spell it the same way. The brand id is required config, so a tag
122
+ * missing one is a config hole, not a second format: no id, no tag.
123
+ *
124
+ * The version is the host's own — @omega.js/backend's functions package
125
+ * version, @omega.js/desktop's app version, the browser blob's app version.
126
+ *
127
+ * @param {object} identity - { id, version }
128
+ * @returns {string|undefined} the tag, or undefined when either half is missing
129
+ */
130
+ function releaseTag(identity) {
131
+ const { id, version } = identity || {};
132
+ if (!id || !version) return undefined;
133
+ return `${id}@${version}`;
134
+ }
135
+
136
+ /**
137
+ * Every stack-frame filename an event carries (exception frames first, then the
138
+ * threads a hung-renderer report uses).
139
+ */
140
+ function eventFrameFilenames(event) {
141
+ const values = [
142
+ ...((event && event.exception && event.exception.values) || []),
143
+ ...((event && event.threads && event.threads.values) || []),
144
+ ];
145
+
146
+ return values.flatMap((value) => ((value.stacktrace && value.stacktrace.frames) || [])
147
+ .map((frame) => frame.filename || frame.abs_path || '')
148
+ .filter(Boolean));
149
+ }
150
+
151
+ /**
152
+ * The client-side doctrine gate (#380): a browser event reports ONLY when our
153
+ * own framework code is on its stack.
154
+ *
155
+ * An event with no matching frame is DROPPED, and that includes an event with no
156
+ * frames at all — a cross-origin `Script error.` is exactly the third-party noise
157
+ * this exists to kill. Deliberate `omega.sentry().captureException(...)` calls
158
+ * are unaffected: they are thrown from page bundles, which ARE our bundles.
159
+ *
160
+ * @param {string[]} [patterns] - URL fragments identifying our bundles
161
+ * @returns {(event: object) => boolean} true when the event may report
162
+ */
163
+ function createBundleFilter(patterns) {
164
+ const fragments = (patterns || DEFAULTS.bundlePatterns).filter(Boolean);
165
+
166
+ return function isFrameworkEvent(event) {
167
+ const filenames = eventFrameFilenames(event);
168
+ return filenames.some((filename) => fragments.some((fragment) => filename.includes(fragment)));
169
+ };
170
+ }
171
+
172
+ module.exports = {
173
+ DEFAULTS,
174
+ providerOptions,
175
+ resolveConfig,
176
+ normalizeUser,
177
+ releaseTag,
178
+ eventFrameFilenames,
179
+ createBundleFilter,
180
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The monitoring package's log tag — the ONE identity tag every OMEGA surface
3
+ * prints: `[@omega.js/monitoring:<module>]` ([#12](https://github.com/Omega-JS-Stack/omega/issues/12)).
4
+ *
5
+ * The twin of @omega.js/analytics' createLogger, emitting this package's
6
+ * segment: no timestamp, because devtools, Cloud Logging and electron-log all
7
+ * stamp their own lines.
8
+ *
9
+ * CJS like the rest of the package: the backend and the electron main process
10
+ * require() it, the browser bundles import it with standard interop.
11
+ */
12
+
13
+ // The package segment — this file IS @omega.js/monitoring, so it is a literal.
14
+ const PACKAGE = '@omega.js/monitoring';
15
+
16
+ /**
17
+ * Create a tagged console for one module, e.g. createLogger('core').
18
+ * @param {string} module - The module identity segment.
19
+ * @returns {object} A console-shaped logger whose calls carry the tag.
20
+ */
21
+ function createLogger(module) {
22
+ const tag = `[${PACKAGE}:${module}]`;
23
+
24
+ // GETTERS returning a BOUND console method, not wrapper arrows: devtools
25
+ // attributes a line to the frame that called console, so a wrapper would make
26
+ // every line read as coming from this file. Resolution stays at ACCESS time,
27
+ // so a test (or a consumer) that swaps console[method] still sees its own stub.
28
+ const logger = { tag };
29
+ for (const method of ['log', 'info', 'warn', 'error', 'debug']) {
30
+ Object.defineProperty(logger, method, {
31
+ get: () => console[method].bind(console, tag),
32
+ enumerable: true,
33
+ });
34
+ }
35
+
36
+ return logger;
37
+ }
38
+
39
+ module.exports = { createLogger };
@@ -0,0 +1,59 @@
1
+ # Architecture
2
+
3
+ ## Singleton Pattern
4
+
5
+ The library exports a singleton `Manager` instance. Import it directly from any file — it's always the same initialized instance:
6
+
7
+ ```javascript
8
+ import omega from '@omega.js/client';
9
+
10
+ // Same instance everywhere — config, auth, firestore, all ready
11
+ omega.auth().listen((state) => { ... });
12
+ omega.utilities().escapeHTML(untrustedText);
13
+ omega.config.environment; // 'development' or 'production'
14
+ ```
15
+
16
+ **Do NOT create new instances** (`new Manager()`). @omega.js/web, @omega.js/extension, and @omega.js/desktop initialize the singleton — every import gets that same object. Do NOT pass `omega` through function params or store it in module-level variables — just import it.
17
+
18
+ ## Directory Structure
19
+
20
+ ```
21
+ @omega.js/client/
22
+ ├── src/ # Source code (ES6+)
23
+ │ ├── index.js # Manager class, initialization, Firebase setup
24
+ │ └── modules/ # Feature modules
25
+ │ ├── auth.js # Firebase Auth wrapper
26
+ │ ├── bindings.js # Reactive DOM data binding
27
+ │ ├── dom.js # loadScript, ready utilities
28
+ │ ├── firestore.js # Firestore wrapper with chainable queries
29
+ │ ├── notifications.js # FCM push notifications
30
+ │ ├── sentry.js # Error tracking integration
31
+ │ ├── service-worker.js # SW registration and messaging
32
+ │ ├── storage.js # localStorage/sessionStorage wrapper
33
+ │ └── utilities.js # Helper functions (clipboard, escape, etc.)
34
+ ├── dist/ # Transpiled ES5 output (generated)
35
+ ├── _legacy/ # Old implementation (reference only, DO NOT MODIFY)
36
+ └── test/ # Mocha tests
37
+ ```
38
+
39
+ ## Module Dependencies
40
+
41
+ ```
42
+ Manager (index.js)
43
+ ├── Storage (standalone, no deps)
44
+ ├── Auth → Manager, Bindings, Storage, Firestore
45
+ ├── Bindings → Manager
46
+ ├── Firestore → Manager (lazy Firebase import)
47
+ ├── Notifications → Manager, Storage, Firestore
48
+ ├── ServiceWorker → Manager
49
+ ├── Sentry → Manager (dynamic import)
50
+ ├── DOM utilities (standalone)
51
+ └── Utilities (standalone)
52
+ ```
53
+
54
+ ## Firebase Initialization
55
+
56
+ `initialize(config)` boots Firebase only when a usable web SDK config resolves:
57
+
58
+ - `_resolveFirebaseConfig()` checks `cloud.config` first (the omega.json5 role shape — desktop passes its resolved config through), then the nested `firebase.app.config` (the web/extension bridge contract shape). A blob only counts when **at least one value is non-empty** — framework config merges (e.g. UJM's Jekyll chain) inject all-empty-string blobs into Firebase-less sites, and those resolve to `null` (no init, no URL derivation).
59
+ - Initialization additionally requires a **non-empty `apiKey`** — the SDK cannot boot without one (it crashes the page with `auth/invalid-api-key`). Configs carrying only `projectId` still resolve so `getFunctionsUrl()` can derive its URL (`getApiUrl()` derives from `brand.url`, not the Firebase blob), but Firebase itself stays uninitialized and the console logs `[Firebase] Skipped: config has no apiKey ...` (same idiom as `[Analytics] Skipped:`).