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