@oxvo/browser 7.3.0 → 7.4.15

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 (41) hide show
  1. package/README.md +32 -0
  2. package/dist/cjs/entry.js +1961 -377
  3. package/dist/cjs/entry.js.map +1 -1
  4. package/dist/cjs/index.js +1955 -377
  5. package/dist/cjs/index.js.map +1 -1
  6. package/dist/cjs/main/app/canvas.d.ts +5 -0
  7. package/dist/cjs/main/app/cross_domain_iframe.d.ts +44 -0
  8. package/dist/cjs/main/app/debugRedaction.d.ts +11 -0
  9. package/dist/cjs/main/app/index.d.ts +70 -14
  10. package/dist/cjs/main/app/messages.gen.d.ts +2 -0
  11. package/dist/cjs/main/app/session.d.ts +1 -0
  12. package/dist/cjs/main/entry.d.ts +5 -5
  13. package/dist/cjs/main/index.d.ts +6 -1
  14. package/dist/cjs/main/modules/analytics/index.d.ts +5 -0
  15. package/dist/cjs/main/singleton.d.ts +5 -0
  16. package/dist/cjs/package.json +1 -0
  17. package/dist/lib/entry.js +1961 -377
  18. package/dist/lib/entry.js.map +1 -1
  19. package/dist/lib/index.js +1955 -377
  20. package/dist/lib/index.js.map +1 -1
  21. package/dist/lib/main/app/canvas.d.ts +5 -0
  22. package/dist/lib/main/app/cross_domain_iframe.d.ts +44 -0
  23. package/dist/lib/main/app/debugRedaction.d.ts +11 -0
  24. package/dist/lib/main/app/index.d.ts +70 -14
  25. package/dist/lib/main/app/messages.gen.d.ts +2 -0
  26. package/dist/lib/main/app/session.d.ts +1 -0
  27. package/dist/lib/main/entry.d.ts +5 -5
  28. package/dist/lib/main/index.d.ts +6 -1
  29. package/dist/lib/main/modules/analytics/index.d.ts +5 -0
  30. package/dist/lib/main/singleton.d.ts +5 -0
  31. package/dist/types/main/app/canvas.d.ts +5 -0
  32. package/dist/types/main/app/cross_domain_iframe.d.ts +44 -0
  33. package/dist/types/main/app/debugRedaction.d.ts +11 -0
  34. package/dist/types/main/app/index.d.ts +70 -14
  35. package/dist/types/main/app/messages.gen.d.ts +2 -0
  36. package/dist/types/main/app/session.d.ts +1 -0
  37. package/dist/types/main/entry.d.ts +5 -5
  38. package/dist/types/main/index.d.ts +6 -1
  39. package/dist/types/main/modules/analytics/index.d.ts +5 -0
  40. package/dist/types/main/singleton.d.ts +5 -0
  41. package/package.json +5 -4
package/dist/lib/index.js CHANGED
@@ -530,7 +530,7 @@ function dec2hex(dec) {
530
530
  return dec.toString(16).padStart(2, '0');
531
531
  }
532
532
  function generateRandomId(len) {
533
- const arr = new Uint8Array((len) / 2);
533
+ const arr = new Uint8Array((len || 40) / 2);
534
534
  // msCrypto = IE11
535
535
  // @ts-ignore
536
536
  const safeCrypto = window.crypto || window.msCrypto;
@@ -539,7 +539,7 @@ function generateRandomId(len) {
539
539
  return Array.from(arr, dec2hex).join('');
540
540
  }
541
541
  else {
542
- return Array.from({ length: len }, () => dec2hex(Math.floor(Math.random() * 16))).join('');
542
+ return Array.from({ length: len || 40 }, () => dec2hex(Math.floor(Math.random() * 16))).join('');
543
543
  }
544
544
  }
545
545
  function inIframe() {
@@ -736,6 +736,88 @@ function throttleWithTrailing(fn, interval) {
736
736
 
737
737
  // Auto-generated, do not edit
738
738
  /* eslint-disable */
739
+ const MESSAGE_FIELD_TYPES = {
740
+ 0: ['number'],
741
+ 4: ['string', 'string', 'number'],
742
+ 5: ['number', 'number'],
743
+ 6: ['number', 'number'],
744
+ 7: [],
745
+ 8: ['number', 'number', 'number', 'string', 'boolean'],
746
+ 9: ['number', 'number', 'number'],
747
+ 10: ['number', 'number', 'number'],
748
+ 11: ['number'],
749
+ 12: ['number', 'string', 'string'],
750
+ 13: ['number', 'string'],
751
+ 14: ['number', 'string'],
752
+ 16: ['number', 'number', 'number'],
753
+ 17: ['number', 'string'],
754
+ 18: ['number', 'string', 'number'],
755
+ 19: ['number', 'boolean'],
756
+ 20: ['number', 'number'],
757
+ 21: ['string', 'string', 'string', 'string', 'string', 'number', 'number', 'number'],
758
+ 22: ['string', 'string'],
759
+ 23: ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'],
760
+ 24: ['number', 'number', 'number'],
761
+ 27: ['string', 'string'],
762
+ 28: ['string'],
763
+ 29: ['string'],
764
+ 30: ['string', 'string'],
765
+ 34: ['number', 'string'],
766
+ 35: ['number', 'number', 'number'],
767
+ 36: ['number', 'string'],
768
+ 40: ['string', 'number', 'string', 'string'],
769
+ 41: ['string', 'string'],
770
+ 42: ['string'],
771
+ 44: ['string', 'string', 'number'],
772
+ 45: ['string', 'string'],
773
+ 46: ['string', 'string'],
774
+ 47: ['string', 'string', 'number'],
775
+ 48: ['string', 'string', 'string', 'string', 'number'],
776
+ 49: ['number', 'number', 'number', 'number'],
777
+ 50: ['number', 'string'],
778
+ 51: ['number', 'number', 'number'],
779
+ 43: ['string', 'string'],
780
+ 52: ['number', 'string', 'string'],
781
+ 53: ['number', 'number', 'number', 'number', 'number', 'number', 'string', 'string'],
782
+ 54: ['number', 'string'],
783
+ 55: ['boolean'],
784
+ 57: ['number', 'string', 'string', 'string'],
785
+ 58: ['number'],
786
+ 60: ['number', 'string', 'string', 'string'],
787
+ 61: ['number', 'string', 'string'],
788
+ 63: ['string', 'string'],
789
+ 64: ['string', 'string'],
790
+ 65: ['number', 'number'],
791
+ 68: ['number', 'number', 'string', 'string', 'number', 'number'],
792
+ 69: ['number', 'number', 'string', 'string'],
793
+ 70: ['number', 'number'],
794
+ 71: ['number', 'string', 'string'],
795
+ 73: ['number', 'string', 'number', 'string'],
796
+ 75: ['number', 'number'],
797
+ 76: ['number', 'number'],
798
+ 77: ['number', 'number'],
799
+ 78: ['string', 'string', 'string', 'string'],
800
+ 79: ['string', 'string'],
801
+ 81: ['number', 'number', 'number', 'number', 'string'],
802
+ 83: ['string', 'string', 'string', 'string', 'string', 'number', 'number', 'number', 'number'],
803
+ 84: ['string', 'string', 'string', 'number', 'string', 'string'],
804
+ 85: ['number', 'number', 'number', 'number', 'number', 'number', 'string', 'string', 'number', 'boolean', 'number', 'number', 'number', 'number', 'number', 'number', 'number'],
805
+ 87: ['string', 'number', 'number'],
806
+ 89: ['string', 'number', 'number', 'number', 'number', 'string'],
807
+ 112: ['number', 'string', 'boolean', 'string', 'number', 'number'],
808
+ 113: ['number', 'number', 'string'],
809
+ 114: ['number'],
810
+ 115: ['number'],
811
+ 116: ['number', 'number', 'number', 'number', 'number', 'number', 'string', 'string', 'number', 'boolean'],
812
+ 117: ['string'],
813
+ 118: ['string'],
814
+ 119: ['string', 'number'],
815
+ 120: ['number'],
816
+ 121: ['string', 'string', 'number', 'number'],
817
+ 122: ['string', 'string', 'number', 'string'],
818
+ 123: ['string', 'string', 'string', 'string', 'number'],
819
+ 124: ['string', 'string'],
820
+ };
739
821
  function Timestamp(timestamp) {
740
822
  return [
741
823
  0 /* Messages.Type.Timestamp */,
@@ -1418,6 +1500,7 @@ var _Messages = /*#__PURE__*/Object.freeze({
1418
1500
  JSException: JSException,
1419
1501
  LoadFontFace: LoadFontFace,
1420
1502
  LongAnimationTask: LongAnimationTask$1,
1503
+ MESSAGE_FIELD_TYPES: MESSAGE_FIELD_TYPES,
1421
1504
  Metadata: Metadata,
1422
1505
  MobX: MobX,
1423
1506
  MouseClick: MouseClick,
@@ -1798,6 +1881,8 @@ class CanvasRecorder {
1798
1881
  this.uploadQueue = 0;
1799
1882
  this.MAX_CONCURRENT_UPLOADS = 2;
1800
1883
  this.MAX_QUEUE_SIZE = 50; // ~500 images max (50 batches × 10 images)
1884
+ this.MAX_UPLOAD_ATTEMPTS = 5;
1885
+ this.MAX_RETRY_DELAY_MS = 10000;
1801
1886
  this.pendingBatches = [];
1802
1887
  this.isProcessingQueue = false;
1803
1888
  /**
@@ -1938,26 +2023,41 @@ class CanvasRecorder {
1938
2023
  this.app.debug.warn('Upload queue full, dropping canvas batch');
1939
2024
  return;
1940
2025
  }
1941
- this.pendingBatches.push({ images, canvasId, createdAt });
2026
+ const token = this.app.session.getSessionSecret();
2027
+ if (!token) {
2028
+ this.app.debug.warn('Canvas batch has no session token');
2029
+ return;
2030
+ }
2031
+ this.pendingBatches.push({ images, canvasId, createdAt, token });
1942
2032
  if (!this.isProcessingQueue) {
1943
- this.processUploadQueue();
2033
+ void this.processUploadQueue();
1944
2034
  }
1945
2035
  }
1946
2036
  async processUploadQueue() {
2037
+ if (this.isProcessingQueue)
2038
+ return;
1947
2039
  this.isProcessingQueue = true;
1948
- while (this.pendingBatches.length > 0) {
1949
- if (this.uploadQueue >= this.MAX_CONCURRENT_UPLOADS) {
1950
- await new Promise((resolve) => setTimeout(resolve, 100));
1951
- continue;
2040
+ const worker = async () => {
2041
+ while (true) {
2042
+ const batch = this.pendingBatches.shift();
2043
+ if (!batch)
2044
+ return;
2045
+ await this.uploadBatch(batch.images, batch.canvasId, batch.createdAt, batch.token);
2046
+ }
2047
+ };
2048
+ try {
2049
+ await Promise.all(Array.from({ length: this.MAX_CONCURRENT_UPLOADS }, () => worker()));
2050
+ }
2051
+ finally {
2052
+ this.isProcessingQueue = false;
2053
+ // A batch can be appended between the final worker check and the flag
2054
+ // reset. Restart once so that race cannot strand queued frames.
2055
+ if (this.pendingBatches.length > 0) {
2056
+ void this.processUploadQueue();
1952
2057
  }
1953
- const batch = this.pendingBatches.shift();
1954
- if (!batch)
1955
- break;
1956
- this.uploadBatch(batch.images, batch.canvasId, batch.createdAt);
1957
2058
  }
1958
- this.isProcessingQueue = false;
1959
2059
  }
1960
- async uploadBatch(images, canvasId, createdAt) {
2060
+ async uploadBatch(images, canvasId, createdAt, token) {
1961
2061
  if (this.options.isDebug) {
1962
2062
  const packed = await packFrames(images);
1963
2063
  if (packed) {
@@ -2025,6 +2125,9 @@ class CanvasRecorder {
2025
2125
  });
2026
2126
  }
2027
2127
  const initRestart = () => {
2128
+ if (this.app.session.getSessionSecret() !== token) {
2129
+ return;
2130
+ }
2028
2131
  this.app.debug.log('Restarting tracker; token expired');
2029
2132
  this.app.stop(false);
2030
2133
  setTimeout(() => {
@@ -2034,25 +2137,72 @@ class CanvasRecorder {
2034
2137
  this.uploadQueue++;
2035
2138
  const base = this.app.options.endpoint.replace(/\/$/, '');
2036
2139
  const ingestPoint = base.endsWith('/oxvo') ? base : `${base}/oxvo`;
2037
- fetch(ingestPoint + '/v1/assets', {
2038
- method: 'POST',
2039
- headers: {
2040
- Authorization: `Bearer ${this.app.session.getSessionSecret() ?? ''}`,
2041
- },
2042
- body: formData,
2043
- })
2044
- .then((r) => {
2045
- if (r.status === 401) {
2046
- return initRestart();
2140
+ try {
2141
+ for (let attempt = 1; attempt <= this.MAX_UPLOAD_ATTEMPTS; attempt++) {
2142
+ try {
2143
+ const response = await fetch(ingestPoint + '/v1/assets', {
2144
+ method: 'POST',
2145
+ headers: {
2146
+ Authorization: `Bearer ${token}`,
2147
+ },
2148
+ body: formData,
2149
+ });
2150
+ if (response.ok)
2151
+ return;
2152
+ if (response.status === 401) {
2153
+ initRestart();
2154
+ return;
2155
+ }
2156
+ if (response.status === 413) {
2157
+ this.app.debug.warn('Canvas batch exceeds the upload size limit');
2158
+ return;
2159
+ }
2160
+ if (!this.isRetryableUploadStatus(response.status)) {
2161
+ this.app.debug.error(`Canvas upload failed with status ${response.status}`);
2162
+ return;
2163
+ }
2164
+ if (attempt === this.MAX_UPLOAD_ATTEMPTS) {
2165
+ this.app.debug.error(`Canvas upload failed after ${attempt} attempts (status ${response.status})`);
2166
+ return;
2167
+ }
2168
+ await this.waitBeforeUploadRetry(response, attempt);
2169
+ }
2170
+ catch (error) {
2171
+ if (attempt === this.MAX_UPLOAD_ATTEMPTS) {
2172
+ this.app.debug.error('Canvas upload failed after retrying', error);
2173
+ return;
2174
+ }
2175
+ await this.waitBeforeUploadRetry(undefined, attempt);
2176
+ }
2047
2177
  }
2048
- return true;
2049
- })
2050
- .catch((e) => {
2051
- this.app.debug.error('error saving canvas', e);
2052
- })
2053
- .finally(() => {
2178
+ }
2179
+ finally {
2054
2180
  this.uploadQueue--;
2055
- });
2181
+ }
2182
+ }
2183
+ isRetryableUploadStatus(status) {
2184
+ return status === 429 || status === 408 || status >= 500;
2185
+ }
2186
+ async waitBeforeUploadRetry(response, attempt) {
2187
+ const retryAfter = response?.headers.get('Retry-After');
2188
+ let retryDelayMs = 0;
2189
+ if (retryAfter) {
2190
+ const seconds = Number(retryAfter);
2191
+ if (Number.isFinite(seconds) && seconds >= 0) {
2192
+ retryDelayMs = seconds * 1000;
2193
+ }
2194
+ else {
2195
+ const retryAt = Date.parse(retryAfter);
2196
+ if (Number.isFinite(retryAt)) {
2197
+ retryDelayMs = Math.max(0, retryAt - Date.now());
2198
+ }
2199
+ }
2200
+ }
2201
+ if (retryDelayMs === 0) {
2202
+ retryDelayMs = 250 * 2 ** (attempt - 1);
2203
+ }
2204
+ retryDelayMs = Math.min(retryDelayMs, this.MAX_RETRY_DELAY_MS);
2205
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
2056
2206
  }
2057
2207
  cleanupCanvas(id) {
2058
2208
  if (this.snapshots[id]) {
@@ -2075,8 +2225,7 @@ class CanvasRecorder {
2075
2225
  }
2076
2226
  delete this.snapshots[id];
2077
2227
  }
2078
- clear() {
2079
- // Flush remaining images before cleanup
2228
+ flushPendingSnapshots() {
2080
2229
  Object.keys(this.snapshots).forEach((idStr) => {
2081
2230
  const id = parseInt(idStr, 10);
2082
2231
  const snapshot = this.snapshots[id];
@@ -2085,6 +2234,9 @@ class CanvasRecorder {
2085
2234
  snapshot.images = [];
2086
2235
  }
2087
2236
  });
2237
+ }
2238
+ clear() {
2239
+ this.flushPendingSnapshots();
2088
2240
  Object.keys(this.snapshots).forEach((idStr) => {
2089
2241
  const id = parseInt(idStr, 10);
2090
2242
  this.cleanupCanvas(id);
@@ -2151,6 +2303,505 @@ async function packFrames(images) {
2151
2303
  return packed;
2152
2304
  }
2153
2305
 
2306
+ const CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION = 1;
2307
+ const SECRET_PATTERN = /^[A-Za-z0-9_-]{32,128}$/;
2308
+ const CONTEXT_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
2309
+ const MAX_MESSAGE_FIELDS = 24;
2310
+ const MAX_MESSAGE_STRING_LENGTH = 100000;
2311
+ const MAX_BATCH_MESSAGES = 25000;
2312
+ const MAX_BATCH_BYTES = 2 * 1024 * 1024;
2313
+ // Cross-frame batches may contain only recording events. Identity, dictionary,
2314
+ // batch-header, and tab/session control opcodes remain owned by the top-level
2315
+ // tracker.
2316
+ const CROSS_DOMAIN_IFRAME_ALLOWED_MESSAGE_TYPES = new Set([
2317
+ 4 /* MType.SetPageLocationDeprecated */,
2318
+ 5 /* MType.SetViewportSize */,
2319
+ 6 /* MType.SetViewportScroll */,
2320
+ 7 /* MType.CreateDocument */,
2321
+ 8 /* MType.CreateElementNode */,
2322
+ 9 /* MType.CreateTextNode */,
2323
+ 10 /* MType.MoveNode */,
2324
+ 11 /* MType.RemoveNode */,
2325
+ 12 /* MType.SetNodeAttribute */,
2326
+ 13 /* MType.RemoveNodeAttribute */,
2327
+ 14 /* MType.SetNodeData */,
2328
+ 16 /* MType.SetNodeScroll */,
2329
+ 17 /* MType.SetInputTarget */,
2330
+ 18 /* MType.SetInputValue */,
2331
+ 19 /* MType.SetInputChecked */,
2332
+ 20 /* MType.MouseMove */,
2333
+ 21 /* MType.NetworkRequestDeprecated */,
2334
+ 22 /* MType.ConsoleLog */,
2335
+ 23 /* MType.PageLoadTiming */,
2336
+ 24 /* MType.PageRenderTiming */,
2337
+ 27 /* MType.CustomEvent */,
2338
+ 36 /* MType.NodeAnimationResult */,
2339
+ 40 /* MType.Profiler */,
2340
+ 41 /* MType.OTable */,
2341
+ 42 /* MType.StateAction */,
2342
+ 44 /* MType.ReduxDeprecated */,
2343
+ 45 /* MType.Vuex */,
2344
+ 46 /* MType.MobX */,
2345
+ 47 /* MType.NgRx */,
2346
+ 48 /* MType.GraphQLDeprecated */,
2347
+ 49 /* MType.PerformanceTrack */,
2348
+ 53 /* MType.ResourceTimingDeprecatedDeprecated */,
2349
+ 54 /* MType.ConnectionInformation */,
2350
+ 55 /* MType.SetPageVisibility */,
2351
+ 57 /* MType.LoadFontFace */,
2352
+ 58 /* MType.SetNodeFocus */,
2353
+ 60 /* MType.SetNodeAttributeURLBased */,
2354
+ 61 /* MType.SetCSSDataURLBased */,
2355
+ 63 /* MType.TechnicalInfo */,
2356
+ 64 /* MType.CustomIssue */,
2357
+ 65 /* MType.SetNodeSlot */,
2358
+ 68 /* MType.MouseClick */,
2359
+ 69 /* MType.MouseClickDeprecated */,
2360
+ 70 /* MType.CreateIFrameDocument */,
2361
+ 71 /* MType.AdoptedSSReplaceURLBased */,
2362
+ 73 /* MType.AdoptedSSInsertRuleURLBased */,
2363
+ 75 /* MType.AdoptedSSDeleteRule */,
2364
+ 76 /* MType.AdoptedSSAddOwner */,
2365
+ 77 /* MType.AdoptedSSRemoveOwner */,
2366
+ 78 /* MType.JSException */,
2367
+ 79 /* MType.Zustand */,
2368
+ 83 /* MType.NetworkRequest */,
2369
+ 84 /* MType.WSChannel */,
2370
+ 85 /* MType.ResourceTiming */,
2371
+ 87 /* MType.Incident */,
2372
+ 89 /* MType.LongAnimationTask */,
2373
+ 112 /* MType.InputChange */,
2374
+ 113 /* MType.SelectionChange */,
2375
+ 114 /* MType.MouseThrashing */,
2376
+ 115 /* MType.UnbindNodes */,
2377
+ 116 /* MType.ResourceTimingDeprecated */,
2378
+ 119 /* MType.CanvasNode */,
2379
+ 120 /* MType.TagTrigger */,
2380
+ 121 /* MType.Redux */,
2381
+ 122 /* MType.SetPageLocation */,
2382
+ 123 /* MType.GraphQL */,
2383
+ 124 /* MType.WebVitals */,
2384
+ ]);
2385
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
2386
+ const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
2387
+ const isSafeIntegerAtLeast = (value, minimum) => typeof value === 'number' && Number.isSafeInteger(value) && value >= minimum;
2388
+ const isBoundedString = (value, maxLength) => typeof value === 'string' && value.length > 0 && value.length <= maxLength;
2389
+ const isCanvasQuality = (value) => value === 'low' || value === 'medium' || value === 'high';
2390
+ function normalizeCrossDomainOrigin(value) {
2391
+ if (typeof value !== 'string' || value.trim() === '' || value.trim() === '*') {
2392
+ return null;
2393
+ }
2394
+ try {
2395
+ const url = new URL(value.trim());
2396
+ if (!['http:', 'https:'].includes(url.protocol) ||
2397
+ url.origin === 'null' ||
2398
+ url.username !== '' ||
2399
+ url.password !== '' ||
2400
+ (url.pathname !== '' && url.pathname !== '/') ||
2401
+ url.search !== '' ||
2402
+ url.hash !== '') {
2403
+ return null;
2404
+ }
2405
+ return url.origin;
2406
+ }
2407
+ catch {
2408
+ return null;
2409
+ }
2410
+ }
2411
+ function normalizeCrossDomainOrigins(values) {
2412
+ if (!Array.isArray(values)) {
2413
+ return new Set();
2414
+ }
2415
+ return new Set(values.map(normalizeCrossDomainOrigin).filter((origin) => origin !== null));
2416
+ }
2417
+ function createCrossDomainSecret() {
2418
+ if (!globalThis.crypto?.getRandomValues) {
2419
+ return null;
2420
+ }
2421
+ const bytes = new Uint8Array(24);
2422
+ globalThis.crypto.getRandomValues(bytes);
2423
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
2424
+ }
2425
+ function parseCrossDomainHandshake(data, expectedLine, expectedProjectKey) {
2426
+ if (!isRecord(data) ||
2427
+ data.line !== expectedLine ||
2428
+ data.protocolVersion !== CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION ||
2429
+ data.projectKey !== expectedProjectKey ||
2430
+ !isBoundedString(data.context, 128) ||
2431
+ !CONTEXT_PATTERN.test(data.context) ||
2432
+ !isBoundedString(data.nonce, 128) ||
2433
+ !SECRET_PATTERN.test(data.nonce)) {
2434
+ return null;
2435
+ }
2436
+ return {
2437
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
2438
+ context: data.context,
2439
+ nonce: data.nonce,
2440
+ projectKey: expectedProjectKey,
2441
+ };
2442
+ }
2443
+ function matchesCrossDomainEnvelope(data, expectedLine, expected) {
2444
+ return (isRecord(data) &&
2445
+ data.line === expectedLine &&
2446
+ data.protocolVersion === CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION &&
2447
+ data.context === expected.context &&
2448
+ data.nonce === expected.nonce &&
2449
+ data.capability === expected.capability);
2450
+ }
2451
+ function parseCrossDomainFrameInit(data, expectedLine, expected) {
2452
+ if (!isRecord(data) ||
2453
+ data.line !== expectedLine ||
2454
+ data.protocolVersion !== CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION ||
2455
+ data.context !== expected.context ||
2456
+ data.nonce !== expected.nonce ||
2457
+ !isBoundedString(data.capability, 128) ||
2458
+ !SECRET_PATTERN.test(data.capability) ||
2459
+ !isSafeIntegerAtLeast(data.id, 0) ||
2460
+ !isSafeIntegerAtLeast(data.frameOrderNumber, 1) ||
2461
+ !isSafeIntegerAtLeast(data.frameLevel, 0) ||
2462
+ !isSafeIntegerAtLeast(data.capabilityExpiresAt, 1) ||
2463
+ data.capabilityExpiresAt <= Date.now() ||
2464
+ !isRecord(data.session)) {
2465
+ return null;
2466
+ }
2467
+ const session = data.session;
2468
+ if (!isBoundedString(session.sessionID, 256) ||
2469
+ !isFiniteNumber(session.startedAt) ||
2470
+ !isFiniteNumber(session.delayMs) ||
2471
+ typeof session.userUUID !== 'string' ||
2472
+ session.userUUID.length > 256) {
2473
+ return null;
2474
+ }
2475
+ let projectID;
2476
+ if (session.projectID !== undefined) {
2477
+ if (!isBoundedString(session.projectID, 256)) {
2478
+ return null;
2479
+ }
2480
+ projectID = session.projectID;
2481
+ }
2482
+ const canvas = parseCanvasConfig(data.canvas);
2483
+ if (data.canvas !== undefined && canvas === null) {
2484
+ return null;
2485
+ }
2486
+ return {
2487
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
2488
+ context: data.context,
2489
+ nonce: data.nonce,
2490
+ capability: data.capability,
2491
+ id: data.id,
2492
+ frameOrderNumber: data.frameOrderNumber,
2493
+ frameLevel: data.frameLevel,
2494
+ capabilityExpiresAt: data.capabilityExpiresAt,
2495
+ session: {
2496
+ sessionID: session.sessionID,
2497
+ projectID,
2498
+ startedAt: session.startedAt,
2499
+ delayMs: session.delayMs,
2500
+ userUUID: session.userUUID,
2501
+ },
2502
+ ...(canvas ? { canvas } : {}),
2503
+ };
2504
+ }
2505
+ function parseCanvasConfig(value) {
2506
+ if (value === undefined) {
2507
+ return null;
2508
+ }
2509
+ if (!isRecord(value) || typeof value.enabled !== 'boolean') {
2510
+ return null;
2511
+ }
2512
+ let quality;
2513
+ if (value.quality !== undefined) {
2514
+ if (!isCanvasQuality(value.quality)) {
2515
+ return null;
2516
+ }
2517
+ quality = value.quality;
2518
+ }
2519
+ let fps;
2520
+ if (value.fps !== undefined) {
2521
+ if (!isFiniteNumber(value.fps)) {
2522
+ return null;
2523
+ }
2524
+ fps = value.fps;
2525
+ }
2526
+ let framesSupport;
2527
+ if (value.framesSupport !== undefined) {
2528
+ if (typeof value.framesSupport !== 'boolean') {
2529
+ return null;
2530
+ }
2531
+ framesSupport = value.framesSupport;
2532
+ }
2533
+ return {
2534
+ enabled: value.enabled,
2535
+ quality,
2536
+ fps,
2537
+ framesSupport,
2538
+ };
2539
+ }
2540
+ function parseCrossDomainIframeBatch(messages) {
2541
+ if (!Array.isArray(messages) || messages.length > MAX_BATCH_MESSAGES) {
2542
+ return null;
2543
+ }
2544
+ let bytes = 0;
2545
+ for (const message of messages) {
2546
+ if (!Array.isArray(message) || message.length < 1 || message.length > MAX_MESSAGE_FIELDS) {
2547
+ return null;
2548
+ }
2549
+ const messageType = message[0];
2550
+ if (typeof messageType !== 'number' ||
2551
+ !Number.isInteger(messageType) ||
2552
+ !CROSS_DOMAIN_IFRAME_ALLOWED_MESSAGE_TYPES.has(messageType)) {
2553
+ return null;
2554
+ }
2555
+ const expectedFieldTypes = MESSAGE_FIELD_TYPES[messageType];
2556
+ if (expectedFieldTypes === undefined || message.length !== expectedFieldTypes.length + 1) {
2557
+ return null;
2558
+ }
2559
+ bytes += 8;
2560
+ for (let fieldIndex = 0; fieldIndex < expectedFieldTypes.length; fieldIndex++) {
2561
+ const field = message[fieldIndex + 1];
2562
+ if (typeof field !== expectedFieldTypes[fieldIndex]) {
2563
+ return null;
2564
+ }
2565
+ if (typeof field === 'string') {
2566
+ if (field.length > MAX_MESSAGE_STRING_LENGTH) {
2567
+ return null;
2568
+ }
2569
+ bytes += field.length * 3;
2570
+ }
2571
+ else if (typeof field === 'number') {
2572
+ if (!Number.isFinite(field)) {
2573
+ return null;
2574
+ }
2575
+ bytes += 8;
2576
+ }
2577
+ else if (typeof field === 'boolean') {
2578
+ bytes += 1;
2579
+ }
2580
+ else {
2581
+ return null;
2582
+ }
2583
+ if (bytes > MAX_BATCH_BYTES) {
2584
+ return null;
2585
+ }
2586
+ }
2587
+ }
2588
+ return { messages: messages, bytes };
2589
+ }
2590
+
2591
+ const REDACTED_DEBUG_VALUE = '[REDACTED]';
2592
+ const TRUNCATED_DEBUG_VALUE = '[TRUNCATED]';
2593
+ const MAX_DEBUG_TEXT_LENGTH = 2048;
2594
+ const MAX_DEBUG_CREDENTIAL_LOOKAHEAD = 512;
2595
+ const MAX_DEBUG_REDACTION_DEPTH = 5;
2596
+ const MAX_DEBUG_REDACTION_ITEMS = 50;
2597
+ const DEBUG_FIELD_VALUE_PATTERN = /(["']?)([ \t]*[a-z0-9_-]{1,64}[ \t]*)\1(\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^\s,;&{}\[\]()\r\n]+)/gim;
2598
+ const DEBUG_AUTHORIZATION_FIELD_PATTERN = /(["']?)((?:proxy[-_]?authorization|authorization)(?:[-_]?header)?)\1(\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|\[(?:REDACTED|TRUNCATED)\]|[^,;&}\]\)\r\n]+)/gi;
2599
+ const DEBUG_COOKIE_FIELD_PATTERN = /(["']?)((?:set[-_]?cookie|cookies?)(?:[-_]?header)?)\1(\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|\[(?:REDACTED|TRUNCATED)\]|[^\r\n]+)/gi;
2600
+ const DEBUG_AUTH_SCHEME_VALUE_PATTERN = /(\b(?:ApiKey|Basic|Bearer|Digest|Negotiate)\s+)[a-z0-9._~+/=-]+/gi;
2601
+ const DEBUG_TOKEN_AUTH_SCHEME_VALUE_PATTERN = /(\bToken\s+)[a-z0-9._~+/=-]+/g;
2602
+ const DEBUG_JWT_VALUE_PATTERN = /\beyJ[a-z0-9_-]{5,}\.[a-z0-9_-]{5,}\.[a-z0-9_-]{5,}\b/gi;
2603
+ const DEBUG_URL_CREDENTIAL_PATTERN = /(\bhttps?:\/\/)([^\s/:@]+):([^\s/@]+)@/gi;
2604
+ const DEBUG_ENCODED_FIELD_VALUE_PATTERN = /([a-z0-9_-]{0,48}(?:token|secret|grant|credential|api[-_]?key|private[-_]?key|signing[-_]?key|authorization|cookie))(%3a|%3d)(?:%22|%27)?[a-z0-9._~+/%=-]+/gi;
2605
+ const DEBUG_PROVIDER_CREDENTIAL_PATTERNS = [
2606
+ /sk-(?:(?:proj|svcacct|ant)-)?[a-z0-9_-]{8,}/gi,
2607
+ /AIza[a-z0-9_-]{20,}/gi,
2608
+ /(?:AKIA|ASIA)[A-Z0-9]{16}/g,
2609
+ /gh[pousr]_[a-z0-9]{20,}/gi,
2610
+ /xox[a-z]-[a-z0-9-]{10,}/gi,
2611
+ ];
2612
+ const DEBUG_PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/g;
2613
+ const PROVIDER_KEY_PREFIXES = [
2614
+ 'anthropic',
2615
+ 'azureopenai',
2616
+ 'cohere',
2617
+ 'deepseek',
2618
+ 'elevenlabs',
2619
+ 'gemini',
2620
+ 'google',
2621
+ 'groq',
2622
+ 'mistral',
2623
+ 'openai',
2624
+ 'openrouter',
2625
+ 'perplexity',
2626
+ ];
2627
+ const isSensitiveDebugField = (key) => {
2628
+ const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
2629
+ return (normalized === 'auth' ||
2630
+ normalized.includes('authorization') ||
2631
+ normalized.includes('bearer') ||
2632
+ normalized.includes('cookie') ||
2633
+ normalized === 'sessionhash' ||
2634
+ normalized === 'oxvolinkageassertion' ||
2635
+ normalized === 'xoxvosessionstrace' ||
2636
+ normalized.includes('password') ||
2637
+ normalized === 'privatekey' ||
2638
+ normalized === 'privatekeyid' ||
2639
+ normalized === 'signingkey' ||
2640
+ normalized === 'jwt' ||
2641
+ normalized.endsWith('jwt') ||
2642
+ normalized.endsWith('token') ||
2643
+ normalized.endsWith('grant') ||
2644
+ normalized.includes('secret') ||
2645
+ normalized.endsWith('apikey') ||
2646
+ normalized.endsWith('accesskey') ||
2647
+ normalized.endsWith('accesskeyid') ||
2648
+ normalized.endsWith('subscriptionkey') ||
2649
+ normalized.endsWith('credential') ||
2650
+ normalized.endsWith('credentials') ||
2651
+ (normalized.includes('provider') && normalized.endsWith('key')) ||
2652
+ (normalized.endsWith('key') &&
2653
+ PROVIDER_KEY_PREFIXES.some((provider) => normalized.startsWith(provider))));
2654
+ };
2655
+ const redactedFieldValue = (rawValue) => {
2656
+ const valueQuote = rawValue[0];
2657
+ return valueQuote === '"' || valueQuote === "'"
2658
+ ? `${valueQuote}${REDACTED_DEBUG_VALUE}${valueQuote}`
2659
+ : REDACTED_DEBUG_VALUE;
2660
+ };
2661
+ const redactDebugTextToLength = (value, maxOutputLength) => {
2662
+ const truncated = value.length > maxOutputLength;
2663
+ let redacted = value
2664
+ .slice(0, maxOutputLength + MAX_DEBUG_CREDENTIAL_LOOKAHEAD)
2665
+ .replace(DEBUG_AUTHORIZATION_FIELD_PATTERN, (_match, keyQuote, key, separator, rawValue) => `${keyQuote}${key}${keyQuote}${separator}${redactedFieldValue(rawValue)}`)
2666
+ .replace(DEBUG_COOKIE_FIELD_PATTERN, (_match, keyQuote, key, separator, rawValue) => `${keyQuote}${key}${keyQuote}${separator}${redactedFieldValue(rawValue)}`)
2667
+ .replace(DEBUG_AUTH_SCHEME_VALUE_PATTERN, `$1${REDACTED_DEBUG_VALUE}`)
2668
+ .replace(DEBUG_TOKEN_AUTH_SCHEME_VALUE_PATTERN, `$1${REDACTED_DEBUG_VALUE}`)
2669
+ .replace(DEBUG_FIELD_VALUE_PATTERN, (match, keyQuote, key, separator, rawValue) => {
2670
+ if (!isSensitiveDebugField(key)) {
2671
+ return match;
2672
+ }
2673
+ return `${keyQuote}${key}${keyQuote}${separator}${redactedFieldValue(rawValue)}`;
2674
+ })
2675
+ .replace(DEBUG_ENCODED_FIELD_VALUE_PATTERN, (_match, key, separator) => `${key}${separator}${REDACTED_DEBUG_VALUE}`)
2676
+ .replace(DEBUG_URL_CREDENTIAL_PATTERN, `$1${REDACTED_DEBUG_VALUE}:${REDACTED_DEBUG_VALUE}@`)
2677
+ .replace(DEBUG_JWT_VALUE_PATTERN, REDACTED_DEBUG_VALUE)
2678
+ .replace(DEBUG_PRIVATE_KEY_PATTERN, REDACTED_DEBUG_VALUE);
2679
+ for (const credentialPattern of DEBUG_PROVIDER_CREDENTIAL_PATTERNS) {
2680
+ redacted = redacted.replace(credentialPattern, REDACTED_DEBUG_VALUE);
2681
+ }
2682
+ const bounded = redacted.slice(0, maxOutputLength);
2683
+ return truncated ? `${bounded} ${TRUNCATED_DEBUG_VALUE}` : bounded;
2684
+ };
2685
+ const redactDebugText = (value) => redactDebugTextToLength(value, MAX_DEBUG_TEXT_LENGTH);
2686
+ const createDebugRedactionState = () => ({
2687
+ seen: new WeakSet(),
2688
+ remainingItems: MAX_DEBUG_REDACTION_ITEMS,
2689
+ remainingText: MAX_DEBUG_TEXT_LENGTH,
2690
+ });
2691
+ const redactDebugTextWithBudget = (value, state) => {
2692
+ if (state.remainingText <= 0) {
2693
+ return TRUNCATED_DEBUG_VALUE;
2694
+ }
2695
+ const permittedLength = Math.min(value.length, state.remainingText);
2696
+ state.remainingText -= permittedLength;
2697
+ return redactDebugTextToLength(value, permittedLength);
2698
+ };
2699
+ const redactDebugValue = (value, state = createDebugRedactionState(), depth = 0) => {
2700
+ if (typeof value === 'string') {
2701
+ return redactDebugTextWithBudget(value, state);
2702
+ }
2703
+ if (value instanceof Error) {
2704
+ const redacted = new Error(redactDebugTextWithBudget(value.message, state));
2705
+ redacted.name = redactDebugTextWithBudget(value.name, state);
2706
+ if (value.stack) {
2707
+ redacted.stack = redactDebugTextWithBudget(value.stack, state);
2708
+ }
2709
+ return redacted;
2710
+ }
2711
+ if (value === null || typeof value === 'number' || typeof value === 'boolean') {
2712
+ return value;
2713
+ }
2714
+ if (typeof value === 'undefined') {
2715
+ return undefined;
2716
+ }
2717
+ if (typeof value === 'bigint' || typeof value === 'symbol') {
2718
+ return String(value);
2719
+ }
2720
+ if (typeof value === 'function') {
2721
+ return '[Function]';
2722
+ }
2723
+ const objectValue = value;
2724
+ if (depth >= MAX_DEBUG_REDACTION_DEPTH || state.remainingItems <= 0) {
2725
+ return TRUNCATED_DEBUG_VALUE;
2726
+ }
2727
+ if (state.seen.has(objectValue)) {
2728
+ return '[Circular]';
2729
+ }
2730
+ state.seen.add(objectValue);
2731
+ if (Array.isArray(value)) {
2732
+ const count = Math.min(value.length, state.remainingItems);
2733
+ state.remainingItems -= count;
2734
+ const redacted = value.slice(0, count).map((entry) => redactDebugValue(entry, state, depth + 1));
2735
+ if (count < value.length) {
2736
+ redacted.push(TRUNCATED_DEBUG_VALUE);
2737
+ }
2738
+ return redacted;
2739
+ }
2740
+ const redacted = {};
2741
+ let allKeys;
2742
+ try {
2743
+ allKeys = Object.keys(objectValue);
2744
+ }
2745
+ catch {
2746
+ return '[Unavailable]';
2747
+ }
2748
+ const keys = allKeys.slice(0, state.remainingItems);
2749
+ state.remainingItems -= keys.length;
2750
+ for (const key of keys) {
2751
+ if (isSensitiveDebugField(key)) {
2752
+ redacted[key] = REDACTED_DEBUG_VALUE;
2753
+ continue;
2754
+ }
2755
+ try {
2756
+ redacted[key] = redactDebugValue(value[key], state, depth + 1);
2757
+ }
2758
+ catch {
2759
+ redacted[key] = '[Unavailable]';
2760
+ }
2761
+ }
2762
+ if (keys.length < allKeys.length) {
2763
+ redacted.__truncated__ = TRUNCATED_DEBUG_VALUE;
2764
+ }
2765
+ return redacted;
2766
+ };
2767
+ const redactDebugArgs = (values) => {
2768
+ const state = createDebugRedactionState();
2769
+ return values.map((value) => redactDebugValue(value, state));
2770
+ };
2771
+ const debugValueText = (value) => {
2772
+ if (value instanceof Error) {
2773
+ return `${value.name}: ${value.message}`;
2774
+ }
2775
+ if (typeof value === 'string') {
2776
+ return value;
2777
+ }
2778
+ let serialized;
2779
+ try {
2780
+ serialized = JSON.stringify(value);
2781
+ }
2782
+ catch {
2783
+ return '[Unavailable]';
2784
+ }
2785
+ if (serialized === undefined) {
2786
+ return String(value);
2787
+ }
2788
+ return serialized.length > MAX_DEBUG_TEXT_LENGTH
2789
+ ? `${serialized.slice(0, MAX_DEBUG_TEXT_LENGTH)} ${TRUNCATED_DEBUG_VALUE}`
2790
+ : serialized;
2791
+ };
2792
+ const redactedErrorFrom = (value, fallback, knownSecrets = []) => {
2793
+ const uniqueKnownSecrets = [
2794
+ ...new Set(knownSecrets.filter((secret) => !!secret)),
2795
+ ].sort((left, right) => right.length - left.length);
2796
+ const redacted = value instanceof Error || typeof value === 'string' ? value : redactDebugValue(value);
2797
+ let message = redacted instanceof Error ? redacted.message.trim() : debugValueText(redacted).trim();
2798
+ for (const secret of uniqueKnownSecrets) {
2799
+ message = message.split(secret).join(REDACTED_DEBUG_VALUE);
2800
+ }
2801
+ message = redactDebugText(message);
2802
+ return new Error(message.length > 0 ? message : fallback);
2803
+ };
2804
+
2154
2805
  const LogLevel = {
2155
2806
  Verbose: 5,
2156
2807
  Log: 4,
@@ -2165,26 +2816,22 @@ class Logger {
2165
2816
  };
2166
2817
  this.info = (...args) => {
2167
2818
  if (this.shouldLog(LogLevel.Verbose)) {
2168
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2169
- console.info(...args);
2819
+ console.info(...redactDebugArgs(args));
2170
2820
  }
2171
2821
  };
2172
2822
  this.log = (...args) => {
2173
2823
  if (this.shouldLog(LogLevel.Log)) {
2174
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2175
- console.log(...args);
2824
+ console.log(...redactDebugArgs(args));
2176
2825
  }
2177
2826
  };
2178
2827
  this.warn = (...args) => {
2179
2828
  if (this.shouldLog(LogLevel.Warnings)) {
2180
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2181
- console.warn(...args);
2829
+ console.warn(...redactDebugArgs(args));
2182
2830
  }
2183
2831
  };
2184
2832
  this.error = (...args) => {
2185
2833
  if (this.shouldLog(LogLevel.Errors)) {
2186
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2187
- console.error(...args);
2834
+ console.error(...redactDebugArgs(args));
2188
2835
  }
2189
2836
  };
2190
2837
  this.level = debugLevel;
@@ -3954,6 +4601,10 @@ class Session {
3954
4601
  this.token = `${token}${tokenSeparator}${projectKey}`;
3955
4602
  this.app.sessionStorage.setItem(this.options.session_token_key, `${token}${tokenSeparator}${projectKey}`);
3956
4603
  };
4604
+ this.clearSessionSecret = () => {
4605
+ this.token = undefined;
4606
+ this.app.sessionStorage.removeItem(this.options.session_token_key);
4607
+ };
3957
4608
  this.app = params.app;
3958
4609
  this.options = params.options;
3959
4610
  this.createTabId();
@@ -4050,7 +4701,7 @@ class Session {
4050
4701
  };
4051
4702
  }
4052
4703
  reset() {
4053
- this.app.sessionStorage.removeItem(this.options.session_token_key);
4704
+ this.clearSessionSecret();
4054
4705
  this.metadata = {};
4055
4706
  this.userID = null;
4056
4707
  this.sessionID = undefined;
@@ -4108,10 +4759,67 @@ class Ticker {
4108
4759
  * this value is injected during build time via rollup
4109
4760
  * */
4110
4761
  // @ts-ignore
4111
- const workerBodyFn = "!function(){\"use strict\";class t{constructor(t,s,i,e=10,n=250,h,r){this.onUnauthorised=s,this.onFailure=i,this.MAX_ATTEMPTS_COUNT=e,this.ATTEMPT_TIMEOUT=n,this.onCompress=h,this.pageNo=r,this.attemptsCount=0,this.busy=!1,this.queue=[],this.token=null,this.lastBatchNum=0,this.inflightKeepaliveBytes=0,this.ingestURL=t+\"/v1/events\",this.isCompressing=void 0!==h}getQueueStatus(){return 0===this.queue.length&&!this.busy}authorise(t){this.token=t,this.busy||this.sendNext()}push(t,s=\"player\"){if(this.busy||!this.token)this.queue.push({batch:t,dataType:s});else if(this.busy=!0,this.isCompressing&&this.onCompress)this.onCompress(t,s);else{const i=++this.lastBatchNum;this.sendBatch(t,!1,i,s)}}sendNext(){const t=this.queue.shift();if(t)if(this.busy=!0,this.isCompressing&&this.onCompress)this.onCompress(t.batch,t.dataType);else{const s=++this.lastBatchNum;this.sendBatch(t.batch,!1,s,t.dataType)}else this.busy=!1}retry(t,s,i,e=\"player\"){if(this.attemptsCount>=this.MAX_ATTEMPTS_COUNT)return void this.onFailure(`Failed to send batch after ${this.attemptsCount} attempts.`);this.attemptsCount++;const n=new Uint8Array(t);setTimeout((()=>this.sendBatch(n,s,i,e)),this.ATTEMPT_TIMEOUT*this.attemptsCount)}sendBatch(t,s,i,e=\"player\"){var n;if(0===t.length)return console.error(\"OxvoSessions: refusing to send 0-byte batch.\",{batchNum:i,dataType:e,isCompressed:s,batch:t}),this.attemptsCount=0,void this.sendNext();const h=null==i?void 0:i.toString().replace(/^([^_]+)_([^_]+).*/,\"$1_$2_$3\");this.busy=!0;const r={Authorization:`Bearer ${this.token}`,DataType:e};if(s&&(r[\"Content-Encoding\"]=\"gzip\"),null===this.token)return void setTimeout((()=>{this.sendBatch(t,s,`${null!=i?i:\"noBatchNum\"}_newToken`,e)}),500);const a=t.length<65536&&this.inflightKeepaliveBytes+t.length<=65536;a&&(this.inflightKeepaliveBytes+=t.length);const o=()=>{a&&(this.inflightKeepaliveBytes-=t.length)},u=`${null!==(n=this.pageNo)&&void 0!==n?n:\"noPageNum\"}_${null!=h?h:\"noBatchNum\"}`;fetch(`${this.ingestURL}?seq=${u}&keepalive=${a?\"yes\":\"no\"}`,{body:t,method:\"POST\",headers:r,keepalive:a}).then((n=>{var h;if(o(),null===(h=n.body)||void 0===h||h.cancel().catch((()=>{})),401===n.status)return this.busy=!1,void this.onUnauthorised();n.status>=400?this.retry(t,s,`${null!=i?i:\"noBatchNum\"}_network:${n.status}`,e):(this.attemptsCount=0,this.sendNext())})).catch((n=>{o(),console.warn(\"OxvoSessions:\",n),this.retry(t,s,`${null!=i?i:\"noBatchNum\"}_reject:${n.message}`,e)}))}sendCompressed(t,s=\"player\"){const i=++this.lastBatchNum;this.sendBatch(t,!0,i,s)}sendUncompressed(t,s=\"player\"){const i=++this.lastBatchNum;this.sendBatch(t,!1,i,s)}clean(){this.sendNext(),setTimeout((()=>{this.token=null,this.queue.length=0}),10)}}const s=new Set([60,61,71,73]),i=new Set([21,22,40,41,44,45,46,47,48,79,83,84,85,87,89,116,120,121,123]),e=new Set([17,23,24,27,28,29,30,42,63,64,78,112,115,124]),n=\"function\"==typeof TextEncoder?new TextEncoder:{encode(t){const s=t.length,i=new Uint8Array(3*s);let e=-1;for(let n=0,h=0,r=0;r!==s;){if(n=t.charCodeAt(r),r+=1,n>=55296&&n<=56319){if(r===s){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;break}if(h=t.charCodeAt(r),!(h>=56320&&h<=57343)){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;continue}if(n=1024*(n-55296)+h-56320+65536,r+=1,n>65535){i[e+=1]=240|n>>>18,i[e+=1]=128|n>>>12&63,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n;continue}}n<=127?i[e+=1]=0|n:n<=2047?(i[e+=1]=192|n>>>6,i[e+=1]=128|63&n):(i[e+=1]=224|n>>>12,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n)}return i.subarray(0,e+1)}};class h{constructor(t){this.size=t,this.offset=0,this.checkpointOffset=0,this.data=new Uint8Array(t)}getCurrentOffset(){return this.offset}getCurrentCheckpoint(){return this.checkpointOffset}checkpoint(){this.checkpointOffset=this.offset}get isEmpty(){return 0===this.offset}skip(t){return this.offset+=t,this.offset<=this.size}set(t,s){this.data.set(t,s)}boolean(t){return this.data[this.offset++]=+t,this.offset<=this.size}uint(t){for((t<0||t>Number.MAX_SAFE_INTEGER)&&(t=0);t>=128;)this.data[this.offset++]=t%256|128,t=Math.floor(t/128);return this.data[this.offset++]=t,this.offset<=this.size}int(t){return t=Math.round(t),this.uint(t>=0?2*t:-2*t-1)}string(t){const s=n.encode(t),i=s.byteLength;return!(!this.uint(i)||this.offset+i>this.size)&&(this.data.set(s,this.offset),this.offset+=i,!0)}reset(){this.offset=0,this.checkpointOffset=0}rewind(t,s){t>this.offset||s>this.checkpointOffset||(this.offset=t,this.checkpointOffset=s)}flush(){const t=this.data.slice(0,this.checkpointOffset);return this.reset(),t}}class r extends h{encode(t){switch(t[0]){case 0:case 11:case 114:case 115:return this.uint(t[1]);case 4:case 44:case 47:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3]);case 5:case 20:case 65:case 70:case 75:case 76:case 77:return this.uint(t[1])&&this.uint(t[2]);case 6:return this.int(t[1])&&this.int(t[2]);case 7:return!0;case 8:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.string(t[4])&&this.boolean(t[5]);case 9:case 10:case 24:case 35:case 51:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3]);case 12:case 52:case 61:case 71:return this.uint(t[1])&&this.string(t[2])&&this.string(t[3]);case 13:case 14:case 17:case 34:case 36:case 50:case 54:return this.uint(t[1])&&this.string(t[2]);case 16:return this.uint(t[1])&&this.int(t[2])&&this.int(t[3]);case 18:return this.uint(t[1])&&this.string(t[2])&&this.int(t[3]);case 19:return this.uint(t[1])&&this.boolean(t[2]);case 21:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.string(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8]);case 22:case 27:case 30:case 41:case 45:case 46:case 43:case 63:case 64:case 79:case 124:return this.string(t[1])&&this.string(t[2]);case 23:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8])&&this.uint(t[9]);case 28:case 29:case 42:case 117:case 118:return this.string(t[1]);case 40:return this.string(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4]);case 48:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.int(t[5]);case 49:return this.int(t[1])&&this.int(t[2])&&this.uint(t[3])&&this.uint(t[4]);case 53:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8]);case 55:return this.boolean(t[1]);case 57:case 60:return this.uint(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4]);case 58:case 120:return this.int(t[1]);case 68:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4])&&this.uint(t[5])&&this.uint(t[6]);case 69:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4]);case 73:return this.uint(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.string(t[4]);case 78:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4]);case 81:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.int(t[4])&&this.string(t[5]);case 83:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.string(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8])&&this.uint(t[9]);case 84:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.uint(t[4])&&this.string(t[5])&&this.string(t[6]);case 85:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8])&&this.uint(t[9])&&this.boolean(t[10])&&this.uint(t[11])&&this.uint(t[12])&&this.uint(t[13])&&this.uint(t[14])&&this.uint(t[15])&&this.uint(t[16])&&this.uint(t[17]);case 87:return this.string(t[1])&&this.int(t[2])&&this.int(t[3]);case 89:return this.string(t[1])&&this.int(t[2])&&this.int(t[3])&&this.int(t[4])&&this.int(t[5])&&this.string(t[6]);case 112:return this.uint(t[1])&&this.string(t[2])&&this.boolean(t[3])&&this.string(t[4])&&this.int(t[5])&&this.int(t[6]);case 113:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3]);case 116:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8])&&this.uint(t[9])&&this.boolean(t[10]);case 119:return this.string(t[1])&&this.uint(t[2]);case 121:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.uint(t[4]);case 122:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.string(t[4]);case 123:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.uint(t[5])}}}class a{constructor(t,s,i){this.bufferSize=t,this.version=s,this.dataType=i,this.sizeBuffer=new Uint8Array(3),this.snap=null,this.hasNonTimestamp=!1,this.lastPushedTs=0,this.encoder=new r(t)}push(t,s){const i=this.encoder,e=null===this.snap,n=i.getCurrentOffset(),h=i.getCurrentCheckpoint();if(e){const t={pageNo:s.pageNo,firstIndex:s.index,timestamp:s.timestamp,url:s.url,tabId:s.tabId};if(!this.writeHeader(t))return i.rewind(n,h),!1;this.snap=t,this.lastPushedTs=s.timestamp}return 0===t[0]||s.timestamp===this.lastPushedTs||this.writeMessageWithSize([0,s.timestamp])?this.writeMessageWithSize(t)?(this.lastPushedTs=s.timestamp,0!==t[0]&&(this.hasNonTimestamp=!0),!0):(i.rewind(n,h),e&&(this.snap=null),!1):(i.rewind(n,h),!1)}hasContent(){return null!==this.snap&&this.hasNonTimestamp}flush(){if(!this.hasContent())return this.reset(),null;const t=this.encoder.flush();return this.snap=null,this.hasNonTimestamp=!1,this.lastPushedTs=0,t}reset(){this.encoder.reset(),this.snap=null,this.hasNonTimestamp=!1,this.lastPushedTs=0}writeHeader(t){const s=this.encoder,i=[81,this.version,t.pageNo,t.firstIndex,t.timestamp,t.url];return!!s.uint(i[0])&&(!!s.encode(i)&&(!(s.getCurrentOffset()>this.bufferSize)&&(s.checkpoint(),!!this.writeMessageWithSize([0,t.timestamp])&&!!this.writeMessageWithSize([118,t.tabId]))))}writeMessageWithSize(t){const s=this.encoder;if(!s.uint(t[0])||!s.skip(3))return!1;const i=s.getCurrentOffset();if(!s.encode(t))return!1;const e=s.getCurrentOffset(),n=e-i;return n>16777215?(console.warn(\"OxvoSessions: max message size overflow.\"),!1):!(e>this.bufferSize)&&(this.writeSizeAt(n,i-3),s.checkpoint(),!0)}writeSizeAt(t,s){for(let s=0;s<3;s++)this.sizeBuffer[s]=t>>8*s;this.encoder.set(this.sizeBuffer,s)}}class o{constructor(t,s,i,e,n,h,r=!1,o){this.pageNo=t,this.timestamp=s,this.url=i,this.onBatch=e,this.tabId=n,this.onOfflineEnd=h,this.localDebug=r,this.onLocalSave=o,this.nextIndex=0,this.beaconSize=2e5,this.beaconSizeLimit=1e6,this.protocolVersion=1,this.playerBuilder=new a(this.beaconSize,this.playerVersion(),\"player\"),this.assetBuilder=new a(this.beaconSize,3,\"assets\"),this.devtoolsBuilder=new a(this.beaconSize,4,\"devtools\"),this.analyticsBuilder=new a(this.beaconSize,5,\"analytics\")}playerVersion(){return 2===this.protocolVersion?2:1}currentCtx(){return{pageNo:this.pageNo,index:this.nextIndex,timestamp:this.timestamp,url:this.url,tabId:this.tabId}}setBeaconSizeLimit(t){this.beaconSizeLimit=t}setProtocolVersion(t){this.protocolVersion!==t&&(this.protocolVersion=t,this.playerBuilder.reset(),this.playerBuilder=new a(this.beaconSize,this.playerVersion(),\"player\"))}writeMessage(t){if(-1===t[0])return this.finaliseBatch(),this.onOfflineEnd();0===t[0]&&(this.timestamp=t[1]),122===t[0]&&(this.url=t[1]);const s=this.routeMessage(t);this.pushTo(s,t)}routeMessage(t){if(2===this.protocolVersion){const n=t[0];if(s.has(n))return this.assetBuilder;if(i.has(n))return this.devtoolsBuilder;if(e.has(n))return this.analyticsBuilder}return this.playerBuilder}pushTo(t,s){const i=this.currentCtx();if(t.push(s,i))return void this.nextIndex++;if(this.flushBuilder(t),t.push(s,i))return void this.nextIndex++;const e=new a(this.beaconSizeLimit,t.version,t.dataType);if(!e.push(s,i))return void console.warn(\"OxvoSessions: beacon size overflow. Skipping large message.\",s);this.nextIndex++;const n=e.flush();n&&this.emitBatch(n,t.dataType,!1)}flushBuilder(t,s=!1){const i=t.flush();return!!i&&(this.emitBatch(i,t.dataType,s),!0)}emitBatch(t,s,i){this.localDebug&&this.onLocalSave&&this.onLocalSave(`${s}-${Date.now()}`,t.slice()),this.onBatch(t,i,s)}finaliseBatch(t=!1){this.flushBuilder(this.playerBuilder,t),this.flushBuilder(this.assetBuilder,t),this.flushBuilder(this.devtoolsBuilder,t),this.flushBuilder(this.analyticsBuilder,t)}clean(){this.playerBuilder.reset(),this.assetBuilder.reset(),this.devtoolsBuilder.reset(),this.analyticsBuilder.reset()}}var u;!function(t){t[t.NotActive=0]=\"NotActive\",t[t.Starting=1]=\"Starting\",t[t.Stopping=2]=\"Stopping\",t[t.Active=3]=\"Active\",t[t.Stopped=4]=\"Stopped\"}(u||(u={}));let c=null,l=null,d=u.NotActive;function p(t){l&&l.finaliseBatch(t)}function f(){return new Promise((t=>{d=u.Stopping,null!==b&&(clearInterval(b),b=null),l&&(l.clean(),l=null),c&&(c.clean(),setTimeout((()=>{c=null}),20)),setTimeout((()=>{d=u.NotActive,t(null)}),100)}))}function g(){[u.Stopped,u.Stopping].includes(d)||(postMessage(\"a_stop\"),f().then((()=>{postMessage(\"a_start\")})))}let m,b=null;self.onmessage=({data:s})=>{var i;if(\"stop\"===s)return p(),void f().then((()=>{d=u.Stopped}));if(\"forceFlushBatch\"!==s)if(\"closing\"!==s){if(!Array.isArray(s)){if(\"compressed\"===s.type){if(!c)return console.debug(\"OR WebWorker: sender not initialised. Compressed batch.\"),void g();s.batch&&c.sendCompressed(s.batch,s.dataType)}if(\"uncompressed\"===s.type){if(!c)return console.debug(\"OR WebWorker: sender not initialised. Uncompressed batch.\"),void g();s.batch&&c.sendUncompressed(s.batch,s.dataType)}return\"start\"===s.type?(d=u.Starting,c=new t(s.ingestPoint,(()=>{g()}),(t=>{!function(t){postMessage({type:\"failure\",reason:t}),f()}(t)}),s.connAttemptCount,s.connAttemptGap,((t,s)=>{postMessage({type:\"compress\",batch:t,dataType:s},[t.buffer])}),s.pageNo),l=new o(s.pageNo,s.timestamp,s.url,((t,s,i=\"player\")=>{c&&(s?c.sendUncompressed(t,i):c.push(t,i))}),s.tabId,(()=>postMessage({type:\"queue_empty\"})),null!==(i=s.localDebug)&&void 0!==i&&i,((t,s)=>{postMessage({type:\"local_save\",name:t,batch:s},[s.buffer])})),null===b&&(b=setInterval(p,3e4)),d=u.Active):\"auth\"===s.type?c?l?(c.authorise(s.token),s.beaconSizeLimit&&l.setBeaconSizeLimit(s.beaconSizeLimit),void(s.protocolVersion&&l.setProtocolVersion(s.protocolVersion))):(console.debug(\"OR WebWorker: writer not initialised. Received auth.\"),void g()):(console.debug(\"OR WebWorker: sender not initialised. Received auth.\"),void g()):void 0}if(l){const t=l;s.forEach((s=>{55===s[0]&&(s[1]?m=setTimeout((()=>g()),18e5):clearTimeout(m)),t.writeMessage(s)}))}else postMessage(\"not_init\"),g()}else p(!0);else p()}}();\n";
4762
+ const workerBodyFn = "!function(){\"use strict\";class t{constructor(t,s,i,e=10,n=250,h,r){this.onUnauthorised=s,this.onFailure=i,this.MAX_ATTEMPTS_COUNT=e,this.ATTEMPT_TIMEOUT=n,this.onCompress=h,this.pageNo=r,this.attemptsCount=0,this.busy=!1,this.queue=[],this.token=null,this.lastBatchNum=0,this.inflightKeepaliveBytes=0,this.ingestURL=t+\"/v1/events\",this.analyticsIngestURL=t+\"/v1/analytics\",this.isCompressing=void 0!==h}getQueueStatus(){return 0===this.queue.length&&!this.busy}authorise(t){this.token=t,this.busy||this.sendNext()}push(t,s=\"player\"){if(this.busy||!this.token)this.queue.push({batch:t,dataType:s});else if(this.busy=!0,this.isCompressing&&this.onCompress)this.onCompress(t,s);else{const i=++this.lastBatchNum;this.sendBatch(t,!1,i,s)}}sendNext(){const t=this.queue.shift();if(t)if(this.busy=!0,this.isCompressing&&this.onCompress)this.onCompress(t.batch,t.dataType);else{const s=++this.lastBatchNum;this.sendBatch(t.batch,!1,s,t.dataType)}else this.busy=!1}retry(t,s,i,e=\"player\"){if(this.attemptsCount>=this.MAX_ATTEMPTS_COUNT)return void this.onFailure(`Failed to send batch after ${this.attemptsCount} attempts.`);this.attemptsCount++;const n=new Uint8Array(t);setTimeout((()=>this.sendBatch(n,s,i,e)),this.ATTEMPT_TIMEOUT*this.attemptsCount)}sendBatch(t,s,i,e=\"player\"){var n;if(0===t.length)return console.error(\"OxvoSessions: refusing to send 0-byte batch.\",{batchNum:i,dataType:e,isCompressed:s,batch:t}),this.attemptsCount=0,void this.sendNext();const h=null==i?void 0:i.toString().replace(/^([^_]+)_([^_]+).*/,\"$1_$2_$3\");this.busy=!0;const r={Authorization:`Bearer ${this.token}`,DataType:e};if(s&&(r[\"Content-Encoding\"]=\"gzip\"),null===this.token)return void setTimeout((()=>{this.sendBatch(t,s,`${null!=i?i:\"noBatchNum\"}_newToken`,e)}),500);const a=t.length<65536&&this.inflightKeepaliveBytes+t.length<=65536;a&&(this.inflightKeepaliveBytes+=t.length);const o=()=>{a&&(this.inflightKeepaliveBytes-=t.length)},u=`${null!==(n=this.pageNo)&&void 0!==n?n:\"noPageNum\"}_${null!=h?h:\"noBatchNum\"}`,c=\"analytics\"===e?this.analyticsIngestURL:this.ingestURL;fetch(`${c}?seq=${u}&keepalive=${a?\"yes\":\"no\"}`,{body:t,method:\"POST\",headers:r,keepalive:a}).then((n=>{var h;if(o(),null===(h=n.body)||void 0===h||h.cancel().catch((()=>{})),401===n.status)return this.busy=!1,void this.onUnauthorised();n.status>=400?this.retry(t,s,`${null!=i?i:\"noBatchNum\"}_network:${n.status}`,e):(this.attemptsCount=0,this.sendNext())})).catch((n=>{o(),console.warn(\"OxvoSessions:\",n),this.retry(t,s,`${null!=i?i:\"noBatchNum\"}_reject:${n.message}`,e)}))}sendCompressed(t,s=\"player\"){const i=++this.lastBatchNum;this.sendBatch(t,!0,i,s)}sendUncompressed(t,s=\"player\"){const i=++this.lastBatchNum;this.sendBatch(t,!1,i,s)}clean(){this.sendNext(),setTimeout((()=>{this.token=null,this.queue.length=0}),10)}}const s=new Set([60,61,71,73]),i=new Set([21,22,40,41,44,45,46,47,48,79,83,84,85,87,89,116,120,121,123]),e=new Set([17,23,24,27,28,29,30,42,63,64,78,112,115,124]),n=\"function\"==typeof TextEncoder?new TextEncoder:{encode(t){const s=t.length,i=new Uint8Array(3*s);let e=-1;for(let n=0,h=0,r=0;r!==s;){if(n=t.charCodeAt(r),r+=1,n>=55296&&n<=56319){if(r===s){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;break}if(h=t.charCodeAt(r),!(h>=56320&&h<=57343)){i[e+=1]=239,i[e+=1]=191,i[e+=1]=189;continue}if(n=1024*(n-55296)+h-56320+65536,r+=1,n>65535){i[e+=1]=240|n>>>18,i[e+=1]=128|n>>>12&63,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n;continue}}n<=127?i[e+=1]=0|n:n<=2047?(i[e+=1]=192|n>>>6,i[e+=1]=128|63&n):(i[e+=1]=224|n>>>12,i[e+=1]=128|n>>>6&63,i[e+=1]=128|63&n)}return i.subarray(0,e+1)}};class h{constructor(t){this.size=t,this.offset=0,this.checkpointOffset=0,this.data=new Uint8Array(t)}getCurrentOffset(){return this.offset}getCurrentCheckpoint(){return this.checkpointOffset}checkpoint(){this.checkpointOffset=this.offset}get isEmpty(){return 0===this.offset}skip(t){return this.offset+=t,this.offset<=this.size}set(t,s){this.data.set(t,s)}boolean(t){return this.data[this.offset++]=+t,this.offset<=this.size}uint(t){for((t<0||t>Number.MAX_SAFE_INTEGER)&&(t=0);t>=128;)this.data[this.offset++]=t%256|128,t=Math.floor(t/128);return this.data[this.offset++]=t,this.offset<=this.size}int(t){return t=Math.round(t),this.uint(t>=0?2*t:-2*t-1)}string(t){const s=n.encode(t),i=s.byteLength;return!(!this.uint(i)||this.offset+i>this.size)&&(this.data.set(s,this.offset),this.offset+=i,!0)}reset(){this.offset=0,this.checkpointOffset=0}rewind(t,s){t>this.offset||s>this.checkpointOffset||(this.offset=t,this.checkpointOffset=s)}flush(){const t=this.data.slice(0,this.checkpointOffset);return this.reset(),t}}class r extends h{encode(t){switch(t[0]){case 0:case 11:case 114:case 115:return this.uint(t[1]);case 4:case 44:case 47:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3]);case 5:case 20:case 65:case 70:case 75:case 76:case 77:return this.uint(t[1])&&this.uint(t[2]);case 6:return this.int(t[1])&&this.int(t[2]);case 7:return!0;case 8:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.string(t[4])&&this.boolean(t[5]);case 9:case 10:case 24:case 35:case 51:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3]);case 12:case 52:case 61:case 71:return this.uint(t[1])&&this.string(t[2])&&this.string(t[3]);case 13:case 14:case 17:case 34:case 36:case 50:case 54:return this.uint(t[1])&&this.string(t[2]);case 16:return this.uint(t[1])&&this.int(t[2])&&this.int(t[3]);case 18:return this.uint(t[1])&&this.string(t[2])&&this.int(t[3]);case 19:return this.uint(t[1])&&this.boolean(t[2]);case 21:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.string(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8]);case 22:case 27:case 30:case 41:case 45:case 46:case 43:case 63:case 64:case 79:case 124:return this.string(t[1])&&this.string(t[2]);case 23:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8])&&this.uint(t[9]);case 28:case 29:case 42:case 117:case 118:return this.string(t[1]);case 40:return this.string(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4]);case 48:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.int(t[5]);case 49:return this.int(t[1])&&this.int(t[2])&&this.uint(t[3])&&this.uint(t[4]);case 53:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8]);case 55:return this.boolean(t[1]);case 57:case 60:return this.uint(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4]);case 58:case 120:return this.int(t[1]);case 68:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4])&&this.uint(t[5])&&this.uint(t[6]);case 69:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3])&&this.string(t[4]);case 73:return this.uint(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.string(t[4]);case 78:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4]);case 81:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.int(t[4])&&this.string(t[5]);case 83:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.string(t[5])&&this.uint(t[6])&&this.uint(t[7])&&this.uint(t[8])&&this.uint(t[9]);case 84:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.uint(t[4])&&this.string(t[5])&&this.string(t[6]);case 85:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8])&&this.uint(t[9])&&this.boolean(t[10])&&this.uint(t[11])&&this.uint(t[12])&&this.uint(t[13])&&this.uint(t[14])&&this.uint(t[15])&&this.uint(t[16])&&this.uint(t[17]);case 87:return this.string(t[1])&&this.int(t[2])&&this.int(t[3]);case 89:return this.string(t[1])&&this.int(t[2])&&this.int(t[3])&&this.int(t[4])&&this.int(t[5])&&this.string(t[6]);case 112:return this.uint(t[1])&&this.string(t[2])&&this.boolean(t[3])&&this.string(t[4])&&this.int(t[5])&&this.int(t[6]);case 113:return this.uint(t[1])&&this.uint(t[2])&&this.string(t[3]);case 116:return this.uint(t[1])&&this.uint(t[2])&&this.uint(t[3])&&this.uint(t[4])&&this.uint(t[5])&&this.uint(t[6])&&this.string(t[7])&&this.string(t[8])&&this.uint(t[9])&&this.boolean(t[10]);case 119:return this.string(t[1])&&this.uint(t[2]);case 121:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.uint(t[4]);case 122:return this.string(t[1])&&this.string(t[2])&&this.uint(t[3])&&this.string(t[4]);case 123:return this.string(t[1])&&this.string(t[2])&&this.string(t[3])&&this.string(t[4])&&this.uint(t[5])}}}class a{constructor(t,s,i){this.bufferSize=t,this.version=s,this.dataType=i,this.sizeBuffer=new Uint8Array(3),this.snap=null,this.hasNonTimestamp=!1,this.lastPushedTs=0,this.encoder=new r(t)}push(t,s){const i=this.encoder,e=null===this.snap,n=i.getCurrentOffset(),h=i.getCurrentCheckpoint();if(e){const t={pageNo:s.pageNo,firstIndex:s.index,timestamp:s.timestamp,url:s.url,tabId:s.tabId};if(!this.writeHeader(t))return i.rewind(n,h),!1;this.snap=t,this.lastPushedTs=s.timestamp}return 0===t[0]||s.timestamp===this.lastPushedTs||this.writeMessageWithSize([0,s.timestamp])?this.writeMessageWithSize(t)?(this.lastPushedTs=s.timestamp,0!==t[0]&&(this.hasNonTimestamp=!0),!0):(i.rewind(n,h),e&&(this.snap=null),!1):(i.rewind(n,h),!1)}hasContent(){return null!==this.snap&&this.hasNonTimestamp}flush(){if(!this.hasContent())return this.reset(),null;const t=this.encoder.flush();return this.snap=null,this.hasNonTimestamp=!1,this.lastPushedTs=0,t}reset(){this.encoder.reset(),this.snap=null,this.hasNonTimestamp=!1,this.lastPushedTs=0}writeHeader(t){const s=this.encoder,i=[81,this.version,t.pageNo,t.firstIndex,t.timestamp,t.url];return!!s.uint(i[0])&&(!!s.encode(i)&&(!(s.getCurrentOffset()>this.bufferSize)&&(s.checkpoint(),!!this.writeMessageWithSize([0,t.timestamp])&&!!this.writeMessageWithSize([118,t.tabId]))))}writeMessageWithSize(t){const s=this.encoder;if(!s.uint(t[0])||!s.skip(3))return!1;const i=s.getCurrentOffset();if(!s.encode(t))return!1;const e=s.getCurrentOffset(),n=e-i;return n>16777215?(console.warn(\"OxvoSessions: max message size overflow.\"),!1):!(e>this.bufferSize)&&(this.writeSizeAt(n,i-3),s.checkpoint(),!0)}writeSizeAt(t,s){for(let s=0;s<3;s++)this.sizeBuffer[s]=t>>8*s;this.encoder.set(this.sizeBuffer,s)}}class o{constructor(t,s,i,e,n,h,r=!1,o){this.pageNo=t,this.timestamp=s,this.url=i,this.onBatch=e,this.tabId=n,this.onOfflineEnd=h,this.localDebug=r,this.onLocalSave=o,this.nextIndex=0,this.beaconSize=2e5,this.beaconSizeLimit=1e6,this.protocolVersion=1,this.playerBuilder=new a(this.beaconSize,this.playerVersion(),\"player\"),this.assetBuilder=new a(this.beaconSize,3,\"assets\"),this.devtoolsBuilder=new a(this.beaconSize,4,\"devtools\"),this.analyticsBuilder=new a(this.beaconSize,5,\"analytics\")}playerVersion(){return 2===this.protocolVersion?2:1}currentCtx(){return{pageNo:this.pageNo,index:this.nextIndex,timestamp:this.timestamp,url:this.url,tabId:this.tabId}}setBeaconSizeLimit(t){this.beaconSizeLimit=t}setProtocolVersion(t){this.protocolVersion!==t&&(this.protocolVersion=t,this.playerBuilder.reset(),this.playerBuilder=new a(this.beaconSize,this.playerVersion(),\"player\"))}writeMessage(t){if(-1===t[0])return this.finaliseBatch(),this.onOfflineEnd();0===t[0]&&(this.timestamp=t[1]),122===t[0]&&(this.url=t[1]);const s=this.routeMessage(t);this.pushTo(s,t)}routeMessage(t){if(2===this.protocolVersion){const n=t[0];if(s.has(n))return this.assetBuilder;if(i.has(n))return this.devtoolsBuilder;if(e.has(n))return this.analyticsBuilder}return this.playerBuilder}pushTo(t,s){const i=this.currentCtx();if(t.push(s,i))return void this.nextIndex++;if(this.flushBuilder(t),t.push(s,i))return void this.nextIndex++;const e=new a(this.beaconSizeLimit,t.version,t.dataType);if(!e.push(s,i))return void console.warn(\"OxvoSessions: beacon size overflow. Skipping large message.\",s);this.nextIndex++;const n=e.flush();n&&this.emitBatch(n,t.dataType,!1)}flushBuilder(t,s=!1){const i=t.flush();return!!i&&(this.emitBatch(i,t.dataType,s),!0)}emitBatch(t,s,i){this.localDebug&&this.onLocalSave&&this.onLocalSave(`${s}-${Date.now()}`,t.slice()),this.onBatch(t,i,s)}finaliseBatch(t=!1){this.flushBuilder(this.playerBuilder,t),this.flushBuilder(this.assetBuilder,t),this.flushBuilder(this.devtoolsBuilder,t),this.flushBuilder(this.analyticsBuilder,t)}clean(){this.playerBuilder.reset(),this.assetBuilder.reset(),this.devtoolsBuilder.reset(),this.analyticsBuilder.reset()}}var u;!function(t){t[t.NotActive=0]=\"NotActive\",t[t.Starting=1]=\"Starting\",t[t.Stopping=2]=\"Stopping\",t[t.Active=3]=\"Active\",t[t.Stopped=4]=\"Stopped\"}(u||(u={}));let c=null,l=null,d=u.NotActive;function p(t){l&&l.finaliseBatch(t)}function f(){return new Promise((t=>{d=u.Stopping,null!==y&&(clearInterval(y),y=null),l&&(l.clean(),l=null),c&&(c.clean(),setTimeout((()=>{c=null}),20)),setTimeout((()=>{d=u.NotActive,t(null)}),100)}))}function g(){[u.Stopped,u.Stopping].includes(d)||(postMessage(\"a_stop\"),f().then((()=>{postMessage(\"a_start\")})))}let m,y=null;self.onmessage=({data:s})=>{var i;if(\"stop\"===s)return p(),void f().then((()=>{d=u.Stopped}));if(\"forceFlushBatch\"!==s)if(\"closing\"!==s){if(!Array.isArray(s)){if(\"compressed\"===s.type){if(!c)return console.debug(\"OR WebWorker: sender not initialised. Compressed batch.\"),void g();s.batch&&c.sendCompressed(s.batch,s.dataType)}if(\"uncompressed\"===s.type){if(!c)return console.debug(\"OR WebWorker: sender not initialised. Uncompressed batch.\"),void g();s.batch&&c.sendUncompressed(s.batch,s.dataType)}return\"start\"===s.type?(d=u.Starting,c=new t(s.ingestPoint,(()=>{g()}),(t=>{!function(t){postMessage({type:\"failure\",reason:t}),f()}(t)}),s.connAttemptCount,s.connAttemptGap,((t,s)=>{postMessage({type:\"compress\",batch:t,dataType:s},[t.buffer])}),s.pageNo),l=new o(s.pageNo,s.timestamp,s.url,((t,s,i=\"player\")=>{c&&(s?c.sendUncompressed(t,i):c.push(t,i))}),s.tabId,(()=>postMessage({type:\"queue_empty\"})),null!==(i=s.localDebug)&&void 0!==i&&i,((t,s)=>{postMessage({type:\"local_save\",name:t,batch:s},[s.buffer])})),null===y&&(y=setInterval(p,3e4)),d=u.Active):\"auth\"===s.type?c?l?(c.authorise(s.token),s.beaconSizeLimit&&l.setBeaconSizeLimit(s.beaconSizeLimit),void(s.protocolVersion&&l.setProtocolVersion(s.protocolVersion))):(console.debug(\"OR WebWorker: writer not initialised. Received auth.\"),void g()):(console.debug(\"OR WebWorker: sender not initialised. Received auth.\"),void g()):void 0}if(l){const t=l;s.forEach((s=>{55===s[0]&&(s[1]?m=setTimeout((()=>g()),18e5):clearTimeout(m)),t.writeMessage(s)}))}else postMessage(\"not_init\"),g()}else p(!0);else p()}}();\n";
4112
4763
  const CANCELED = 'canceled';
4113
4764
  const bufferStorageKey = 'or_buffer_1';
4114
4765
  const PROTO_VERSION = '2';
4766
+ const BOOTSTRAP_ATTEMPT_TTL_MS = 24 * 60 * 60 * 1000;
4767
+ const MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH = 1024 * 1024;
4768
+ const MAX_SESSION_TOKEN_LENGTH = 4096;
4769
+ const MAX_ASSIST_SESSION_GRANT_LENGTH = 4096;
4770
+ const LINKAGE_ASSERTION_METADATA_KEY = 'oxvo_linkage_assertion';
4771
+ const MESSENGER_LINKAGE_CHALLENGE_PATTERN = /^[a-f0-9]{64}$/;
4772
+ const MIN_MESSENGER_LINKAGE_CHALLENGE_TTL_SECONDS = 5;
4773
+ const MAX_MESSENGER_LINKAGE_CHALLENGE_TTL_SECONDS = 120;
4774
+ const asRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
4775
+ ? value
4776
+ : {};
4777
+ const isValidBootstrapCredential = (value, maxLength) => typeof value === 'string' &&
4778
+ value.length > 0 &&
4779
+ value.length <= maxLength &&
4780
+ value.trim() === value;
4781
+ const parseMessengerLinkageChallenge = (value, expectedSessionId) => {
4782
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
4783
+ return null;
4784
+ }
4785
+ const record = value;
4786
+ const keys = Object.keys(record).sort();
4787
+ if (keys.length !== 3 ||
4788
+ keys[0] !== 'challenge' ||
4789
+ keys[1] !== 'expiresIn' ||
4790
+ keys[2] !== 'sessionId' ||
4791
+ typeof record.challenge !== 'string' ||
4792
+ !MESSENGER_LINKAGE_CHALLENGE_PATTERN.test(record.challenge) ||
4793
+ record.sessionId !== expectedSessionId ||
4794
+ !Number.isInteger(record.expiresIn) ||
4795
+ record.expiresIn < MIN_MESSENGER_LINKAGE_CHALLENGE_TTL_SECONDS ||
4796
+ record.expiresIn > MAX_MESSENGER_LINKAGE_CHALLENGE_TTL_SECONDS) {
4797
+ return null;
4798
+ }
4799
+ return {
4800
+ challenge: record.challenge,
4801
+ sessionId: expectedSessionId,
4802
+ expiresIn: record.expiresIn,
4803
+ };
4804
+ };
4805
+ const bootstrapHttpError = async (response, responseText, knownSecrets = []) => {
4806
+ let body;
4807
+ try {
4808
+ body = responseText ?? (await response.text());
4809
+ }
4810
+ catch (error) {
4811
+ return redactedErrorFrom(error, `Server error: ${response.status}.`, knownSecrets);
4812
+ }
4813
+ return redactedErrorFrom(`Server error: ${response.status}. ${body}`, `Server error: ${response.status}.`, knownSecrets);
4814
+ };
4815
+ const bootstrapJson = async (response, knownSecrets = []) => {
4816
+ try {
4817
+ return asRecord(await response.json());
4818
+ }
4819
+ catch (error) {
4820
+ throw redactedErrorFrom(error, 'Incorrect server response (invalid JSON)', knownSecrets);
4821
+ }
4822
+ };
4115
4823
  const UnsuccessfulStart = (reason) => ({ reason, success: false });
4116
4824
  const SuccessfulStart = (body) => ({ ...body, success: true });
4117
4825
  var ActivityState;
@@ -4142,18 +4850,25 @@ const proto = {
4142
4850
  resp: 'never-gonna-let-you-down',
4143
4851
  // regenerating id (copied other tab)
4144
4852
  reg: 'never-gonna-run-around-and-desert-you',
4145
- iframeSignal: 'tracker inside a child iframe',
4146
- iframeId: 'getting node id for child iframe',
4147
- iframeBatch: 'batch of messages from an iframe window',
4148
- parentAlive: 'signal that parent is live',
4149
- killIframe: 'stop tracker inside frame',
4150
- startIframe: 'start tracker inside frame',
4853
+ iframeSignal: 'oxvo:cross-frame:v1:handshake',
4854
+ iframeId: 'oxvo:cross-frame:v1:ready',
4855
+ iframeBatch: 'oxvo:cross-frame:v1:batch',
4856
+ parentAlive: 'oxvo:cross-frame:v1:pong',
4857
+ killIframe: 'oxvo:cross-frame:v1:stop',
4858
+ startIframe: 'oxvo:cross-frame:v1:start',
4151
4859
  // checking updates
4152
- polling: 'hello-how-are-you-im-under-the-water-please-help-me',
4860
+ polling: 'oxvo:cross-frame:v1:ping',
4153
4861
  // happens if tab is old and has outdated token but
4154
4862
  // not communicating with backend to update it (for whatever reason)
4155
4863
  reset: 'reset-your-session-please',
4156
4864
  };
4865
+ const FRAME_RATE_WINDOW_MS = 1000;
4866
+ const MAX_FRAME_ENVELOPES_PER_WINDOW = 120;
4867
+ const MAX_FRAME_MESSAGES_PER_WINDOW = 50000;
4868
+ const MAX_FRAME_BYTES_PER_WINDOW = 4 * 1024 * 1024;
4869
+ const FRAME_CAPABILITY_TTL_MS = 5 * 60 * 1000;
4870
+ const FRAME_CAPABILITY_REFRESH_LEAD_MS = 60 * 1000;
4871
+ const FRAME_PREVIOUS_CAPABILITY_GRACE_MS = 5 * 1000;
4157
4872
  class App {
4158
4873
  get tagMatcher() {
4159
4874
  return this.markerWatcher.matcher;
@@ -4174,18 +4889,32 @@ class App {
4174
4889
  this.stopCallbacks = [];
4175
4890
  this.commitCallbacks = [];
4176
4891
  this.activityState = ActivityState.NotActive;
4177
- this.version = '7.3.0'; // TODO: version compatability check inside each plugin.
4892
+ this.version = '7.4.15'; // TODO: version compatability check inside each plugin.
4178
4893
  this.socketMode = false;
4179
4894
  this.compressionThreshold = 24 * 1000;
4180
4895
  this.bc = null;
4181
4896
  this.canvasRecorder = null;
4182
4897
  this.conditionsManager = null;
4898
+ this.bootstrapAttempts = {};
4899
+ this.assistSessionGrant = null;
4183
4900
  this.canStart = false;
4184
4901
  this.rootId = null;
4185
4902
  this.pageFrames = [];
4186
4903
  this.frameOderNumber = 0;
4187
4904
  this.frameLevel = 0;
4188
4905
  this.emptyBatchCounter = 0;
4906
+ this.crossDomainFrameCapability = null;
4907
+ this.crossDomainFrameCapabilityExpiresAt = 0;
4908
+ this.crossDomainFrameSession = null;
4909
+ this.crossDomainFrameCanvas = null;
4910
+ this.crossDomainResumeAfterHandshake = false;
4911
+ this.crossDomainConnectionRequested = false;
4912
+ this.crossDomainFrames = new Map();
4913
+ this.crossDomainEnvelopeBudgets = new WeakMap();
4914
+ this.pendingCrossDomainFrames = new Map();
4915
+ this.crossDomainAuthorizationGeneration = 0;
4916
+ this.nextCrossDomainFrameOrder = 1;
4917
+ this.crossDomainCanvasConfig = null;
4189
4918
  /** used by child iframes for crossdomain only */
4190
4919
  this.parentActive = false;
4191
4920
  this.checkStatus = () => {
@@ -4193,230 +4922,201 @@ class App {
4193
4922
  };
4194
4923
  this.parentCrossDomainFrameListener = (event) => {
4195
4924
  const { data } = event;
4196
- if (!data || event.source === window)
4925
+ if (!data ||
4926
+ this.crossDomainParentOrigin === null ||
4927
+ this.crossDomainFrameNonce === null ||
4928
+ event.source !== window.parent ||
4929
+ event.origin !== this.crossDomainParentOrigin) {
4930
+ return;
4931
+ }
4932
+ if (data.line === proto.iframeId) {
4933
+ const init = parseCrossDomainFrameInit(data, proto.iframeId, {
4934
+ context: this.contextId,
4935
+ nonce: this.crossDomainFrameNonce,
4936
+ });
4937
+ if (init === null)
4938
+ return;
4939
+ const sessionChanged = this.crossDomainFrameSession !== null &&
4940
+ this.crossDomainFrameSession.sessionID !== init.session.sessionID;
4941
+ if (sessionChanged && this.activityState === ActivityState.Starting) {
4942
+ this.postHandshakeToParent();
4943
+ return;
4944
+ }
4945
+ const shouldResume = this.crossDomainResumeAfterHandshake || (sessionChanged && this.active());
4946
+ if (sessionChanged && this.active()) {
4947
+ this.stop();
4948
+ }
4949
+ this.applyCrossDomainFrameInit(init);
4950
+ if (shouldResume &&
4951
+ this.activityState !== ActivityState.Active &&
4952
+ this.activityState !== ActivityState.Starting) {
4953
+ this.crossDomainResumeAfterHandshake = false;
4954
+ this.startCrossDomainFrame(this.prevOpts);
4955
+ }
4956
+ this.debug.log('Starting secure cross-domain iframe tracking');
4957
+ return;
4958
+ }
4959
+ const expectedLine = data.line === proto.parentAlive
4960
+ ? proto.parentAlive
4961
+ : data.line === proto.startIframe
4962
+ ? proto.startIframe
4963
+ : data.line === proto.killIframe
4964
+ ? proto.killIframe
4965
+ : null;
4966
+ if (this.crossDomainFrameCapability === null ||
4967
+ expectedLine === null ||
4968
+ (expectedLine !== proto.killIframe &&
4969
+ Date.now() >= this.crossDomainFrameCapabilityExpiresAt) ||
4970
+ !matchesCrossDomainEnvelope(data, expectedLine, {
4971
+ context: this.contextId,
4972
+ nonce: this.crossDomainFrameNonce,
4973
+ capability: this.crossDomainFrameCapability,
4974
+ })) {
4197
4975
  return;
4976
+ }
4977
+ if (data.line === proto.parentAlive) {
4978
+ this.parentActive = true;
4979
+ return;
4980
+ }
4198
4981
  if (data.line === proto.startIframe) {
4199
4982
  // Avoid corrupting an in-flight start; let it complete.
4200
4983
  if (this.activityState === ActivityState.Starting)
4201
4984
  return;
4985
+ const init = parseCrossDomainFrameInit(data, proto.startIframe, {
4986
+ context: this.contextId,
4987
+ nonce: this.crossDomainFrameNonce,
4988
+ });
4989
+ if (init === null)
4990
+ return;
4202
4991
  try {
4203
4992
  if (this.active()) {
4204
4993
  this.stop();
4205
4994
  }
4206
- if (data.token) {
4207
- this.session.setSessionSecret(data.token, this.projectKey);
4208
- }
4209
- if (data.id !== undefined) {
4210
- this.rootId = data.id;
4211
- }
4212
- this.allowAppStart();
4213
- void this.start();
4995
+ this.applyCrossDomainFrameInit(init);
4996
+ this.startCrossDomainFrame(this.prevOpts);
4214
4997
  }
4215
4998
  catch (e) {
4216
- console.error('children frame restart failed:', e);
4999
+ console.error('children frame restart failed:', redactDebugValue(e));
4217
5000
  }
4218
- }
4219
- if (data.line === proto.parentAlive) {
4220
- this.parentActive = true;
4221
- }
4222
- if (data.line === proto.iframeId) {
4223
- this.parentActive = true;
4224
- this.rootId = data.id;
4225
- this.session.setSessionSecret(data.token, this.projectKey);
4226
- this.frameOderNumber = data.frameOrderNumber;
4227
- this.frameLevel = data.frameLevel;
4228
- this.debug.log('starting iframe tracking', data);
4229
- this.allowAppStart();
5001
+ return;
4230
5002
  }
4231
5003
  if (data.line === proto.killIframe) {
4232
- if (this.active()) {
5004
+ const shouldResume = this.activityState === ActivityState.Active || this.activityState === ActivityState.Starting;
5005
+ if (this.activityState !== ActivityState.NotActive) {
4233
5006
  this.stop();
4234
5007
  }
5008
+ this.clearCrossDomainFrameAuthorization();
5009
+ this.crossDomainResumeAfterHandshake = shouldResume;
5010
+ this.crossDomainConnectionRequested = shouldResume;
4235
5011
  }
4236
5012
  };
4237
- this.trackedFrames = [];
4238
- this.frameLastSeen = new Map();
4239
- this.FRAME_STALE_MS = 1500;
4240
5013
  this.crossDomainIframeListener = (event) => {
4241
- if (event.source === window)
4242
- return;
4243
5014
  const { data } = event;
4244
- if (!data)
5015
+ if (!data || event.source === window)
5016
+ return;
5017
+ if (data.line === proto.iframeSignal) {
5018
+ const handshake = parseCrossDomainHandshake(data, proto.iframeSignal, this.projectKey);
5019
+ if (handshake !== null) {
5020
+ void this.establishCrossDomainFrame(event, handshake);
5021
+ }
4245
5022
  return;
4246
- // Record liveness regardless of our own active state so the queue can prune
4247
- // stale contexts reliably once we resume dispatching commands after a cycle.
4248
- if ((data.line === proto.polling || data.line === proto.iframeSignal) && data.context) {
4249
- this.frameLastSeen.set(data.context, Date.now());
4250
5023
  }
4251
5024
  if (!this.active())
4252
5025
  return;
4253
- if (data.line === proto.iframeSignal) {
4254
- // @ts-ignore
4255
- event.source?.postMessage({ ping: true, line: proto.parentAlive }, '*');
4256
- const signalId = async () => {
4257
- if (event.source === null) {
4258
- return console.error('Couldnt connect to event.source for child iframe tracking');
4259
- }
4260
- const id = await this.checkNodeId(event.source);
4261
- if (!id) {
4262
- this.debug.log('Couldnt get node id for iframe', event.source);
4263
- return;
4264
- }
4265
- try {
4266
- if (this.trackedFrames.includes(data.context)) {
4267
- this.debug.log('Trying to observe already added iframe; ignore if its a restart');
4268
- }
4269
- else {
4270
- this.trackedFrames.push(data.context);
4271
- }
4272
- await this.waitStarted();
4273
- const token = this.session.getSessionSecret(this.projectKey);
4274
- const order = this.trackedFrames.findIndex((f) => f === data.context) + 1;
4275
- if (order === 0) {
4276
- this.debug.error('Couldnt get order number for iframe', data.context, this.trackedFrames);
4277
- }
4278
- const iframeData = {
4279
- line: proto.iframeId,
4280
- id,
4281
- token,
4282
- frameOrderNumber: order,
4283
- frameLevel: this.frameLevel + 1,
4284
- };
4285
- this.debug.log('Got child frame signal; nodeId', id, event.source, iframeData);
4286
- // @ts-ignore
4287
- event.source?.postMessage(iframeData, '*');
4288
- }
4289
- catch (e) {
4290
- console.error(e);
4291
- }
4292
- };
4293
- void signalId();
5026
+ if (data.line === proto.polling) {
5027
+ const state = this.validatedCrossDomainFrame(event, proto.polling);
5028
+ if (state !== null && this.consumeCrossDomainEnvelopeBudget(state.source)) {
5029
+ this.postToCrossDomainFrame(state, proto.parentAlive);
5030
+ }
5031
+ return;
4294
5032
  }
4295
5033
  /**
4296
5034
  * proxying messages from iframe to main body, so they can be in one batch (same indexes, etc)
4297
5035
  * plus we rewrite some of the messages to be relative to the main context/window
4298
5036
  * */
4299
5037
  if (data.line === proto.iframeBatch) {
4300
- const msgBatch = data.messages;
5038
+ const state = this.validatedCrossDomainFrame(event, proto.iframeBatch);
5039
+ if (state === null || !this.consumeCrossDomainEnvelopeBudget(state.source)) {
5040
+ return;
5041
+ }
5042
+ const batch = parseCrossDomainIframeBatch(data.messages);
5043
+ if (batch === null ||
5044
+ !this.consumeCrossDomainFrameBudget(state, batch.messages.length, batch.bytes)) {
5045
+ return;
5046
+ }
4301
5047
  const mappedMessages = [];
4302
- msgBatch.forEach((msg) => {
5048
+ batch.messages.forEach((msg) => {
5049
+ let fixedMessage = msg;
4303
5050
  if (msg[0] === 20 /* MType.MouseMove */) {
4304
- let fixedMessage = msg;
4305
- this.pageFrames.forEach((frame) => {
4306
- if (frame.contentWindow === event.source) {
4307
- const [type, x, y] = msg;
4308
- const { left, top } = frame.getBoundingClientRect();
4309
- fixedMessage = [type, x + left, y + top];
4310
- }
4311
- });
4312
- mappedMessages.push(fixedMessage);
5051
+ const [type, x, y] = msg;
5052
+ const { left, top } = state.frame.getBoundingClientRect();
5053
+ fixedMessage = [type, x + left, y + top];
4313
5054
  }
4314
5055
  if (msg[0] === 68 /* MType.MouseClick */) {
4315
- let fixedMessage = msg;
4316
- this.pageFrames.forEach((frame) => {
4317
- if (frame.contentWindow === event.source) {
4318
- const [type, id, hesitationTime, label, selector, normX, normY] = msg;
4319
- const { left, top, width, height } = frame.getBoundingClientRect();
4320
- const contentWidth = document.documentElement.scrollWidth;
4321
- const contentHeight = document.documentElement.scrollHeight;
4322
- // (normalizedX * frameWidth + frameLeftOffset)/docSize
4323
- const fullX = (normX / 100) * width + left;
4324
- const fullY = (normY / 100) * height + top;
4325
- const fixedX = fullX / contentWidth;
4326
- const fixedY = fullY / contentHeight;
4327
- fixedMessage = [
4328
- type,
4329
- id,
4330
- hesitationTime,
4331
- label,
4332
- selector,
4333
- Math.round(fixedX * 1e3) / 1e1,
4334
- Math.round(fixedY * 1e3) / 1e1,
4335
- ];
4336
- }
4337
- });
4338
- mappedMessages.push(fixedMessage);
4339
- }
4340
- if (![28 /* MType.UserID */, 29 /* MType.UserAnonymousID */, 30 /* MType.Metadata */].includes(msg[0])) {
4341
- mappedMessages.push(msg);
5056
+ const [type, id, hesitationTime, label, selector, normX, normY] = msg;
5057
+ const { left, top, width, height } = state.frame.getBoundingClientRect();
5058
+ const contentWidth = document.documentElement.scrollWidth;
5059
+ const contentHeight = document.documentElement.scrollHeight;
5060
+ const fullX = (normX / 100) * width + left;
5061
+ const fullY = (normY / 100) * height + top;
5062
+ const fixedX = fullX / contentWidth;
5063
+ const fixedY = fullY / contentHeight;
5064
+ fixedMessage = [
5065
+ type,
5066
+ id,
5067
+ hesitationTime,
5068
+ label,
5069
+ selector,
5070
+ Math.round(fixedX * 1e3) / 1e1,
5071
+ Math.round(fixedY * 1e3) / 1e1,
5072
+ ];
4342
5073
  }
5074
+ mappedMessages.push(fixedMessage);
4343
5075
  });
4344
5076
  this.messages.push(...mappedMessages);
4345
5077
  }
4346
- if (data.line === proto.polling) {
4347
- if (!this.pollingQueue.order.length) {
4348
- return;
4349
- }
4350
- this.pruneStaleFrames();
4351
- const liveSet = new Set(this.trackedFrames);
4352
- while (this.pollingQueue.order.length > 0) {
4353
- const head = this.pollingQueue.order[0];
4354
- this.pollingQueue[head] = this.pollingQueue[head].filter((ctx) => liveSet.has(ctx));
4355
- if (this.pollingQueue[head].length === 0) {
4356
- delete this.pollingQueue[head];
4357
- this.pollingQueue.order.shift();
4358
- }
4359
- else {
4360
- break;
4361
- }
5078
+ };
5079
+ this.bootChildrenFrames = async () => {
5080
+ await this.waitStarted();
5081
+ this.pruneDetachedCrossDomainFrames();
5082
+ for (const state of this.crossDomainFrames.values()) {
5083
+ const authorizationGeneration = this.crossDomainAuthorizationGeneration;
5084
+ const id = await this.checkNodeId(state.source);
5085
+ const session = this.currentCrossDomainSession();
5086
+ if (id === null ||
5087
+ session === null ||
5088
+ authorizationGeneration !== this.crossDomainAuthorizationGeneration ||
5089
+ this.crossDomainFrames.get(state.source) !== state ||
5090
+ !this.active()) {
5091
+ continue;
4362
5092
  }
4363
- if (!this.pollingQueue.order.length) {
4364
- return;
5093
+ const rotated = this.crossDomainFrameNeedsRotation(state, session.sessionID);
5094
+ if (rotated && !this.rotateCrossDomainFrameCapability(state, session.sessionID)) {
5095
+ continue;
4365
5096
  }
4366
- const nextCommand = this.pollingQueue.order[0];
4367
- if (this.pollingQueue[nextCommand].includes(data.context)) {
4368
- this.pollingQueue[nextCommand] = this.pollingQueue[nextCommand].filter((c) => c !== data.context);
4369
- const message = { line: nextCommand };
4370
- if (nextCommand === proto.startIframe) {
4371
- message.token = this.session.getSessionSecret(this.projectKey);
4372
- const targetFrame = this.pageFrames.find((f) => f.contentWindow === event.source) ||
4373
- Array.from(document.querySelectorAll('iframe')).find((f) => f.contentWindow === event.source);
4374
- if (targetFrame) {
4375
- const nodeId = targetFrame[this.options.node_id];
4376
- if (nodeId !== undefined) {
4377
- message.id = nodeId;
4378
- }
4379
- }
4380
- }
4381
- // @ts-ignore
4382
- event.source?.postMessage(message, '*');
4383
- if (this.pollingQueue[nextCommand].length === 0) {
4384
- delete this.pollingQueue[nextCommand];
4385
- this.pollingQueue.order.shift();
4386
- }
5097
+ const payload = id === null ? null : this.frameInitPayload(state, id);
5098
+ if (payload) {
5099
+ this.postToCrossDomainFrame(state, rotated ? proto.iframeId : proto.startIframe, payload);
4387
5100
  }
4388
5101
  }
4389
5102
  };
4390
- /**
4391
- * { command : [remaining iframes] }
4392
- * + order of commands
4393
- **/
4394
- this.pollingQueue = {
4395
- order: [],
4396
- };
4397
- this.addCommand = (cmd) => {
4398
- this.pruneStaleFrames();
4399
- if (this.pollingQueue[cmd]) {
4400
- this.pollingQueue[cmd] = Array.from(new Set([...this.pollingQueue[cmd], ...this.trackedFrames]));
4401
- return;
4402
- }
4403
- this.pollingQueue.order.push(cmd);
4404
- this.pollingQueue[cmd] = [...this.trackedFrames];
4405
- };
4406
- this.bootChildrenFrames = async () => {
4407
- await this.waitStarted();
4408
- this.addCommand(proto.startIframe);
4409
- };
4410
5103
  this.killChildrenFrames = () => {
4411
- this.addCommand(proto.killIframe);
5104
+ this.pruneDetachedCrossDomainFrames();
5105
+ const states = Array.from(this.crossDomainFrames.values());
5106
+ this.crossDomainAuthorizationGeneration += 1;
5107
+ this.crossDomainFrames.clear();
5108
+ this.pendingCrossDomainFrames.clear();
5109
+ states.forEach((state) => {
5110
+ this.postToCrossDomainFrame(state, proto.killIframe);
5111
+ });
4412
5112
  };
4413
5113
  this.signalIframeTracker = () => {
4414
- const thisTab = this.session.getTabId();
4415
- window.parent.postMessage({
4416
- line: proto.iframeSignal,
4417
- source: thisTab,
4418
- context: this.contextId,
4419
- }, this.options.crossdomain?.parentDomain ?? '*');
5114
+ if (this.crossDomainParentOrigin === null || this.crossDomainFrameNonce === null) {
5115
+ this.debug.error('Cross-domain iframe capture requires an explicit parentDomain and secure random support.');
5116
+ return;
5117
+ }
5118
+ this.crossDomainConnectionRequested = true;
5119
+ this.postHandshakeToParent();
4420
5120
  /**
4421
5121
  * since we need to wait uncertain amount of time
4422
5122
  * and I don't want to have recursion going on,
@@ -4428,20 +5128,16 @@ class App {
4428
5128
  let cumulativeDelay = 0;
4429
5129
  let stopAttempts = false;
4430
5130
  const checkAndSendMessage = () => {
4431
- if (stopAttempts || this.checkStatus()) {
5131
+ if (stopAttempts || !this.crossDomainConnectionRequested || this.checkStatus()) {
4432
5132
  stopAttempts = true;
4433
5133
  return;
4434
5134
  }
4435
- window.parent.postMessage({
4436
- line: proto.iframeSignal,
4437
- source: thisTab,
4438
- context: this.contextId,
4439
- }, this.options.crossdomain?.parentDomain ?? '*');
5135
+ this.postHandshakeToParent();
4440
5136
  this.debug.info('Trying to signal to parent, attempt:', retries + 1);
4441
5137
  retries++;
4442
5138
  };
4443
5139
  for (let i = 0; i < maxRetries; i++) {
4444
- if (this.checkStatus()) {
5140
+ if (!this.crossDomainConnectionRequested || this.checkStatus()) {
4445
5141
  stopAttempts = true;
4446
5142
  break;
4447
5143
  }
@@ -4459,10 +5155,10 @@ class App {
4459
5155
  this.allowAppStart();
4460
5156
  this.start(this.prevOpts, true)
4461
5157
  .then((r) => {
4462
- this.debug.info('Session restart', r);
5158
+ this.debug.info('Session restart', redactDebugValue(r));
4463
5159
  })
4464
5160
  .catch((e) => {
4465
- this.debug.error('Session restart failed', e);
5161
+ this.debug.error('Session restart failed', redactDebugValue(e));
4466
5162
  });
4467
5163
  });
4468
5164
  };
@@ -4598,9 +5294,7 @@ class App {
4598
5294
  maskTextEmails: false,
4599
5295
  maskTextNumbers: false,
4600
5296
  disableStringDict: false,
4601
- crossdomain: {
4602
- parentDomain: '*',
4603
- },
5297
+ crossdomain: {},
4604
5298
  canvas: {
4605
5299
  disableCanvas: false,
4606
5300
  fixedCanvasScaling: false,
@@ -4613,6 +5307,9 @@ class App {
4613
5307
  disableThrottling: false,
4614
5308
  };
4615
5309
  this.options = simpleMerge(defaultOptions, options);
5310
+ this.crossDomainChildOrigins = normalizeCrossDomainOrigins(this.options.crossdomain?.childOrigins);
5311
+ this.crossDomainParentOrigin = normalizeCrossDomainOrigin(this.options.crossdomain?.parentDomain);
5312
+ this.crossDomainFrameNonce = this.insideIframe ? createCrossDomainSecret() : null;
4616
5313
  if (!this.insideIframe &&
4617
5314
  !this.options.forceSingleTab &&
4618
5315
  globalThis &&
@@ -4643,7 +5340,7 @@ class App {
4643
5340
  });
4644
5341
  this.markerWatcher = new MarkerWatcher({
4645
5342
  sessionStorage: this.sessionStorage,
4646
- errLog: this.debug.error,
5343
+ errLog: (...args) => this.debug.error(...args.map((arg) => redactDebugValue(arg))),
4647
5344
  onMarkerHit: (markerId) => this.send(TagTrigger(markerId)),
4648
5345
  });
4649
5346
  this.session.attachUpdateCallback(({ userID, metadata }) => {
@@ -4659,22 +5356,33 @@ class App {
4659
5356
  }
4660
5357
  });
4661
5358
  // @deprecated (use sessionHash on start instead)
4662
- if (sessionToken != null) {
5359
+ if (sessionToken != null && !this.insideIframe) {
4663
5360
  this.session.applySessionHash(sessionToken);
4664
5361
  }
5362
+ if (this.insideIframe) {
5363
+ this.session.clearSessionSecret();
5364
+ }
4665
5365
  const thisTab = this.session.getTabId();
4666
5366
  if (this.insideIframe) {
4667
- /**
4668
- * listen for messages from parent window, so we can signal that we're alive
4669
- * */
4670
- window.addEventListener('message', this.parentCrossDomainFrameListener);
4671
- window.addEventListener('message', this.crossDomainIframeListener);
4672
- setInterval(() => {
4673
- window.parent.postMessage({
4674
- line: proto.polling,
4675
- context: this.contextId,
4676
- }, options.crossdomain?.parentDomain ?? '*');
4677
- }, 250);
5367
+ if (this.crossDomainParentOrigin !== null && this.crossDomainFrameNonce !== null) {
5368
+ /**
5369
+ * listen for messages from parent window, so we can signal that we're alive
5370
+ * */
5371
+ window.addEventListener('message', this.parentCrossDomainFrameListener);
5372
+ setInterval(() => {
5373
+ if (!this.crossDomainConnectionRequested) {
5374
+ return;
5375
+ }
5376
+ if (this.crossDomainFrameCapability === null ||
5377
+ Date.now() >=
5378
+ this.crossDomainFrameCapabilityExpiresAt - FRAME_CAPABILITY_REFRESH_LEAD_MS) {
5379
+ this.postHandshakeToParent();
5380
+ }
5381
+ else {
5382
+ this.postToParent(proto.polling);
5383
+ }
5384
+ }, 1000);
5385
+ }
4678
5386
  }
4679
5387
  else {
4680
5388
  this.initWorker();
@@ -4682,7 +5390,9 @@ class App {
4682
5390
  * if we get a signal from child iframes, we check for their node_id and send it back,
4683
5391
  * so they can act as if it was just a same-domain iframe
4684
5392
  * */
4685
- window.addEventListener('message', this.crossDomainIframeListener);
5393
+ if (this.options.crossdomain?.enabled && this.crossDomainChildOrigins.size > 0) {
5394
+ window.addEventListener('message', this.crossDomainIframeListener);
5395
+ }
4686
5396
  }
4687
5397
  if (this.bc !== null) {
4688
5398
  this.bc.postMessage({
@@ -4696,10 +5406,10 @@ class App {
4696
5406
  }, 250);
4697
5407
  this.bc.onmessage = (ev) => {
4698
5408
  if (ev.data.context === this.contextId || this.projectKey !== ev.data.projectKey) {
4699
- this.debug.log('same ctx event', ev);
5409
+ this.debug.log('same ctx event', redactDebugValue(ev.data));
4700
5410
  return;
4701
5411
  }
4702
- this.debug.log(ev);
5412
+ this.debug.log('broadcast event', redactDebugValue(ev.data));
4703
5413
  if (ev.data.line === proto.resp) {
4704
5414
  const sessionToken = ev.data.token;
4705
5415
  this.session.setSessionSecret(sessionToken, this.projectKey);
@@ -4732,15 +5442,318 @@ class App {
4732
5442
  };
4733
5443
  }
4734
5444
  }
4735
- pruneStaleFrames() {
4736
- const staleAfter = Date.now() - this.FRAME_STALE_MS;
4737
- this.trackedFrames = this.trackedFrames.filter((ctx) => {
4738
- const last = this.frameLastSeen.get(ctx);
4739
- if (last !== undefined && last >= staleAfter)
4740
- return true;
4741
- this.frameLastSeen.delete(ctx);
5445
+ postToParent(line, extra = {}) {
5446
+ if (this.crossDomainParentOrigin === null ||
5447
+ this.crossDomainFrameNonce === null ||
5448
+ this.crossDomainFrameCapability === null ||
5449
+ Date.now() >= this.crossDomainFrameCapabilityExpiresAt) {
5450
+ return;
5451
+ }
5452
+ window.parent.postMessage({
5453
+ line,
5454
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
5455
+ context: this.contextId,
5456
+ nonce: this.crossDomainFrameNonce,
5457
+ capability: this.crossDomainFrameCapability,
5458
+ ...extra,
5459
+ }, this.crossDomainParentOrigin);
5460
+ }
5461
+ postHandshakeToParent() {
5462
+ if (this.crossDomainParentOrigin === null || this.crossDomainFrameNonce === null) {
5463
+ return;
5464
+ }
5465
+ window.parent.postMessage({
5466
+ line: proto.iframeSignal,
5467
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
5468
+ context: this.contextId,
5469
+ nonce: this.crossDomainFrameNonce,
5470
+ projectKey: this.projectKey,
5471
+ }, this.crossDomainParentOrigin);
5472
+ }
5473
+ applyCrossDomainFrameInit(init) {
5474
+ this.parentActive = true;
5475
+ this.crossDomainConnectionRequested = true;
5476
+ this.crossDomainFrameCapability = init.capability;
5477
+ this.crossDomainFrameCapabilityExpiresAt = init.capabilityExpiresAt;
5478
+ this.crossDomainFrameSession = init.session;
5479
+ this.crossDomainFrameCanvas = init.canvas ?? null;
5480
+ this.rootId = init.id;
5481
+ this.frameOderNumber = init.frameOrderNumber;
5482
+ this.frameLevel = init.frameLevel;
5483
+ this.allowAppStart();
5484
+ }
5485
+ clearCrossDomainFrameAuthorization() {
5486
+ this.parentActive = false;
5487
+ this.canStart = false;
5488
+ this.crossDomainFrameCapability = null;
5489
+ this.crossDomainFrameCapabilityExpiresAt = 0;
5490
+ this.crossDomainFrameSession = null;
5491
+ this.crossDomainFrameCanvas = null;
5492
+ this.rootId = null;
5493
+ }
5494
+ findCrossDomainFrame(source) {
5495
+ const frame = this.pageFrames.find((candidate) => candidate.contentWindow === source) ??
5496
+ Array.from(document.querySelectorAll('iframe')).find((candidate) => candidate.contentWindow === source);
5497
+ if (!frame)
5498
+ return null;
5499
+ if (!this.pageFrames.includes(frame)) {
5500
+ this.pageFrames.push(frame);
5501
+ }
5502
+ return frame;
5503
+ }
5504
+ revokeCrossDomainFrame(source, expectedState) {
5505
+ const state = this.crossDomainFrames.get(source);
5506
+ if (!state || (expectedState !== undefined && state !== expectedState)) {
5507
+ return;
5508
+ }
5509
+ this.crossDomainFrames.delete(source);
5510
+ }
5511
+ pruneDetachedCrossDomainFrames() {
5512
+ this.crossDomainFrames.forEach((state, source) => {
5513
+ if (!state.frame.isConnected || state.frame.contentWindow !== source) {
5514
+ this.revokeCrossDomainFrame(source);
5515
+ }
5516
+ });
5517
+ }
5518
+ currentCrossDomainSession() {
5519
+ const session = this.session.getInfo();
5520
+ if (!session.sessionID) {
5521
+ return null;
5522
+ }
5523
+ return {
5524
+ sessionID: session.sessionID,
5525
+ projectID: session.projectID,
5526
+ startedAt: session.timestamp,
5527
+ delayMs: this.delay,
5528
+ userUUID: this.localStorage.getItem(this.options.local_uuid_key) ?? '',
5529
+ };
5530
+ }
5531
+ frameInitPayload(state, id) {
5532
+ const session = this.currentCrossDomainSession();
5533
+ if (session === null ||
5534
+ session.sessionID !== state.sessionID ||
5535
+ Date.now() >= state.capabilityExpiresAt) {
5536
+ return null;
5537
+ }
5538
+ return {
5539
+ id,
5540
+ frameOrderNumber: state.frameOrderNumber,
5541
+ frameLevel: this.frameLevel + 1,
5542
+ capabilityExpiresAt: state.capabilityExpiresAt,
5543
+ session,
5544
+ ...(this.crossDomainCanvasConfig ? { canvas: this.crossDomainCanvasConfig } : {}),
5545
+ };
5546
+ }
5547
+ rotateCrossDomainFrameCapability(state, sessionID, now = Date.now()) {
5548
+ const capability = createCrossDomainSecret();
5549
+ if (capability === null) {
5550
+ return false;
5551
+ }
5552
+ const preservePreviousCapability = state.sessionID === sessionID && now < state.capabilityExpiresAt;
5553
+ state.previousCapability = preservePreviousCapability ? state.capability : null;
5554
+ state.previousCapabilityExpiresAt = preservePreviousCapability
5555
+ ? Math.min(state.capabilityExpiresAt, now + FRAME_PREVIOUS_CAPABILITY_GRACE_MS)
5556
+ : 0;
5557
+ state.capability = capability;
5558
+ state.sessionID = sessionID;
5559
+ state.capabilityExpiresAt = now + FRAME_CAPABILITY_TTL_MS;
5560
+ return true;
5561
+ }
5562
+ crossDomainFrameNeedsRotation(state, sessionID, now = Date.now()) {
5563
+ return (state.sessionID !== sessionID ||
5564
+ now >= state.capabilityExpiresAt - FRAME_CAPABILITY_REFRESH_LEAD_MS);
5565
+ }
5566
+ postToCrossDomainFrame(state, line, extra = {}) {
5567
+ state.source.postMessage({
5568
+ line,
5569
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
5570
+ context: state.context,
5571
+ nonce: state.nonce,
5572
+ capability: state.capability,
5573
+ ...extra,
5574
+ }, state.origin);
5575
+ }
5576
+ consumeCrossDomainEnvelopeBudget(source) {
5577
+ const now = Date.now();
5578
+ let budget = this.crossDomainEnvelopeBudgets.get(source);
5579
+ if (budget === undefined || now - budget.windowStartedAt >= FRAME_RATE_WINDOW_MS) {
5580
+ budget = {
5581
+ windowStartedAt: now,
5582
+ envelopesInWindow: 0,
5583
+ };
5584
+ this.crossDomainEnvelopeBudgets.set(source, budget);
5585
+ }
5586
+ if (budget.envelopesInWindow >= MAX_FRAME_ENVELOPES_PER_WINDOW) {
5587
+ return false;
5588
+ }
5589
+ budget.envelopesInWindow += 1;
5590
+ return true;
5591
+ }
5592
+ async establishCrossDomainFrame(event, handshake) {
5593
+ if (!this.active() ||
5594
+ event.source === null ||
5595
+ !this.crossDomainChildOrigins.has(event.origin)) {
5596
+ return;
5597
+ }
5598
+ const candidateSource = event.source;
5599
+ if (!this.consumeCrossDomainEnvelopeBudget(candidateSource)) {
5600
+ return;
5601
+ }
5602
+ const frame = this.findCrossDomainFrame(candidateSource);
5603
+ if (frame === null || !frame.isConnected || frame.contentWindow === null) {
5604
+ return;
5605
+ }
5606
+ const source = frame.contentWindow;
5607
+ const existing = this.crossDomainFrames.get(source);
5608
+ if (this.pendingCrossDomainFrames.has(source)) {
5609
+ return;
5610
+ }
5611
+ const existingMatches = existing !== undefined &&
5612
+ existing.origin === event.origin &&
5613
+ existing.context === handshake.context &&
5614
+ existing.nonce === handshake.nonce &&
5615
+ existing.frame === frame;
5616
+ if (existing !== undefined && !existingMatches) {
5617
+ return;
5618
+ }
5619
+ const authorizationGeneration = this.crossDomainAuthorizationGeneration;
5620
+ const pendingToken = Symbol();
5621
+ this.pendingCrossDomainFrames.set(source, pendingToken);
5622
+ let navigated = false;
5623
+ const markNavigated = () => {
5624
+ navigated = true;
5625
+ };
5626
+ frame.addEventListener('load', markNavigated, { once: true });
5627
+ try {
5628
+ const id = await this.checkNodeId(source);
5629
+ if (id === null ||
5630
+ navigated ||
5631
+ !this.active() ||
5632
+ authorizationGeneration !== this.crossDomainAuthorizationGeneration ||
5633
+ !frame.isConnected ||
5634
+ frame.contentWindow !== source) {
5635
+ return;
5636
+ }
5637
+ const session = this.currentCrossDomainSession();
5638
+ if (session === null) {
5639
+ return;
5640
+ }
5641
+ const now = Date.now();
5642
+ let state;
5643
+ if (existingMatches) {
5644
+ state = existing;
5645
+ if (this.crossDomainFrameNeedsRotation(state, session.sessionID, now) &&
5646
+ !this.rotateCrossDomainFrameCapability(state, session.sessionID, now)) {
5647
+ return;
5648
+ }
5649
+ state.lastSeenAt = now;
5650
+ }
5651
+ else {
5652
+ const capability = createCrossDomainSecret();
5653
+ if (capability === null) {
5654
+ return;
5655
+ }
5656
+ state = {
5657
+ source,
5658
+ frame,
5659
+ origin: event.origin,
5660
+ protocolVersion: CROSS_DOMAIN_IFRAME_PROTOCOL_VERSION,
5661
+ context: handshake.context,
5662
+ nonce: handshake.nonce,
5663
+ capability,
5664
+ frameOrderNumber: this.nextCrossDomainFrameOrder++,
5665
+ sessionID: session.sessionID,
5666
+ capabilityExpiresAt: now + FRAME_CAPABILITY_TTL_MS,
5667
+ previousCapability: null,
5668
+ previousCapabilityExpiresAt: 0,
5669
+ lastSeenAt: now,
5670
+ rateWindowStartedAt: now,
5671
+ messagesInRateWindow: 0,
5672
+ bytesInRateWindow: 0,
5673
+ };
5674
+ }
5675
+ const payload = this.frameInitPayload(state, id);
5676
+ if (payload === null) {
5677
+ return;
5678
+ }
5679
+ if (!existingMatches) {
5680
+ if (existing !== undefined) {
5681
+ this.revokeCrossDomainFrame(source, existing);
5682
+ }
5683
+ this.crossDomainFrames.set(source, state);
5684
+ frame.addEventListener('load', () => {
5685
+ this.revokeCrossDomainFrame(source, state);
5686
+ }, { once: true });
5687
+ }
5688
+ this.postToCrossDomainFrame(state, proto.iframeId, payload);
5689
+ }
5690
+ finally {
5691
+ frame.removeEventListener('load', markNavigated);
5692
+ if (this.pendingCrossDomainFrames.get(source) === pendingToken) {
5693
+ this.pendingCrossDomainFrames.delete(source);
5694
+ }
5695
+ }
5696
+ }
5697
+ validatedCrossDomainFrame(event, expectedLine) {
5698
+ if (event.source === null ||
5699
+ !this.crossDomainChildOrigins.has(event.origin) ||
5700
+ event.source === window) {
5701
+ return null;
5702
+ }
5703
+ const source = event.source;
5704
+ const state = this.crossDomainFrames.get(source);
5705
+ const now = Date.now();
5706
+ const currentSessionID = this.session.getInfo().sessionID;
5707
+ if (!state ||
5708
+ state.origin !== event.origin ||
5709
+ !state.frame.isConnected ||
5710
+ state.frame.contentWindow !== source) {
5711
+ if (state) {
5712
+ this.revokeCrossDomainFrame(source);
5713
+ }
5714
+ return null;
5715
+ }
5716
+ if (currentSessionID === undefined || state.sessionID !== currentSessionID) {
5717
+ this.revokeCrossDomainFrame(source);
5718
+ return null;
5719
+ }
5720
+ const matchesCurrentCapability = now < state.capabilityExpiresAt && matchesCrossDomainEnvelope(event.data, expectedLine, state);
5721
+ const matchesPreviousCapability = state.previousCapability !== null &&
5722
+ now < state.previousCapabilityExpiresAt &&
5723
+ matchesCrossDomainEnvelope(event.data, expectedLine, {
5724
+ protocolVersion: state.protocolVersion,
5725
+ context: state.context,
5726
+ nonce: state.nonce,
5727
+ capability: state.previousCapability,
5728
+ });
5729
+ if (!matchesCurrentCapability && !matchesPreviousCapability) {
5730
+ if (now >= state.capabilityExpiresAt && now >= state.previousCapabilityExpiresAt) {
5731
+ this.revokeCrossDomainFrame(source);
5732
+ }
5733
+ return null;
5734
+ }
5735
+ if ((matchesCurrentCapability && state.previousCapability !== null) ||
5736
+ now >= state.previousCapabilityExpiresAt) {
5737
+ state.previousCapability = null;
5738
+ state.previousCapabilityExpiresAt = 0;
5739
+ }
5740
+ state.lastSeenAt = now;
5741
+ return state;
5742
+ }
5743
+ consumeCrossDomainFrameBudget(state, messageCount, bytes) {
5744
+ const now = Date.now();
5745
+ if (now - state.rateWindowStartedAt >= FRAME_RATE_WINDOW_MS) {
5746
+ state.rateWindowStartedAt = now;
5747
+ state.messagesInRateWindow = 0;
5748
+ state.bytesInRateWindow = 0;
5749
+ }
5750
+ if (state.messagesInRateWindow + messageCount > MAX_FRAME_MESSAGES_PER_WINDOW ||
5751
+ state.bytesInRateWindow + bytes > MAX_FRAME_BYTES_PER_WINDOW) {
4742
5752
  return false;
4743
- });
5753
+ }
5754
+ state.messagesInRateWindow += messageCount;
5755
+ state.bytesInRateWindow += bytes;
5756
+ return true;
4744
5757
  }
4745
5758
  allowAppStart() {
4746
5759
  this.canStart = true;
@@ -4801,6 +5814,7 @@ class App {
4801
5814
  setTimeout(() => {
4802
5815
  closing = false;
4803
5816
  }, 500);
5817
+ this.canvasRecorder?.flushPendingSnapshots();
4804
5818
  if (this.worker) {
4805
5819
  this.worker.postMessage('closing');
4806
5820
  }
@@ -4824,10 +5838,10 @@ class App {
4824
5838
  this.allowAppStart();
4825
5839
  this.start(this.prevOpts, true)
4826
5840
  .then((r) => {
4827
- this.debug.info('Worker restart, session too long', r);
5841
+ this.debug.info('Worker restart, session too long', redactDebugValue(r));
4828
5842
  })
4829
5843
  .catch((e) => {
4830
- this.debug.error('Worker restart failed', e);
5844
+ this.debug.error('Worker restart failed', redactDebugValue(e));
4831
5845
  });
4832
5846
  });
4833
5847
  }
@@ -4836,7 +5850,7 @@ class App {
4836
5850
  }
4837
5851
  else if (data.type === 'failure') {
4838
5852
  this.stop(false);
4839
- this.debug.error('worker_failed', data.reason);
5853
+ this.debug.error('worker_failed', redactDebugValue(data.reason));
4840
5854
  this._debug('worker_failed', data.reason);
4841
5855
  }
4842
5856
  else if (data.type === 'compress') {
@@ -4857,7 +5871,7 @@ class App {
4857
5871
  });
4858
5872
  })
4859
5873
  .catch((err) => {
4860
- this.debug.error('OxvoSessions compression error:', err);
5874
+ this.debug.error('OxvoSessions compression error:', redactDebugValue(err));
4861
5875
  this.worker?.postMessage({ type: 'uncompressed', batch: batch, dataType });
4862
5876
  });
4863
5877
  }
@@ -4879,18 +5893,18 @@ class App {
4879
5893
  }
4880
5894
  }
4881
5895
  _debug(context, e) {
5896
+ const redactedError = redactDebugValue(e);
4882
5897
  if (this.options.__debug_report_edp !== null) {
4883
5898
  void fetch(this.options.__debug_report_edp, {
4884
5899
  method: 'POST',
4885
5900
  headers: { 'Content-Type': 'application/json' },
4886
5901
  body: JSON.stringify({
4887
5902
  context,
4888
- // @ts-ignore
4889
- error: `${e}`,
5903
+ error: debugValueText(redactedError),
4890
5904
  }),
4891
5905
  });
4892
5906
  }
4893
- this.debug.error('OxvoSessions error: ', context, e);
5907
+ this.debug.error('OxvoSessions error: ', context, redactedError);
4894
5908
  }
4895
5909
  /**
4896
5910
  * Normal workflow: add timestamp and tab data to batch, then commit it
@@ -4904,10 +5918,7 @@ class App {
4904
5918
  return;
4905
5919
  }
4906
5920
  if (this.insideIframe) {
4907
- window.parent.postMessage({
4908
- line: proto.iframeBatch,
4909
- messages: this.messages,
4910
- }, this.options.crossdomain?.parentDomain ?? '*');
5921
+ this.postToParent(proto.iframeBatch, { messages: this.messages });
4911
5922
  this.commitCallbacks.forEach((cb) => cb(this.messages));
4912
5923
  this.messages.length = 0;
4913
5924
  return;
@@ -5063,12 +6074,230 @@ class App {
5063
6074
  },
5064
6075
  };
5065
6076
  }
6077
+ getBootstrapAttemptStorageKey(kind) {
6078
+ return `${this.options.session_token_key}_bootstrap_idempotency_${kind}`;
6079
+ }
6080
+ isValidBootstrapAttempt(value, kind, currentTime) {
6081
+ if (typeof value !== 'object' || value === null) {
6082
+ return false;
6083
+ }
6084
+ const attempt = value;
6085
+ if (attempt.version !== 1 ||
6086
+ attempt.projectKey !== this.projectKey ||
6087
+ typeof attempt.createdAt !== 'number' ||
6088
+ !Number.isFinite(attempt.createdAt) ||
6089
+ currentTime - attempt.createdAt >= BOOTSTRAP_ATTEMPT_TTL_MS ||
6090
+ typeof attempt.idempotencyKey !== 'string' ||
6091
+ attempt.idempotencyKey.length < 8 ||
6092
+ attempt.idempotencyKey.length > 128 ||
6093
+ !/^[A-Za-z0-9._:-]+$/.test(attempt.idempotencyKey) ||
6094
+ typeof attempt.intentFingerprint !== 'string' ||
6095
+ attempt.intentFingerprint.length === 0 ||
6096
+ attempt.intentFingerprint.length > MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH ||
6097
+ typeof attempt.requestTimestamp !== 'number' ||
6098
+ !Number.isFinite(attempt.requestTimestamp) ||
6099
+ typeof attempt.workerTimestamp !== 'number' ||
6100
+ !Number.isFinite(attempt.workerTimestamp) ||
6101
+ typeof attempt.requestBody !== 'string' ||
6102
+ attempt.requestBody.length > MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH) {
6103
+ return false;
6104
+ }
6105
+ try {
6106
+ const body = JSON.parse(attempt.requestBody);
6107
+ const intent = this.parseBootstrapIntent(attempt.intentFingerprint);
6108
+ return (intent?.projectKey === this.projectKey &&
6109
+ intent.kind === kind &&
6110
+ body.app?.key === this.projectKey &&
6111
+ body.session?.dry === false &&
6112
+ body.session?.offline === (kind === 'offline') &&
6113
+ typeof body.session?.token === 'string' &&
6114
+ body.session.token === intent.token &&
6115
+ body.session?.rule === intent.rule &&
6116
+ (body.user?.id ?? '') === intent.userID &&
6117
+ (body.user?.uuid ?? '') === intent.userUUID &&
6118
+ body.meta?.ts === attempt.requestTimestamp &&
6119
+ attempt.idempotencyKey.startsWith(`bootstrap:${kind}:`));
6120
+ }
6121
+ catch {
6122
+ return false;
6123
+ }
6124
+ }
6125
+ removeStoredBootstrapAttempt(kind) {
6126
+ delete this.bootstrapAttempts[kind];
6127
+ try {
6128
+ this.sessionStorage.removeItem(this.getBootstrapAttemptStorageKey(kind));
6129
+ }
6130
+ catch {
6131
+ // The in-memory copy is still cleared when storage is unavailable.
6132
+ }
6133
+ }
6134
+ loadBootstrapAttempt(kind) {
6135
+ const currentTime = Date.now();
6136
+ const inMemoryAttempt = this.bootstrapAttempts[kind];
6137
+ if (this.isValidBootstrapAttempt(inMemoryAttempt, kind, currentTime)) {
6138
+ return inMemoryAttempt;
6139
+ }
6140
+ delete this.bootstrapAttempts[kind];
6141
+ try {
6142
+ const storedAttempt = this.sessionStorage.getItem(this.getBootstrapAttemptStorageKey(kind));
6143
+ if (storedAttempt === null || storedAttempt.length > MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH) {
6144
+ if (storedAttempt !== null) {
6145
+ this.removeStoredBootstrapAttempt(kind);
6146
+ }
6147
+ return undefined;
6148
+ }
6149
+ const parsedAttempt = JSON.parse(storedAttempt);
6150
+ if (this.isValidBootstrapAttempt(parsedAttempt, kind, currentTime)) {
6151
+ this.bootstrapAttempts[kind] = parsedAttempt;
6152
+ return parsedAttempt;
6153
+ }
6154
+ }
6155
+ catch {
6156
+ // Treat corrupt or inaccessible storage as an absent pending attempt.
6157
+ }
6158
+ this.removeStoredBootstrapAttempt(kind);
6159
+ return undefined;
6160
+ }
6161
+ getOrCreateBootstrapAttempt(kind, request) {
6162
+ const existingAttempt = this.loadBootstrapAttempt(kind);
6163
+ if (existingAttempt &&
6164
+ this.bootstrapIntentsMatch(existingAttempt.intentFingerprint, request.intentFingerprint)) {
6165
+ return existingAttempt;
6166
+ }
6167
+ if (existingAttempt) {
6168
+ this.removeStoredBootstrapAttempt(kind);
6169
+ }
6170
+ const attempt = {
6171
+ version: 1,
6172
+ projectKey: this.projectKey,
6173
+ createdAt: Date.now(),
6174
+ idempotencyKey: `bootstrap:${kind}:${generateRandomId(40)}`,
6175
+ ...request,
6176
+ };
6177
+ this.bootstrapAttempts[kind] = attempt;
6178
+ try {
6179
+ this.sessionStorage.setItem(this.getBootstrapAttemptStorageKey(kind), JSON.stringify(attempt));
6180
+ }
6181
+ catch {
6182
+ // The in-memory copy still protects retries within this tracker instance.
6183
+ }
6184
+ return attempt;
6185
+ }
6186
+ clearBootstrapAttempt(kind) {
6187
+ this.removeStoredBootstrapAttempt(kind);
6188
+ }
6189
+ buildBootstrapIntentFingerprint(params) {
6190
+ const tracker = this.getTrackerInfo();
6191
+ return JSON.stringify({
6192
+ projectKey: this.projectKey,
6193
+ kind: params.kind,
6194
+ token: params.token ?? '',
6195
+ rule: params.rule ?? '',
6196
+ userID: this.session.getInfo().userID ?? '',
6197
+ userUUID: tracker.userUUID ?? '',
6198
+ });
6199
+ }
6200
+ parseBootstrapIntent(value) {
6201
+ try {
6202
+ const intent = JSON.parse(value);
6203
+ if (intent.projectKey !== this.projectKey ||
6204
+ (intent.kind !== 'recording' && intent.kind !== 'offline') ||
6205
+ typeof intent.token !== 'string' ||
6206
+ typeof intent.rule !== 'string' ||
6207
+ typeof intent.userID !== 'string' ||
6208
+ typeof intent.userUUID !== 'string') {
6209
+ return undefined;
6210
+ }
6211
+ return intent;
6212
+ }
6213
+ catch {
6214
+ return undefined;
6215
+ }
6216
+ }
6217
+ bootstrapIntentsMatch(existingFingerprint, currentFingerprint) {
6218
+ const existing = this.parseBootstrapIntent(existingFingerprint);
6219
+ const current = this.parseBootstrapIntent(currentFingerprint);
6220
+ if (!existing || !current) {
6221
+ return false;
6222
+ }
6223
+ return (existing.projectKey === current.projectKey &&
6224
+ existing.kind === current.kind &&
6225
+ existing.token === current.token &&
6226
+ (current.rule === '' || existing.rule === current.rule) &&
6227
+ (current.userID === '' || existing.userID === current.userID) &&
6228
+ (current.userUUID === '' || existing.userUUID === current.userUUID));
6229
+ }
5066
6230
  getSessionMeta() {
6231
+ const sessionInfo = this.session.getInfo();
6232
+ const metadata = { ...sessionInfo.metadata };
6233
+ Object.keys(metadata).forEach((key) => {
6234
+ if (key.trim().toLowerCase() === LINKAGE_ASSERTION_METADATA_KEY) {
6235
+ delete metadata[key];
6236
+ }
6237
+ });
5067
6238
  return {
5068
- ...this.session.getInfo(),
6239
+ ...sessionInfo,
6240
+ metadata,
5069
6241
  ...this.getTrackerInfo(),
5070
6242
  };
5071
6243
  }
6244
+ async requestMessengerLinkageChallenge() {
6245
+ const sessionId = this.session.getInfo().sessionID;
6246
+ const grantLineage = this.assistSessionGrant;
6247
+ const grant = this.getAssistSessionGrant();
6248
+ if (!sessionId || !grantLineage || !grant || grant.sessionId !== sessionId) {
6249
+ return null;
6250
+ }
6251
+ const tabId = this.session.getTabId();
6252
+ try {
6253
+ const response = await fetch(`${normalizeEndpoint(this.options.endpoint)}/v1/linkage-challenge`, {
6254
+ method: 'POST',
6255
+ headers: {
6256
+ Authorization: `Bearer ${grant.token}`,
6257
+ 'Content-Type': 'application/json',
6258
+ },
6259
+ body: JSON.stringify({ tabId }),
6260
+ cache: 'no-store',
6261
+ credentials: 'omit',
6262
+ referrerPolicy: 'no-referrer',
6263
+ });
6264
+ if (response.status !== 200) {
6265
+ return null;
6266
+ }
6267
+ const challenge = parseMessengerLinkageChallenge(await response.json(), sessionId);
6268
+ if (!challenge) {
6269
+ return null;
6270
+ }
6271
+ const currentGrant = this.getAssistSessionGrant();
6272
+ if (!currentGrant ||
6273
+ this.assistSessionGrant !== grantLineage ||
6274
+ currentGrant.sessionId !== grant.sessionId ||
6275
+ currentGrant.token !== grant.token ||
6276
+ this.session.getInfo().sessionID !== sessionId ||
6277
+ this.session.getTabId() !== tabId) {
6278
+ return null;
6279
+ }
6280
+ return challenge;
6281
+ }
6282
+ catch {
6283
+ return null;
6284
+ }
6285
+ }
6286
+ getAssistSessionGrant() {
6287
+ return this.assistSessionGrant ? { ...this.assistSessionGrant } : null;
6288
+ }
6289
+ setAssistSessionGrant(sessionId, token) {
6290
+ this.assistSessionGrant =
6291
+ typeof token === 'string' &&
6292
+ token.length > 0 &&
6293
+ token.length <= MAX_ASSIST_SESSION_GRANT_LENGTH &&
6294
+ token.trim() === token
6295
+ ? { sessionId, token }
6296
+ : null;
6297
+ }
6298
+ clearAssistSessionGrant() {
6299
+ this.assistSessionGrant = null;
6300
+ }
5072
6301
  getSessionSecret() {
5073
6302
  return this.session.getSessionSecret(this.projectKey);
5074
6303
  }
@@ -5133,12 +6362,19 @@ class App {
5133
6362
  const needReset = this.sessionStorage.getItem(this.options.session_reset_key) !== null;
5134
6363
  let needNewSessionID = startNewSession || needReset;
5135
6364
  const sessionToken = this.session.getSessionSecret(this.projectKey);
6365
+ if (needReset) {
6366
+ this.sessionStorage.removeItem(this.options.session_reset_key);
6367
+ }
5136
6368
  const storedVersion = this.sessionStorage.getItem(`${this.options.session_token_key}_version`);
5137
6369
  if (!storedVersion || storedVersion !== PROTO_VERSION) {
5138
6370
  needNewSessionID = true;
5139
6371
  this.sessionStorage.setItem(`${this.options.session_token_key}_version`, PROTO_VERSION);
5140
6372
  }
5141
- return needNewSessionID || !sessionToken;
6373
+ const isNewSession = needNewSessionID || !sessionToken;
6374
+ if (isNewSession && sessionToken) {
6375
+ this.session.clearSessionSecret();
6376
+ }
6377
+ return isNewSession;
5142
6378
  }
5143
6379
  /**
5144
6380
  * start buffering messages without starting the actual session, which gives
@@ -5194,23 +6430,48 @@ class App {
5194
6430
  async setupConditionalStart(startOpts) {
5195
6431
  this.conditionsManager = new ConditionsManager(this, startOpts);
5196
6432
  const ingestPoint = normalizeEndpoint(this.options.endpoint);
5197
- const r = await fetch(ingestPoint + '/v1/bootstrap', {
5198
- method: 'POST',
5199
- headers: {
5200
- 'Content-Type': 'application/json',
5201
- },
5202
- body: JSON.stringify(this.buildStartPayload({
5203
- timestamp: now(),
5204
- dry: true,
5205
- bufferMs: 0,
5206
- token: '',
5207
- width: window.screen.width,
5208
- height: window.screen.height,
5209
- referrer: document.referrer,
5210
- })),
5211
- });
5212
- const { session = {}, client = {}, device = {} } = await r.json();
5213
- const { token, projectId } = session;
6433
+ let r;
6434
+ try {
6435
+ r = await fetch(ingestPoint + '/v1/bootstrap', {
6436
+ method: 'POST',
6437
+ headers: {
6438
+ 'Content-Type': 'application/json',
6439
+ },
6440
+ body: JSON.stringify(this.buildStartPayload({
6441
+ timestamp: now(),
6442
+ dry: true,
6443
+ bufferMs: 0,
6444
+ token: '',
6445
+ width: window.screen.width,
6446
+ height: window.screen.height,
6447
+ referrer: document.referrer,
6448
+ })),
6449
+ });
6450
+ }
6451
+ catch (error) {
6452
+ throw redactedErrorFrom(error, 'Conditional bootstrap request failed.');
6453
+ }
6454
+ if (r.status !== 200) {
6455
+ throw await bootstrapHttpError(r);
6456
+ }
6457
+ const response = await bootstrapJson(r);
6458
+ const session = asRecord(response.session);
6459
+ const client = asRecord(response.client);
6460
+ const device = asRecord(response.device);
6461
+ const { token, projectId, assistToken } = session;
6462
+ const invalidResponseFields = [
6463
+ !isValidBootstrapCredential(token, MAX_SESSION_TOKEN_LENGTH) ? 'session.token' : null,
6464
+ typeof assistToken !== 'undefined' &&
6465
+ !isValidBootstrapCredential(assistToken, MAX_ASSIST_SESSION_GRANT_LENGTH)
6466
+ ? 'session.assistToken'
6467
+ : null,
6468
+ typeof session.id !== 'string' ? 'session.id' : null,
6469
+ typeof projectId !== 'string' ? 'session.projectId' : null,
6470
+ typeof device.id !== 'string' ? 'device.id' : null,
6471
+ ].filter((field) => field !== null);
6472
+ if (invalidResponseFields.length > 0) {
6473
+ throw new Error(`Incorrect server response (status ${r.status}; invalid fields: ${invalidResponseFields.join(', ')})`);
6474
+ }
5214
6475
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
5215
6476
  this.session.assign({ projectID: projectId });
5216
6477
  this.session.setUserInfo({
@@ -5223,12 +6484,18 @@ class App {
5223
6484
  });
5224
6485
  const onStartInfo = {
5225
6486
  sessionToken: token,
5226
- userUUID: device?.id || '',
5227
- sessionID: session?.id || '',
6487
+ assistToken,
6488
+ userUUID: device.id,
6489
+ sessionID: session.id,
5228
6490
  };
5229
- this.startCallbacks.forEach((cb) => cb(onStartInfo));
5230
- await this.conditionsManager?.fetchConditions(projectId, token);
5231
- await this.markerWatcher.fetchTags(normalizeEndpoint(this.options.endpoint), token);
6491
+ try {
6492
+ this.startCallbacks.forEach((cb) => cb(onStartInfo));
6493
+ await this.conditionsManager?.fetchConditions(projectId, token);
6494
+ await this.markerWatcher.fetchTags(normalizeEndpoint(this.options.endpoint), token);
6495
+ }
6496
+ catch (error) {
6497
+ throw redactedErrorFrom(error, 'Conditional bootstrap failed.', [token, assistToken]);
6498
+ }
5232
6499
  }
5233
6500
  /**
5234
6501
  * Starts offline session recording
@@ -5236,9 +6503,9 @@ class App {
5236
6503
  * @param {Function} onSessionSent - callback that will be called once session is fully sent
5237
6504
  * */
5238
6505
  offlineRecording(startOpts = {}, onSessionSent) {
6506
+ this.clearAssistSessionGrant();
5239
6507
  this.onSessionSent = onSessionSent;
5240
6508
  this.singleBuffer = true;
5241
- const isNewSession = this.checkSessionToken(startOpts.startNewSession);
5242
6509
  adjustTimeOrigin();
5243
6510
  this.coldStartTs = now();
5244
6511
  const saverBuffer = this.localStorage.getItem(bufferStorageKey);
@@ -5256,6 +6523,10 @@ class App {
5256
6523
  if (startOpts.startNewSession) {
5257
6524
  this.session.reset();
5258
6525
  }
6526
+ const isNewSession = this.checkSessionToken(startOpts.startNewSession);
6527
+ this.offlineRecordingSessionToken = isNewSession
6528
+ ? undefined
6529
+ : this.session.getSessionSecret(this.projectKey);
5259
6530
  this.session.assign({
5260
6531
  userID: startOpts.userID,
5261
6532
  metadata: startOpts.metadata,
@@ -5305,12 +6576,29 @@ class App {
5305
6576
  * */
5306
6577
  async uploadOfflineRecording() {
5307
6578
  this.stop(false);
5308
- const timestamp = now();
6579
+ const freshTimestamp = now();
6580
+ const bootstrapToken = this.offlineRecordingSessionToken ?? '';
6581
+ const freshRequestBody = () => JSON.stringify(this.buildStartPayload({
6582
+ timestamp: freshTimestamp,
6583
+ dry: false,
6584
+ offline: true,
6585
+ bufferMs: freshTimestamp - this.coldStartTs,
6586
+ token: bootstrapToken,
6587
+ }));
6588
+ const bootstrapAttempt = this.getOrCreateBootstrapAttempt('offline', {
6589
+ intentFingerprint: this.buildBootstrapIntentFingerprint({
6590
+ kind: 'offline',
6591
+ token: bootstrapToken,
6592
+ }),
6593
+ requestTimestamp: freshTimestamp,
6594
+ workerTimestamp: this.coldStartTs,
6595
+ requestBody: freshRequestBody(),
6596
+ });
5309
6597
  this.worker?.postMessage({
5310
6598
  type: 'start',
5311
6599
  pageNo: this.session.incPageNo(),
5312
6600
  ingestPoint: normalizeEndpoint(this.options.endpoint),
5313
- timestamp: this.coldStartTs,
6601
+ timestamp: bootstrapAttempt.workerTimestamp,
5314
6602
  url: document.URL,
5315
6603
  connAttemptCount: this.options.connAttemptCount,
5316
6604
  connAttemptGap: this.options.connAttemptGap,
@@ -5318,28 +6606,45 @@ class App {
5318
6606
  localDebug: this.options.__local_debug,
5319
6607
  });
5320
6608
  const ingestPoint = normalizeEndpoint(this.options.endpoint);
5321
- const r = await fetch(ingestPoint + '/v1/bootstrap', {
5322
- method: 'POST',
5323
- headers: {
5324
- 'Content-Type': 'application/json',
5325
- },
5326
- body: JSON.stringify(this.buildStartPayload({
5327
- timestamp,
5328
- dry: false,
5329
- bufferMs: timestamp - this.coldStartTs,
5330
- token: '',
5331
- })),
5332
- });
5333
- const { session = {}, client = {}, limits = {}, protocolVersion: offlineProtocolVersion } = await r.json();
6609
+ const headers = {
6610
+ 'Content-Type': 'application/json',
6611
+ 'Idempotency-Key': bootstrapAttempt.idempotencyKey,
6612
+ };
6613
+ let r;
6614
+ try {
6615
+ r = await fetch(ingestPoint + '/v1/bootstrap', {
6616
+ method: 'POST',
6617
+ headers,
6618
+ body: bootstrapAttempt.requestBody,
6619
+ });
6620
+ }
6621
+ catch (error) {
6622
+ throw redactedErrorFrom(error, 'Offline bootstrap request failed.', [bootstrapToken]);
6623
+ }
6624
+ if (r.status !== 200) {
6625
+ throw await bootstrapHttpError(r, undefined, [bootstrapToken]);
6626
+ }
6627
+ const response = await bootstrapJson(r, [bootstrapToken]);
6628
+ const session = asRecord(response.session);
6629
+ const client = asRecord(response.client);
6630
+ const limits = asRecord(response.limits);
6631
+ const offlineProtocolVersion = response.protocolVersion;
5334
6632
  const { token, projectId } = session;
5335
6633
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
5336
6634
  const { beacon: beaconSizeLimit } = limits;
5337
- this.worker?.postMessage({
5338
- type: 'auth',
5339
- token,
5340
- beaconSizeLimit,
5341
- protocolVersion: offlineProtocolVersion,
5342
- });
6635
+ const invalidResponseFields = [
6636
+ !isValidBootstrapCredential(token, MAX_SESSION_TOKEN_LENGTH) ? 'session.token' : null,
6637
+ typeof projectId !== 'string' ? 'session.projectId' : null,
6638
+ typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined'
6639
+ ? 'limits.beacon'
6640
+ : null,
6641
+ ].filter((field) => field !== null);
6642
+ if (invalidResponseFields.length > 0) {
6643
+ throw new Error(`Incorrect server response (status ${r.status}; invalid fields: ${invalidResponseFields.join(', ')})`);
6644
+ }
6645
+ this.session.setSessionSecret(token, this.projectKey);
6646
+ this.clearBootstrapAttempt('offline');
6647
+ this.offlineRecordingSessionToken = token;
5343
6648
  this.session.assign({ projectID: projectId });
5344
6649
  this.session.setUserInfo({
5345
6650
  userBrowser,
@@ -5349,12 +6654,83 @@ class App {
5349
6654
  userOS,
5350
6655
  userState,
5351
6656
  });
5352
- while (this.bufferedMessages1.length > 0) {
5353
- await this.flushBuffer(this.bufferedMessages1);
6657
+ this.worker?.postMessage({
6658
+ type: 'auth',
6659
+ token,
6660
+ beaconSizeLimit,
6661
+ protocolVersion: offlineProtocolVersion,
6662
+ });
6663
+ try {
6664
+ while (this.bufferedMessages1.length > 0) {
6665
+ await this.flushBuffer(this.bufferedMessages1);
6666
+ }
6667
+ }
6668
+ catch (error) {
6669
+ throw redactedErrorFrom(error, 'Offline recording upload failed.', [token, bootstrapToken]);
5354
6670
  }
5355
6671
  this.postToWorker([[-1]]);
5356
6672
  this.clearBuffers();
5357
6673
  }
6674
+ startCrossDomainFrame(startOpts = {}) {
6675
+ if (this.activityState === ActivityState.Active ||
6676
+ this.activityState === ActivityState.Starting) {
6677
+ const reason = 'OxvoSessions: trying to call `start()` on the instance that has been started already.';
6678
+ return UnsuccessfulStart(reason);
6679
+ }
6680
+ if (this.crossDomainFrameSession === null ||
6681
+ this.crossDomainFrameCapability === null ||
6682
+ this.rootId === null) {
6683
+ return UnsuccessfulStart('Cross-domain iframe handshake was not established.');
6684
+ }
6685
+ this.clearAssistSessionGrant();
6686
+ if (Object.keys(startOpts).length !== 0) {
6687
+ this.prevOpts = startOpts;
6688
+ }
6689
+ if (startOpts.startCallback) {
6690
+ this.userStartCallback = startOpts.startCallback;
6691
+ }
6692
+ try {
6693
+ adjustTimeOrigin();
6694
+ this.activityState = ActivityState.Starting;
6695
+ this.delay = this.crossDomainFrameSession.delayMs;
6696
+ this.session.clearSessionSecret();
6697
+ this.session.assign({
6698
+ sessionID: this.crossDomainFrameSession.sessionID,
6699
+ projectID: this.crossDomainFrameSession.projectID,
6700
+ timestamp: this.crossDomainFrameSession.startedAt,
6701
+ });
6702
+ const onStartInfo = {
6703
+ sessionToken: '',
6704
+ userUUID: this.crossDomainFrameSession.userUUID,
6705
+ sessionID: this.crossDomainFrameSession.sessionID,
6706
+ };
6707
+ this.startCallbacks.forEach((callback) => callback(onStartInfo));
6708
+ this.userStartCallback?.(SuccessfulStart(onStartInfo));
6709
+ this.activityState = ActivityState.Active;
6710
+ if (this.crossDomainFrameCanvas?.enabled && !this.options.canvas.disableCanvas) {
6711
+ this.canvasRecorder =
6712
+ this.canvasRecorder ??
6713
+ new CanvasRecorder(this, {
6714
+ fps: this.crossDomainFrameCanvas.fps ?? 15,
6715
+ quality: this.crossDomainFrameCanvas.quality ?? 'medium',
6716
+ isDebug: this.options.canvas.__save_canvas_locally,
6717
+ fixedScaling: this.options.canvas.fixedCanvasScaling,
6718
+ useAnimationFrame: this.options.canvas.useAnimationFrame,
6719
+ framesSupport: this.crossDomainFrameCanvas.framesSupport,
6720
+ });
6721
+ }
6722
+ this.observer.crossdomainObserve(this.rootId, this.frameOderNumber, this.frameLevel);
6723
+ this.ticker.start();
6724
+ this.canvasRecorder?.startTracking();
6725
+ return SuccessfulStart(onStartInfo);
6726
+ }
6727
+ catch (reason) {
6728
+ this.stop();
6729
+ const message = reason instanceof Error ? reason.message : String(reason);
6730
+ this.signalError(message, []);
6731
+ return UnsuccessfulStart(message);
6732
+ }
6733
+ }
5358
6734
  async _start(startOpts = {}, resetByWorker = false, conditionName) {
5359
6735
  if (Object.keys(startOpts).length !== 0) {
5360
6736
  this.prevOpts = startOpts;
@@ -5376,6 +6752,7 @@ class App {
5376
6752
  const reason = 'OxvoSessions: trying to call `start()` on the instance that has been started already.';
5377
6753
  return Promise.resolve(UnsuccessfulStart(reason));
5378
6754
  }
6755
+ this.clearAssistSessionGrant();
5379
6756
  this.activityState = ActivityState.Starting;
5380
6757
  if (!isColdStart) {
5381
6758
  adjustTimeOrigin();
@@ -5393,68 +6770,112 @@ class App {
5393
6770
  userID: userId || undefined,
5394
6771
  metadata: startOpts.metadata,
5395
6772
  });
5396
- const timestamp = now();
6773
+ const sessionToken = this.session.getSessionSecret(this.projectKey);
6774
+ const isNewSession = this.checkSessionToken(startOpts.startNewSession);
6775
+ const freshTimestamp = now();
6776
+ const freshWorkerTimestamp = isColdStart ? this.coldStartTs : freshTimestamp;
6777
+ const bootstrapToken = isNewSession ? '' : sessionToken;
6778
+ const freshRequestBody = () => JSON.stringify(this.buildStartPayload({
6779
+ timestamp: freshTimestamp,
6780
+ dry: false,
6781
+ bufferMs: this.coldStartTs ? freshTimestamp - this.coldStartTs : 0,
6782
+ token: bootstrapToken,
6783
+ rule: conditionName,
6784
+ width: window.screen.width,
6785
+ height: window.screen.height,
6786
+ }));
6787
+ const bootstrapAttempt = this.getOrCreateBootstrapAttempt('recording', {
6788
+ intentFingerprint: this.buildBootstrapIntentFingerprint({
6789
+ kind: 'recording',
6790
+ token: bootstrapToken,
6791
+ rule: conditionName,
6792
+ }),
6793
+ requestTimestamp: freshTimestamp,
6794
+ workerTimestamp: freshWorkerTimestamp,
6795
+ requestBody: freshRequestBody(),
6796
+ });
6797
+ const timestamp = bootstrapAttempt.requestTimestamp;
5397
6798
  this.worker?.postMessage({
5398
6799
  type: 'start',
5399
6800
  pageNo: this.session.incPageNo(),
5400
6801
  ingestPoint: normalizeEndpoint(this.options.endpoint),
5401
- timestamp: isColdStart ? this.coldStartTs : timestamp,
6802
+ timestamp: bootstrapAttempt.workerTimestamp,
5402
6803
  url: document.URL,
5403
6804
  connAttemptCount: this.options.connAttemptCount,
5404
6805
  connAttemptGap: this.options.connAttemptGap,
5405
6806
  tabId: this.session.getTabId(),
5406
6807
  localDebug: this.options.__local_debug,
5407
6808
  });
5408
- const sessionToken = this.session.getSessionSecret(this.projectKey);
5409
- const isNewSession = this.checkSessionToken(startOpts.startNewSession);
5410
- this.sessionStorage.removeItem(this.options.session_reset_key);
5411
- this.debug.log('OxvoSessions: starting session; need new session id?', isNewSession, 'session token: ', sessionToken);
6809
+ this.debug.log('OxvoSessions: starting session; need new session id?', isNewSession, 'has existing session token:', Boolean(sessionToken));
6810
+ const bootstrapSecrets = [sessionToken, bootstrapToken];
5412
6811
  try {
5413
6812
  const ingestPoint = normalizeEndpoint(this.options.endpoint);
6813
+ const headers = {
6814
+ 'Content-Type': 'application/json',
6815
+ 'Idempotency-Key': bootstrapAttempt.idempotencyKey,
6816
+ };
5414
6817
  const r = await window.fetch(ingestPoint + '/v1/bootstrap', {
5415
6818
  method: 'POST',
5416
- headers: {
5417
- 'Content-Type': 'application/json',
5418
- },
5419
- body: JSON.stringify(this.buildStartPayload({
5420
- timestamp,
5421
- dry: false,
5422
- bufferMs: this.coldStartTs ? timestamp - this.coldStartTs : 0,
5423
- token: isNewSession ? '' : sessionToken,
5424
- rule: conditionName,
5425
- width: window.screen.width,
5426
- height: window.screen.height,
5427
- })),
6819
+ headers,
6820
+ body: bootstrapAttempt.requestBody,
5428
6821
  });
5429
6822
  if (r.status !== 200) {
5430
6823
  const error = await r.text();
5431
- const reason = error === CANCELED ? CANCELED : `Server error: ${r.status}. ${error}`;
5432
- return UnsuccessfulStart(reason);
6824
+ if (error === CANCELED) {
6825
+ this.stop();
6826
+ this.signalError(CANCELED, []);
6827
+ return UnsuccessfulStart(CANCELED);
6828
+ }
6829
+ throw await bootstrapHttpError(r, error, bootstrapSecrets);
5433
6830
  }
5434
6831
  if (!this.worker && !this.insideIframe) {
5435
6832
  const reason = 'no worker found after start request (this should not happen in real world)';
5436
- this.signalError(reason, []);
5437
- return UnsuccessfulStart(reason);
5438
- }
5439
- const { session = {}, device = {}, client = {}, limits = {}, canvas = {}, flags = {}, protocolVersion, } = await r.json();
5440
- const { token, id: sessionID, projectId: projectID, delayMs: delay, startedAt: startTimestamp, } = session;
6833
+ throw new Error(reason);
6834
+ }
6835
+ const response = await bootstrapJson(r, bootstrapSecrets);
6836
+ const session = asRecord(response.session);
6837
+ const device = asRecord(response.device);
6838
+ const client = asRecord(response.client);
6839
+ const limits = asRecord(response.limits);
6840
+ const canvas = asRecord(response.canvas);
6841
+ const flags = asRecord(response.flags);
6842
+ const protocolVersion = response.protocolVersion;
6843
+ const { token, assistToken, id: sessionID, projectId: projectID, delayMs: delay, startedAt: startTimestamp, } = session;
5441
6844
  const { id: userUUID } = device;
5442
6845
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
5443
6846
  const { beacon: beaconSizeLimit, compressAt: compressionThreshold } = limits;
5444
6847
  const { enabled: canvasEnabled, quality: canvasQuality, fps: canvasFPS, framesSupport, } = canvas;
5445
6848
  const socketOnly = flags?.socketOnly;
5446
- if (typeof token !== 'string' ||
5447
- typeof userUUID !== 'string' ||
5448
- (typeof startTimestamp !== 'number' && typeof startTimestamp !== 'undefined') ||
5449
- typeof sessionID !== 'string' ||
5450
- typeof delay !== 'number' ||
5451
- (typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined')) {
5452
- const reason = `Incorrect server response: ${JSON.stringify(r)}`;
5453
- this.signalError(reason, []);
5454
- return UnsuccessfulStart(reason);
5455
- }
6849
+ const invalidResponseFields = [
6850
+ !isValidBootstrapCredential(token, MAX_SESSION_TOKEN_LENGTH) ? 'session.token' : null,
6851
+ typeof assistToken !== 'undefined' &&
6852
+ !isValidBootstrapCredential(assistToken, MAX_ASSIST_SESSION_GRANT_LENGTH)
6853
+ ? 'session.assistToken'
6854
+ : null,
6855
+ typeof userUUID !== 'string' ? 'device.id' : null,
6856
+ typeof startTimestamp !== 'number' && typeof startTimestamp !== 'undefined'
6857
+ ? 'session.startedAt'
6858
+ : null,
6859
+ typeof sessionID !== 'string' ? 'session.id' : null,
6860
+ typeof projectID !== 'string' ? 'session.projectId' : null,
6861
+ typeof delay !== 'number' ? 'session.delayMs' : null,
6862
+ typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined'
6863
+ ? 'limits.beacon'
6864
+ : null,
6865
+ ].filter((field) => field !== null);
6866
+ if (invalidResponseFields.length > 0) {
6867
+ throw new Error(`Incorrect server response (status ${r.status}; invalid fields: ${invalidResponseFields.join(', ')})`);
6868
+ }
6869
+ bootstrapSecrets.push(token, assistToken);
6870
+ this.crossDomainCanvasConfig = {
6871
+ enabled: canvasEnabled === true,
6872
+ ...(['low', 'medium', 'high'].includes(canvasQuality) ? { quality: canvasQuality } : {}),
6873
+ ...(typeof canvasFPS === 'number' ? { fps: canvasFPS } : {}),
6874
+ ...(typeof framesSupport === 'boolean' ? { framesSupport } : {}),
6875
+ };
5456
6876
  this.delay = delay;
5457
6877
  this.session.setSessionSecret(token, this.projectKey);
6878
+ this.clearBootstrapAttempt('recording');
5458
6879
  if (sessionToken && sessionToken !== token) {
5459
6880
  this.bc?.postMessage({
5460
6881
  type: proto.reset,
@@ -5474,6 +6895,7 @@ class App {
5474
6895
  timestamp: startTimestamp || timestamp,
5475
6896
  projectID,
5476
6897
  });
6898
+ this.setAssistSessionGrant(sessionID, assistToken);
5477
6899
  if (socketOnly) {
5478
6900
  this.socketMode = true;
5479
6901
  this.worker?.postMessage('stop');
@@ -5495,7 +6917,7 @@ class App {
5495
6917
  Object.entries(this.session.getInfo().metadata).forEach(([key, value]) => this.send(Metadata(key, value)));
5496
6918
  this.localStorage.setItem(this.options.local_uuid_key, userUUID);
5497
6919
  this.compressionThreshold = compressionThreshold;
5498
- const onStartInfo = { sessionToken: token, userUUID, sessionID };
6920
+ const onStartInfo = { sessionToken: token, assistToken, userUUID, sessionID };
5499
6921
  // TODO: start as early as possible (before receiving the token)
5500
6922
  /** after start */
5501
6923
  this.startCallbacks.forEach((cb) => cb(onStartInfo)); // MBTODO: callbacks after DOM "mounted" (observed)
@@ -5543,7 +6965,6 @@ class App {
5543
6965
  }
5544
6966
  catch (reason) {
5545
6967
  this.stop();
5546
- this.session.reset();
5547
6968
  if (!reason) {
5548
6969
  console.error('Unknown error during start');
5549
6970
  this.signalError('Unknown error', []);
@@ -5553,8 +6974,9 @@ class App {
5553
6974
  this.signalError(CANCELED, []);
5554
6975
  return UnsuccessfulStart(CANCELED);
5555
6976
  }
5556
- this._debug('session_start', reason);
5557
- const errorMessage = reason instanceof Error ? reason.message : reason.toString();
6977
+ const redactedError = redactedErrorFrom(reason, 'Session start failed.', bootstrapSecrets);
6978
+ this._debug('session_start', redactedError);
6979
+ const errorMessage = redactedError.message;
5558
6980
  this.signalError(errorMessage, []);
5559
6981
  return UnsuccessfulStart(errorMessage);
5560
6982
  }
@@ -5569,6 +6991,23 @@ class App {
5569
6991
  }, 100);
5570
6992
  });
5571
6993
  }
6994
+ async waitCrossDomainStart(timeoutMs = 15000) {
6995
+ if (this.canStart)
6996
+ return true;
6997
+ return new Promise((resolve) => {
6998
+ const interval = setInterval(() => {
6999
+ if (!this.canStart)
7000
+ return;
7001
+ clearInterval(interval);
7002
+ clearTimeout(timeout);
7003
+ resolve(true);
7004
+ }, 100);
7005
+ const timeout = setTimeout(() => {
7006
+ clearInterval(interval);
7007
+ resolve(false);
7008
+ }, timeoutMs);
7009
+ });
7010
+ }
5572
7011
  async waitStarted() {
5573
7012
  return this.waitStatus(ActivityState.Active);
5574
7013
  }
@@ -5596,20 +7035,35 @@ class App {
5596
7035
  return Promise.resolve(UnsuccessfulStart(reason));
5597
7036
  }
5598
7037
  if (this.insideIframe) {
7038
+ if (this.crossDomainParentOrigin === null || this.crossDomainFrameNonce === null) {
7039
+ return UnsuccessfulStart('Cross-domain iframe capture requires an explicit parentDomain and secure random support.');
7040
+ }
5599
7041
  this.signalIframeTracker();
5600
7042
  }
5601
- if (!document.hidden) {
7043
+ const startWhenReady = async () => {
7044
+ if (this.insideIframe) {
7045
+ const ready = await this.waitCrossDomainStart();
7046
+ if (!ready) {
7047
+ this.crossDomainConnectionRequested = false;
7048
+ this.crossDomainResumeAfterHandshake = false;
7049
+ this.clearCrossDomainFrameAuthorization();
7050
+ return UnsuccessfulStart('Cross-domain iframe handshake timed out.');
7051
+ }
7052
+ return this.startCrossDomainFrame(args[0]);
7053
+ }
5602
7054
  await this.waitStart();
5603
7055
  return this._start(...args);
7056
+ };
7057
+ if (!document.hidden) {
7058
+ return startWhenReady();
5604
7059
  }
5605
7060
  else {
5606
7061
  return new Promise((resolve) => {
5607
7062
  const onVisibilityChange = async () => {
5608
7063
  if (!document.hidden) {
5609
- await this.waitStart();
5610
7064
  // eslint-disable-next-line
5611
7065
  document.removeEventListener('visibilitychange', onVisibilityChange);
5612
- resolve(this._start(...args));
7066
+ resolve(startWhenReady());
5613
7067
  }
5614
7068
  };
5615
7069
  // eslint-disable-next-line
@@ -5645,6 +7099,7 @@ class App {
5645
7099
  };
5646
7100
  }
5647
7101
  stop(stopWorker = true) {
7102
+ this.clearAssistSessionGrant();
5648
7103
  if (this.activityState !== ActivityState.NotActive) {
5649
7104
  try {
5650
7105
  if (this.options.crossdomain?.enabled) {
@@ -5670,6 +7125,11 @@ class App {
5670
7125
  this.debug.log('OxvoSessions tracking stopped.');
5671
7126
  }
5672
7127
  }
7128
+ if (this.insideIframe) {
7129
+ this.crossDomainConnectionRequested = false;
7130
+ this.crossDomainResumeAfterHandshake = false;
7131
+ this.clearCrossDomainFrameAuthorization();
7132
+ }
5673
7133
  }
5674
7134
  }
5675
7135
 
@@ -7320,10 +8780,14 @@ function Fonts (app) {
7320
8780
  }
7321
8781
 
7322
8782
  function axiosSpy (app, instance, opts, sanitize, stringify) {
7323
- app.debug.log('OxvoSessions: attaching axios spy to instance', instance);
8783
+ app.debug.log('OxvoSessions: attaching axios spy to configured instance');
7324
8784
  function captureResponseData(axiosResponseObj) {
7325
- app.debug.log('OxvoSessions: capturing axios response data', axiosResponseObj);
7326
- const { headers: reqHs, data: reqData, method, url, baseURL } = axiosResponseObj.config;
8785
+ app.debug.log('OxvoSessions: capturing axios response data', {
8786
+ method: axiosResponseObj.config.method,
8787
+ status: axiosResponseObj.status,
8788
+ url: axiosResponseObj.config.url,
8789
+ });
8790
+ const { headers: reqHs, data: reqData, method, url } = axiosResponseObj.config;
7327
8791
  const { data: rData, headers: rHs, status: globStatus, response } = axiosResponseObj;
7328
8792
  const { data: resData, headers: resHs, status: resStatus } = response || {};
7329
8793
  const ihOpt = opts.ignoreHeaders;
@@ -7382,11 +8846,18 @@ function axiosSpy (app, instance, opts, sanitize, stringify) {
7382
8846
  }
7383
8847
  const requestStart = axiosResponseObj.config.__oxvosessions_timing;
7384
8848
  const duration = performance.now() - requestStart;
7385
- app.debug.log('OxvoSessions: final req object', reqResInfo);
8849
+ app.debug.log('OxvoSessions: sanitized axios request is ready', {
8850
+ method: reqResInfo.method,
8851
+ status: reqResInfo.status,
8852
+ url: reqResInfo.url,
8853
+ });
7386
8854
  app.send(NetworkRequest('xhr', String(method), String(reqResInfo.url), stringify(reqResInfo.request), stringify(reqResInfo.response), reqResInfo.status, requestStart + getTimeOrigin(), duration, 0));
7387
8855
  }
7388
8856
  function getStartTime(config) {
7389
- app.debug.log('OxvoSessions: capturing API request', config);
8857
+ app.debug.log('OxvoSessions: capturing axios request', {
8858
+ method: config.method,
8859
+ url: config.url,
8860
+ });
7390
8861
  config.__oxvosessions_timing = performance.now();
7391
8862
  if (opts.sessionTokenHeader) {
7392
8863
  const header = typeof opts.sessionTokenHeader === 'string'
@@ -7406,17 +8877,23 @@ function axiosSpy (app, instance, opts, sanitize, stringify) {
7406
8877
  return response;
7407
8878
  }
7408
8879
  function captureNetworkError(error) {
7409
- app.debug.log('OxvoSessions: capturing API request error', error);
8880
+ app.debug.log('OxvoSessions: capturing axios request error', {
8881
+ hasResponse: Boolean(error.response),
8882
+ isAxiosError: isAxiosError(error),
8883
+ message: typeof error.message === 'string' ? error.message : undefined,
8884
+ });
7410
8885
  if (isAxiosError(error) && Boolean(error.response)) {
7411
8886
  captureResponseData(error.response);
7412
8887
  }
7413
8888
  else if (error instanceof Error) {
7414
8889
  app.send(getExceptionMessage(error, []));
7415
8890
  }
8891
+ // Axios rejection handlers must preserve the original Axios error object for downstream code.
8892
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
7416
8893
  return Promise.reject(error);
7417
8894
  }
7418
- function logRequestError(ev) {
7419
- app.debug.log('OxvoSessions: failed API request, skipping', ev);
8895
+ function logRequestError() {
8896
+ app.debug.log('OxvoSessions: failed axios request, skipping');
7420
8897
  }
7421
8898
  const reqInt = instance.interceptors.request.use(getStartTime, logRequestError, {
7422
8899
  synchronous: true,
@@ -9229,7 +10706,7 @@ class ConstantProperties {
9229
10706
  user_id: this.user_id,
9230
10707
  distinct_id: this.deviceId,
9231
10708
  sdk_edition: 'web',
9232
- sdk_version: '7.3.0',
10709
+ sdk_version: '7.4.15',
9233
10710
  timezone: getUTCOffsetString(),
9234
10711
  search_engine: this.searchEngine,
9235
10712
  };
@@ -9812,6 +11289,8 @@ class Batcher {
9812
11289
  }
9813
11290
 
9814
11291
  const STORAGEKEY = '__or_sdk_analytics_token';
11292
+ const START_ATTEMPT_STORAGE_KEY = '__or_sdk_analytics_start_attempt';
11293
+ const START_ATTEMPT_TTL_MS = 24 * 60 * 60 * 1000;
9815
11294
  const normalizeIngestPoint = (ingest) => {
9816
11295
  const base = ingest.replace(/\/$/, '');
9817
11296
  return base.endsWith('/oxvo') ? base : `${base}/oxvo`;
@@ -9835,6 +11314,7 @@ class Analytics {
9835
11314
  constructor(options) {
9836
11315
  this.token = null;
9837
11316
  this.standalone = false;
11317
+ this.pendingStartAttempt = null;
9838
11318
  this._getToken = () => {
9839
11319
  if (this.standalone) {
9840
11320
  return this.token;
@@ -9878,9 +11358,14 @@ class Analytics {
9878
11358
  timezone: defaultFields.timezone,
9879
11359
  search_engine: defaultFields.search_engine,
9880
11360
  };
11361
+ const attempt = this.startAttempt(JSON.stringify(data));
9881
11362
  const resp = await fetch(apiEdp, {
9882
11363
  method: 'POST',
9883
- body: JSON.stringify(data),
11364
+ headers: {
11365
+ 'Content-Type': 'application/json',
11366
+ 'Idempotency-Key': attempt.idempotencyKey,
11367
+ },
11368
+ body: attempt.requestBody,
9884
11369
  });
9885
11370
  if (!resp.ok) {
9886
11371
  throw new Error(`HTTP error! status: ${resp.status}`);
@@ -9889,6 +11374,7 @@ class Analytics {
9889
11374
  if (result.token) {
9890
11375
  this.token = result.token;
9891
11376
  this.sessionStorage.setItem(STORAGEKEY, result.token);
11377
+ this.clearStartAttempt();
9892
11378
  }
9893
11379
  else {
9894
11380
  throw new Error('No token received from server');
@@ -9915,6 +11401,7 @@ class Analytics {
9915
11401
  if (this.standalone) {
9916
11402
  this.token = null;
9917
11403
  this.sessionStorage.setItem(STORAGEKEY, '');
11404
+ this.clearStartAttempt();
9918
11405
  }
9919
11406
  };
9920
11407
  /**
@@ -9953,6 +11440,91 @@ class Analytics {
9953
11440
  this.init();
9954
11441
  }
9955
11442
  }
11443
+ loadStartAttempt() {
11444
+ if (this.isValidStartAttempt(this.pendingStartAttempt)) {
11445
+ return this.pendingStartAttempt;
11446
+ }
11447
+ this.pendingStartAttempt = null;
11448
+ let serialized;
11449
+ try {
11450
+ serialized = this.sessionStorage.getItem(START_ATTEMPT_STORAGE_KEY);
11451
+ }
11452
+ catch {
11453
+ return null;
11454
+ }
11455
+ if (!serialized || serialized.length > 1024 * 1024) {
11456
+ return null;
11457
+ }
11458
+ try {
11459
+ const attempt = JSON.parse(serialized);
11460
+ if (this.isValidStartAttempt(attempt)) {
11461
+ this.pendingStartAttempt = attempt;
11462
+ return attempt;
11463
+ }
11464
+ }
11465
+ catch {
11466
+ // Treat corrupt storage as an absent pending attempt.
11467
+ }
11468
+ return null;
11469
+ }
11470
+ isValidStartAttempt(value) {
11471
+ if (typeof value !== 'object' || value === null) {
11472
+ return false;
11473
+ }
11474
+ const attempt = value;
11475
+ const currentTime = Date.now();
11476
+ if (attempt.version !== 1 ||
11477
+ attempt.projectKey !== this.projectKey ||
11478
+ typeof attempt.createdAt !== 'number' ||
11479
+ !Number.isFinite(attempt.createdAt) ||
11480
+ attempt.createdAt > currentTime ||
11481
+ currentTime - attempt.createdAt >= START_ATTEMPT_TTL_MS ||
11482
+ typeof attempt.idempotencyKey !== 'string' ||
11483
+ attempt.idempotencyKey.length < 8 ||
11484
+ attempt.idempotencyKey.length > 128 ||
11485
+ !/^[A-Za-z0-9._:-]+$/.test(attempt.idempotencyKey) ||
11486
+ typeof attempt.requestBody !== 'string' ||
11487
+ attempt.requestBody.length > 1024 * 1024) {
11488
+ return false;
11489
+ }
11490
+ try {
11491
+ const request = JSON.parse(attempt.requestBody);
11492
+ return request.projectKey === this.projectKey;
11493
+ }
11494
+ catch {
11495
+ return false;
11496
+ }
11497
+ }
11498
+ startAttempt(requestBody) {
11499
+ const existing = this.loadStartAttempt();
11500
+ if (existing) {
11501
+ return existing;
11502
+ }
11503
+ const attempt = {
11504
+ version: 1,
11505
+ projectKey: this.projectKey,
11506
+ createdAt: Date.now(),
11507
+ idempotencyKey: `insights:start:${generateRandomId(40)}`,
11508
+ requestBody,
11509
+ };
11510
+ this.pendingStartAttempt = attempt;
11511
+ try {
11512
+ this.sessionStorage.setItem(START_ATTEMPT_STORAGE_KEY, JSON.stringify(attempt));
11513
+ }
11514
+ catch {
11515
+ // The in-memory copy still protects retries within this Analytics instance.
11516
+ }
11517
+ return attempt;
11518
+ }
11519
+ clearStartAttempt() {
11520
+ this.pendingStartAttempt = null;
11521
+ try {
11522
+ this.sessionStorage.setItem(START_ATTEMPT_STORAGE_KEY, '');
11523
+ }
11524
+ catch {
11525
+ // The in-memory copy is still cleared when storage is unavailable.
11526
+ }
11527
+ }
9956
11528
  }
9957
11529
 
9958
11530
  const Messages = _Messages;
@@ -9999,7 +11571,7 @@ class API {
9999
11571
  this.signalStartIssue = (reason, missingApi) => {
10000
11572
  const doNotTrack = this.checkDoNotTrack();
10001
11573
  console.log("Tracker couldn't start due to:", JSON.stringify({
10002
- trackerVersion: '7.3.0',
11574
+ trackerVersion: '7.4.15',
10003
11575
  siteKey: this.options.siteKey,
10004
11576
  doNotTrack,
10005
11577
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
@@ -10365,6 +11937,12 @@ class API {
10365
11937
  }
10366
11938
  return this.app.getSessionSecret();
10367
11939
  }
11940
+ requestMessengerLinkageChallenge() {
11941
+ if (this.app === null) {
11942
+ return Promise.resolve(null);
11943
+ }
11944
+ return this.app.requestMessengerLinkageChallenge();
11945
+ }
10368
11946
  getSessionMeta() {
10369
11947
  if (this.app === null) {
10370
11948
  return null;