@corbado/observe 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/analysis/client.mjs +37 -1605
  2. package/dist/analysis/config.mjs +1 -4
  3. package/dist/analysis/default-tracker.mjs +16 -0
  4. package/dist/analysis/device.mjs +0 -79
  5. package/dist/analysis/index.mjs +29 -41
  6. package/dist/analysis/low-events/grouped-input-tracker.mjs +2 -3
  7. package/dist/analysis/low-events/input-change-recorder.mjs +35 -12
  8. package/dist/analysis/low-events/input-tracker.mjs +0 -58
  9. package/dist/analysis/plugins/cookie-storage.mjs +11 -0
  10. package/dist/analysis/plugins/cross-origin-session.mjs +171 -0
  11. package/dist/analysis/plugins/probes.mjs +69 -0
  12. package/dist/analysis/plugins/raw-errors.mjs +8 -0
  13. package/dist/analysis/probe-lane.mjs +175 -0
  14. package/dist/analysis/queue.mjs +18 -141
  15. package/dist/analysis/readers/network.mjs +5 -0
  16. package/dist/analysis/session-keys.mjs +11 -0
  17. package/dist/analysis/tracker-core.mjs +1352 -0
  18. package/dist/analysis/tracker-features.mjs +80 -0
  19. package/dist/analysis/tracker-functions.mjs +103 -0
  20. package/dist/analysis/transports/fetch.mjs +16 -1
  21. package/dist/analysis/utils/appconfirmation-operation-full.mjs +5 -1
  22. package/dist/analysis/utils/captcha-operation-full.mjs +5 -1
  23. package/dist/analysis/utils/email-link-operation-full.mjs +9 -1
  24. package/dist/analysis/utils/email-otp-operation-full.mjs +5 -1
  25. package/dist/analysis/utils/form-operation-full.mjs +5 -1
  26. package/dist/analysis/utils/key-operation-full.mjs +9 -1
  27. package/dist/analysis/utils/operation-full-provide-identifier-with-cui.mjs +8 -1
  28. package/dist/analysis/utils/operation-full.mjs +4 -3
  29. package/dist/analysis/utils/passkey-deletion-operation-full.mjs +5 -1
  30. package/dist/analysis/utils/passkey-enrollment-operation-full.mjs +5 -1
  31. package/dist/analysis/utils/passkey-login-operation-full.mjs +5 -1
  32. package/dist/analysis/utils/password-enrollment-operation-full.mjs +5 -1
  33. package/dist/analysis/utils/password-login-operation-full.mjs +5 -1
  34. package/dist/analysis/utils/provide-data-operation-full.mjs +5 -1
  35. package/dist/analysis/utils/sms-otp-operation-full.mjs +5 -1
  36. package/dist/analysis/utils/social-operation-login.mjs +5 -1
  37. package/dist/analysis/utils/sso-operation-login.mjs +5 -1
  38. package/dist/analysis/utils/totp-operation-full.mjs +5 -1
  39. package/dist/analysis/utils/trusted-device-operation-full.mjs +9 -1
  40. package/dist/analysis/version.mjs +1 -1
  41. package/dist/cdn/script.global.js +4 -4
  42. package/dist/index.d.mts +1349 -888
  43. package/dist/index.d.ts +1349 -888
  44. package/dist/index.js +4 -4
  45. package/dist/index.mjs +4 -4
  46. package/package.json +1 -1
  47. package/dist/analysis/readers/password-manager-dom.mjs +0 -112
@@ -183,13 +183,10 @@ function resolveFixedDataConfig(raw) {
183
183
  return void 0;
184
184
  }
185
185
  const value = raw;
186
- const pmd = value.pmd;
187
- const pmdExplicit = typeof pmd === "boolean";
188
186
  const crossOrigin = value.crossOriginSession;
189
187
  const cookieDomain = crossOrigin && typeof crossOrigin === "object" && typeof crossOrigin.cookieDomain === "string" ? crossOrigin.cookieDomain : void 0;
190
188
  return {
191
- pmd: pmdExplicit ? pmd : false,
192
- pmdExplicit,
189
+ pmd: asBoolean(value.pmd, false),
193
190
  privateMode: asBoolean(value.privateMode, true),
194
191
  ...cookieDomain !== void 0 && { crossOriginSession: { cookieDomain } }
195
192
  };
@@ -0,0 +1,16 @@
1
+ // src/default-tracker.ts
2
+ var current;
3
+ function setDefaultTracker(tracker) {
4
+ current = tracker;
5
+ }
6
+ function getDefaultTracker() {
7
+ return current;
8
+ }
9
+ function isTracker(value) {
10
+ return !!value && typeof value === "object" && typeof value.track === "function" && typeof value.enqueueLowEvent === "function";
11
+ }
12
+ export {
13
+ getDefaultTracker,
14
+ isTracker,
15
+ setDefaultTracker
16
+ };
@@ -7,7 +7,6 @@ import {
7
7
  readConditionalMediationAvailable,
8
8
  readJavaScriptHighEntropy,
9
9
  readMaxTouchPoints,
10
- readUserAgent,
11
10
  readUserVerifyingPlatformAuthenticatorAvailable,
12
11
  readWebdriver
13
12
  } from "./readers/device.mjs";
@@ -515,84 +514,6 @@ var DeviceCollector = class {
515
514
  this.logger.error("Failed to report device info timing", error);
516
515
  }
517
516
  }
518
- /**
519
- * Error situations:
520
- * - navigator is not available: user agent is treated as empty, osName and browserName are "other"
521
- * - navigator is available but userAgent is null or undefined: same as above
522
- */
523
- static getOsAndBrowserInfo() {
524
- return {
525
- osName: this.getOsName(),
526
- browserName: this.getBrowserName()
527
- };
528
- }
529
- static getUserAgent() {
530
- return readUserAgent({
531
- key: "browser-api:device.user-agent",
532
- title: "Browser and operating system",
533
- accessNote: "navigator.userAgent",
534
- group: "Device information",
535
- origin: "observe-external",
536
- fields: [
537
- {
538
- path: "result",
539
- label: "Browser and operating system",
540
- exposure: "class",
541
- transform: {
542
- classes: [
543
- "iOS",
544
- "Android",
545
- "Windows",
546
- "macOS",
547
- "Linux",
548
- "ChromeOS",
549
- "Chrome",
550
- "Firefox",
551
- "Safari",
552
- "Edge",
553
- "Opera",
554
- "IE",
555
- "other"
556
- ]
557
- }
558
- }
559
- ]
560
- });
561
- }
562
- static getOsName() {
563
- const ua = this.getUserAgent();
564
- if (ua.includes("iOS")) {
565
- return "iOS";
566
- } else if (ua.includes("Android")) {
567
- return "Android";
568
- } else if (ua.includes("Windows")) {
569
- return "Windows";
570
- } else if (ua.includes("Macintosh")) {
571
- return "macOS";
572
- } else if (ua.includes("Linux")) {
573
- return "Linux";
574
- } else if (ua.includes("ChromeOS")) {
575
- return "ChromeOS";
576
- }
577
- return "other";
578
- }
579
- static getBrowserName() {
580
- const ua = this.getUserAgent();
581
- if (ua.includes("Chrome")) {
582
- return "Chrome";
583
- } else if (ua.includes("Firefox")) {
584
- return "Firefox";
585
- } else if (ua.includes("Safari")) {
586
- return "Safari";
587
- } else if (ua.includes("Edge")) {
588
- return "Edge";
589
- } else if (ua.includes("Opera")) {
590
- return "Opera";
591
- } else if (ua.includes("IE")) {
592
- return "IE";
593
- }
594
- return "other";
595
- }
596
517
  getClientEnvHandle() {
597
518
  const existingClientEnvHandle = this.storage.getItem("cbo_client_env_handle");
598
519
  if (existingClientEnvHandle) {
@@ -1,8 +1,17 @@
1
1
  // src/index.ts
2
2
  import { CorbadoTracker } from "./client.mjs";
3
+ import { CorbadoTrackerCore } from "./tracker-core.mjs";
4
+ import { getDefaultTracker, setDefaultTracker } from "./default-tracker.mjs";
3
5
  import { SDK_NAME, SDK_VERSION } from "./version.mjs";
4
6
  export * from "./types.mjs";
5
7
  export * from "./client.mjs";
8
+ export * from "./tracker-core.mjs";
9
+ export * from "./tracker-functions.mjs";
10
+ export * from "./plugins/cookie-storage.mjs";
11
+ export * from "./plugins/cross-origin-session.mjs";
12
+ export * from "./plugins/raw-errors.mjs";
13
+ export * from "./plugins/probes.mjs";
14
+ export * from "./probe-lane.mjs";
6
15
  export * from "./transport.mjs";
7
16
  export * from "./queue.mjs";
8
17
  export * from "./config.mjs";
@@ -41,78 +50,57 @@ import {
41
50
  sanitizeRequestOptions,
42
51
  sanitizeAssertionResponse
43
52
  } from "./utils/webauthn-payloads.mjs";
44
- var currentTracker;
45
53
  function init(options) {
46
54
  const tracker = new CorbadoTracker(options);
47
- currentTracker = tracker;
55
+ setDefaultTracker(tracker);
56
+ return tracker;
57
+ }
58
+ function initCore(options) {
59
+ const tracker = new CorbadoTrackerCore(options);
60
+ setDefaultTracker(tracker);
48
61
  return tracker;
49
62
  }
50
63
  function getTracker() {
51
- return currentTracker;
64
+ const tracker = getDefaultTracker();
65
+ return tracker?.isFullTracker() ? tracker : void 0;
52
66
  }
53
- function resetSession() {
54
- return currentTracker?.resetSession();
67
+ function getTrackerCore() {
68
+ return getDefaultTracker();
55
69
  }
56
70
  function getSessionId() {
57
- return currentTracker?.getSessionId();
58
- }
59
- function setUser(user) {
60
- currentTracker?.setUser(user);
61
- }
62
- function setFlowTags(tags, options) {
63
- currentTracker?.setFlowTags(tags, options);
64
- }
65
- function setExperiment(key, variant) {
66
- currentTracker?.setExperiment(key, variant);
71
+ return getDefaultTracker()?.getSessionId();
67
72
  }
68
73
  function setExperiments(assignments) {
69
- currentTracker?.setExperiments(assignments);
70
- }
71
- function clearExperiment(key) {
72
- currentTracker?.clearExperiment(key);
73
- }
74
- function clearExperiments() {
75
- currentTracker?.clearExperiments();
74
+ getDefaultTracker()?.setExperiments(assignments);
76
75
  }
77
76
  function getExperiments() {
78
- return currentTracker?.getExperiments() ?? {};
77
+ return getDefaultTracker()?.getExperiments() ?? {};
79
78
  }
80
79
  function telemetry(level, message) {
81
- currentTracker?.telemetry(level, message);
82
- }
83
- function logInfo(message) {
84
- currentTracker?.logInfo(message);
85
- }
86
- function logError(message) {
87
- currentTracker?.logError(message);
80
+ getDefaultTracker()?.telemetry(level, message);
88
81
  }
89
82
  async function destroy() {
90
- if (!currentTracker) return;
91
- await currentTracker.destroy();
92
- currentTracker = void 0;
83
+ const tracker = getDefaultTracker();
84
+ if (!tracker) return;
85
+ await tracker.destroy();
86
+ setDefaultTracker(void 0);
93
87
  }
94
88
  export {
95
89
  READER_EXPOSURES,
96
90
  SDK_NAME,
97
91
  SDK_VERSION,
98
- clearExperiment,
99
- clearExperiments,
100
92
  destroy,
101
93
  detectPasswordManagerExtensions,
102
94
  getExperiments,
103
95
  getSessionId,
104
96
  getTracker,
97
+ getTrackerCore,
105
98
  init,
106
- logError,
107
- logInfo,
108
- resetSession,
99
+ initCore,
109
100
  sanitizeAssertionResponse,
110
101
  sanitizeCreationOptions,
111
102
  sanitizeCreationResponse,
112
103
  sanitizeRequestOptions,
113
- setExperiment,
114
104
  setExperiments,
115
- setFlowTags,
116
- setUser,
117
105
  telemetry
118
106
  };
@@ -99,7 +99,7 @@ var LowEventGroupedInputTracker = class {
99
99
  this.composing ||= composing;
100
100
  if (this.changeTimer !== null) return;
101
101
  this.changeTime = Date.now();
102
- this.changeSeq = sink.reserveCaptureSeq?.();
102
+ this.changes.observe();
103
103
  this.changeTimer = setTimeout(() => this.safe(() => this.flushChanges()), 0);
104
104
  },
105
105
  focus: (relatedTarget) => {
@@ -174,7 +174,6 @@ var LowEventGroupedInputTracker = class {
174
174
  listeners = [];
175
175
  changeTimer = null;
176
176
  changeTime = 0;
177
- changeSeq;
178
177
  composing = false;
179
178
  destroyed = false;
180
179
  contains(target) {
@@ -214,7 +213,7 @@ var LowEventGroupedInputTracker = class {
214
213
  flushChanges() {
215
214
  if (this.changeTimer === null) return;
216
215
  this.cancelChange();
217
- if (!this.paste.pending) this.changes.record(this.length(), this.changeTime, this.composing, this.changeSeq);
216
+ if (!this.paste.pending) this.changes.record(this.length(), this.changeTime, this.composing);
218
217
  this.composing = false;
219
218
  }
220
219
  emit(lowType) {
@@ -18,9 +18,28 @@ var InputChangeRecorder = class {
18
18
  autofillTimestamps = [];
19
19
  autofillStreak = null;
20
20
  idleTimer = null;
21
+ /** Order of the pending run of changes: reserved when it starts, taken by the first event it sends. */
22
+ runSeq;
21
23
  markInputObserved() {
22
24
  this.prefillSettled = true;
23
25
  }
26
+ /**
27
+ * Observation of a change begins: reserves the capture order of a new run of changes. One number per
28
+ * run, not per change: every reserved number is sent, so the gaps between sent events never count
29
+ * keystrokes (which would reveal the length of the typed value).
30
+ */
31
+ observe() {
32
+ if (this.runSeq !== void 0 || this.inputBatch || this.autofillTimestamps.length > 0 || this.autofillStreak) {
33
+ return;
34
+ }
35
+ this.runSeq = this.reserveCaptureSeq?.();
36
+ }
37
+ /** The run's reserved order for its first event; later events of the run reserve when they are created. */
38
+ takeSeq() {
39
+ const seq = this.runSeq;
40
+ this.runSeq = void 0;
41
+ return seq ?? this.reserveCaptureSeq?.();
42
+ }
24
43
  capturePrefill(length, newCapture = false) {
25
44
  if (newCapture) this.prefillSettled = false;
26
45
  if (this.prefillSettled) return;
@@ -50,17 +69,17 @@ var InputChangeRecorder = class {
50
69
  }
51
70
  }, INPUT_IDLE_MS);
52
71
  }
53
- extendInputBatch(timestamp, effect = this.currentEffect, captureSeq) {
72
+ extendInputBatch(timestamp, effect = this.currentEffect) {
54
73
  const batch = this.inputBatch;
55
74
  if (!batch) {
56
- this.inputBatch = { firstTimestamp: timestamp, lastTimestamp: timestamp, effect, captureSeq };
75
+ this.inputBatch = { firstTimestamp: timestamp, lastTimestamp: timestamp, effect, captureSeq: this.takeSeq() };
57
76
  return;
58
77
  }
59
78
  batch.lastTimestamp = timestamp;
60
79
  }
61
80
  drainAutofillRingToBatch() {
62
81
  for (const ts of this.autofillTimestamps) {
63
- this.extendInputBatch(ts.ts, this.ringEffect, ts.captureSeq);
82
+ this.extendInputBatch(ts, this.ringEffect);
64
83
  }
65
84
  this.autofillTimestamps.length = 0;
66
85
  }
@@ -101,7 +120,7 @@ var InputChangeRecorder = class {
101
120
  let minInterval = Infinity;
102
121
  let maxInterval = -Infinity;
103
122
  for (let i = 1; i < ring.length; i++) {
104
- const interval = ring[i].ts - ring[i - 1].ts;
123
+ const interval = ring[i] - ring[i - 1];
105
124
  if (interval > AUTOFILL_FAST_INTERVAL_MS) {
106
125
  return false;
107
126
  }
@@ -114,7 +133,8 @@ var InputChangeRecorder = class {
114
133
  }
115
134
  return maxInterval - minInterval <= AUTOFILL_UNIFORM_SPREAD_MS;
116
135
  }
117
- record(newLength, timestamp = Date.now(), composing = false, captureSeq = this.reserveCaptureSeq?.()) {
136
+ record(newLength, timestamp = Date.now(), composing = false) {
137
+ this.observe();
118
138
  this.markInputObserved();
119
139
  this.scheduleIdle();
120
140
  const oldLength = this.previousLength;
@@ -123,6 +143,7 @@ var InputChangeRecorder = class {
123
143
  this.previousLength = newLength;
124
144
  if (newLength === 0 && oldLength > 0) {
125
145
  this.flush();
146
+ const captureSeq = this.takeSeq();
126
147
  this.pushLow({
127
148
  ...captureSeq === void 0 ? {} : { captureSeq },
128
149
  lowType: "input-clear",
@@ -134,12 +155,13 @@ var InputChangeRecorder = class {
134
155
  if (composing) {
135
156
  this.flushAutofillStreak();
136
157
  this.drainAutofillRingToBatch();
137
- this.extendInputBatch(timestamp, this.currentEffect, captureSeq);
158
+ this.extendInputBatch(timestamp, this.currentEffect);
138
159
  return;
139
160
  }
140
161
  if (delta >= BIG_INPUT_DELTA_THRESHOLD) {
141
162
  this.flushInputBatch();
142
163
  this.flushAutofillStreak();
164
+ const captureSeq = this.takeSeq();
143
165
  this.pushLow({
144
166
  ...captureSeq === void 0 ? {} : { captureSeq },
145
167
  lowType: "big-input-add",
@@ -151,6 +173,7 @@ var InputChangeRecorder = class {
151
173
  if (delta <= -BIG_INPUT_DELTA_THRESHOLD) {
152
174
  this.flushInputBatch();
153
175
  this.flushAutofillStreak();
176
+ const captureSeq = this.takeSeq();
154
177
  this.pushLow({
155
178
  ...captureSeq === void 0 ? {} : { captureSeq },
156
179
  lowType: "big-input-rem",
@@ -169,15 +192,15 @@ var InputChangeRecorder = class {
169
192
  }
170
193
  if (delta !== 1) {
171
194
  this.drainAutofillRingToBatch();
172
- this.extendInputBatch(timestamp, this.currentEffect, captureSeq);
195
+ this.extendInputBatch(timestamp, this.currentEffect);
173
196
  return;
174
197
  }
175
198
  const ring = this.autofillTimestamps;
176
199
  if (ring.length === 0) this.ringEffect = this.currentEffect;
177
- ring.push({ ts: timestamp, captureSeq });
200
+ ring.push(timestamp);
178
201
  if (ring.length > AUTOFILL_STREAK_LENGTH) {
179
202
  const graduated = ring.shift();
180
- this.extendInputBatch(graduated.ts, this.ringEffect, graduated.captureSeq);
203
+ this.extendInputBatch(graduated, this.ringEffect);
181
204
  this.ringEffect = "modify";
182
205
  }
183
206
  if (!this.detectAutofillStreak()) {
@@ -188,9 +211,9 @@ var InputChangeRecorder = class {
188
211
  ring.length = 0;
189
212
  this.flushInputBatch();
190
213
  this.autofillStreak = {
191
- firstTimestamp: streakStart.ts,
192
- captureSeq: streakStart.captureSeq,
193
- lastTimestamp: streakLast.ts,
214
+ firstTimestamp: streakStart,
215
+ captureSeq: this.takeSeq(),
216
+ lastTimestamp: streakLast,
194
217
  effect: this.ringEffect
195
218
  };
196
219
  }
@@ -3,9 +3,6 @@ import { InputChangeRecorder } from "./input-change-recorder.mjs";
3
3
  import { InputPasteObserver } from "../readers/input-paste.mjs";
4
4
  import { observeInputField, readFocusedField, readInputContentLength } from "../readers/input.mjs";
5
5
  import { listenWindow } from "../readers/page.mjs";
6
- import { readPasswordManagerDomSignals } from "../readers/password-manager-dom.mjs";
7
- var MOUNT_PROBE_DELAY_MS = 500;
8
- var FOCUS_PROBE_DELAY_MS = 250;
9
6
  function fieldContentLength(field) {
10
7
  return readInputContentLength(
11
8
  {
@@ -85,10 +82,6 @@ var LowEventInputTracker = class {
85
82
  removePageHide;
86
83
  changes;
87
84
  pasteObserver;
88
- emittedSignals = /* @__PURE__ */ new Set();
89
- firstFocusHandled = false;
90
- mountProbeTimer = null;
91
- focusProbeTimer = null;
92
85
  destroyed = false;
93
86
  destroy() {
94
87
  if (this.destroyed) {
@@ -96,14 +89,6 @@ var LowEventInputTracker = class {
96
89
  }
97
90
  this.destroyed = true;
98
91
  try {
99
- if (this.mountProbeTimer !== null) {
100
- clearTimeout(this.mountProbeTimer);
101
- this.mountProbeTimer = null;
102
- }
103
- if (this.focusProbeTimer !== null) {
104
- clearTimeout(this.focusProbeTimer);
105
- this.focusProbeTimer = null;
106
- }
107
92
  this.changes.flush();
108
93
  this.unregisterInputs?.();
109
94
  this.pasteObserver.destroy();
@@ -204,9 +189,6 @@ var LowEventInputTracker = class {
204
189
  this.handleFocus();
205
190
  }
206
191
  }
207
- if (typeof window !== "undefined") {
208
- this.mountProbeTimer = setTimeout(() => this.runEnvProbe(), MOUNT_PROBE_DELAY_MS);
209
- }
210
192
  }
211
193
  detach() {
212
194
  this.removeFieldListeners?.();
@@ -217,45 +199,6 @@ var LowEventInputTracker = class {
217
199
  pushLow = (low) => {
218
200
  this.sink.enqueueLowEvent(low);
219
201
  };
220
- runEnvProbe() {
221
- try {
222
- if (typeof __CBO_DATA_PMD__ === "boolean" ? __CBO_DATA_PMD__ : this.sink.passwordManagerDomScan?.() !== false) {
223
- const matches = readPasswordManagerDomSignals({
224
- key: "dom:password-managers.dom-signals",
225
- title: "Password manager traces in the page",
226
- accessNote: "DOM markers that password manager extensions insert (attributes, elements, styles)",
227
- group: "Password manager detection",
228
- origin: "observe-external",
229
- fields: [
230
- {
231
- path: "signals[]",
232
- label: "Password manager traces found",
233
- exposure: "enum",
234
- transform: { values: ["pa-1", "pa-2", "pa-3", "pa-4", "pa-5", "pa-6"] }
235
- }
236
- ],
237
- dataConfig: "pmd"
238
- });
239
- const now = Date.now();
240
- for (const eventType of matches) {
241
- if (this.emittedSignals.has(eventType)) {
242
- continue;
243
- }
244
- this.emittedSignals.add(eventType);
245
- this.pushLow({ lowType: eventType, ts: now });
246
- }
247
- }
248
- } catch (error) {
249
- this.logger.error("Failed to run input env probe", error);
250
- }
251
- }
252
- scheduleFocusProbe() {
253
- if (this.firstFocusHandled) {
254
- return;
255
- }
256
- this.firstFocusHandled = true;
257
- this.focusProbeTimer = setTimeout(() => this.runEnvProbe(), FOCUS_PROBE_DELAY_MS);
258
- }
259
202
  capturePrefill(newCapture = false) {
260
203
  if (this.destroyed) return;
261
204
  this.flushPending();
@@ -270,7 +213,6 @@ var LowEventInputTracker = class {
270
213
  this.pushLow({ lowType: eventType, ts: Date.now() });
271
214
  }
272
215
  handleFocus() {
273
- this.scheduleFocusProbe();
274
216
  this.enqueueNonInputEvent("focus");
275
217
  }
276
218
  handleBlur() {
@@ -0,0 +1,11 @@
1
+ // src/plugins/cookie-storage.ts
2
+ import { CookieStorage } from "../storage/index.mjs";
3
+ function cookieStorage(options = {}) {
4
+ const cookieDomain = options?.cookieDomain;
5
+ return {
6
+ storage: (logger) => ({ engine: new CookieStorage(logger, cookieDomain), source: "cookie" })
7
+ };
8
+ }
9
+ export {
10
+ cookieStorage
11
+ };
@@ -0,0 +1,171 @@
1
+ // src/plugins/cross-origin-session.ts
2
+ import { CrossOriginSession, isValidCrossOriginContext } from "../cross-origin-session.mjs";
3
+ import {
4
+ SEQ_STORAGE_KEY,
5
+ SESSION_CONTINUITY_KEY,
6
+ SESSION_STORAGE_KEY
7
+ } from "../session-keys.mjs";
8
+ function crossOriginSession(config) {
9
+ const cookieDomain = config && typeof config === "object" ? config.cookieDomain : void 0;
10
+ return {
11
+ session: (tracker) => typeof cookieDomain === "string" ? CrossOriginBridge.create(tracker, cookieDomain) : void 0
12
+ };
13
+ }
14
+ var CrossOriginBridge = class _CrossOriginBridge {
15
+ constructor(tracker, cookie) {
16
+ this.tracker = tracker;
17
+ this.cookie = cookie;
18
+ }
19
+ /** The session this load took from the cross-origin cookie, if any (`meta.crossOriginSession`). */
20
+ adoptedId;
21
+ /** The bridge when the cookie is usable here; undefined (feature off) otherwise. Never throws. */
22
+ static create(tracker, cookieDomain) {
23
+ try {
24
+ if (tracker["isTrackingBlocked"]()) {
25
+ return void 0;
26
+ }
27
+ const cookie = CrossOriginSession.create({
28
+ projectId: tracker["options"].projectId,
29
+ cookieDomain,
30
+ logger: tracker.getLogger(),
31
+ inactivityMs: () => tracker["config"].sessionInactivityMs,
32
+ report: (level, message) => tracker["reportTelemetry"](level, message)
33
+ });
34
+ return cookie ? new _CrossOriginBridge(tracker, cookie) : void 0;
35
+ } catch (error) {
36
+ tracker.getLogger().error("Failed to set up the cross-origin session", error);
37
+ return void 0;
38
+ }
39
+ }
40
+ resolve() {
41
+ const tracker = this.tracker;
42
+ const localActivity = this.readLocalSessionActivity();
43
+ tracker["sessionId"] = tracker["resolveSessionId"]();
44
+ this.boot(localActivity);
45
+ return tracker["sessionId"];
46
+ }
47
+ claim(sessionId, seq, now) {
48
+ return this.cookie.claim(sessionId, seq, now);
49
+ }
50
+ /** `meta.crossOriginSession` of a batch of `sessionId`; undefined while the cookie is off on this page. */
51
+ meta(sessionId) {
52
+ if (!this.cookie.isEnabled()) {
53
+ return void 0;
54
+ }
55
+ return sessionId && sessionId === this.adoptedId ? "adopted" : "own";
56
+ }
57
+ reset(sessionId) {
58
+ this.cookie.reset(sessionId);
59
+ }
60
+ stop() {
61
+ this.cookie.stop();
62
+ }
63
+ /** The seq the tracker continues from (persisted counter under continuity). */
64
+ localSeq() {
65
+ const tracker = this.tracker;
66
+ return tracker["config"].sessionContinuity ? Math.max(tracker["seq"], tracker["persistentStorage"].getItem(SEQ_STORAGE_KEY) ?? 0) : tracker["seq"];
67
+ }
68
+ /**
69
+ * Last activity of the local session records BEFORE this load rewrites them (both paths, by id), so the
70
+ * cross-origin boot can tell a session that was active here from one this load just created.
71
+ */
72
+ readLocalSessionActivity() {
73
+ const tracker = this.tracker;
74
+ const activity = /* @__PURE__ */ new Map();
75
+ try {
76
+ const continuity = tracker["persistentStorage"].getItem(SESSION_CONTINUITY_KEY);
77
+ if (continuity?.id && typeof continuity.lastActiveAt === "number") {
78
+ activity.set(continuity.id, continuity.lastActiveAt);
79
+ }
80
+ const perTab = tracker["sessionStorage"].getItem(SESSION_STORAGE_KEY);
81
+ if (perTab && typeof perTab === "object" && perTab.id && typeof perTab.lastActiveAt === "number") {
82
+ activity.set(perTab.id, Math.max(activity.get(perTab.id) ?? 0, perTab.lastActiveAt));
83
+ }
84
+ } catch (error) {
85
+ tracker.getLogger().error("Failed to read local session activity", error);
86
+ }
87
+ return activity;
88
+ }
89
+ /**
90
+ * Reconcile the session this load resolved locally with the cross-origin cookie before the first event:
91
+ * adopt the cookie's session (and persist it locally, so reloads and same-origin tabs follow) or keep
92
+ * the local one, and continue the seq at or above the cookie's mark. Never throws.
93
+ */
94
+ boot(localActivity) {
95
+ const tracker = this.tracker;
96
+ try {
97
+ this.adoptedId = void 0;
98
+ const now = Date.now();
99
+ const continuity = tracker["config"].sessionContinuity;
100
+ const sessionId = tracker["sessionId"];
101
+ const result = this.cookie.boot(
102
+ {
103
+ sessionId,
104
+ lastActiveAt: sessionId && localActivity.get(sessionId) || 0,
105
+ seq: this.localSeq()
106
+ },
107
+ now
108
+ );
109
+ if (!result || !result.sessionId) {
110
+ return;
111
+ }
112
+ if (result.adopted) {
113
+ tracker["sessionId"] = result.sessionId;
114
+ this.adoptedId = result.sessionId;
115
+ if (continuity) {
116
+ tracker["persistContinuitySession"](result.sessionId, now);
117
+ } else {
118
+ tracker["persistPerTabSession"](result.sessionId, now);
119
+ }
120
+ tracker.getLogger().debug("Adopted cross-origin session id", result.sessionId);
121
+ }
122
+ tracker["seq"] = result.seq;
123
+ if (continuity) {
124
+ tracker["persistentStorage"].setItem(SEQ_STORAGE_KEY, result.seq);
125
+ }
126
+ } catch (error) {
127
+ tracker.getLogger().error("Failed to apply the cross-origin session", error);
128
+ }
129
+ }
130
+ };
131
+ function setCrossOriginContext(tracker, context) {
132
+ try {
133
+ const bridge = tracker["bridge"];
134
+ if (!(bridge instanceof CrossOriginBridge)) {
135
+ return false;
136
+ }
137
+ const value = context === null || context === void 0 || context === "" ? void 0 : context;
138
+ if (value !== void 0 && !isValidCrossOriginContext(value)) {
139
+ tracker.getLogger().debug("Ignoring invalid cross-origin context (expected <= 128 chars of [A-Za-z0-9._~-])");
140
+ return false;
141
+ }
142
+ const next = bridge.cookie.setContext(tracker["sessionId"], bridge.localSeq(), value);
143
+ if (next === void 0) {
144
+ return false;
145
+ }
146
+ if (next > tracker["seq"]) {
147
+ tracker["seq"] = next;
148
+ if (tracker["config"].sessionContinuity) {
149
+ tracker["persistentStorage"].setItem(SEQ_STORAGE_KEY, next);
150
+ }
151
+ }
152
+ return true;
153
+ } catch (error) {
154
+ tracker.getLogger().error("Failed to set the cross-origin context", error);
155
+ return false;
156
+ }
157
+ }
158
+ function getCrossOriginContext(tracker) {
159
+ try {
160
+ const bridge = tracker["bridge"];
161
+ return bridge instanceof CrossOriginBridge ? bridge.cookie.getContext(tracker["sessionId"]) : void 0;
162
+ } catch (error) {
163
+ tracker.getLogger().error("Failed to read the cross-origin context", error);
164
+ return void 0;
165
+ }
166
+ }
167
+ export {
168
+ crossOriginSession,
169
+ getCrossOriginContext,
170
+ setCrossOriginContext
171
+ };