@corbado/observe 1.9.1 → 1.10.1

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.
@@ -62,6 +62,7 @@ import {
62
62
  } from "./utils/trusted-device-operation-full.mjs";
63
63
  import { ProvideDataOperationFull } from "./utils/provide-data-operation-full.mjs";
64
64
  import { PasswordManagerSampler } from "./utils/password-manager-sampler.mjs";
65
+ import { CrossOriginSession, isValidCrossOriginContext } from "./cross-origin-session.mjs";
65
66
  var SDK_INFO = {
66
67
  name: SDK_NAME,
67
68
  version: SDK_VERSION
@@ -161,7 +162,8 @@ var CorbadoTracker = class _CorbadoTracker {
161
162
  logger: this.logger,
162
163
  url
163
164
  });
164
- this.sessionId = this.resolveSessionId();
165
+ this.crossOrigin = this.createCrossOriginSession();
166
+ this.sessionId = this.resolveSession();
165
167
  this.queue = new RequestQueue(
166
168
  this.logger,
167
169
  transport,
@@ -173,6 +175,7 @@ var CorbadoTracker = class _CorbadoTracker {
173
175
  requestConfig: false,
174
176
  outboxLockName: namespacedKey(OUTBOX_LOCK_NAME, namespace),
175
177
  getDataPolicy: () => this.dataPolicy,
178
+ getCrossOriginSession: (sessionId) => this.crossOriginSessionMeta(sessionId),
176
179
  fixedData: this.fixedData !== void 0 || typeof __CBO_DATA_PMD__ === "boolean" || typeof __CBO_DATA_PRIVATE_MODE__ === "boolean",
177
180
  beforeEventBatchSend: (events) => this.attachUndeliveredDeviceInfo(events),
178
181
  // The queue is created before the device info manager; both are only called once it exists.
@@ -290,6 +293,10 @@ var CorbadoTracker = class _CorbadoTracker {
290
293
  probeSeqs = /* @__PURE__ */ new Map();
291
294
  windowObserver;
292
295
  passwordManagerSampler;
296
+ /** Cross-origin session cookie (`fixedDataConfig.crossOriginSession`); undefined when off. */
297
+ crossOrigin;
298
+ /** The session this load took from the cross-origin cookie, if any (`meta.crossOriginSession`). */
299
+ crossOriginAdoptedId;
293
300
  /** @internal Shared passive page observation and per-operation context registration. */
294
301
  getWindowObserver() {
295
302
  return this.windowObserver ??= new SharedWindowObserver(
@@ -526,6 +533,108 @@ var CorbadoTracker = class _CorbadoTracker {
526
533
  })
527
534
  );
528
535
  }
536
+ /** The cross-origin session cookie when `fixedDataConfig.crossOriginSession` is set and usable here. */
537
+ createCrossOriginSession() {
538
+ const option = this.fixedData?.crossOriginSession;
539
+ if (!option) {
540
+ return void 0;
541
+ }
542
+ try {
543
+ if (this.isTrackingBlocked()) {
544
+ return void 0;
545
+ }
546
+ return CrossOriginSession.create({
547
+ projectId: this.options.projectId,
548
+ cookieDomain: option.cookieDomain,
549
+ logger: this.logger,
550
+ inactivityMs: () => this.config.sessionInactivityMs,
551
+ report: (level, message) => this.reportTelemetry(level, message)
552
+ });
553
+ } catch (error) {
554
+ this.logger.error("Failed to set up the cross-origin session", error);
555
+ return void 0;
556
+ }
557
+ }
558
+ /**
559
+ * Last activity of the local session records BEFORE this load rewrites them (both paths, by id), so the
560
+ * cross-origin boot can tell a session that was active here from one this load just created.
561
+ */
562
+ readLocalSessionActivity() {
563
+ const activity = /* @__PURE__ */ new Map();
564
+ try {
565
+ const continuity = this.persistentStorage.getItem(SESSION_CONTINUITY_KEY);
566
+ if (continuity?.id && typeof continuity.lastActiveAt === "number") {
567
+ activity.set(continuity.id, continuity.lastActiveAt);
568
+ }
569
+ const perTab = this.sessionStorage.getItem(SESSION_STORAGE_KEY);
570
+ if (perTab && typeof perTab === "object" && perTab.id && typeof perTab.lastActiveAt === "number") {
571
+ activity.set(perTab.id, Math.max(activity.get(perTab.id) ?? 0, perTab.lastActiveAt));
572
+ }
573
+ } catch (error) {
574
+ this.logger.error("Failed to read local session activity", error);
575
+ }
576
+ return activity;
577
+ }
578
+ /**
579
+ * Reconcile the session this load resolved locally with the cross-origin cookie before the first event:
580
+ * adopt the cookie's session (and persist it locally, so reloads and same-origin tabs follow) or keep
581
+ * the local one, and continue the seq at or above the cookie's mark. Never throws.
582
+ */
583
+ bootCrossOriginSession(localActivity) {
584
+ try {
585
+ this.crossOriginAdoptedId = void 0;
586
+ const now = Date.now();
587
+ const continuity = this.config.sessionContinuity;
588
+ const localSeq = continuity ? Math.max(this.seq, this.persistentStorage.getItem(SEQ_STORAGE_KEY) ?? 0) : this.seq;
589
+ const result = this.crossOrigin?.boot(
590
+ {
591
+ sessionId: this.sessionId,
592
+ lastActiveAt: this.sessionId && localActivity.get(this.sessionId) || 0,
593
+ seq: localSeq
594
+ },
595
+ now
596
+ );
597
+ if (!result || !result.sessionId) {
598
+ return;
599
+ }
600
+ if (result.adopted) {
601
+ this.sessionId = result.sessionId;
602
+ this.crossOriginAdoptedId = result.sessionId;
603
+ if (continuity) {
604
+ this.persistContinuitySession(result.sessionId, now);
605
+ } else {
606
+ this.persistPerTabSession(result.sessionId, now);
607
+ }
608
+ this.logger.debug("Adopted cross-origin session id", result.sessionId);
609
+ }
610
+ this.seq = result.seq;
611
+ if (continuity) {
612
+ this.persistentStorage.setItem(SEQ_STORAGE_KEY, result.seq);
613
+ }
614
+ } catch (error) {
615
+ this.logger.error("Failed to apply the cross-origin session", error);
616
+ }
617
+ }
618
+ /**
619
+ * Resolve the session of this load: locally ({@link resolveSessionId}), then reconciled with the
620
+ * cross-origin cookie when that option is on. Used at boot and by the recovery in `getSessionId`.
621
+ */
622
+ resolveSession() {
623
+ if (!this.crossOrigin) {
624
+ return this.resolveSessionId();
625
+ }
626
+ const localActivity = this.readLocalSessionActivity();
627
+ this.sessionId = this.resolveSessionId();
628
+ this.bootCrossOriginSession(localActivity);
629
+ return this.sessionId;
630
+ }
631
+ /** `meta.crossOriginSession` of a batch of `sessionId`; undefined while the option is off on this page. */
632
+ crossOriginSessionMeta(sessionId) {
633
+ if (!this.crossOrigin?.isEnabled()) {
634
+ return void 0;
635
+ }
636
+ return sessionId && sessionId === this.crossOriginAdoptedId ? "adopted" : "own";
637
+ }
529
638
  resolveSessionId() {
530
639
  try {
531
640
  if (this.config.sessionContinuity) {
@@ -659,7 +768,9 @@ var CorbadoTracker = class _CorbadoTracker {
659
768
  if (this.sessionId && now - this.lastActivityWriteAt >= SESSION_ACTIVITY_WRITE_INTERVAL_MS) {
660
769
  this.persistPerTabSession(this.sessionId, now);
661
770
  }
662
- return this.seq++;
771
+ const current = this.crossOrigin ? this.crossOrigin.claim(this.sessionId, this.seq, now) : this.seq;
772
+ this.seq = current + 1;
773
+ return current;
663
774
  }
664
775
  const allocate = () => {
665
776
  const now = Date.now();
@@ -689,7 +800,10 @@ var CorbadoTracker = class _CorbadoTracker {
689
800
  if (sessionChanged) {
690
801
  this.seq = typeof stored === "number" ? stored : 0;
691
802
  }
692
- const current = typeof stored === "number" && stored > this.seq ? stored : this.seq;
803
+ let current = typeof stored === "number" && stored > this.seq ? stored : this.seq;
804
+ if (this.crossOrigin) {
805
+ current = this.crossOrigin.claim(this.sessionId, current, now);
806
+ }
693
807
  this.seq = current + 1;
694
808
  this.persistentStorage.setItem(SEQ_STORAGE_KEY, this.seq);
695
809
  return current;
@@ -1514,16 +1628,6 @@ var CorbadoTracker = class _CorbadoTracker {
1514
1628
  this.logger.error("Failed to flush queue keepalive", error);
1515
1629
  }
1516
1630
  }
1517
- /**
1518
- * Whether input trackers run the DOM password-manager scan (`pa-1`..`pa-6` lows): only an explicitly
1519
- * set `fixedDataConfig.pmd` decides; without `fixedDataConfig` or with `pmd` omitted it always runs
1520
- * (historical behaviour).
1521
- *
1522
- * @internal
1523
- */
1524
- isPasswordManagerDomScanEnabled() {
1525
- return this.fixedData?.pmdExplicit ? this.fixedData.pmd : true;
1526
- }
1527
1631
  /**
1528
1632
  * Keepalive flush requested by a low-event tracker's `pagehide` teardown. Gated by the
1529
1633
  * server-controlled trigger switch `tlf` (boot-snapshot, default true = historical behavior):
@@ -1780,6 +1884,7 @@ var CorbadoTracker = class _CorbadoTracker {
1780
1884
  * resources are released either way.
1781
1885
  */
1782
1886
  async destroy() {
1887
+ this.crossOrigin?.stop();
1783
1888
  this.configManager?.destroy();
1784
1889
  this.passwordManagerSampler?.destroy();
1785
1890
  this.windowObserver?.destroy();
@@ -1827,7 +1932,7 @@ var CorbadoTracker = class _CorbadoTracker {
1827
1932
  */
1828
1933
  getSessionId() {
1829
1934
  if (!this.sessionId) {
1830
- const recovered = this.resolveSessionId();
1935
+ const recovered = this.resolveSession();
1831
1936
  if (recovered) {
1832
1937
  this.sessionId = recovered;
1833
1938
  this.queue.setSessionId(recovered);
@@ -1835,6 +1940,69 @@ var CorbadoTracker = class _CorbadoTracker {
1835
1940
  }
1836
1941
  return this.sessionId || void 0;
1837
1942
  }
1943
+ /**
1944
+ * Store an opaque context string with the cross-origin session, for the integration's own use on the
1945
+ * next origin (e.g. a journey id and stage). Requires `fixedDataConfig.crossOriginSession`.
1946
+ *
1947
+ * @remarks
1948
+ * The value travels in the shared cookie only (see {@link CrossOriginSessionConfig}): it is never sent
1949
+ * to Corbado, but the browser sends the cookie with every request to every subdomain of the configured
1950
+ * domain. It belongs to the current session: a tracker that adopts the session reads it with
1951
+ * {@link CorbadoTracker.getCrossOriginContext}, and a new session (inactivity, `resetSession`) starts
1952
+ * without it. At most 128 characters of `A-Z a-z 0-9 . _ ~ -`; never put personal data in it.
1953
+ * `null`, `undefined` or `""` clears it.
1954
+ *
1955
+ * Never throws.
1956
+ *
1957
+ * @param context - The value to store; `null`, `undefined` or `""` clears it.
1958
+ * @returns `true` when the value was accepted and written; `false` when it is invalid or the feature
1959
+ * is off on this page (not configured, cookie blocked, domain mismatch, not HTTPS).
1960
+ *
1961
+ * @example
1962
+ * ```typescript
1963
+ * tracker.setCrossOriginContext(`v1.${journeyId}.passkey.b`);
1964
+ * ```
1965
+ */
1966
+ setCrossOriginContext(context) {
1967
+ try {
1968
+ if (!this.crossOrigin) {
1969
+ return false;
1970
+ }
1971
+ const value = context === null || context === void 0 || context === "" ? void 0 : context;
1972
+ if (value !== void 0 && !isValidCrossOriginContext(value)) {
1973
+ this.logger.debug("Ignoring invalid cross-origin context (expected <= 128 chars of [A-Za-z0-9._~-])");
1974
+ return false;
1975
+ }
1976
+ const seq = this.config.sessionContinuity ? Math.max(this.seq, this.persistentStorage.getItem(SEQ_STORAGE_KEY) ?? 0) : this.seq;
1977
+ const next = this.crossOrigin.setContext(this.sessionId, seq, value);
1978
+ if (next === void 0) {
1979
+ return false;
1980
+ }
1981
+ if (next > this.seq) {
1982
+ this.seq = next;
1983
+ if (this.config.sessionContinuity) {
1984
+ this.persistentStorage.setItem(SEQ_STORAGE_KEY, next);
1985
+ }
1986
+ }
1987
+ return true;
1988
+ } catch (error) {
1989
+ this.logger.error("Failed to set the cross-origin context", error);
1990
+ return false;
1991
+ }
1992
+ }
1993
+ /**
1994
+ * The context string stored with the current cross-origin session (see
1995
+ * {@link CorbadoTracker.setCrossOriginContext}), or `undefined` when none is set, the session has no
1996
+ * context, or the feature is off on this page. Never throws.
1997
+ */
1998
+ getCrossOriginContext() {
1999
+ try {
2000
+ return this.crossOrigin?.getContext(this.sessionId);
2001
+ } catch (error) {
2002
+ this.logger.error("Failed to read the cross-origin context", error);
2003
+ return void 0;
2004
+ }
2005
+ }
1838
2006
  /** @internal */
1839
2007
  resetSession() {
1840
2008
  const newSessionId = uuidv7();
@@ -1847,6 +2015,7 @@ var CorbadoTracker = class _CorbadoTracker {
1847
2015
  this.sessionId = newSessionId;
1848
2016
  this.seq = 0;
1849
2017
  this.queue.setSessionId(newSessionId);
2018
+ this.crossOrigin?.reset(newSessionId);
1850
2019
  this.logger.debug(`Session reset: ${newSessionId}`);
1851
2020
  return newSessionId;
1852
2021
  }
@@ -183,12 +183,12 @@ 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";
186
+ const crossOrigin = value.crossOriginSession;
187
+ const cookieDomain = crossOrigin && typeof crossOrigin === "object" && typeof crossOrigin.cookieDomain === "string" ? crossOrigin.cookieDomain : void 0;
188
188
  return {
189
- pmd: pmdExplicit ? pmd : false,
190
- pmdExplicit,
191
- privateMode: asBoolean(value.privateMode, true)
189
+ pmd: asBoolean(value.pmd, false),
190
+ privateMode: asBoolean(value.privateMode, true),
191
+ ...cookieDomain !== void 0 && { crossOriginSession: { cookieDomain } }
192
192
  };
193
193
  } catch {
194
194
  return void 0;