@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
import Storage from './modules/storage.js';
|
|
2
|
+
import Utilities from './modules/utilities.js';
|
|
3
|
+
import * as domUtils from './modules/dom.js';
|
|
4
|
+
import Analytics from './modules/analytics.js';
|
|
5
|
+
import Auth from './modules/auth.js';
|
|
6
|
+
import Bindings from './modules/bindings.js';
|
|
7
|
+
import Firestore from './modules/firestore.js';
|
|
8
|
+
import Notifications from './modules/notifications.js';
|
|
9
|
+
import ServiceWorker from './modules/service-worker.js';
|
|
10
|
+
import Sentry from './modules/sentry.js';
|
|
11
|
+
import Device from './modules/device.js';
|
|
12
|
+
import Verts from './modules/verts.js';
|
|
13
|
+
import { createRequest, mergeUsageIntoBindings } from './modules/request.js';
|
|
14
|
+
import { createLogger } from './modules/logger.js';
|
|
15
|
+
import { pathPrefix } from './modules/path-prefix.js';
|
|
16
|
+
|
|
17
|
+
const firebaseLogger = createLogger('firebase');
|
|
18
|
+
const analyticsLogger = createLogger('analytics');
|
|
19
|
+
const chatsyLogger = createLogger('chatsy');
|
|
20
|
+
const versionLogger = createLogger('version');
|
|
21
|
+
|
|
22
|
+
// Classic dev ports (N7) — the browser-side fallbacks when no resolved map is
|
|
23
|
+
// provided. Lockstep with @omega.js/config's CLASSIC_PORTS: browser code can't
|
|
24
|
+
// import that Node module (fs/net), so the numbers live here too.
|
|
25
|
+
const DEV_PORT_FALLBACKS = {
|
|
26
|
+
auth: 9099,
|
|
27
|
+
firestore: 8080,
|
|
28
|
+
functions: 5001,
|
|
29
|
+
hosting: 5002,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// The classic dev WEBSITE ORIGIN (#262) — the same lockstep-with-@omega.js/config
|
|
33
|
+
// deal as the ports above (its CLASSIC_DEV_ORIGIN). Protocol included, because a
|
|
34
|
+
// port alone cannot say it: `omega dev` fronts the public port with the mkcert
|
|
35
|
+
// proxy by default, so the assumption is https.
|
|
36
|
+
const DEV_ORIGIN_FALLBACK = 'https://localhost:4000';
|
|
37
|
+
|
|
38
|
+
class Manager {
|
|
39
|
+
constructor() {
|
|
40
|
+
// Configuration from init()
|
|
41
|
+
this.config = {};
|
|
42
|
+
|
|
43
|
+
// Runtime state
|
|
44
|
+
this.state = {
|
|
45
|
+
serviceWorker: null
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Auth settler: resolves when Firebase auth first determines user state
|
|
49
|
+
this._firebaseAuthInitialized = false;
|
|
50
|
+
this._authReadyResolve = null;
|
|
51
|
+
this._authReady = new Promise((resolve) => {
|
|
52
|
+
this._authReadyResolve = resolve;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// The session probe's moments are registered ONCE per instance (#798)
|
|
56
|
+
this._sessionProbeMomentsWired = false;
|
|
57
|
+
|
|
58
|
+
// Initialize modules
|
|
59
|
+
this._storage = new Storage();
|
|
60
|
+
this._utilities = new Utilities(this);
|
|
61
|
+
this._analytics = new Analytics(this);
|
|
62
|
+
this._auth = new Auth(this);
|
|
63
|
+
this._bindings = new Bindings(this);
|
|
64
|
+
this._firestore = new Firestore(this);
|
|
65
|
+
this._notifications = new Notifications(this);
|
|
66
|
+
this._serviceWorker = new ServiceWorker(this);
|
|
67
|
+
this._sentry = new Sentry(this);
|
|
68
|
+
this._device = new Device(this);
|
|
69
|
+
this._verts = new Verts(this);
|
|
70
|
+
|
|
71
|
+
// Harmonized API fetch (omega.request) — fresh Bearer token when signed in,
|
|
72
|
+
// omega-properties processed on every response (server usage → bindings)
|
|
73
|
+
this._request = createRequest({
|
|
74
|
+
getApiUrl: () => this.getApiUrl(),
|
|
75
|
+
getIdToken: (force) => this._firebaseAuth?.currentUser
|
|
76
|
+
? this._auth.getIdToken(force)
|
|
77
|
+
: null,
|
|
78
|
+
onProperties: (properties) => mergeUsageIntoBindings(this._bindings, properties),
|
|
79
|
+
// A 401 is the third moment of doubt (#798): the backend refused this
|
|
80
|
+
// token, so the client asks the Auth server whether the session is still
|
|
81
|
+
// there at all. Fire-and-forget: the caller's error is unchanged.
|
|
82
|
+
onUnauthorized: () => this._auth.probeSession(),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Make an API request: `omega.request('/omega/user/token', { method: 'POST', body: {} })`.
|
|
87
|
+
// Route-relative paths resolve through getApiUrl(); pass `auth: false` for public routes,
|
|
88
|
+
// `output: 'complete'` for { status, ok, headers, data, properties }, and
|
|
89
|
+
// `wakeup: true` for a fire-and-forget ping that warms a cold backend.
|
|
90
|
+
request(url, options) {
|
|
91
|
+
return this._request(url, options);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Module getters
|
|
95
|
+
storage() {
|
|
96
|
+
return this._storage;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
auth() {
|
|
100
|
+
return this._auth;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
bindings() {
|
|
104
|
+
return this._bindings;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
firestore() {
|
|
108
|
+
return this._firestore;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
notifications() {
|
|
112
|
+
return this._notifications;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
serviceWorker() {
|
|
116
|
+
return this._serviceWorker;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
sentry() {
|
|
120
|
+
return this._sentry;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
device() {
|
|
124
|
+
return this._device;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
analytics() {
|
|
128
|
+
return this._analytics;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
verts() {
|
|
132
|
+
return this._verts;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// DOM utilities
|
|
136
|
+
dom() {
|
|
137
|
+
return domUtils;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
utilities() {
|
|
141
|
+
return this._utilities;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Initialize the manager
|
|
145
|
+
async initialize(configuration) {
|
|
146
|
+
try {
|
|
147
|
+
// Store configuration as-is
|
|
148
|
+
this.config = this._processConfiguration(configuration);
|
|
149
|
+
|
|
150
|
+
// Set platform and runtime on HTML element
|
|
151
|
+
this._setHtmlDataAttributes();
|
|
152
|
+
|
|
153
|
+
// Initialize Firebase if a config blob is present (presence-driven — matches @omega.js/backend
|
|
154
|
+
// convention). Reads `cloud.config` (the omega.json5 canonical shape — desktop passes its
|
|
155
|
+
// resolved config through) and falls back to nested `firebase.app.config` (the web/extension
|
|
156
|
+
// bridge contract shape).
|
|
157
|
+
// Initialize Firebase only when the resolved config can actually boot the
|
|
158
|
+
// SDK — apiKey is mandatory (init without one crashes with auth/invalid-api-key).
|
|
159
|
+
// Configs carrying only projectId still resolve for getFunctionsUrl derivation.
|
|
160
|
+
if (this._resolveFirebaseConfig()?.apiKey) {
|
|
161
|
+
await this._initializeFirebase();
|
|
162
|
+
} else {
|
|
163
|
+
firebaseLogger.log('Skipped: config has no apiKey (Firebase-less site or empty framework merge blob)');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Initialize Sentry if enabled
|
|
167
|
+
if (this.config.sentry?.enabled) {
|
|
168
|
+
await this._sentry.init(this.config.sentry.config);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Initialize Analytics when the google provider is configured
|
|
172
|
+
// (canonical shape: analytics.providers.google.{id,secret} — C4 cp106a;
|
|
173
|
+
// projectId feeds the cross-surface uuidv5 identity namespace)
|
|
174
|
+
// Web is the exception: its transport is the page's own gtag, so there
|
|
175
|
+
// is no id or secret to require here (#159): the api_secret must never
|
|
176
|
+
// reach a page at all.
|
|
177
|
+
// Desktop's renderer is the other exception: it never sends at all
|
|
178
|
+
// ([#411](https://github.com/Omega-JS-Stack/omega/issues/411)). When the
|
|
179
|
+
// host injected an analytics bridge, every event forwards over IPC to the
|
|
180
|
+
// main process, whose sender owns the one device id, the one session id
|
|
181
|
+
// and the real engagement time — so a bridged renderer initializes with
|
|
182
|
+
// no id and no secret of its own.
|
|
183
|
+
const googleAnalytics = this.config.analytics?.providers?.google;
|
|
184
|
+
const isWebRuntime = this._utilities.getRuntime() === 'web';
|
|
185
|
+
const analyticsBridge = this._resolveAnalyticsBridge();
|
|
186
|
+
if (isWebRuntime || analyticsBridge || (googleAnalytics?.id && googleAnalytics?.secret)) {
|
|
187
|
+
this._analytics.init({
|
|
188
|
+
id: googleAnalytics?.id || null,
|
|
189
|
+
secret: googleAnalytics?.secret,
|
|
190
|
+
projectId: this._resolveFirebaseConfig()?.projectId || this.config.brand?.id || null,
|
|
191
|
+
bridge: analyticsBridge,
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
analyticsLogger.log('Skipped: missing analytics.providers.google id or secret');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Initialize service worker if enabled — dev included (push/caching are
|
|
198
|
+
// testable locally). Registering at the page's scope ('/', or the base
|
|
199
|
+
// path the site is mounted under — #360) REPLACES whatever worker last
|
|
200
|
+
// claimed it (a different project on the same localhost port), and the
|
|
201
|
+
// worker itself evicts foreign caches on boot. When a
|
|
202
|
+
// project explicitly disables the SW, sweep the origin clean instead so
|
|
203
|
+
// a previous project's worker can't keep serving its stale caches.
|
|
204
|
+
// Only http(s) origins are eligible: file:// (desktop) and
|
|
205
|
+
// chrome-extension:// (extension) pages ship the API but cannot host a
|
|
206
|
+
// page-scope worker, so they skip the branch entirely — no register, no sweep.
|
|
207
|
+
const originProtocol = window.location?.protocol;
|
|
208
|
+
|
|
209
|
+
if (originProtocol === 'http:' || originProtocol === 'https:') {
|
|
210
|
+
if (this.config.serviceWorker?.enabled) {
|
|
211
|
+
this._serviceWorker.register({
|
|
212
|
+
path: this.config.serviceWorker?.config?.path
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
this._serviceWorker.unregisterAll();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Start version checking if enabled
|
|
220
|
+
if (this.config.refreshNewVersion?.enabled) {
|
|
221
|
+
this._startVersionCheck();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Set up auth event listeners (uses event delegation, no need to wait for DOM)
|
|
225
|
+
this._auth.setupEventListeners();
|
|
226
|
+
|
|
227
|
+
// Set up push notifications
|
|
228
|
+
if (this.config.pushNotifications?.enabled) {
|
|
229
|
+
this._notifications.initialize(this.config.pushNotifications.config);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Initialize Chatsy chat widget if enabled
|
|
233
|
+
const chatsy = this.config.inbound?.chat?.providers?.chatsy;
|
|
234
|
+
if (chatsy?.enabled && chatsy?.agentId) {
|
|
235
|
+
this._initializeChatsy();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Old IE force polyfill
|
|
239
|
+
// await this._loadPolyfillsIfNeeded();
|
|
240
|
+
|
|
241
|
+
// Initialize local device-stats tracking (installed/session/version)
|
|
242
|
+
await this._device.initialize();
|
|
243
|
+
|
|
244
|
+
// Update bindings with config and device data. `device` is the LOCAL
|
|
245
|
+
// stats key — the `usage` key belongs to SERVER usage (seeded on auth
|
|
246
|
+
// settle, refreshed from omega-properties by omega.request()).
|
|
247
|
+
this.bindings().update({
|
|
248
|
+
config: this.config,
|
|
249
|
+
device: this._device.getBindingData(),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return this;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error('Manager initialization error:', error);
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
_processConfiguration(configuration) {
|
|
260
|
+
// Default configuration structure
|
|
261
|
+
const defaults = {
|
|
262
|
+
runtime: null, // Auto-detect if not provided (web, browser-extension, electron, node)
|
|
263
|
+
environment: 'production',
|
|
264
|
+
buildTime: Date.now(),
|
|
265
|
+
brand: {
|
|
266
|
+
id: 'brand',
|
|
267
|
+
name: 'Brand',
|
|
268
|
+
description: '',
|
|
269
|
+
type: 'Organization',
|
|
270
|
+
images: {
|
|
271
|
+
brandmark: '',
|
|
272
|
+
wordmark: '',
|
|
273
|
+
combomark: ''
|
|
274
|
+
},
|
|
275
|
+
contact: {
|
|
276
|
+
email: '',
|
|
277
|
+
phone: ''
|
|
278
|
+
},
|
|
279
|
+
address: {}
|
|
280
|
+
},
|
|
281
|
+
auth: {
|
|
282
|
+
enabled: true,
|
|
283
|
+
config: {
|
|
284
|
+
policy: null,
|
|
285
|
+
redirects: {
|
|
286
|
+
authenticated: '/dashboard/account',
|
|
287
|
+
unauthenticated: '/signup'
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
firebase: {
|
|
292
|
+
app: {
|
|
293
|
+
enabled: true,
|
|
294
|
+
config: {}
|
|
295
|
+
},
|
|
296
|
+
appCheck: {
|
|
297
|
+
enabled: false,
|
|
298
|
+
config: {
|
|
299
|
+
siteKey: ''
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
// Consent (#383) — the region-gated banner. The palette/theme keys the
|
|
304
|
+
// old `cookieConsent` blob carried are gone: the panel paints itself from
|
|
305
|
+
// the --omega-* token sheet, which is the only way it is correct in both
|
|
306
|
+
// color modes. `type` is gone too — the visitor's timezone decides opt-in
|
|
307
|
+
// vs opt-out, never a config key.
|
|
308
|
+
consent: {
|
|
309
|
+
enabled: true,
|
|
310
|
+
config: {
|
|
311
|
+
position: 'bottom-left',
|
|
312
|
+
content: {
|
|
313
|
+
message: 'We use cookies to improve your experience, measure traffic, and personalize marketing. See our { terms }.',
|
|
314
|
+
panelIntro: 'We and our partners use cookies and similar technologies to operate this site, measure how it is used, and personalize marketing. Necessary technologies are always active; the rest are yours to switch on or off, here or later, and a choice takes effect the moment you make it. See our { cookies } and { terms }.',
|
|
315
|
+
accept: 'Accept',
|
|
316
|
+
customize: 'Customize',
|
|
317
|
+
acceptAll: 'Accept all',
|
|
318
|
+
acceptNone: 'Accept none'
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
// ONE home (#23): the manager provisions the agent and writes agentId
|
|
323
|
+
// here, and the widget's presentation settings sit beside it — there is
|
|
324
|
+
// no second `chatsy` blob to keep in sync.
|
|
325
|
+
inbound: {
|
|
326
|
+
chat: {
|
|
327
|
+
providers: {
|
|
328
|
+
chatsy: {
|
|
329
|
+
enabled: false,
|
|
330
|
+
agentId: '',
|
|
331
|
+
settings: {
|
|
332
|
+
button: {
|
|
333
|
+
backgroundColor: '#237afc',
|
|
334
|
+
textColor: '#FFFFFF',
|
|
335
|
+
position: 'bottom-right',
|
|
336
|
+
type: 'round',
|
|
337
|
+
icon: 'default',
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
sentry: {
|
|
345
|
+
enabled: true,
|
|
346
|
+
config: {
|
|
347
|
+
dsn: '',
|
|
348
|
+
release: '',
|
|
349
|
+
replaysSessionSampleRate: 0.01,
|
|
350
|
+
replaysOnErrorSampleRate: 0.01
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
exitPopup: {
|
|
354
|
+
enabled: true,
|
|
355
|
+
config: {
|
|
356
|
+
timeout: 1000 * 60 * 60 * 4,
|
|
357
|
+
title: 'Want 15% off?',
|
|
358
|
+
message: 'Get 15% off your purchase of our Premium plans.',
|
|
359
|
+
okButton: {
|
|
360
|
+
text: 'Claim 15% Discount',
|
|
361
|
+
link: '/pricing'
|
|
362
|
+
},
|
|
363
|
+
// Social-proof faces above the offer (foot.html renders them);
|
|
364
|
+
// an empty list renders four neutral glyph slots
|
|
365
|
+
avatars: []
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
lazyLoading: {
|
|
369
|
+
enabled: true,
|
|
370
|
+
config: {
|
|
371
|
+
selector: '[data-lazy]',
|
|
372
|
+
rootMargin: '50px 0px', // Start loading 50px before element comes into view
|
|
373
|
+
threshold: 0.01, // Trigger when 1% of element is visible
|
|
374
|
+
loadedClass: 'lazy-loaded',
|
|
375
|
+
loadingClass: 'lazy-loading',
|
|
376
|
+
errorClass: 'lazy-error'
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
socialSharing: {
|
|
380
|
+
enabled: false,
|
|
381
|
+
config: {
|
|
382
|
+
selector: '[data-social-share]',
|
|
383
|
+
defaultPlatforms: ['facebook', 'twitter', 'linkedin', 'pinterest', 'reddit', 'email', 'copy'],
|
|
384
|
+
buttonClass: '',
|
|
385
|
+
showLabels: false,
|
|
386
|
+
openInNewWindow: true,
|
|
387
|
+
windowWidth: 600,
|
|
388
|
+
windowHeight: 400,
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
pushNotifications: {
|
|
392
|
+
enabled: true,
|
|
393
|
+
config: {
|
|
394
|
+
autoRequest: 1000 * 60
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
validRedirectHosts: [],
|
|
398
|
+
payment: {
|
|
399
|
+
providers: {},
|
|
400
|
+
products: [],
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
// Non-configurable defaults
|
|
404
|
+
refreshNewVersion: {
|
|
405
|
+
enabled: true,
|
|
406
|
+
config: {
|
|
407
|
+
interval: 1000 * 60 * 60, // Check every hour
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
serviceWorker: {
|
|
411
|
+
enabled: true,
|
|
412
|
+
config: {
|
|
413
|
+
path: '/service-worker.js'
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
analytics: {
|
|
417
|
+
providers: {
|
|
418
|
+
google: { id: '', secret: '' },
|
|
419
|
+
meta: { id: '' },
|
|
420
|
+
tiktok: { id: '' },
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Deep merge configuration with defaults
|
|
426
|
+
const merged = this._deepMerge(defaults, configuration);
|
|
427
|
+
|
|
428
|
+
// Evaluate string expressions for timeout values
|
|
429
|
+
if (merged.exitPopup?.config?.timeout) {
|
|
430
|
+
merged.exitPopup.config.timeout = safeEvaluate(merged.exitPopup.config.timeout);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (merged.pushNotifications?.config?.autoRequest) {
|
|
434
|
+
merged.pushNotifications.config.autoRequest = safeEvaluate(merged.pushNotifications.config.autoRequest);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (merged.refreshNewVersion?.config?.interval) {
|
|
438
|
+
merged.refreshNewVersion.config.interval = safeEvaluate(merged.refreshNewVersion.config.interval);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Calculate buildTimeISO from buildTime
|
|
442
|
+
if (merged.buildTime) {
|
|
443
|
+
merged.buildTimeISO = new Date(merged.buildTime).toISOString();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Return merged configuration
|
|
447
|
+
return merged;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
_deepMerge(target, source) {
|
|
451
|
+
const output = Object.assign({}, target);
|
|
452
|
+
if (isObject(target) && isObject(source)) {
|
|
453
|
+
Object.keys(source).forEach(key => {
|
|
454
|
+
if (isObject(source[key])) {
|
|
455
|
+
if (!(key in target))
|
|
456
|
+
Object.assign(output, { [key]: source[key] });
|
|
457
|
+
else
|
|
458
|
+
output[key] = this._deepMerge(target[key], source[key]);
|
|
459
|
+
} else {
|
|
460
|
+
Object.assign(output, { [key]: source[key] });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return output;
|
|
465
|
+
|
|
466
|
+
function isObject(item) {
|
|
467
|
+
return item && typeof item === 'object' && !Array.isArray(item);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
_setHtmlDataAttributes() {
|
|
472
|
+
// Skip if not in browser environment
|
|
473
|
+
if (typeof document === 'undefined') {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const $html = document.documentElement;
|
|
478
|
+
|
|
479
|
+
// Set platform (OS) - windows, mac, linux, ios, android, chromeos, unknown
|
|
480
|
+
$html.dataset.platform = this._utilities.getPlatform();
|
|
481
|
+
|
|
482
|
+
// Set browser - chrome, firefox, safari, edge, opera, brave
|
|
483
|
+
$html.dataset.browser = this._utilities.getBrowser();
|
|
484
|
+
|
|
485
|
+
// Set runtime - web, browser-extension, electron, node
|
|
486
|
+
$html.dataset.runtime = this._utilities.getRuntime();
|
|
487
|
+
|
|
488
|
+
// Set device - mobile, tablet, desktop
|
|
489
|
+
$html.dataset.device = this._utilities.getDevice();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Resolve the desktop renderer's analytics bridge — the ONE seam that turns
|
|
493
|
+
// this client into a forwarder instead of a sender
|
|
494
|
+
// ([#411](https://github.com/Omega-JS-Stack/omega/issues/411)).
|
|
495
|
+
//
|
|
496
|
+
// INJECTED by the host, never sniffed off a global: @omega.js/desktop's
|
|
497
|
+
// renderer passes its preload's analytics surface as `config.analyticsBridge`
|
|
498
|
+
// when it boots this client. A page that merely happens to carry a
|
|
499
|
+
// `window.desktop` can never bridge a brand's analytics into a void, and web
|
|
500
|
+
// and the extension inject nothing, so they keep every path they have today.
|
|
501
|
+
//
|
|
502
|
+
// An injected value with no `event()` is a broken host, not a runtime
|
|
503
|
+
// condition — it raises rather than quietly falling back to a sender the
|
|
504
|
+
// desktop app must not have.
|
|
505
|
+
_resolveAnalyticsBridge() {
|
|
506
|
+
const bridge = this.config?.analyticsBridge;
|
|
507
|
+
|
|
508
|
+
if (!bridge) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (typeof bridge.event !== 'function') {
|
|
513
|
+
throw new Error('config.analyticsBridge carries no event() — the host must inject its preload\'s analytics surface, or nothing at all');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return bridge;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Resolve the Firebase web SDK config blob. `cloud.config` first (canonical
|
|
520
|
+
// omega.json5 role shape — desktop passes its resolved config through), then nested
|
|
521
|
+
// `firebase.app.config` (the web/extension bridge contract shape).
|
|
522
|
+
// A blob only counts when at least one value is non-empty — framework config merges
|
|
523
|
+
// (e.g. UJM's Jekyll chain) inject all-empty-string blobs into Firebase-less sites,
|
|
524
|
+
// and those must resolve to null (no init, no URL derivation).
|
|
525
|
+
_resolveFirebaseConfig() {
|
|
526
|
+
const hasValues = (blob) => !!blob
|
|
527
|
+
&& typeof blob === 'object'
|
|
528
|
+
&& Object.values(blob).some((value) => value);
|
|
529
|
+
|
|
530
|
+
const cloud = this.config.cloud?.config;
|
|
531
|
+
if (hasValues(cloud)) {
|
|
532
|
+
return cloud;
|
|
533
|
+
}
|
|
534
|
+
const nested = this.config.firebase?.app?.config;
|
|
535
|
+
if (hasValues(nested)) {
|
|
536
|
+
return nested;
|
|
537
|
+
}
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async _initializeFirebase() {
|
|
542
|
+
const firebaseConfig = this._resolveFirebaseConfig();
|
|
543
|
+
|
|
544
|
+
// Dynamically import Firebase v12
|
|
545
|
+
const { initializeApp } = await import('firebase/app');
|
|
546
|
+
const { getAuth, onAuthStateChanged } = await import('firebase/auth');
|
|
547
|
+
const { initializeFirestore } = await import('firebase/firestore');
|
|
548
|
+
const { getMessaging } = await import('firebase/messaging');
|
|
549
|
+
|
|
550
|
+
// If we're in devmode, set the firebase config authDomain to the current host
|
|
551
|
+
// if (this.isDevelopment() && firebaseConfig) {
|
|
552
|
+
// firebaseConfig.authDomain = window.location.hostname;
|
|
553
|
+
// }
|
|
554
|
+
|
|
555
|
+
// Initialize Firebase. Re-init guards: if there's already a [DEFAULT] app
|
|
556
|
+
// (live reload, re-init in tests), get the existing one rather than throwing
|
|
557
|
+
// `app/duplicate-app`.
|
|
558
|
+
const { getApp, getApps } = await import('firebase/app');
|
|
559
|
+
const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig);
|
|
560
|
+
|
|
561
|
+
// Store Firebase references
|
|
562
|
+
this._firebaseApp = app;
|
|
563
|
+
this._firebaseAuth = getAuth(app);
|
|
564
|
+
this._firebaseFirestore = initializeFirestore(app, {});
|
|
565
|
+
|
|
566
|
+
// Connect to the local emulator suite in development — ZERO flags (N5): dev mode
|
|
567
|
+
// means LOCAL Firebase, period. environment=development (what `omega dev` injects)
|
|
568
|
+
// auto-connects so dev can mutate data, test rules instantly, and seed the
|
|
569
|
+
// frontend; production builds (environment=production) never connect. There is
|
|
570
|
+
// deliberately NO live-Firebase opt-out for dev — build production locally if you
|
|
571
|
+
// truly need live. Ports come from the resolved dev map when one was provided
|
|
572
|
+
// (N7: the `dev.ports` chrome, then `window.__OMEGA_DEV_PORTS__` for the keys it
|
|
573
|
+
// omits), classic defaults otherwise — and an assumed port says so out loud.
|
|
574
|
+
// Both connects live HERE, immediately after the instances are created: the auth
|
|
575
|
+
// module reads accounts via `manager.firebaseFirestore` directly, so connecting
|
|
576
|
+
// lazily (or in only one module) leaves early reads pointed at LIVE Firebase.
|
|
577
|
+
// Auth warnings banner disabled: it injects a DOM overlay that interferes with
|
|
578
|
+
// page content in automated flows.
|
|
579
|
+
if (this.isDevelopment()) {
|
|
580
|
+
const ports = this._devPorts();
|
|
581
|
+
const authEmulatorUrl = this._authEmulatorUrl();
|
|
582
|
+
this._warnClassicPortAssumption();
|
|
583
|
+
firebaseLogger.log(`Connecting to emulators (auth ${authEmulatorUrl}, firestore :${ports.firestore})`);
|
|
584
|
+
const { connectAuthEmulator } = await import('firebase/auth');
|
|
585
|
+
const { connectFirestoreEmulator } = await import('firebase/firestore');
|
|
586
|
+
connectAuthEmulator(this._firebaseAuth, authEmulatorUrl, { disableWarnings: true });
|
|
587
|
+
connectFirestoreEmulator(this._firebaseFirestore, 'localhost', ports.firestore);
|
|
588
|
+
firebaseLogger.log('Emulators connected');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Only initialize messaging if service workers AND push are supported —
|
|
592
|
+
// getMessaging() floats an unhandled unsupported-browser rejection otherwise
|
|
593
|
+
if ('serviceWorker' in navigator && typeof window !== 'undefined' && 'PushManager' in window) {
|
|
594
|
+
this._firebaseMessaging = getMessaging(app);
|
|
595
|
+
} else {
|
|
596
|
+
console.warn('Service workers or push not available - Firebase Messaging disabled');
|
|
597
|
+
this._firebaseMessaging = null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Initialize Firebase App Check if enabled
|
|
601
|
+
if (this.config.firebase.appCheck?.enabled) {
|
|
602
|
+
const { initializeAppCheck, ReCaptchaEnterpriseProvider } = await import('firebase/app-check');
|
|
603
|
+
const siteKey = this.config.firebase.appCheck.config.siteKey;
|
|
604
|
+
|
|
605
|
+
if (siteKey) {
|
|
606
|
+
initializeAppCheck(app, {
|
|
607
|
+
provider: new ReCaptchaEnterpriseProvider(siteKey),
|
|
608
|
+
isTokenAutoRefreshEnabled: true
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Setup auth state listener
|
|
614
|
+
onAuthStateChanged(this._firebaseAuth, (user) => {
|
|
615
|
+
// Mark auth as initialized and resolve the settler promise on first callback
|
|
616
|
+
if (!this._firebaseAuthInitialized) {
|
|
617
|
+
this._firebaseAuthInitialized = true;
|
|
618
|
+
this._authReadyResolve();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Let auth module handle everything including DOM updates
|
|
622
|
+
this._auth._handleAuthStateChange(user);
|
|
623
|
+
|
|
624
|
+
// Analytics follows auth: the identity on every runtime (user_id =
|
|
625
|
+
// uuidv5(uid, namespace)), plus the login/logout events on the runtimes
|
|
626
|
+
// that own them — web's auth pages fire their own (#328 gap 8)
|
|
627
|
+
this._analytics.handleAuthChange(user);
|
|
628
|
+
|
|
629
|
+
// Update Chatsy with current user
|
|
630
|
+
if (this._chatsy) {
|
|
631
|
+
const resolved = this._auth.getUser();
|
|
632
|
+
this._chatsy.setUser(resolved ? { id: resolved.uid, email: resolved.email, firstName: resolved.displayName, photoURL: resolved.photoURL } : null);
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// The moments of doubt ([#798](https://github.com/Omega-JS-Stack/omega/issues/798)).
|
|
637
|
+
// Firebase asks the Auth server about the persisted session on page load
|
|
638
|
+
// and at the hourly refresh and never again, so a revoked, disabled or
|
|
639
|
+
// deleted account (or a dev backend whose emulator restarted) keeps this
|
|
640
|
+
// tab signed in until a reload. The tab coming back into view and the
|
|
641
|
+
// network coming back are the two free moments to re-ask; the third is a
|
|
642
|
+
// 401 (wired as the request layer's onUnauthorized dep). No timer: a
|
|
643
|
+
// periodic ping would be a request per open tab for nothing.
|
|
644
|
+
if (!this._sessionProbeMomentsWired) {
|
|
645
|
+
this._sessionProbeMomentsWired = true;
|
|
646
|
+
|
|
647
|
+
// Swallowed at the call site: the probe classifies its own answer, so the
|
|
648
|
+
// only way it rejects is a sign-out that failed, and an event handler is
|
|
649
|
+
// nobody's promise to catch.
|
|
650
|
+
//
|
|
651
|
+
// The extension's background service worker has no document, and a
|
|
652
|
+
// headless host may carry neither
|
|
653
|
+
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
|
|
654
|
+
document.addEventListener('visibilitychange', () => {
|
|
655
|
+
if (document.visibilityState === 'visible') {
|
|
656
|
+
this._auth.probeSession().catch(() => {});
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
662
|
+
window.addEventListener('online', () => {
|
|
663
|
+
this._auth.probeSession().catch(() => {});
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Getters for Firebase services
|
|
670
|
+
get firebaseApp() { return this._firebaseApp; }
|
|
671
|
+
get firebaseAuth() { return this._firebaseAuth; }
|
|
672
|
+
get firebaseFirestore() { return this._firebaseFirestore; }
|
|
673
|
+
get firebaseMessaging() { return this._firebaseMessaging; }
|
|
674
|
+
|
|
675
|
+
isDevelopment() {
|
|
676
|
+
return this.config.environment === 'development';
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// The dev port map a page was actually GIVEN (N7), without fallbacks — two
|
|
680
|
+
// channels, and the BAKED CHROME WINS: `config.dev.ports` is written by
|
|
681
|
+
// `omega dev` at render time, so it is the live map of the stack this page
|
|
682
|
+
// was served by. `window.__OMEGA_DEV_PORTS__` is a driver-injected fallback
|
|
683
|
+
// for pages whose chrome carries nothing — a statically built site the
|
|
684
|
+
// devkit e2e harness serves, say — and it must not be able to OVERRIDE the
|
|
685
|
+
// real channel, or a green suite proves only the side channel
|
|
686
|
+
// ([#300](https://github.com/Omega-JS-Stack/omega/issues/300)). Presence of
|
|
687
|
+
// a key means a RESOLVED fact about a live stack; absence means "assume the
|
|
688
|
+
// classics".
|
|
689
|
+
_providedDevPorts() {
|
|
690
|
+
return {
|
|
691
|
+
...(typeof window !== 'undefined' && window.__OMEGA_DEV_PORTS__ || {}),
|
|
692
|
+
...(this.config.dev?.ports || {}),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
_devPorts() {
|
|
697
|
+
return { ...DEV_PORT_FALLBACKS, ...this._providedDevPorts() };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// One loud line, dev only, when a port is an ASSUMPTION rather than a
|
|
701
|
+
// resolved fact (#300). Nothing identity-checks what answers on a classic
|
|
702
|
+
// port, so a neighbouring project's emulator holding it reads as an auth
|
|
703
|
+
// mystery (`auth/user-not-found` for hours) instead of a port problem. This
|
|
704
|
+
// says which numbers are guesses, before the first connect.
|
|
705
|
+
_warnClassicPortAssumption() {
|
|
706
|
+
const provided = this._providedDevPorts();
|
|
707
|
+
const assumed = Object.keys(DEV_PORT_FALLBACKS).filter((name) => !provided[name]);
|
|
708
|
+
if (!assumed.length) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
firebaseLogger.warn(
|
|
713
|
+
`No resolved dev port for ${assumed.join(', ')}; assuming the classic ${assumed.map((name) => `${name} :${DEV_PORT_FALLBACKS[name]}`).join(', ')}. `
|
|
714
|
+
+ 'If another project\'s emulator holds those ports, this page is talking to IT, not your stack. '
|
|
715
|
+
+ 'Boot the backend with `omega dev` (or `omega emulator`) so the resolved map reaches the page.',
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Where the dev WEBSITE answers — the one shared answer for every surface
|
|
720
|
+
// that links to it in dev (#262). A resolved fact when the stack published
|
|
721
|
+
// one: `omega dev` puts its origin in the same `dev` map as the ports, and
|
|
722
|
+
// web reads it from the page chrome while desktop/extension read it from
|
|
723
|
+
// their build-time bake of that same map. Protocol is part of the fact —
|
|
724
|
+
// the dev server fronts its public port with the mkcert proxy by default,
|
|
725
|
+
// so a port alone would still be a guess about the scheme.
|
|
726
|
+
getDevWebsiteOrigin() {
|
|
727
|
+
const provided = this.config.dev?.origin;
|
|
728
|
+
|
|
729
|
+
if (provided) {
|
|
730
|
+
return provided;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
this._warnClassicOriginAssumption();
|
|
734
|
+
return DEV_ORIGIN_FALLBACK;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// The origin's half of the classic-assumption warning (#300's pattern, #262):
|
|
738
|
+
// one loud line when the answer is a guess rather than a published fact,
|
|
739
|
+
// because a wrong dev origin fails as a silent connection refusal.
|
|
740
|
+
_warnClassicOriginAssumption() {
|
|
741
|
+
firebaseLogger.warn(
|
|
742
|
+
`No resolved dev website origin; assuming the classic ${DEV_ORIGIN_FALLBACK}. `
|
|
743
|
+
+ 'If your `omega dev` bumped its port (or runs without mkcert), this is the wrong origin. '
|
|
744
|
+
+ 'Boot the website with `omega dev` so the resolved origin reaches this surface.',
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Where the auth emulator answers FROM THE BROWSER'S POINT OF VIEW (#156).
|
|
749
|
+
// A surface whose dev server proxies the emulator under its own origin says
|
|
750
|
+
// so with `dev.authEmulatorProxy` — and then the emulator URL must be that
|
|
751
|
+
// origin, so the OAuth handler and the SDK's helper iframe are first-party
|
|
752
|
+
// and the redirect credential survives storage partitioning. Every other
|
|
753
|
+
// surface (desktop, extension, a page with no proxy) keeps talking straight
|
|
754
|
+
// to the emulator's own port.
|
|
755
|
+
// Origin only, never a path: connectAuthEmulator() discards any path on the
|
|
756
|
+
// URL it is handed, so the proxy has to be mounted at the site root.
|
|
757
|
+
_authEmulatorUrl() {
|
|
758
|
+
if (this.config.dev?.authEmulatorProxy && typeof window !== 'undefined' && window.location?.origin) {
|
|
759
|
+
return window.location.origin;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return `http://localhost:${this._devPorts().auth}`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
getFunctionsUrl(environment) {
|
|
766
|
+
const env = environment || this.config.environment;
|
|
767
|
+
const projectId = this._resolveFirebaseConfig()?.projectId;
|
|
768
|
+
|
|
769
|
+
if (!projectId) {
|
|
770
|
+
throw new Error('Firebase project ID not configured');
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (env === 'development') {
|
|
774
|
+
return `http://localhost:${this._devPorts().functions}/${projectId}/us-central1`;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return `https://us-central1-${projectId}.cloudfunctions.net`;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
getApiUrl(environment, url) {
|
|
781
|
+
// Precedence: passed environment > query string > config.environment
|
|
782
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
783
|
+
const queryEnv = searchParams.get('_dev_apiEnvironment');
|
|
784
|
+
const env = environment
|
|
785
|
+
|| queryEnv
|
|
786
|
+
|| this.config.environment;
|
|
787
|
+
|
|
788
|
+
if (env === 'development') {
|
|
789
|
+
// Scheme follows what the provided dev map says is actually running (N7):
|
|
790
|
+
// - `https` key → `mgr serve`'s mkcert proxy (it owns publishing that key).
|
|
791
|
+
// - `hosting` key → an allocator-booted emulator stack; the hosting
|
|
792
|
+
// emulator speaks plain http on 127.0.0.1 (rewrites /omega/** to the API).
|
|
793
|
+
// - no map → classic assumption: `mgr serve`'s HTTPS proxy on 5002
|
|
794
|
+
// (since @omega.js/backend 5.7.0) — plain http:// cannot connect to it.
|
|
795
|
+
const provided = this._providedDevPorts();
|
|
796
|
+
if (provided.https) {
|
|
797
|
+
return `https://localhost:${provided.https}`;
|
|
798
|
+
}
|
|
799
|
+
if (provided.hosting) {
|
|
800
|
+
return `http://127.0.0.1:${provided.hosting}`;
|
|
801
|
+
}
|
|
802
|
+
return 'https://localhost:5002';
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// The API rides the BRAND domain (api.<brand host>). Never derive from
|
|
806
|
+
// authDomain: it is an auth concern (the brand host, with /__/auth/*
|
|
807
|
+
// self-hosted at build time) and its value must stay free to change
|
|
808
|
+
// without moving the API base.
|
|
809
|
+
const brandUrl = this.config.brand?.url; // schema enforces an http(s) URL
|
|
810
|
+
const baseUrl = url || brandUrl || window.location.origin;
|
|
811
|
+
|
|
812
|
+
// Prepend 'api.' subdomain, hostname-only (playground.omegajs.dev ->
|
|
813
|
+
// api.playground.omegajs.dev) — any path/port on brand.url is dropped,
|
|
814
|
+
// exactly like the desktop/extension url-helpers mirrors.
|
|
815
|
+
return `https://api.${new URL(baseUrl).hostname}`;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
isValidRedirectUrl(url) {
|
|
819
|
+
try {
|
|
820
|
+
const currentUrlObject = new URL(window.location.href);
|
|
821
|
+
const decoded = decodeURIComponent(url);
|
|
822
|
+
|
|
823
|
+
// Path-relative values ('/pricing') resolve against the page origin so they
|
|
824
|
+
// reach the checks below as an absolute URL instead of throwing and falling
|
|
825
|
+
// back to the policy default. Only a leading '/' counts: anything else must
|
|
826
|
+
// parse as an absolute URL on its own, so garbage ('not-a-url') still fails.
|
|
827
|
+
// A protocol-relative value ('//evil.com') resolves to its own host and is
|
|
828
|
+
// then rejected by the same-host check, exactly like the absolute form.
|
|
829
|
+
const returnUrlObject = decoded.startsWith('/')
|
|
830
|
+
? new URL(decoded, currentUrlObject.origin)
|
|
831
|
+
: new URL(decoded);
|
|
832
|
+
|
|
833
|
+
// Loopback returns (RFC 8252 §7.3) are valid while the SITE runs in development:
|
|
834
|
+
// native apps (Electron Manager) can't OS-register their custom scheme in dev, so
|
|
835
|
+
// their sign-in flow returns to an ephemeral 127.0.0.1 listener instead. Any port —
|
|
836
|
+
// the app binds it at flow start. Production sites never match this branch.
|
|
837
|
+
if (this.isDevelopment() && ['127.0.0.1', '[::1]', 'localhost'].includes(returnUrlObject.hostname)) {
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return returnUrlObject.host === currentUrlObject.host
|
|
842
|
+
|| returnUrlObject.protocol === `${this.config.brand?.id}:`
|
|
843
|
+
|| (this.config.validRedirectHosts || []).includes(returnUrlObject.host);
|
|
844
|
+
} catch (e) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// The web build's public config filter inlines `settings: null` when a
|
|
850
|
+
// brand sets none, and Chatsy's constructor rejects a null blob — omit the
|
|
851
|
+
// key instead so the widget applies its own defaults (#377).
|
|
852
|
+
_chatsyOptions(config) {
|
|
853
|
+
return config.settings ? { settings: config.settings } : {};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async _initializeChatsy() {
|
|
857
|
+
try {
|
|
858
|
+
const { default: Chatsy } = await import('chatsy');
|
|
859
|
+
const config = this.config.inbound.chat.providers.chatsy;
|
|
860
|
+
|
|
861
|
+
this._chatsy = new Chatsy(config.agentId, this._chatsyOptions(config));
|
|
862
|
+
|
|
863
|
+
chatsyLogger.log('Initialized');
|
|
864
|
+
} catch (error) {
|
|
865
|
+
chatsyLogger.error('Failed to initialize:', error);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
_startVersionCheck() {
|
|
870
|
+
// Quit if window is not available
|
|
871
|
+
if (typeof window !== 'undefined') {
|
|
872
|
+
// Re-focus events
|
|
873
|
+
window.addEventListener('focus', () => {
|
|
874
|
+
this._checkVersion();
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
window.addEventListener('online', () => {
|
|
878
|
+
this._checkVersion();
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Set up interval — re-initializing replaces the timer, never stacks a
|
|
883
|
+
// second one on top of the first
|
|
884
|
+
clearInterval(this._versionCheckInterval);
|
|
885
|
+
this._versionCheckInterval = setInterval(() => {
|
|
886
|
+
this._checkVersion();
|
|
887
|
+
}, this.config.refreshNewVersion.config.interval);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
// async _loadPolyfillsIfNeeded() {
|
|
892
|
+
// // Check if polyfills are needed by testing for ES6 features
|
|
893
|
+
// const featuresPass = (
|
|
894
|
+
// typeof Symbol !== 'undefined'
|
|
895
|
+
// );
|
|
896
|
+
|
|
897
|
+
// // If all features are supported, no polyfills needed
|
|
898
|
+
// if (featuresPass) {
|
|
899
|
+
// return;
|
|
900
|
+
// }
|
|
901
|
+
|
|
902
|
+
// // Load polyfills for older browsers (especially IE)
|
|
903
|
+
// try {
|
|
904
|
+
// await domUtils.loadScript({
|
|
905
|
+
// src: 'https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?flags=always%2Cgated&features=default%2Ces5%2Ces6%2Ces7%2CPromise.prototype.finally%2C%7Ehtml5-elements%2ClocalStorage%2Cfetch%2CURLSearchParams',
|
|
906
|
+
// crossorigin: 'anonymous'
|
|
907
|
+
// });
|
|
908
|
+
// console.log('Polyfills loaded for older browser');
|
|
909
|
+
// } catch (error) {
|
|
910
|
+
// console.error('Failed to load polyfills:', error);
|
|
911
|
+
// // Continue initialization even if polyfills fail to load
|
|
912
|
+
// }
|
|
913
|
+
// }
|
|
914
|
+
|
|
915
|
+
async _checkVersion() {
|
|
916
|
+
if (this.isDevelopment()) {
|
|
917
|
+
/* @dev-only:start */
|
|
918
|
+
{
|
|
919
|
+
versionLogger.log('Skipping version check in development mode');
|
|
920
|
+
}
|
|
921
|
+
/* @dev-only:end */
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
try {
|
|
926
|
+
// The manifest is served from the site's own mount (#364): under a URL
|
|
927
|
+
// path (#355) a root-relative fetch lands off-site and 404s forever. No
|
|
928
|
+
// stamp means the domain root and an unchanged URL.
|
|
929
|
+
const response = await fetch(`${pathPrefix()}/build.json?cb=${Date.now()}`);
|
|
930
|
+
if (!response.ok) {
|
|
931
|
+
throw new Error(`Failed to fetch build.json (${response.status})`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const data = await response.json();
|
|
935
|
+
if (!data.timestamp) {
|
|
936
|
+
throw new Error('No timestamp found in build.json');
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const buildTimeLive = new Date(data.timestamp);
|
|
940
|
+
const buildTimeCurrent = new Date(this.config.buildTime);
|
|
941
|
+
|
|
942
|
+
// Add 1 hour to current build time to account for npm build process
|
|
943
|
+
buildTimeCurrent.setHours(buildTimeCurrent.getHours() + 1);
|
|
944
|
+
|
|
945
|
+
// Log version info
|
|
946
|
+
versionLogger.log(`Current build time: ${buildTimeCurrent.toISOString()}, Live build time: ${buildTimeLive.toISOString()}`);
|
|
947
|
+
|
|
948
|
+
// If live version is newer, reload the page
|
|
949
|
+
if (buildTimeCurrent >= buildTimeLive) {
|
|
950
|
+
return; // No update needed
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// New version detected
|
|
954
|
+
versionLogger.log('New version detected, reloading page...');
|
|
955
|
+
|
|
956
|
+
// If running in a non-browser environment, warn and return
|
|
957
|
+
if (typeof window === 'undefined') {
|
|
958
|
+
versionLogger.warn('Cannot reload in non-browser environment');
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Force page reload
|
|
963
|
+
window.onbeforeunload = function () {
|
|
964
|
+
return undefined;
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
window.location.reload(true);
|
|
968
|
+
} catch (error) {
|
|
969
|
+
versionLogger.warn('Failed version check:', error);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Safely evaluate timeout string expressions
|
|
975
|
+
const safeEvaluate = (str) => {
|
|
976
|
+
if (typeof str !== 'string') return str;
|
|
977
|
+
|
|
978
|
+
// Only allow numbers, *, +, -, /, parentheses, and whitespace
|
|
979
|
+
if (!/^[\d\s\*\+\-\/\(\)]+$/.test(str)) {
|
|
980
|
+
console.warn('Invalid expression format:', str);
|
|
981
|
+
return str;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
try {
|
|
985
|
+
// Use Function constructor instead of eval for safer evaluation
|
|
986
|
+
return new Function(`return ${str}`)();
|
|
987
|
+
} catch (e) {
|
|
988
|
+
console.warn('Failed to evaluate expression:', str, e);
|
|
989
|
+
return str;
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
// Create singleton instance
|
|
994
|
+
const manager = new Manager();
|
|
995
|
+
|
|
996
|
+
// Export for different environments
|
|
997
|
+
export default manager;
|
|
998
|
+
export { Manager };
|
|
999
|
+
|