@oxvo/browser 7.3.4 → 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.
package/dist/cjs/entry.js CHANGED
@@ -2592,6 +2592,220 @@ function parseCrossDomainIframeBatch(messages) {
2592
2592
  return { messages: messages, bytes };
2593
2593
  }
2594
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
+
2595
2809
  const LogLevel = {
2596
2810
  Verbose: 5,
2597
2811
  Log: 4,
@@ -2606,26 +2820,22 @@ class Logger {
2606
2820
  };
2607
2821
  this.info = (...args) => {
2608
2822
  if (this.shouldLog(LogLevel.Verbose)) {
2609
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2610
- console.info(...args);
2823
+ console.info(...redactDebugArgs(args));
2611
2824
  }
2612
2825
  };
2613
2826
  this.log = (...args) => {
2614
2827
  if (this.shouldLog(LogLevel.Log)) {
2615
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2616
- console.log(...args);
2828
+ console.log(...redactDebugArgs(args));
2617
2829
  }
2618
2830
  };
2619
2831
  this.warn = (...args) => {
2620
2832
  if (this.shouldLog(LogLevel.Warnings)) {
2621
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2622
- console.warn(...args);
2833
+ console.warn(...redactDebugArgs(args));
2623
2834
  }
2624
2835
  };
2625
2836
  this.error = (...args) => {
2626
2837
  if (this.shouldLog(LogLevel.Errors)) {
2627
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2628
- console.error(...args);
2838
+ console.error(...redactDebugArgs(args));
2629
2839
  }
2630
2840
  };
2631
2841
  this.level = debugLevel;
@@ -4559,6 +4769,61 @@ const bufferStorageKey = 'or_buffer_1';
4559
4769
  const PROTO_VERSION = '2';
4560
4770
  const BOOTSTRAP_ATTEMPT_TTL_MS = 24 * 60 * 60 * 1000;
4561
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
+ };
4562
4827
  const UnsuccessfulStart = (reason) => ({ reason, success: false });
4563
4828
  const SuccessfulStart = (body) => ({ ...body, success: true });
4564
4829
  var ActivityState;
@@ -4628,13 +4893,14 @@ class App {
4628
4893
  this.stopCallbacks = [];
4629
4894
  this.commitCallbacks = [];
4630
4895
  this.activityState = ActivityState.NotActive;
4631
- this.version = '7.3.4'; // TODO: version compatability check inside each plugin.
4896
+ this.version = '7.4.15'; // TODO: version compatability check inside each plugin.
4632
4897
  this.socketMode = false;
4633
4898
  this.compressionThreshold = 24 * 1000;
4634
4899
  this.bc = null;
4635
4900
  this.canvasRecorder = null;
4636
4901
  this.conditionsManager = null;
4637
4902
  this.bootstrapAttempts = {};
4903
+ this.assistSessionGrant = null;
4638
4904
  this.canStart = false;
4639
4905
  this.rootId = null;
4640
4906
  this.pageFrames = [];
@@ -4734,7 +5000,7 @@ class App {
4734
5000
  this.startCrossDomainFrame(this.prevOpts);
4735
5001
  }
4736
5002
  catch (e) {
4737
- console.error('children frame restart failed:', e);
5003
+ console.error('children frame restart failed:', redactDebugValue(e));
4738
5004
  }
4739
5005
  return;
4740
5006
  }
@@ -4893,10 +5159,10 @@ class App {
4893
5159
  this.allowAppStart();
4894
5160
  this.start(this.prevOpts, true)
4895
5161
  .then((r) => {
4896
- this.debug.info('Session restart', r);
5162
+ this.debug.info('Session restart', redactDebugValue(r));
4897
5163
  })
4898
5164
  .catch((e) => {
4899
- this.debug.error('Session restart failed', e);
5165
+ this.debug.error('Session restart failed', redactDebugValue(e));
4900
5166
  });
4901
5167
  });
4902
5168
  };
@@ -5078,7 +5344,7 @@ class App {
5078
5344
  });
5079
5345
  this.markerWatcher = new MarkerWatcher({
5080
5346
  sessionStorage: this.sessionStorage,
5081
- errLog: this.debug.error,
5347
+ errLog: (...args) => this.debug.error(...args.map((arg) => redactDebugValue(arg))),
5082
5348
  onMarkerHit: (markerId) => this.send(TagTrigger(markerId)),
5083
5349
  });
5084
5350
  this.session.attachUpdateCallback(({ userID, metadata }) => {
@@ -5144,10 +5410,10 @@ class App {
5144
5410
  }, 250);
5145
5411
  this.bc.onmessage = (ev) => {
5146
5412
  if (ev.data.context === this.contextId || this.projectKey !== ev.data.projectKey) {
5147
- this.debug.log('same ctx event', ev);
5413
+ this.debug.log('same ctx event', redactDebugValue(ev.data));
5148
5414
  return;
5149
5415
  }
5150
- this.debug.log(ev);
5416
+ this.debug.log('broadcast event', redactDebugValue(ev.data));
5151
5417
  if (ev.data.line === proto.resp) {
5152
5418
  const sessionToken = ev.data.token;
5153
5419
  this.session.setSessionSecret(sessionToken, this.projectKey);
@@ -5576,10 +5842,10 @@ class App {
5576
5842
  this.allowAppStart();
5577
5843
  this.start(this.prevOpts, true)
5578
5844
  .then((r) => {
5579
- this.debug.info('Worker restart, session too long', r);
5845
+ this.debug.info('Worker restart, session too long', redactDebugValue(r));
5580
5846
  })
5581
5847
  .catch((e) => {
5582
- this.debug.error('Worker restart failed', e);
5848
+ this.debug.error('Worker restart failed', redactDebugValue(e));
5583
5849
  });
5584
5850
  });
5585
5851
  }
@@ -5588,7 +5854,7 @@ class App {
5588
5854
  }
5589
5855
  else if (data.type === 'failure') {
5590
5856
  this.stop(false);
5591
- this.debug.error('worker_failed', data.reason);
5857
+ this.debug.error('worker_failed', redactDebugValue(data.reason));
5592
5858
  this._debug('worker_failed', data.reason);
5593
5859
  }
5594
5860
  else if (data.type === 'compress') {
@@ -5609,7 +5875,7 @@ class App {
5609
5875
  });
5610
5876
  })
5611
5877
  .catch((err) => {
5612
- this.debug.error('OxvoSessions compression error:', err);
5878
+ this.debug.error('OxvoSessions compression error:', redactDebugValue(err));
5613
5879
  this.worker?.postMessage({ type: 'uncompressed', batch: batch, dataType });
5614
5880
  });
5615
5881
  }
@@ -5631,18 +5897,18 @@ class App {
5631
5897
  }
5632
5898
  }
5633
5899
  _debug(context, e) {
5900
+ const redactedError = redactDebugValue(e);
5634
5901
  if (this.options.__debug_report_edp !== null) {
5635
5902
  void fetch(this.options.__debug_report_edp, {
5636
5903
  method: 'POST',
5637
5904
  headers: { 'Content-Type': 'application/json' },
5638
5905
  body: JSON.stringify({
5639
5906
  context,
5640
- // @ts-ignore
5641
- error: `${e}`,
5907
+ error: debugValueText(redactedError),
5642
5908
  }),
5643
5909
  });
5644
5910
  }
5645
- this.debug.error('OxvoSessions error: ', context, e);
5911
+ this.debug.error('OxvoSessions error: ', context, redactedError);
5646
5912
  }
5647
5913
  /**
5648
5914
  * Normal workflow: add timestamp and tab data to batch, then commit it
@@ -5966,11 +6232,76 @@ class App {
5966
6232
  (current.userUUID === '' || existing.userUUID === current.userUUID));
5967
6233
  }
5968
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
+ });
5969
6242
  return {
5970
- ...this.session.getInfo(),
6243
+ ...sessionInfo,
6244
+ metadata,
5971
6245
  ...this.getTrackerInfo(),
5972
6246
  };
5973
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
+ }
5974
6305
  getSessionSecret() {
5975
6306
  return this.session.getSessionSecret(this.projectKey);
5976
6307
  }
@@ -6103,23 +6434,48 @@ class App {
6103
6434
  async setupConditionalStart(startOpts) {
6104
6435
  this.conditionsManager = new ConditionsManager(this, startOpts);
6105
6436
  const ingestPoint = normalizeEndpoint(this.options.endpoint);
6106
- const r = await fetch(ingestPoint + '/v1/bootstrap', {
6107
- method: 'POST',
6108
- headers: {
6109
- 'Content-Type': 'application/json',
6110
- },
6111
- body: JSON.stringify(this.buildStartPayload({
6112
- timestamp: now(),
6113
- dry: true,
6114
- bufferMs: 0,
6115
- token: '',
6116
- width: window.screen.width,
6117
- height: window.screen.height,
6118
- referrer: document.referrer,
6119
- })),
6120
- });
6121
- const { session = {}, client = {}, device = {} } = await r.json();
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);
6122
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
+ }
6123
6479
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
6124
6480
  this.session.assign({ projectID: projectId });
6125
6481
  this.session.setUserInfo({
@@ -6132,13 +6488,18 @@ class App {
6132
6488
  });
6133
6489
  const onStartInfo = {
6134
6490
  sessionToken: token,
6135
- assistToken: typeof assistToken === 'string' ? assistToken : undefined,
6136
- userUUID: device?.id || '',
6137
- sessionID: session?.id || '',
6491
+ assistToken,
6492
+ userUUID: device.id,
6493
+ sessionID: session.id,
6138
6494
  };
6139
- this.startCallbacks.forEach((cb) => cb(onStartInfo));
6140
- await this.conditionsManager?.fetchConditions(projectId, token);
6141
- 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
+ }
6142
6503
  }
6143
6504
  /**
6144
6505
  * Starts offline session recording
@@ -6146,6 +6507,7 @@ class App {
6146
6507
  * @param {Function} onSessionSent - callback that will be called once session is fully sent
6147
6508
  * */
6148
6509
  offlineRecording(startOpts = {}, onSessionSent) {
6510
+ this.clearAssistSessionGrant();
6149
6511
  this.onSessionSent = onSessionSent;
6150
6512
  this.singleBuffer = true;
6151
6513
  adjustTimeOrigin();
@@ -6252,22 +6614,37 @@ class App {
6252
6614
  'Content-Type': 'application/json',
6253
6615
  'Idempotency-Key': bootstrapAttempt.idempotencyKey,
6254
6616
  };
6255
- const r = await fetch(ingestPoint + '/v1/bootstrap', {
6256
- method: 'POST',
6257
- headers,
6258
- body: bootstrapAttempt.requestBody,
6259
- });
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
+ }
6260
6628
  if (r.status !== 200) {
6261
- throw new Error(`Server error: ${r.status}. ${await r.text()}`);
6629
+ throw await bootstrapHttpError(r, undefined, [bootstrapToken]);
6262
6630
  }
6263
- const { session = {}, client = {}, limits = {}, protocolVersion: offlineProtocolVersion, } = await r.json();
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;
6264
6636
  const { token, projectId } = session;
6265
6637
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
6266
6638
  const { beacon: beaconSizeLimit } = limits;
6267
- if (typeof token !== 'string' ||
6268
- token.length === 0 ||
6269
- (typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined')) {
6270
- throw new Error('Incorrect server response for offline recording bootstrap');
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(', ')})`);
6271
6648
  }
6272
6649
  this.session.setSessionSecret(token, this.projectKey);
6273
6650
  this.clearBootstrapAttempt('offline');
@@ -6287,8 +6664,13 @@ class App {
6287
6664
  beaconSizeLimit,
6288
6665
  protocolVersion: offlineProtocolVersion,
6289
6666
  });
6290
- while (this.bufferedMessages1.length > 0) {
6291
- await this.flushBuffer(this.bufferedMessages1);
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]);
6292
6674
  }
6293
6675
  this.postToWorker([[-1]]);
6294
6676
  this.clearBuffers();
@@ -6304,6 +6686,7 @@ class App {
6304
6686
  this.rootId === null) {
6305
6687
  return UnsuccessfulStart('Cross-domain iframe handshake was not established.');
6306
6688
  }
6689
+ this.clearAssistSessionGrant();
6307
6690
  if (Object.keys(startOpts).length !== 0) {
6308
6691
  this.prevOpts = startOpts;
6309
6692
  }
@@ -6373,6 +6756,7 @@ class App {
6373
6756
  const reason = 'OxvoSessions: trying to call `start()` on the instance that has been started already.';
6374
6757
  return Promise.resolve(UnsuccessfulStart(reason));
6375
6758
  }
6759
+ this.clearAssistSessionGrant();
6376
6760
  this.activityState = ActivityState.Starting;
6377
6761
  if (!isColdStart) {
6378
6762
  adjustTimeOrigin();
@@ -6426,7 +6810,8 @@ class App {
6426
6810
  tabId: this.session.getTabId(),
6427
6811
  localDebug: this.options.__local_debug,
6428
6812
  });
6429
- 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];
6430
6815
  try {
6431
6816
  const ingestPoint = normalizeEndpoint(this.options.endpoint);
6432
6817
  const headers = {
@@ -6440,30 +6825,52 @@ class App {
6440
6825
  });
6441
6826
  if (r.status !== 200) {
6442
6827
  const error = await r.text();
6443
- const reason = error === CANCELED ? CANCELED : `Server error: ${r.status}. ${error}`;
6444
- throw reason;
6828
+ if (error === CANCELED) {
6829
+ this.stop();
6830
+ this.signalError(CANCELED, []);
6831
+ return UnsuccessfulStart(CANCELED);
6832
+ }
6833
+ throw await bootstrapHttpError(r, error, bootstrapSecrets);
6445
6834
  }
6446
6835
  if (!this.worker && !this.insideIframe) {
6447
6836
  const reason = 'no worker found after start request (this should not happen in real world)';
6448
6837
  throw new Error(reason);
6449
6838
  }
6450
- const { session = {}, device = {}, client = {}, limits = {}, canvas = {}, flags = {}, protocolVersion, } = await r.json();
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;
6451
6847
  const { token, assistToken, id: sessionID, projectId: projectID, delayMs: delay, startedAt: startTimestamp, } = session;
6452
6848
  const { id: userUUID } = device;
6453
6849
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
6454
6850
  const { beacon: beaconSizeLimit, compressAt: compressionThreshold } = limits;
6455
6851
  const { enabled: canvasEnabled, quality: canvasQuality, fps: canvasFPS, framesSupport, } = canvas;
6456
6852
  const socketOnly = flags?.socketOnly;
6457
- if (typeof token !== 'string' ||
6458
- (typeof assistToken !== 'string' && typeof assistToken !== 'undefined') ||
6459
- typeof userUUID !== 'string' ||
6460
- (typeof startTimestamp !== 'number' && typeof startTimestamp !== 'undefined') ||
6461
- typeof sessionID !== 'string' ||
6462
- typeof delay !== 'number' ||
6463
- (typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined')) {
6464
- const reason = `Incorrect server response: ${JSON.stringify(r)}`;
6465
- throw new Error(reason);
6466
- }
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);
6467
6874
  this.crossDomainCanvasConfig = {
6468
6875
  enabled: canvasEnabled === true,
6469
6876
  ...(['low', 'medium', 'high'].includes(canvasQuality) ? { quality: canvasQuality } : {}),
@@ -6492,6 +6899,7 @@ class App {
6492
6899
  timestamp: startTimestamp || timestamp,
6493
6900
  projectID,
6494
6901
  });
6902
+ this.setAssistSessionGrant(sessionID, assistToken);
6495
6903
  if (socketOnly) {
6496
6904
  this.socketMode = true;
6497
6905
  this.worker?.postMessage('stop');
@@ -6570,8 +6978,9 @@ class App {
6570
6978
  this.signalError(CANCELED, []);
6571
6979
  return UnsuccessfulStart(CANCELED);
6572
6980
  }
6573
- this._debug('session_start', reason);
6574
- 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;
6575
6984
  this.signalError(errorMessage, []);
6576
6985
  return UnsuccessfulStart(errorMessage);
6577
6986
  }
@@ -6694,6 +7103,7 @@ class App {
6694
7103
  };
6695
7104
  }
6696
7105
  stop(stopWorker = true) {
7106
+ this.clearAssistSessionGrant();
6697
7107
  if (this.activityState !== ActivityState.NotActive) {
6698
7108
  try {
6699
7109
  if (this.options.crossdomain?.enabled) {
@@ -8374,10 +8784,14 @@ function Fonts (app) {
8374
8784
  }
8375
8785
 
8376
8786
  function axiosSpy (app, instance, opts, sanitize, stringify) {
8377
- app.debug.log('OxvoSessions: attaching axios spy to instance', instance);
8787
+ app.debug.log('OxvoSessions: attaching axios spy to configured instance');
8378
8788
  function captureResponseData(axiosResponseObj) {
8379
- app.debug.log('OxvoSessions: capturing axios response data', axiosResponseObj);
8380
- 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;
8381
8795
  const { data: rData, headers: rHs, status: globStatus, response } = axiosResponseObj;
8382
8796
  const { data: resData, headers: resHs, status: resStatus } = response || {};
8383
8797
  const ihOpt = opts.ignoreHeaders;
@@ -8436,11 +8850,18 @@ function axiosSpy (app, instance, opts, sanitize, stringify) {
8436
8850
  }
8437
8851
  const requestStart = axiosResponseObj.config.__oxvosessions_timing;
8438
8852
  const duration = performance.now() - requestStart;
8439
- 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
+ });
8440
8858
  app.send(NetworkRequest('xhr', String(method), String(reqResInfo.url), stringify(reqResInfo.request), stringify(reqResInfo.response), reqResInfo.status, requestStart + getTimeOrigin(), duration, 0));
8441
8859
  }
8442
8860
  function getStartTime(config) {
8443
- 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
+ });
8444
8865
  config.__oxvosessions_timing = performance.now();
8445
8866
  if (opts.sessionTokenHeader) {
8446
8867
  const header = typeof opts.sessionTokenHeader === 'string'
@@ -8460,17 +8881,23 @@ function axiosSpy (app, instance, opts, sanitize, stringify) {
8460
8881
  return response;
8461
8882
  }
8462
8883
  function captureNetworkError(error) {
8463
- 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
+ });
8464
8889
  if (isAxiosError(error) && Boolean(error.response)) {
8465
8890
  captureResponseData(error.response);
8466
8891
  }
8467
8892
  else if (error instanceof Error) {
8468
8893
  app.send(getExceptionMessage(error, []));
8469
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
8470
8897
  return Promise.reject(error);
8471
8898
  }
8472
- function logRequestError(ev) {
8473
- app.debug.log('OxvoSessions: failed API request, skipping', ev);
8899
+ function logRequestError() {
8900
+ app.debug.log('OxvoSessions: failed axios request, skipping');
8474
8901
  }
8475
8902
  const reqInt = instance.interceptors.request.use(getStartTime, logRequestError, {
8476
8903
  synchronous: true,
@@ -10283,7 +10710,7 @@ class ConstantProperties {
10283
10710
  user_id: this.user_id,
10284
10711
  distinct_id: this.deviceId,
10285
10712
  sdk_edition: 'web',
10286
- sdk_version: '7.3.4',
10713
+ sdk_version: '7.4.15',
10287
10714
  timezone: getUTCOffsetString(),
10288
10715
  search_engine: this.searchEngine,
10289
10716
  };
@@ -11148,7 +11575,7 @@ class API {
11148
11575
  this.signalStartIssue = (reason, missingApi) => {
11149
11576
  const doNotTrack = this.checkDoNotTrack();
11150
11577
  console.log("Tracker couldn't start due to:", JSON.stringify({
11151
- trackerVersion: '7.3.4',
11578
+ trackerVersion: '7.4.15',
11152
11579
  siteKey: this.options.siteKey,
11153
11580
  doNotTrack,
11154
11581
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
@@ -11514,6 +11941,12 @@ class API {
11514
11941
  }
11515
11942
  return this.app.getSessionSecret();
11516
11943
  }
11944
+ requestMessengerLinkageChallenge() {
11945
+ if (this.app === null) {
11946
+ return Promise.resolve(null);
11947
+ }
11948
+ return this.app.requestMessengerLinkageChallenge();
11949
+ }
11517
11950
  getSessionMeta() {
11518
11951
  if (this.app === null) {
11519
11952
  return null;
@@ -11672,6 +12105,12 @@ class BrowserSingleton {
11672
12105
  }
11673
12106
  return this.instance.getSessionSecret();
11674
12107
  }
12108
+ requestMessengerLinkageChallenge() {
12109
+ if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
12110
+ return Promise.resolve(null);
12111
+ }
12112
+ return this.instance.requestMessengerLinkageChallenge();
12113
+ }
11675
12114
  track(key, payload = null, issue = false) {
11676
12115
  if (!IN_BROWSER || !this.ensureConfigured() || !this.instance) {
11677
12116
  return;