@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,479 @@
1
+ // Methods are defined as arrow class fields so `this` is permanently bound to the instance.
2
+ // This means consumers can safely alias or destructure methods without losing context:
3
+ // const { escapeHTML } = omega.utilities(); // ✓ works
4
+ // const escape = omega.utilities().escapeHTML; // ✓ works
5
+ // items.map(omega.utilities().escapeHTML); // ✓ works
6
+ // Safe because omega.utilities() is a singleton — only one instance ever exists.
7
+
8
+ // renderMarkdown links are restricted to the two schemes a browser may navigate
9
+ // safely. sanitizeURL already rejects javascript:/data:, but it resolves a bare
10
+ // path against the current origin and returns it — and the bracket syntax is the
11
+ // one place the source supplies an attribute VALUE rather than text, so a link is
12
+ // only ever minted from a URL that says its own scheme out loud.
13
+ const SAFE_HREF = /^https?:\/\//i;
14
+
15
+ // The inline grammar, applied to an already-escaped line.
16
+ const renderInline = (text, sanitizeURL) => text
17
+ // Code first: what is inside a span of backticks is literal, and running the
18
+ // emphasis rules over it would eat the asterisks in a code sample.
19
+ .split(/(`[^`]+`)/)
20
+ .map((part) => {
21
+ if (part.startsWith('`') && part.endsWith('`') && part.length > 1) {
22
+ return `<code>${part.slice(1, -1)}</code>`;
23
+ }
24
+
25
+ // Built anchors are stashed behind a NUL sentinel while the emphasis rules
26
+ // run — an href may legitimately contain asterisks, and the emphasis pass
27
+ // must never see markup it built.
28
+ const anchors = [];
29
+
30
+ return part
31
+ .replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (whole, label, href) => {
32
+ const safe = sanitizeURL(href);
33
+
34
+ // Not a scheme a browser may follow — leave the bracket text as text.
35
+ if (!safe || !SAFE_HREF.test(safe)) {
36
+ return whole;
37
+ }
38
+
39
+ anchors.push(`<a href="${safe}" target="_blank" rel="noopener">${label}</a>`);
40
+ return `\u0000${anchors.length - 1}\u0000`;
41
+ })
42
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
43
+ .replace(/(^|[^*])\*([^*]+)\*/g, '$1<em>$2</em>')
44
+ .replace(/\u0000(\d+)\u0000/g, (match, index) => anchors[Number(index)]);
45
+ })
46
+ .join('');
47
+
48
+ class Utilities {
49
+ constructor(manager) {
50
+ this.manager = manager;
51
+ }
52
+
53
+ // Copy text to clipboard
54
+ //
55
+ // Always a promise, and a REFUSED copy REJECTS
56
+ // ([#726](https://github.com/Omega-JS-Stack/omega/issues/726)). Every caller
57
+ // draws its confirmation off this promise, so a failure that resolves tells
58
+ // the visitor their credential is in the buffer when it is not.
59
+ clipboardCopy = async (input) => {
60
+ // Get the text from the input
61
+ const text = input && input.nodeType
62
+ ? input.value || input.innerText || input.innerHTML
63
+ : input;
64
+
65
+ // Try to use the modern clipboard API — a refusal (no permission, an
66
+ // insecure context, a blurred document) is not a failure until the legacy
67
+ // lane has had its turn too.
68
+ if (navigator.clipboard && navigator.clipboard.writeText) {
69
+ try {
70
+ return await navigator.clipboard.writeText(text);
71
+ } catch (e) {
72
+ return fallbackCopy(text);
73
+ }
74
+ }
75
+
76
+ return fallbackCopy(text);
77
+
78
+ function fallbackCopy(text) {
79
+ const el = document.createElement('textarea');
80
+ el.setAttribute('style', 'width:1px;border:0;opacity:0;');
81
+ el.value = text;
82
+ document.body.appendChild(el);
83
+ el.select();
84
+
85
+ // `execCommand` reports a refusal with a FALSE RETURN rather than a
86
+ // throw — reading the return is the only way this lane can fail.
87
+ let copied = false;
88
+
89
+ try {
90
+ copied = document.execCommand('copy');
91
+ } finally {
92
+ document.body.removeChild(el);
93
+ }
94
+
95
+ if (!copied) {
96
+ throw new Error('Failed to copy to clipboard');
97
+ }
98
+ }
99
+ }
100
+
101
+ // Escape HTML to prevent XSS
102
+ // Accepts a string, object, or array — walks recursively, escaping all string values
103
+ escapeHTML = (input) => {
104
+ // Strings — escape and return
105
+ if (typeof input === 'string') {
106
+ this._shadowElement = this._shadowElement || document.createElement('p');
107
+ this._shadowElement.innerHTML = '';
108
+
109
+ // This automatically escapes HTML entities like <, >, &, etc.
110
+ this._shadowElement.appendChild(document.createTextNode(input));
111
+
112
+ // This is needed to escape quotes to prevent attribute injection
113
+ return this._shadowElement.innerHTML.replace(/["']/g, (m) => {
114
+ switch (m) {
115
+ case '"':
116
+ return '&quot;';
117
+ default:
118
+ return '&#039;';
119
+ }
120
+ });
121
+ }
122
+
123
+ // Null/undefined — pass through
124
+ if (input == null) {
125
+ return input;
126
+ }
127
+
128
+ // Arrays — recurse each item
129
+ if (Array.isArray(input)) {
130
+ return input.map(item => this.escapeHTML(item));
131
+ }
132
+
133
+ // Objects — shallow clone, recurse each value
134
+ if (typeof input === 'object') {
135
+ const result = {};
136
+ for (const [key, value] of Object.entries(input)) {
137
+ result[key] = this.escapeHTML(value);
138
+ }
139
+ return result;
140
+ }
141
+
142
+ // Numbers, booleans, etc. — pass through unchanged
143
+ return input;
144
+ }
145
+
146
+ // Sanitize URL to prevent javascript:, data:, and other dangerous URI schemes
147
+ // Returns the original URL if safe, or '' if rejected
148
+ sanitizeURL = (url) => {
149
+ if (!url || typeof url !== 'string') {
150
+ return '';
151
+ }
152
+
153
+ try {
154
+ const parsed = new URL(url, window.location.origin);
155
+
156
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
157
+ return '';
158
+ }
159
+
160
+ return url;
161
+ } catch (e) {
162
+ return '';
163
+ }
164
+ }
165
+
166
+ // Render hostile text as safe markup with a small markdown grammar: headings,
167
+ // fenced and inline code, lists, bold/italic, and links restricted to http(s).
168
+ //
169
+ // The input is untrusted (an API answer, another user's words), so nothing here
170
+ // ever passes markup through: the text is ESCAPED FIRST, once, and every rule
171
+ // below works on that escaped string — a `<script>` is already `&lt;script&gt;`
172
+ // before any rule decides what a line means, so no rule can resurrect it.
173
+ // Escaping is escapeHTML's job and scheme safety is sanitizeURL's; this method
174
+ // only decides what a line MEANS.
175
+ //
176
+ // Not a markdown engine and not trying to be one — anything outside the grammar
177
+ // renders as the text it was. Empty input renders as '', so the caller can say
178
+ // what empty means in its own words.
179
+ renderMarkdown = (text) => {
180
+ const source = String(text === null || text === undefined ? '' : text);
181
+ if (!source.trim()) {
182
+ return '';
183
+ }
184
+
185
+ const lines = this.escapeHTML(source.replace(/\r\n/g, '\n')).split('\n');
186
+ const out = [];
187
+ let paragraph = [];
188
+ let list = null;
189
+ let code = null;
190
+
191
+ const closeParagraph = () => {
192
+ if (!paragraph.length) {
193
+ return;
194
+ }
195
+
196
+ out.push(`<p>${renderInline(paragraph.join('<br>'), this.sanitizeURL)}</p>`);
197
+ paragraph = [];
198
+ };
199
+
200
+ const closeList = () => {
201
+ if (!list) {
202
+ return;
203
+ }
204
+
205
+ const items = list.items.map((item) => `<li>${renderInline(item, this.sanitizeURL)}</li>`).join('');
206
+ out.push(`<${list.tag}>${items}</${list.tag}>`);
207
+ list = null;
208
+ };
209
+
210
+ const closeBlocks = () => {
211
+ closeParagraph();
212
+ closeList();
213
+ };
214
+
215
+ for (const line of lines) {
216
+ // A fence swallows everything until the next one — inside it, no rule but
217
+ // "this is literal" applies.
218
+ const fence = /^\s*```/.test(line);
219
+ if (code !== null) {
220
+ if (fence) {
221
+ out.push(`<pre class="p-2 rounded"><code>${code.join('\n')}</code></pre>`);
222
+ code = null;
223
+ } else {
224
+ code.push(line);
225
+ }
226
+ continue;
227
+ }
228
+ if (fence) {
229
+ closeBlocks();
230
+ code = [];
231
+ continue;
232
+ }
233
+
234
+ const heading = line.match(/^(#{1,6})\s+(.*)$/);
235
+ if (heading) {
236
+ closeBlocks();
237
+
238
+ // Rendered text is a fragment inside a host page, not a document: its
239
+ // headings start below the host's own title rather than competing with it.
240
+ const level = Math.min(heading[1].length + 3, 6);
241
+ out.push(`<h${level} class="h6 mt-3 mb-2">${renderInline(heading[2], this.sanitizeURL)}</h${level}>`);
242
+ continue;
243
+ }
244
+
245
+ const bullet = line.match(/^\s*[-*]\s+(.*)$/);
246
+ const numbered = line.match(/^\s*\d+[.)]\s+(.*)$/);
247
+ if (bullet || numbered) {
248
+ closeParagraph();
249
+
250
+ const tag = bullet ? 'ul' : 'ol';
251
+ if (list && list.tag !== tag) {
252
+ closeList();
253
+ }
254
+
255
+ list = list || { tag, items: [] };
256
+ list.items.push((bullet || numbered)[1]);
257
+ continue;
258
+ }
259
+
260
+ if (!line.trim()) {
261
+ closeBlocks();
262
+ continue;
263
+ }
264
+
265
+ closeList();
266
+ paragraph.push(line);
267
+ }
268
+
269
+ // An unterminated fence is still content — render what it holds rather than
270
+ // dropping the rest of the text on the floor.
271
+ if (code !== null) {
272
+ out.push(`<pre class="p-2 rounded"><code>${code.join('\n')}</code></pre>`);
273
+ }
274
+ closeBlocks();
275
+
276
+ return out.join('');
277
+ }
278
+
279
+ // Show notification
280
+ showNotification = (message, options = {}) => {
281
+ // Handle different input types
282
+ let text = message;
283
+ let type = options.type || 'info';
284
+
285
+ // If message is an Error object, extract message and default to danger
286
+ if (message instanceof Error) {
287
+ text = message.message;
288
+ type = options.type || 'danger';
289
+ }
290
+
291
+ // Handle string as second parameter for backwards compatibility
292
+ if (typeof options === 'string') {
293
+ options = { type: options };
294
+ type = options.type;
295
+ }
296
+
297
+ // Extract options
298
+ const timeout = options.timeout !== undefined ? options.timeout : 5000;
299
+
300
+ const $notification = document.createElement('div');
301
+ $notification.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
302
+ $notification.style.cssText = 'z-index: 9999; top: 1rem; left: 50%; transform: translateX(-50%); width: calc(100% - 2rem); max-width: 500px;';
303
+
304
+ const $text = document.createElement('span');
305
+ $text.textContent = text;
306
+
307
+ const $closeBtn = document.createElement('button');
308
+ $closeBtn.type = 'button';
309
+ $closeBtn.className = 'btn-close';
310
+ $closeBtn.setAttribute('data-bs-dismiss', 'alert');
311
+
312
+ $notification.appendChild($text);
313
+ $notification.appendChild($closeBtn);
314
+
315
+ document.body.appendChild($notification);
316
+
317
+ // Auto-remove after timeout (unless timeout is 0)
318
+ if (timeout > 0) {
319
+ setTimeout(() => {
320
+ $notification.remove();
321
+ }, timeout);
322
+ }
323
+ }
324
+
325
+ // Get platform (OS)
326
+ getPlatform = () => {
327
+ const ua = navigator.userAgent.toLowerCase();
328
+ const platform = (navigator.userAgentData?.platform || navigator.platform || '').toLowerCase();
329
+
330
+ // Check userAgent for mobile platforms (more reliable than platform string)
331
+ if (/iphone|ipad|ipod/.test(ua)) {
332
+ return 'ios';
333
+ }
334
+ if (/android/.test(ua)) {
335
+ return 'android';
336
+ }
337
+
338
+ // Check platform string for desktop OS
339
+ if (/win/.test(platform)) {
340
+ return 'windows';
341
+ }
342
+ if (/mac/.test(platform)) {
343
+ return 'mac';
344
+ }
345
+ if (/cros/.test(ua)) {
346
+ return 'chromeos';
347
+ }
348
+ if (/linux/.test(platform)) {
349
+ return 'linux';
350
+ }
351
+
352
+ return 'unknown';
353
+ }
354
+
355
+ // Get browser name
356
+ getBrowser = () => {
357
+ const ua = navigator.userAgent;
358
+
359
+ // Order matters - check more specific browsers first
360
+ // Edge before Chrome (Edge includes "Chrome" in UA)
361
+ if (/edg/i.test(ua)) {
362
+ return 'edge';
363
+ }
364
+
365
+ // Opera before Chrome (Opera includes "Chrome" in UA)
366
+ if (/opera|opr/i.test(ua)) {
367
+ return 'opera';
368
+ }
369
+
370
+ // Brave before Chrome (Brave includes "Chrome" in UA)
371
+ if (navigator.brave || /brave/i.test(ua)) {
372
+ return 'brave';
373
+ }
374
+
375
+ // Chrome (including Chromium-based browsers)
376
+ if (/chrome|chromium|crios/i.test(ua)) {
377
+ return 'chrome';
378
+ }
379
+
380
+ // Firefox
381
+ if (/firefox|fxios/i.test(ua)) {
382
+ return 'firefox';
383
+ }
384
+
385
+ // Safari last (most browsers include "Safari" in UA)
386
+ if (/safari/i.test(ua)) {
387
+ return 'safari';
388
+ }
389
+
390
+ // Fallback
391
+ return null;
392
+ }
393
+
394
+ // Get runtime environment
395
+ getRuntime = () => {
396
+ // Use config runtime if provided
397
+ if (this.manager?.config?.runtime) {
398
+ return this.manager.config.runtime;
399
+ }
400
+
401
+ // Browser extension (Chrome, Edge, Opera, Brave, Firefox, Safari, etc.)
402
+ if (
403
+ (typeof chrome !== 'undefined' && chrome.runtime?.id)
404
+ || (typeof browser !== 'undefined' && browser.runtime?.id)
405
+ || (typeof safari !== 'undefined' && safari.extension)
406
+ ) {
407
+ return 'browser-extension';
408
+ }
409
+
410
+ // Default: web browser
411
+ return 'web';
412
+ }
413
+
414
+ // Check if mobile device
415
+ isMobile = () => {
416
+ try {
417
+ // Try modern API first
418
+ const m = navigator.userAgentData?.mobile;
419
+ if (typeof m !== 'undefined') {
420
+ return m === true;
421
+ }
422
+ } catch (e) {
423
+ // Silent fail
424
+ }
425
+
426
+ // Fallback to media query
427
+ try {
428
+ return window.matchMedia('(max-width: 767px)').matches;
429
+ } catch (e) {
430
+ return false;
431
+ }
432
+ }
433
+
434
+ // Get device based on screen width
435
+ getDevice = () => {
436
+ const width = window.innerWidth;
437
+
438
+ // Mobile: < 768px (Bootstrap's md breakpoint)
439
+ if (width < 768) {
440
+ return 'mobile';
441
+ }
442
+
443
+ // Tablet: 768px - 1199px (between md and xl)
444
+ if (width < 1200) {
445
+ return 'tablet';
446
+ }
447
+
448
+ // Desktop: >= 1200px
449
+ return 'desktop';
450
+ }
451
+
452
+ // Get context information
453
+ getContext = () => {
454
+ // Return context information
455
+ return {
456
+ client: {
457
+ language: navigator.language,
458
+ mobile: this.isMobile(),
459
+ device: this.getDevice(),
460
+ platform: this.getPlatform(),
461
+ browser: this.getBrowser(),
462
+ vendor: navigator.vendor,
463
+ runtime: this.getRuntime(),
464
+ userAgent: navigator.userAgent,
465
+ url: window.location.href,
466
+ },
467
+ geolocation: {
468
+ ip: null,
469
+ country: null,
470
+ region: null,
471
+ city: null,
472
+ latitude: null,
473
+ longitude: null,
474
+ },
475
+ };
476
+ }
477
+ }
478
+
479
+ export default Utilities;