@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/lib/index.js CHANGED
@@ -2588,6 +2588,220 @@ function parseCrossDomainIframeBatch(messages) {
2588
2588
  return { messages: messages, bytes };
2589
2589
  }
2590
2590
 
2591
+ const REDACTED_DEBUG_VALUE = '[REDACTED]';
2592
+ const TRUNCATED_DEBUG_VALUE = '[TRUNCATED]';
2593
+ const MAX_DEBUG_TEXT_LENGTH = 2048;
2594
+ const MAX_DEBUG_CREDENTIAL_LOOKAHEAD = 512;
2595
+ const MAX_DEBUG_REDACTION_DEPTH = 5;
2596
+ const MAX_DEBUG_REDACTION_ITEMS = 50;
2597
+ const DEBUG_FIELD_VALUE_PATTERN = /(["']?)([ \t]*[a-z0-9_-]{1,64}[ \t]*)\1(\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|[^\s,;&{}\[\]()\r\n]+)/gim;
2598
+ const DEBUG_AUTHORIZATION_FIELD_PATTERN = /(["']?)((?:proxy[-_]?authorization|authorization)(?:[-_]?header)?)\1(\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|\[(?:REDACTED|TRUNCATED)\]|[^,;&}\]\)\r\n]+)/gi;
2599
+ const DEBUG_COOKIE_FIELD_PATTERN = /(["']?)((?:set[-_]?cookie|cookies?)(?:[-_]?header)?)\1(\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*'|\[(?:REDACTED|TRUNCATED)\]|[^\r\n]+)/gi;
2600
+ const DEBUG_AUTH_SCHEME_VALUE_PATTERN = /(\b(?:ApiKey|Basic|Bearer|Digest|Negotiate)\s+)[a-z0-9._~+/=-]+/gi;
2601
+ const DEBUG_TOKEN_AUTH_SCHEME_VALUE_PATTERN = /(\bToken\s+)[a-z0-9._~+/=-]+/g;
2602
+ const DEBUG_JWT_VALUE_PATTERN = /\beyJ[a-z0-9_-]{5,}\.[a-z0-9_-]{5,}\.[a-z0-9_-]{5,}\b/gi;
2603
+ const DEBUG_URL_CREDENTIAL_PATTERN = /(\bhttps?:\/\/)([^\s/:@]+):([^\s/@]+)@/gi;
2604
+ const DEBUG_ENCODED_FIELD_VALUE_PATTERN = /([a-z0-9_-]{0,48}(?:token|secret|grant|credential|api[-_]?key|private[-_]?key|signing[-_]?key|authorization|cookie))(%3a|%3d)(?:%22|%27)?[a-z0-9._~+/%=-]+/gi;
2605
+ const DEBUG_PROVIDER_CREDENTIAL_PATTERNS = [
2606
+ /sk-(?:(?:proj|svcacct|ant)-)?[a-z0-9_-]{8,}/gi,
2607
+ /AIza[a-z0-9_-]{20,}/gi,
2608
+ /(?:AKIA|ASIA)[A-Z0-9]{16}/g,
2609
+ /gh[pousr]_[a-z0-9]{20,}/gi,
2610
+ /xox[a-z]-[a-z0-9-]{10,}/gi,
2611
+ ];
2612
+ const DEBUG_PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)/g;
2613
+ const PROVIDER_KEY_PREFIXES = [
2614
+ 'anthropic',
2615
+ 'azureopenai',
2616
+ 'cohere',
2617
+ 'deepseek',
2618
+ 'elevenlabs',
2619
+ 'gemini',
2620
+ 'google',
2621
+ 'groq',
2622
+ 'mistral',
2623
+ 'openai',
2624
+ 'openrouter',
2625
+ 'perplexity',
2626
+ ];
2627
+ const isSensitiveDebugField = (key) => {
2628
+ const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase();
2629
+ return (normalized === 'auth' ||
2630
+ normalized.includes('authorization') ||
2631
+ normalized.includes('bearer') ||
2632
+ normalized.includes('cookie') ||
2633
+ normalized === 'sessionhash' ||
2634
+ normalized === 'oxvolinkageassertion' ||
2635
+ normalized === 'xoxvosessionstrace' ||
2636
+ normalized.includes('password') ||
2637
+ normalized === 'privatekey' ||
2638
+ normalized === 'privatekeyid' ||
2639
+ normalized === 'signingkey' ||
2640
+ normalized === 'jwt' ||
2641
+ normalized.endsWith('jwt') ||
2642
+ normalized.endsWith('token') ||
2643
+ normalized.endsWith('grant') ||
2644
+ normalized.includes('secret') ||
2645
+ normalized.endsWith('apikey') ||
2646
+ normalized.endsWith('accesskey') ||
2647
+ normalized.endsWith('accesskeyid') ||
2648
+ normalized.endsWith('subscriptionkey') ||
2649
+ normalized.endsWith('credential') ||
2650
+ normalized.endsWith('credentials') ||
2651
+ (normalized.includes('provider') && normalized.endsWith('key')) ||
2652
+ (normalized.endsWith('key') &&
2653
+ PROVIDER_KEY_PREFIXES.some((provider) => normalized.startsWith(provider))));
2654
+ };
2655
+ const redactedFieldValue = (rawValue) => {
2656
+ const valueQuote = rawValue[0];
2657
+ return valueQuote === '"' || valueQuote === "'"
2658
+ ? `${valueQuote}${REDACTED_DEBUG_VALUE}${valueQuote}`
2659
+ : REDACTED_DEBUG_VALUE;
2660
+ };
2661
+ const redactDebugTextToLength = (value, maxOutputLength) => {
2662
+ const truncated = value.length > maxOutputLength;
2663
+ let redacted = value
2664
+ .slice(0, maxOutputLength + MAX_DEBUG_CREDENTIAL_LOOKAHEAD)
2665
+ .replace(DEBUG_AUTHORIZATION_FIELD_PATTERN, (_match, keyQuote, key, separator, rawValue) => `${keyQuote}${key}${keyQuote}${separator}${redactedFieldValue(rawValue)}`)
2666
+ .replace(DEBUG_COOKIE_FIELD_PATTERN, (_match, keyQuote, key, separator, rawValue) => `${keyQuote}${key}${keyQuote}${separator}${redactedFieldValue(rawValue)}`)
2667
+ .replace(DEBUG_AUTH_SCHEME_VALUE_PATTERN, `$1${REDACTED_DEBUG_VALUE}`)
2668
+ .replace(DEBUG_TOKEN_AUTH_SCHEME_VALUE_PATTERN, `$1${REDACTED_DEBUG_VALUE}`)
2669
+ .replace(DEBUG_FIELD_VALUE_PATTERN, (match, keyQuote, key, separator, rawValue) => {
2670
+ if (!isSensitiveDebugField(key)) {
2671
+ return match;
2672
+ }
2673
+ return `${keyQuote}${key}${keyQuote}${separator}${redactedFieldValue(rawValue)}`;
2674
+ })
2675
+ .replace(DEBUG_ENCODED_FIELD_VALUE_PATTERN, (_match, key, separator) => `${key}${separator}${REDACTED_DEBUG_VALUE}`)
2676
+ .replace(DEBUG_URL_CREDENTIAL_PATTERN, `$1${REDACTED_DEBUG_VALUE}:${REDACTED_DEBUG_VALUE}@`)
2677
+ .replace(DEBUG_JWT_VALUE_PATTERN, REDACTED_DEBUG_VALUE)
2678
+ .replace(DEBUG_PRIVATE_KEY_PATTERN, REDACTED_DEBUG_VALUE);
2679
+ for (const credentialPattern of DEBUG_PROVIDER_CREDENTIAL_PATTERNS) {
2680
+ redacted = redacted.replace(credentialPattern, REDACTED_DEBUG_VALUE);
2681
+ }
2682
+ const bounded = redacted.slice(0, maxOutputLength);
2683
+ return truncated ? `${bounded} ${TRUNCATED_DEBUG_VALUE}` : bounded;
2684
+ };
2685
+ const redactDebugText = (value) => redactDebugTextToLength(value, MAX_DEBUG_TEXT_LENGTH);
2686
+ const createDebugRedactionState = () => ({
2687
+ seen: new WeakSet(),
2688
+ remainingItems: MAX_DEBUG_REDACTION_ITEMS,
2689
+ remainingText: MAX_DEBUG_TEXT_LENGTH,
2690
+ });
2691
+ const redactDebugTextWithBudget = (value, state) => {
2692
+ if (state.remainingText <= 0) {
2693
+ return TRUNCATED_DEBUG_VALUE;
2694
+ }
2695
+ const permittedLength = Math.min(value.length, state.remainingText);
2696
+ state.remainingText -= permittedLength;
2697
+ return redactDebugTextToLength(value, permittedLength);
2698
+ };
2699
+ const redactDebugValue = (value, state = createDebugRedactionState(), depth = 0) => {
2700
+ if (typeof value === 'string') {
2701
+ return redactDebugTextWithBudget(value, state);
2702
+ }
2703
+ if (value instanceof Error) {
2704
+ const redacted = new Error(redactDebugTextWithBudget(value.message, state));
2705
+ redacted.name = redactDebugTextWithBudget(value.name, state);
2706
+ if (value.stack) {
2707
+ redacted.stack = redactDebugTextWithBudget(value.stack, state);
2708
+ }
2709
+ return redacted;
2710
+ }
2711
+ if (value === null || typeof value === 'number' || typeof value === 'boolean') {
2712
+ return value;
2713
+ }
2714
+ if (typeof value === 'undefined') {
2715
+ return undefined;
2716
+ }
2717
+ if (typeof value === 'bigint' || typeof value === 'symbol') {
2718
+ return String(value);
2719
+ }
2720
+ if (typeof value === 'function') {
2721
+ return '[Function]';
2722
+ }
2723
+ const objectValue = value;
2724
+ if (depth >= MAX_DEBUG_REDACTION_DEPTH || state.remainingItems <= 0) {
2725
+ return TRUNCATED_DEBUG_VALUE;
2726
+ }
2727
+ if (state.seen.has(objectValue)) {
2728
+ return '[Circular]';
2729
+ }
2730
+ state.seen.add(objectValue);
2731
+ if (Array.isArray(value)) {
2732
+ const count = Math.min(value.length, state.remainingItems);
2733
+ state.remainingItems -= count;
2734
+ const redacted = value.slice(0, count).map((entry) => redactDebugValue(entry, state, depth + 1));
2735
+ if (count < value.length) {
2736
+ redacted.push(TRUNCATED_DEBUG_VALUE);
2737
+ }
2738
+ return redacted;
2739
+ }
2740
+ const redacted = {};
2741
+ let allKeys;
2742
+ try {
2743
+ allKeys = Object.keys(objectValue);
2744
+ }
2745
+ catch {
2746
+ return '[Unavailable]';
2747
+ }
2748
+ const keys = allKeys.slice(0, state.remainingItems);
2749
+ state.remainingItems -= keys.length;
2750
+ for (const key of keys) {
2751
+ if (isSensitiveDebugField(key)) {
2752
+ redacted[key] = REDACTED_DEBUG_VALUE;
2753
+ continue;
2754
+ }
2755
+ try {
2756
+ redacted[key] = redactDebugValue(value[key], state, depth + 1);
2757
+ }
2758
+ catch {
2759
+ redacted[key] = '[Unavailable]';
2760
+ }
2761
+ }
2762
+ if (keys.length < allKeys.length) {
2763
+ redacted.__truncated__ = TRUNCATED_DEBUG_VALUE;
2764
+ }
2765
+ return redacted;
2766
+ };
2767
+ const redactDebugArgs = (values) => {
2768
+ const state = createDebugRedactionState();
2769
+ return values.map((value) => redactDebugValue(value, state));
2770
+ };
2771
+ const debugValueText = (value) => {
2772
+ if (value instanceof Error) {
2773
+ return `${value.name}: ${value.message}`;
2774
+ }
2775
+ if (typeof value === 'string') {
2776
+ return value;
2777
+ }
2778
+ let serialized;
2779
+ try {
2780
+ serialized = JSON.stringify(value);
2781
+ }
2782
+ catch {
2783
+ return '[Unavailable]';
2784
+ }
2785
+ if (serialized === undefined) {
2786
+ return String(value);
2787
+ }
2788
+ return serialized.length > MAX_DEBUG_TEXT_LENGTH
2789
+ ? `${serialized.slice(0, MAX_DEBUG_TEXT_LENGTH)} ${TRUNCATED_DEBUG_VALUE}`
2790
+ : serialized;
2791
+ };
2792
+ const redactedErrorFrom = (value, fallback, knownSecrets = []) => {
2793
+ const uniqueKnownSecrets = [
2794
+ ...new Set(knownSecrets.filter((secret) => !!secret)),
2795
+ ].sort((left, right) => right.length - left.length);
2796
+ const redacted = value instanceof Error || typeof value === 'string' ? value : redactDebugValue(value);
2797
+ let message = redacted instanceof Error ? redacted.message.trim() : debugValueText(redacted).trim();
2798
+ for (const secret of uniqueKnownSecrets) {
2799
+ message = message.split(secret).join(REDACTED_DEBUG_VALUE);
2800
+ }
2801
+ message = redactDebugText(message);
2802
+ return new Error(message.length > 0 ? message : fallback);
2803
+ };
2804
+
2591
2805
  const LogLevel = {
2592
2806
  Verbose: 5,
2593
2807
  Log: 4,
@@ -2602,26 +2816,22 @@ class Logger {
2602
2816
  };
2603
2817
  this.info = (...args) => {
2604
2818
  if (this.shouldLog(LogLevel.Verbose)) {
2605
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2606
- console.info(...args);
2819
+ console.info(...redactDebugArgs(args));
2607
2820
  }
2608
2821
  };
2609
2822
  this.log = (...args) => {
2610
2823
  if (this.shouldLog(LogLevel.Log)) {
2611
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2612
- console.log(...args);
2824
+ console.log(...redactDebugArgs(args));
2613
2825
  }
2614
2826
  };
2615
2827
  this.warn = (...args) => {
2616
2828
  if (this.shouldLog(LogLevel.Warnings)) {
2617
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2618
- console.warn(...args);
2829
+ console.warn(...redactDebugArgs(args));
2619
2830
  }
2620
2831
  };
2621
2832
  this.error = (...args) => {
2622
2833
  if (this.shouldLog(LogLevel.Errors)) {
2623
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
2624
- console.error(...args);
2834
+ console.error(...redactDebugArgs(args));
2625
2835
  }
2626
2836
  };
2627
2837
  this.level = debugLevel;
@@ -4555,6 +4765,61 @@ const bufferStorageKey = 'or_buffer_1';
4555
4765
  const PROTO_VERSION = '2';
4556
4766
  const BOOTSTRAP_ATTEMPT_TTL_MS = 24 * 60 * 60 * 1000;
4557
4767
  const MAX_BOOTSTRAP_ATTEMPT_STORAGE_LENGTH = 1024 * 1024;
4768
+ const MAX_SESSION_TOKEN_LENGTH = 4096;
4769
+ const MAX_ASSIST_SESSION_GRANT_LENGTH = 4096;
4770
+ const LINKAGE_ASSERTION_METADATA_KEY = 'oxvo_linkage_assertion';
4771
+ const MESSENGER_LINKAGE_CHALLENGE_PATTERN = /^[a-f0-9]{64}$/;
4772
+ const MIN_MESSENGER_LINKAGE_CHALLENGE_TTL_SECONDS = 5;
4773
+ const MAX_MESSENGER_LINKAGE_CHALLENGE_TTL_SECONDS = 120;
4774
+ const asRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
4775
+ ? value
4776
+ : {};
4777
+ const isValidBootstrapCredential = (value, maxLength) => typeof value === 'string' &&
4778
+ value.length > 0 &&
4779
+ value.length <= maxLength &&
4780
+ value.trim() === value;
4781
+ const parseMessengerLinkageChallenge = (value, expectedSessionId) => {
4782
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
4783
+ return null;
4784
+ }
4785
+ const record = value;
4786
+ const keys = Object.keys(record).sort();
4787
+ if (keys.length !== 3 ||
4788
+ keys[0] !== 'challenge' ||
4789
+ keys[1] !== 'expiresIn' ||
4790
+ keys[2] !== 'sessionId' ||
4791
+ typeof record.challenge !== 'string' ||
4792
+ !MESSENGER_LINKAGE_CHALLENGE_PATTERN.test(record.challenge) ||
4793
+ record.sessionId !== expectedSessionId ||
4794
+ !Number.isInteger(record.expiresIn) ||
4795
+ record.expiresIn < MIN_MESSENGER_LINKAGE_CHALLENGE_TTL_SECONDS ||
4796
+ record.expiresIn > MAX_MESSENGER_LINKAGE_CHALLENGE_TTL_SECONDS) {
4797
+ return null;
4798
+ }
4799
+ return {
4800
+ challenge: record.challenge,
4801
+ sessionId: expectedSessionId,
4802
+ expiresIn: record.expiresIn,
4803
+ };
4804
+ };
4805
+ const bootstrapHttpError = async (response, responseText, knownSecrets = []) => {
4806
+ let body;
4807
+ try {
4808
+ body = responseText ?? (await response.text());
4809
+ }
4810
+ catch (error) {
4811
+ return redactedErrorFrom(error, `Server error: ${response.status}.`, knownSecrets);
4812
+ }
4813
+ return redactedErrorFrom(`Server error: ${response.status}. ${body}`, `Server error: ${response.status}.`, knownSecrets);
4814
+ };
4815
+ const bootstrapJson = async (response, knownSecrets = []) => {
4816
+ try {
4817
+ return asRecord(await response.json());
4818
+ }
4819
+ catch (error) {
4820
+ throw redactedErrorFrom(error, 'Incorrect server response (invalid JSON)', knownSecrets);
4821
+ }
4822
+ };
4558
4823
  const UnsuccessfulStart = (reason) => ({ reason, success: false });
4559
4824
  const SuccessfulStart = (body) => ({ ...body, success: true });
4560
4825
  var ActivityState;
@@ -4624,13 +4889,14 @@ class App {
4624
4889
  this.stopCallbacks = [];
4625
4890
  this.commitCallbacks = [];
4626
4891
  this.activityState = ActivityState.NotActive;
4627
- this.version = '7.3.4'; // TODO: version compatability check inside each plugin.
4892
+ this.version = '7.4.15'; // TODO: version compatability check inside each plugin.
4628
4893
  this.socketMode = false;
4629
4894
  this.compressionThreshold = 24 * 1000;
4630
4895
  this.bc = null;
4631
4896
  this.canvasRecorder = null;
4632
4897
  this.conditionsManager = null;
4633
4898
  this.bootstrapAttempts = {};
4899
+ this.assistSessionGrant = null;
4634
4900
  this.canStart = false;
4635
4901
  this.rootId = null;
4636
4902
  this.pageFrames = [];
@@ -4730,7 +4996,7 @@ class App {
4730
4996
  this.startCrossDomainFrame(this.prevOpts);
4731
4997
  }
4732
4998
  catch (e) {
4733
- console.error('children frame restart failed:', e);
4999
+ console.error('children frame restart failed:', redactDebugValue(e));
4734
5000
  }
4735
5001
  return;
4736
5002
  }
@@ -4889,10 +5155,10 @@ class App {
4889
5155
  this.allowAppStart();
4890
5156
  this.start(this.prevOpts, true)
4891
5157
  .then((r) => {
4892
- this.debug.info('Session restart', r);
5158
+ this.debug.info('Session restart', redactDebugValue(r));
4893
5159
  })
4894
5160
  .catch((e) => {
4895
- this.debug.error('Session restart failed', e);
5161
+ this.debug.error('Session restart failed', redactDebugValue(e));
4896
5162
  });
4897
5163
  });
4898
5164
  };
@@ -5074,7 +5340,7 @@ class App {
5074
5340
  });
5075
5341
  this.markerWatcher = new MarkerWatcher({
5076
5342
  sessionStorage: this.sessionStorage,
5077
- errLog: this.debug.error,
5343
+ errLog: (...args) => this.debug.error(...args.map((arg) => redactDebugValue(arg))),
5078
5344
  onMarkerHit: (markerId) => this.send(TagTrigger(markerId)),
5079
5345
  });
5080
5346
  this.session.attachUpdateCallback(({ userID, metadata }) => {
@@ -5140,10 +5406,10 @@ class App {
5140
5406
  }, 250);
5141
5407
  this.bc.onmessage = (ev) => {
5142
5408
  if (ev.data.context === this.contextId || this.projectKey !== ev.data.projectKey) {
5143
- this.debug.log('same ctx event', ev);
5409
+ this.debug.log('same ctx event', redactDebugValue(ev.data));
5144
5410
  return;
5145
5411
  }
5146
- this.debug.log(ev);
5412
+ this.debug.log('broadcast event', redactDebugValue(ev.data));
5147
5413
  if (ev.data.line === proto.resp) {
5148
5414
  const sessionToken = ev.data.token;
5149
5415
  this.session.setSessionSecret(sessionToken, this.projectKey);
@@ -5572,10 +5838,10 @@ class App {
5572
5838
  this.allowAppStart();
5573
5839
  this.start(this.prevOpts, true)
5574
5840
  .then((r) => {
5575
- this.debug.info('Worker restart, session too long', r);
5841
+ this.debug.info('Worker restart, session too long', redactDebugValue(r));
5576
5842
  })
5577
5843
  .catch((e) => {
5578
- this.debug.error('Worker restart failed', e);
5844
+ this.debug.error('Worker restart failed', redactDebugValue(e));
5579
5845
  });
5580
5846
  });
5581
5847
  }
@@ -5584,7 +5850,7 @@ class App {
5584
5850
  }
5585
5851
  else if (data.type === 'failure') {
5586
5852
  this.stop(false);
5587
- this.debug.error('worker_failed', data.reason);
5853
+ this.debug.error('worker_failed', redactDebugValue(data.reason));
5588
5854
  this._debug('worker_failed', data.reason);
5589
5855
  }
5590
5856
  else if (data.type === 'compress') {
@@ -5605,7 +5871,7 @@ class App {
5605
5871
  });
5606
5872
  })
5607
5873
  .catch((err) => {
5608
- this.debug.error('OxvoSessions compression error:', err);
5874
+ this.debug.error('OxvoSessions compression error:', redactDebugValue(err));
5609
5875
  this.worker?.postMessage({ type: 'uncompressed', batch: batch, dataType });
5610
5876
  });
5611
5877
  }
@@ -5627,18 +5893,18 @@ class App {
5627
5893
  }
5628
5894
  }
5629
5895
  _debug(context, e) {
5896
+ const redactedError = redactDebugValue(e);
5630
5897
  if (this.options.__debug_report_edp !== null) {
5631
5898
  void fetch(this.options.__debug_report_edp, {
5632
5899
  method: 'POST',
5633
5900
  headers: { 'Content-Type': 'application/json' },
5634
5901
  body: JSON.stringify({
5635
5902
  context,
5636
- // @ts-ignore
5637
- error: `${e}`,
5903
+ error: debugValueText(redactedError),
5638
5904
  }),
5639
5905
  });
5640
5906
  }
5641
- this.debug.error('OxvoSessions error: ', context, e);
5907
+ this.debug.error('OxvoSessions error: ', context, redactedError);
5642
5908
  }
5643
5909
  /**
5644
5910
  * Normal workflow: add timestamp and tab data to batch, then commit it
@@ -5962,11 +6228,76 @@ class App {
5962
6228
  (current.userUUID === '' || existing.userUUID === current.userUUID));
5963
6229
  }
5964
6230
  getSessionMeta() {
6231
+ const sessionInfo = this.session.getInfo();
6232
+ const metadata = { ...sessionInfo.metadata };
6233
+ Object.keys(metadata).forEach((key) => {
6234
+ if (key.trim().toLowerCase() === LINKAGE_ASSERTION_METADATA_KEY) {
6235
+ delete metadata[key];
6236
+ }
6237
+ });
5965
6238
  return {
5966
- ...this.session.getInfo(),
6239
+ ...sessionInfo,
6240
+ metadata,
5967
6241
  ...this.getTrackerInfo(),
5968
6242
  };
5969
6243
  }
6244
+ async requestMessengerLinkageChallenge() {
6245
+ const sessionId = this.session.getInfo().sessionID;
6246
+ const grantLineage = this.assistSessionGrant;
6247
+ const grant = this.getAssistSessionGrant();
6248
+ if (!sessionId || !grantLineage || !grant || grant.sessionId !== sessionId) {
6249
+ return null;
6250
+ }
6251
+ const tabId = this.session.getTabId();
6252
+ try {
6253
+ const response = await fetch(`${normalizeEndpoint(this.options.endpoint)}/v1/linkage-challenge`, {
6254
+ method: 'POST',
6255
+ headers: {
6256
+ Authorization: `Bearer ${grant.token}`,
6257
+ 'Content-Type': 'application/json',
6258
+ },
6259
+ body: JSON.stringify({ tabId }),
6260
+ cache: 'no-store',
6261
+ credentials: 'omit',
6262
+ referrerPolicy: 'no-referrer',
6263
+ });
6264
+ if (response.status !== 200) {
6265
+ return null;
6266
+ }
6267
+ const challenge = parseMessengerLinkageChallenge(await response.json(), sessionId);
6268
+ if (!challenge) {
6269
+ return null;
6270
+ }
6271
+ const currentGrant = this.getAssistSessionGrant();
6272
+ if (!currentGrant ||
6273
+ this.assistSessionGrant !== grantLineage ||
6274
+ currentGrant.sessionId !== grant.sessionId ||
6275
+ currentGrant.token !== grant.token ||
6276
+ this.session.getInfo().sessionID !== sessionId ||
6277
+ this.session.getTabId() !== tabId) {
6278
+ return null;
6279
+ }
6280
+ return challenge;
6281
+ }
6282
+ catch {
6283
+ return null;
6284
+ }
6285
+ }
6286
+ getAssistSessionGrant() {
6287
+ return this.assistSessionGrant ? { ...this.assistSessionGrant } : null;
6288
+ }
6289
+ setAssistSessionGrant(sessionId, token) {
6290
+ this.assistSessionGrant =
6291
+ typeof token === 'string' &&
6292
+ token.length > 0 &&
6293
+ token.length <= MAX_ASSIST_SESSION_GRANT_LENGTH &&
6294
+ token.trim() === token
6295
+ ? { sessionId, token }
6296
+ : null;
6297
+ }
6298
+ clearAssistSessionGrant() {
6299
+ this.assistSessionGrant = null;
6300
+ }
5970
6301
  getSessionSecret() {
5971
6302
  return this.session.getSessionSecret(this.projectKey);
5972
6303
  }
@@ -6099,23 +6430,48 @@ class App {
6099
6430
  async setupConditionalStart(startOpts) {
6100
6431
  this.conditionsManager = new ConditionsManager(this, startOpts);
6101
6432
  const ingestPoint = normalizeEndpoint(this.options.endpoint);
6102
- const r = await fetch(ingestPoint + '/v1/bootstrap', {
6103
- method: 'POST',
6104
- headers: {
6105
- 'Content-Type': 'application/json',
6106
- },
6107
- body: JSON.stringify(this.buildStartPayload({
6108
- timestamp: now(),
6109
- dry: true,
6110
- bufferMs: 0,
6111
- token: '',
6112
- width: window.screen.width,
6113
- height: window.screen.height,
6114
- referrer: document.referrer,
6115
- })),
6116
- });
6117
- const { session = {}, client = {}, device = {} } = await r.json();
6433
+ let r;
6434
+ try {
6435
+ r = await fetch(ingestPoint + '/v1/bootstrap', {
6436
+ method: 'POST',
6437
+ headers: {
6438
+ 'Content-Type': 'application/json',
6439
+ },
6440
+ body: JSON.stringify(this.buildStartPayload({
6441
+ timestamp: now(),
6442
+ dry: true,
6443
+ bufferMs: 0,
6444
+ token: '',
6445
+ width: window.screen.width,
6446
+ height: window.screen.height,
6447
+ referrer: document.referrer,
6448
+ })),
6449
+ });
6450
+ }
6451
+ catch (error) {
6452
+ throw redactedErrorFrom(error, 'Conditional bootstrap request failed.');
6453
+ }
6454
+ if (r.status !== 200) {
6455
+ throw await bootstrapHttpError(r);
6456
+ }
6457
+ const response = await bootstrapJson(r);
6458
+ const session = asRecord(response.session);
6459
+ const client = asRecord(response.client);
6460
+ const device = asRecord(response.device);
6118
6461
  const { token, projectId, assistToken } = session;
6462
+ const invalidResponseFields = [
6463
+ !isValidBootstrapCredential(token, MAX_SESSION_TOKEN_LENGTH) ? 'session.token' : null,
6464
+ typeof assistToken !== 'undefined' &&
6465
+ !isValidBootstrapCredential(assistToken, MAX_ASSIST_SESSION_GRANT_LENGTH)
6466
+ ? 'session.assistToken'
6467
+ : null,
6468
+ typeof session.id !== 'string' ? 'session.id' : null,
6469
+ typeof projectId !== 'string' ? 'session.projectId' : null,
6470
+ typeof device.id !== 'string' ? 'device.id' : null,
6471
+ ].filter((field) => field !== null);
6472
+ if (invalidResponseFields.length > 0) {
6473
+ throw new Error(`Incorrect server response (status ${r.status}; invalid fields: ${invalidResponseFields.join(', ')})`);
6474
+ }
6119
6475
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
6120
6476
  this.session.assign({ projectID: projectId });
6121
6477
  this.session.setUserInfo({
@@ -6128,13 +6484,18 @@ class App {
6128
6484
  });
6129
6485
  const onStartInfo = {
6130
6486
  sessionToken: token,
6131
- assistToken: typeof assistToken === 'string' ? assistToken : undefined,
6132
- userUUID: device?.id || '',
6133
- sessionID: session?.id || '',
6487
+ assistToken,
6488
+ userUUID: device.id,
6489
+ sessionID: session.id,
6134
6490
  };
6135
- this.startCallbacks.forEach((cb) => cb(onStartInfo));
6136
- await this.conditionsManager?.fetchConditions(projectId, token);
6137
- await this.markerWatcher.fetchTags(normalizeEndpoint(this.options.endpoint), token);
6491
+ try {
6492
+ this.startCallbacks.forEach((cb) => cb(onStartInfo));
6493
+ await this.conditionsManager?.fetchConditions(projectId, token);
6494
+ await this.markerWatcher.fetchTags(normalizeEndpoint(this.options.endpoint), token);
6495
+ }
6496
+ catch (error) {
6497
+ throw redactedErrorFrom(error, 'Conditional bootstrap failed.', [token, assistToken]);
6498
+ }
6138
6499
  }
6139
6500
  /**
6140
6501
  * Starts offline session recording
@@ -6142,6 +6503,7 @@ class App {
6142
6503
  * @param {Function} onSessionSent - callback that will be called once session is fully sent
6143
6504
  * */
6144
6505
  offlineRecording(startOpts = {}, onSessionSent) {
6506
+ this.clearAssistSessionGrant();
6145
6507
  this.onSessionSent = onSessionSent;
6146
6508
  this.singleBuffer = true;
6147
6509
  adjustTimeOrigin();
@@ -6248,22 +6610,37 @@ class App {
6248
6610
  'Content-Type': 'application/json',
6249
6611
  'Idempotency-Key': bootstrapAttempt.idempotencyKey,
6250
6612
  };
6251
- const r = await fetch(ingestPoint + '/v1/bootstrap', {
6252
- method: 'POST',
6253
- headers,
6254
- body: bootstrapAttempt.requestBody,
6255
- });
6613
+ let r;
6614
+ try {
6615
+ r = await fetch(ingestPoint + '/v1/bootstrap', {
6616
+ method: 'POST',
6617
+ headers,
6618
+ body: bootstrapAttempt.requestBody,
6619
+ });
6620
+ }
6621
+ catch (error) {
6622
+ throw redactedErrorFrom(error, 'Offline bootstrap request failed.', [bootstrapToken]);
6623
+ }
6256
6624
  if (r.status !== 200) {
6257
- throw new Error(`Server error: ${r.status}. ${await r.text()}`);
6625
+ throw await bootstrapHttpError(r, undefined, [bootstrapToken]);
6258
6626
  }
6259
- const { session = {}, client = {}, limits = {}, protocolVersion: offlineProtocolVersion, } = await r.json();
6627
+ const response = await bootstrapJson(r, [bootstrapToken]);
6628
+ const session = asRecord(response.session);
6629
+ const client = asRecord(response.client);
6630
+ const limits = asRecord(response.limits);
6631
+ const offlineProtocolVersion = response.protocolVersion;
6260
6632
  const { token, projectId } = session;
6261
6633
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
6262
6634
  const { beacon: beaconSizeLimit } = limits;
6263
- if (typeof token !== 'string' ||
6264
- token.length === 0 ||
6265
- (typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined')) {
6266
- throw new Error('Incorrect server response for offline recording bootstrap');
6635
+ const invalidResponseFields = [
6636
+ !isValidBootstrapCredential(token, MAX_SESSION_TOKEN_LENGTH) ? 'session.token' : null,
6637
+ typeof projectId !== 'string' ? 'session.projectId' : null,
6638
+ typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined'
6639
+ ? 'limits.beacon'
6640
+ : null,
6641
+ ].filter((field) => field !== null);
6642
+ if (invalidResponseFields.length > 0) {
6643
+ throw new Error(`Incorrect server response (status ${r.status}; invalid fields: ${invalidResponseFields.join(', ')})`);
6267
6644
  }
6268
6645
  this.session.setSessionSecret(token, this.projectKey);
6269
6646
  this.clearBootstrapAttempt('offline');
@@ -6283,8 +6660,13 @@ class App {
6283
6660
  beaconSizeLimit,
6284
6661
  protocolVersion: offlineProtocolVersion,
6285
6662
  });
6286
- while (this.bufferedMessages1.length > 0) {
6287
- await this.flushBuffer(this.bufferedMessages1);
6663
+ try {
6664
+ while (this.bufferedMessages1.length > 0) {
6665
+ await this.flushBuffer(this.bufferedMessages1);
6666
+ }
6667
+ }
6668
+ catch (error) {
6669
+ throw redactedErrorFrom(error, 'Offline recording upload failed.', [token, bootstrapToken]);
6288
6670
  }
6289
6671
  this.postToWorker([[-1]]);
6290
6672
  this.clearBuffers();
@@ -6300,6 +6682,7 @@ class App {
6300
6682
  this.rootId === null) {
6301
6683
  return UnsuccessfulStart('Cross-domain iframe handshake was not established.');
6302
6684
  }
6685
+ this.clearAssistSessionGrant();
6303
6686
  if (Object.keys(startOpts).length !== 0) {
6304
6687
  this.prevOpts = startOpts;
6305
6688
  }
@@ -6369,6 +6752,7 @@ class App {
6369
6752
  const reason = 'OxvoSessions: trying to call `start()` on the instance that has been started already.';
6370
6753
  return Promise.resolve(UnsuccessfulStart(reason));
6371
6754
  }
6755
+ this.clearAssistSessionGrant();
6372
6756
  this.activityState = ActivityState.Starting;
6373
6757
  if (!isColdStart) {
6374
6758
  adjustTimeOrigin();
@@ -6422,7 +6806,8 @@ class App {
6422
6806
  tabId: this.session.getTabId(),
6423
6807
  localDebug: this.options.__local_debug,
6424
6808
  });
6425
- this.debug.log('OxvoSessions: starting session; need new session id?', isNewSession, 'session token: ', sessionToken);
6809
+ this.debug.log('OxvoSessions: starting session; need new session id?', isNewSession, 'has existing session token:', Boolean(sessionToken));
6810
+ const bootstrapSecrets = [sessionToken, bootstrapToken];
6426
6811
  try {
6427
6812
  const ingestPoint = normalizeEndpoint(this.options.endpoint);
6428
6813
  const headers = {
@@ -6436,30 +6821,52 @@ class App {
6436
6821
  });
6437
6822
  if (r.status !== 200) {
6438
6823
  const error = await r.text();
6439
- const reason = error === CANCELED ? CANCELED : `Server error: ${r.status}. ${error}`;
6440
- throw reason;
6824
+ if (error === CANCELED) {
6825
+ this.stop();
6826
+ this.signalError(CANCELED, []);
6827
+ return UnsuccessfulStart(CANCELED);
6828
+ }
6829
+ throw await bootstrapHttpError(r, error, bootstrapSecrets);
6441
6830
  }
6442
6831
  if (!this.worker && !this.insideIframe) {
6443
6832
  const reason = 'no worker found after start request (this should not happen in real world)';
6444
6833
  throw new Error(reason);
6445
6834
  }
6446
- const { session = {}, device = {}, client = {}, limits = {}, canvas = {}, flags = {}, protocolVersion, } = await r.json();
6835
+ const response = await bootstrapJson(r, bootstrapSecrets);
6836
+ const session = asRecord(response.session);
6837
+ const device = asRecord(response.device);
6838
+ const client = asRecord(response.client);
6839
+ const limits = asRecord(response.limits);
6840
+ const canvas = asRecord(response.canvas);
6841
+ const flags = asRecord(response.flags);
6842
+ const protocolVersion = response.protocolVersion;
6447
6843
  const { token, assistToken, id: sessionID, projectId: projectID, delayMs: delay, startedAt: startTimestamp, } = session;
6448
6844
  const { id: userUUID } = device;
6449
6845
  const { browser: userBrowser, city: userCity, country: userCountry, device: userDevice, os: userOS, state: userState, } = client;
6450
6846
  const { beacon: beaconSizeLimit, compressAt: compressionThreshold } = limits;
6451
6847
  const { enabled: canvasEnabled, quality: canvasQuality, fps: canvasFPS, framesSupport, } = canvas;
6452
6848
  const socketOnly = flags?.socketOnly;
6453
- if (typeof token !== 'string' ||
6454
- (typeof assistToken !== 'string' && typeof assistToken !== 'undefined') ||
6455
- typeof userUUID !== 'string' ||
6456
- (typeof startTimestamp !== 'number' && typeof startTimestamp !== 'undefined') ||
6457
- typeof sessionID !== 'string' ||
6458
- typeof delay !== 'number' ||
6459
- (typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined')) {
6460
- const reason = `Incorrect server response: ${JSON.stringify(r)}`;
6461
- throw new Error(reason);
6462
- }
6849
+ const invalidResponseFields = [
6850
+ !isValidBootstrapCredential(token, MAX_SESSION_TOKEN_LENGTH) ? 'session.token' : null,
6851
+ typeof assistToken !== 'undefined' &&
6852
+ !isValidBootstrapCredential(assistToken, MAX_ASSIST_SESSION_GRANT_LENGTH)
6853
+ ? 'session.assistToken'
6854
+ : null,
6855
+ typeof userUUID !== 'string' ? 'device.id' : null,
6856
+ typeof startTimestamp !== 'number' && typeof startTimestamp !== 'undefined'
6857
+ ? 'session.startedAt'
6858
+ : null,
6859
+ typeof sessionID !== 'string' ? 'session.id' : null,
6860
+ typeof projectID !== 'string' ? 'session.projectId' : null,
6861
+ typeof delay !== 'number' ? 'session.delayMs' : null,
6862
+ typeof beaconSizeLimit !== 'number' && typeof beaconSizeLimit !== 'undefined'
6863
+ ? 'limits.beacon'
6864
+ : null,
6865
+ ].filter((field) => field !== null);
6866
+ if (invalidResponseFields.length > 0) {
6867
+ throw new Error(`Incorrect server response (status ${r.status}; invalid fields: ${invalidResponseFields.join(', ')})`);
6868
+ }
6869
+ bootstrapSecrets.push(token, assistToken);
6463
6870
  this.crossDomainCanvasConfig = {
6464
6871
  enabled: canvasEnabled === true,
6465
6872
  ...(['low', 'medium', 'high'].includes(canvasQuality) ? { quality: canvasQuality } : {}),
@@ -6488,6 +6895,7 @@ class App {
6488
6895
  timestamp: startTimestamp || timestamp,
6489
6896
  projectID,
6490
6897
  });
6898
+ this.setAssistSessionGrant(sessionID, assistToken);
6491
6899
  if (socketOnly) {
6492
6900
  this.socketMode = true;
6493
6901
  this.worker?.postMessage('stop');
@@ -6566,8 +6974,9 @@ class App {
6566
6974
  this.signalError(CANCELED, []);
6567
6975
  return UnsuccessfulStart(CANCELED);
6568
6976
  }
6569
- this._debug('session_start', reason);
6570
- const errorMessage = reason instanceof Error ? reason.message : reason.toString();
6977
+ const redactedError = redactedErrorFrom(reason, 'Session start failed.', bootstrapSecrets);
6978
+ this._debug('session_start', redactedError);
6979
+ const errorMessage = redactedError.message;
6571
6980
  this.signalError(errorMessage, []);
6572
6981
  return UnsuccessfulStart(errorMessage);
6573
6982
  }
@@ -6690,6 +7099,7 @@ class App {
6690
7099
  };
6691
7100
  }
6692
7101
  stop(stopWorker = true) {
7102
+ this.clearAssistSessionGrant();
6693
7103
  if (this.activityState !== ActivityState.NotActive) {
6694
7104
  try {
6695
7105
  if (this.options.crossdomain?.enabled) {
@@ -8370,10 +8780,14 @@ function Fonts (app) {
8370
8780
  }
8371
8781
 
8372
8782
  function axiosSpy (app, instance, opts, sanitize, stringify) {
8373
- app.debug.log('OxvoSessions: attaching axios spy to instance', instance);
8783
+ app.debug.log('OxvoSessions: attaching axios spy to configured instance');
8374
8784
  function captureResponseData(axiosResponseObj) {
8375
- app.debug.log('OxvoSessions: capturing axios response data', axiosResponseObj);
8376
- const { headers: reqHs, data: reqData, method, url, baseURL } = axiosResponseObj.config;
8785
+ app.debug.log('OxvoSessions: capturing axios response data', {
8786
+ method: axiosResponseObj.config.method,
8787
+ status: axiosResponseObj.status,
8788
+ url: axiosResponseObj.config.url,
8789
+ });
8790
+ const { headers: reqHs, data: reqData, method, url } = axiosResponseObj.config;
8377
8791
  const { data: rData, headers: rHs, status: globStatus, response } = axiosResponseObj;
8378
8792
  const { data: resData, headers: resHs, status: resStatus } = response || {};
8379
8793
  const ihOpt = opts.ignoreHeaders;
@@ -8432,11 +8846,18 @@ function axiosSpy (app, instance, opts, sanitize, stringify) {
8432
8846
  }
8433
8847
  const requestStart = axiosResponseObj.config.__oxvosessions_timing;
8434
8848
  const duration = performance.now() - requestStart;
8435
- app.debug.log('OxvoSessions: final req object', reqResInfo);
8849
+ app.debug.log('OxvoSessions: sanitized axios request is ready', {
8850
+ method: reqResInfo.method,
8851
+ status: reqResInfo.status,
8852
+ url: reqResInfo.url,
8853
+ });
8436
8854
  app.send(NetworkRequest('xhr', String(method), String(reqResInfo.url), stringify(reqResInfo.request), stringify(reqResInfo.response), reqResInfo.status, requestStart + getTimeOrigin(), duration, 0));
8437
8855
  }
8438
8856
  function getStartTime(config) {
8439
- app.debug.log('OxvoSessions: capturing API request', config);
8857
+ app.debug.log('OxvoSessions: capturing axios request', {
8858
+ method: config.method,
8859
+ url: config.url,
8860
+ });
8440
8861
  config.__oxvosessions_timing = performance.now();
8441
8862
  if (opts.sessionTokenHeader) {
8442
8863
  const header = typeof opts.sessionTokenHeader === 'string'
@@ -8456,17 +8877,23 @@ function axiosSpy (app, instance, opts, sanitize, stringify) {
8456
8877
  return response;
8457
8878
  }
8458
8879
  function captureNetworkError(error) {
8459
- app.debug.log('OxvoSessions: capturing API request error', error);
8880
+ app.debug.log('OxvoSessions: capturing axios request error', {
8881
+ hasResponse: Boolean(error.response),
8882
+ isAxiosError: isAxiosError(error),
8883
+ message: typeof error.message === 'string' ? error.message : undefined,
8884
+ });
8460
8885
  if (isAxiosError(error) && Boolean(error.response)) {
8461
8886
  captureResponseData(error.response);
8462
8887
  }
8463
8888
  else if (error instanceof Error) {
8464
8889
  app.send(getExceptionMessage(error, []));
8465
8890
  }
8891
+ // Axios rejection handlers must preserve the original Axios error object for downstream code.
8892
+ // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
8466
8893
  return Promise.reject(error);
8467
8894
  }
8468
- function logRequestError(ev) {
8469
- app.debug.log('OxvoSessions: failed API request, skipping', ev);
8895
+ function logRequestError() {
8896
+ app.debug.log('OxvoSessions: failed axios request, skipping');
8470
8897
  }
8471
8898
  const reqInt = instance.interceptors.request.use(getStartTime, logRequestError, {
8472
8899
  synchronous: true,
@@ -10279,7 +10706,7 @@ class ConstantProperties {
10279
10706
  user_id: this.user_id,
10280
10707
  distinct_id: this.deviceId,
10281
10708
  sdk_edition: 'web',
10282
- sdk_version: '7.3.4',
10709
+ sdk_version: '7.4.15',
10283
10710
  timezone: getUTCOffsetString(),
10284
10711
  search_engine: this.searchEngine,
10285
10712
  };
@@ -11144,7 +11571,7 @@ class API {
11144
11571
  this.signalStartIssue = (reason, missingApi) => {
11145
11572
  const doNotTrack = this.checkDoNotTrack();
11146
11573
  console.log("Tracker couldn't start due to:", JSON.stringify({
11147
- trackerVersion: '7.3.4',
11574
+ trackerVersion: '7.4.15',
11148
11575
  siteKey: this.options.siteKey,
11149
11576
  doNotTrack,
11150
11577
  reason: missingApi.length ? `missing api: ${missingApi.join(',')}` : reason,
@@ -11510,6 +11937,12 @@ class API {
11510
11937
  }
11511
11938
  return this.app.getSessionSecret();
11512
11939
  }
11940
+ requestMessengerLinkageChallenge() {
11941
+ if (this.app === null) {
11942
+ return Promise.resolve(null);
11943
+ }
11944
+ return this.app.requestMessengerLinkageChallenge();
11945
+ }
11513
11946
  getSessionMeta() {
11514
11947
  if (this.app === null) {
11515
11948
  return null;