@corbado/observe 1.9.0 → 1.10.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.
- package/dist/analysis/client.mjs +183 -4
- package/dist/analysis/config-manager.mjs +143 -14
- package/dist/analysis/config.mjs +16 -5
- package/dist/analysis/cross-origin-session.mjs +422 -0
- package/dist/analysis/queue.mjs +9 -0
- package/dist/analysis/readers/page.mjs +4 -0
- package/dist/analysis/version.mjs +1 -1
- package/dist/cdn/script.global.js +4 -4
- package/dist/index.d.mts +145 -10
- package/dist/index.d.ts +145 -10
- package/dist/index.js +4 -4
- package/dist/index.mjs +4 -4
- package/package.json +1 -1
package/dist/analysis/client.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -1780,6 +1894,7 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1780
1894
|
* resources are released either way.
|
|
1781
1895
|
*/
|
|
1782
1896
|
async destroy() {
|
|
1897
|
+
this.crossOrigin?.stop();
|
|
1783
1898
|
this.configManager?.destroy();
|
|
1784
1899
|
this.passwordManagerSampler?.destroy();
|
|
1785
1900
|
this.windowObserver?.destroy();
|
|
@@ -1827,7 +1942,7 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1827
1942
|
*/
|
|
1828
1943
|
getSessionId() {
|
|
1829
1944
|
if (!this.sessionId) {
|
|
1830
|
-
const recovered = this.
|
|
1945
|
+
const recovered = this.resolveSession();
|
|
1831
1946
|
if (recovered) {
|
|
1832
1947
|
this.sessionId = recovered;
|
|
1833
1948
|
this.queue.setSessionId(recovered);
|
|
@@ -1835,6 +1950,69 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1835
1950
|
}
|
|
1836
1951
|
return this.sessionId || void 0;
|
|
1837
1952
|
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Store an opaque context string with the cross-origin session, for the integration's own use on the
|
|
1955
|
+
* next origin (e.g. a journey id and stage). Requires `fixedDataConfig.crossOriginSession`.
|
|
1956
|
+
*
|
|
1957
|
+
* @remarks
|
|
1958
|
+
* The value travels in the shared cookie only (see {@link CrossOriginSessionConfig}): it is never sent
|
|
1959
|
+
* to Corbado, but the browser sends the cookie with every request to every subdomain of the configured
|
|
1960
|
+
* domain. It belongs to the current session: a tracker that adopts the session reads it with
|
|
1961
|
+
* {@link CorbadoTracker.getCrossOriginContext}, and a new session (inactivity, `resetSession`) starts
|
|
1962
|
+
* without it. At most 128 characters of `A-Z a-z 0-9 . _ ~ -`; never put personal data in it.
|
|
1963
|
+
* `null`, `undefined` or `""` clears it.
|
|
1964
|
+
*
|
|
1965
|
+
* Never throws.
|
|
1966
|
+
*
|
|
1967
|
+
* @param context - The value to store; `null`, `undefined` or `""` clears it.
|
|
1968
|
+
* @returns `true` when the value was accepted and written; `false` when it is invalid or the feature
|
|
1969
|
+
* is off on this page (not configured, cookie blocked, domain mismatch, not HTTPS).
|
|
1970
|
+
*
|
|
1971
|
+
* @example
|
|
1972
|
+
* ```typescript
|
|
1973
|
+
* tracker.setCrossOriginContext(`v1.${journeyId}.passkey.b`);
|
|
1974
|
+
* ```
|
|
1975
|
+
*/
|
|
1976
|
+
setCrossOriginContext(context) {
|
|
1977
|
+
try {
|
|
1978
|
+
if (!this.crossOrigin) {
|
|
1979
|
+
return false;
|
|
1980
|
+
}
|
|
1981
|
+
const value = context === null || context === void 0 || context === "" ? void 0 : context;
|
|
1982
|
+
if (value !== void 0 && !isValidCrossOriginContext(value)) {
|
|
1983
|
+
this.logger.debug("Ignoring invalid cross-origin context (expected <= 128 chars of [A-Za-z0-9._~-])");
|
|
1984
|
+
return false;
|
|
1985
|
+
}
|
|
1986
|
+
const seq = this.config.sessionContinuity ? Math.max(this.seq, this.persistentStorage.getItem(SEQ_STORAGE_KEY) ?? 0) : this.seq;
|
|
1987
|
+
const next = this.crossOrigin.setContext(this.sessionId, seq, value);
|
|
1988
|
+
if (next === void 0) {
|
|
1989
|
+
return false;
|
|
1990
|
+
}
|
|
1991
|
+
if (next > this.seq) {
|
|
1992
|
+
this.seq = next;
|
|
1993
|
+
if (this.config.sessionContinuity) {
|
|
1994
|
+
this.persistentStorage.setItem(SEQ_STORAGE_KEY, next);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
return true;
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
this.logger.error("Failed to set the cross-origin context", error);
|
|
2000
|
+
return false;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* The context string stored with the current cross-origin session (see
|
|
2005
|
+
* {@link CorbadoTracker.setCrossOriginContext}), or `undefined` when none is set, the session has no
|
|
2006
|
+
* context, or the feature is off on this page. Never throws.
|
|
2007
|
+
*/
|
|
2008
|
+
getCrossOriginContext() {
|
|
2009
|
+
try {
|
|
2010
|
+
return this.crossOrigin?.getContext(this.sessionId);
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
this.logger.error("Failed to read the cross-origin context", error);
|
|
2013
|
+
return void 0;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
1838
2016
|
/** @internal */
|
|
1839
2017
|
resetSession() {
|
|
1840
2018
|
const newSessionId = uuidv7();
|
|
@@ -1847,6 +2025,7 @@ var CorbadoTracker = class _CorbadoTracker {
|
|
|
1847
2025
|
this.sessionId = newSessionId;
|
|
1848
2026
|
this.seq = 0;
|
|
1849
2027
|
this.queue.setSessionId(newSessionId);
|
|
2028
|
+
this.crossOrigin?.reset(newSessionId);
|
|
1850
2029
|
this.logger.debug(`Session reset: ${newSessionId}`);
|
|
1851
2030
|
return newSessionId;
|
|
1852
2031
|
}
|
|
@@ -1,36 +1,139 @@
|
|
|
1
1
|
// src/config-manager.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
cacheConfigRecord,
|
|
4
|
+
CONFIG_STORAGE_KEY,
|
|
5
|
+
loadCachedConfig,
|
|
6
|
+
loadConfigRecord,
|
|
7
|
+
parseReliabilityConfig
|
|
8
|
+
} from "./config.mjs";
|
|
3
9
|
import { sendFetch } from "./readers/network.mjs";
|
|
10
|
+
import { listenWindow, readDocumentHidden } from "./readers/page.mjs";
|
|
4
11
|
var CONFIG_FETCH_TIMEOUT_MS = 10 * 1e3;
|
|
5
|
-
var
|
|
12
|
+
var CONFIG_MIN_ATTEMPT_GAP_MS = 60 * 1e3;
|
|
13
|
+
var CONFIG_DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
14
|
+
var CONFIG_MIN_TTL_MS = 5 * 60 * 1e3;
|
|
15
|
+
var CONFIG_MAX_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
6
16
|
var CONFIG_RETRY_DELAYS_MS = [1e3, 3e3];
|
|
17
|
+
function configTtlMs(headers) {
|
|
18
|
+
const maxAge = /(?:^|[\s,])max-age\s*=\s*"?(\d+)"?/i.exec(headers?.get("Cache-Control") ?? "");
|
|
19
|
+
if (!maxAge) return CONFIG_DEFAULT_TTL_MS;
|
|
20
|
+
const age = Number(headers?.get("Age") ?? 0);
|
|
21
|
+
const ttlMs = (Number(maxAge[1]) - (Number.isFinite(age) && age > 0 ? age : 0)) * 1e3;
|
|
22
|
+
return Math.min(Math.max(ttlMs, CONFIG_MIN_TTL_MS), CONFIG_MAX_TTL_MS);
|
|
23
|
+
}
|
|
7
24
|
var ConfigManager = class {
|
|
8
25
|
constructor(url, storage, logger) {
|
|
9
26
|
this.url = url;
|
|
10
27
|
this.storage = storage;
|
|
11
28
|
this.logger = logger;
|
|
12
29
|
this.initialConfig = loadCachedConfig(storage) ?? void 0;
|
|
30
|
+
this.appliedVersion = this.initialConfig?.version;
|
|
13
31
|
}
|
|
14
32
|
initialConfig;
|
|
15
|
-
refreshTimer;
|
|
16
33
|
requestTimer;
|
|
17
34
|
retryTimer;
|
|
18
35
|
controller;
|
|
36
|
+
removers = [];
|
|
19
37
|
fetching = false;
|
|
20
38
|
stopped = false;
|
|
39
|
+
lastAttemptAt;
|
|
40
|
+
/** Version last handed to (or initially applied by) the tracker. */
|
|
41
|
+
appliedVersion;
|
|
21
42
|
onConfig;
|
|
22
43
|
start(onConfig) {
|
|
23
44
|
if (this.stopped || this.onConfig) return;
|
|
24
45
|
this.onConfig = onConfig;
|
|
25
|
-
|
|
46
|
+
this.listen();
|
|
47
|
+
this.refresh();
|
|
26
48
|
}
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
listen() {
|
|
50
|
+
if (typeof window === "undefined") return;
|
|
51
|
+
const onVisibilityChange = () => {
|
|
52
|
+
const hidden = typeof document === "undefined" || readDocumentHidden({
|
|
53
|
+
key: "page:lifecycle.visibility",
|
|
54
|
+
title: "Page visibility",
|
|
55
|
+
accessNote: "document.visibilityState",
|
|
56
|
+
group: "Page lifecycle",
|
|
57
|
+
origin: "observe-external",
|
|
58
|
+
fields: [
|
|
59
|
+
{
|
|
60
|
+
path: "visibilityState",
|
|
61
|
+
label: "Whether the page is hidden",
|
|
62
|
+
exposure: "enum",
|
|
63
|
+
transform: { values: ["hidden", "other"] }
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
if (!hidden) this.refresh();
|
|
68
|
+
};
|
|
69
|
+
this.removers.push(
|
|
70
|
+
listenWindow(
|
|
71
|
+
{
|
|
72
|
+
key: "page:lifecycle.visibilitychange",
|
|
73
|
+
title: "Page visibility changes",
|
|
74
|
+
accessNote: "visibilitychange event on document",
|
|
75
|
+
group: "Page lifecycle",
|
|
76
|
+
origin: "observe-external",
|
|
77
|
+
fields: [
|
|
78
|
+
{
|
|
79
|
+
path: "event.visibilitychange",
|
|
80
|
+
label: "Page hidden or shown again",
|
|
81
|
+
exposure: "presence",
|
|
82
|
+
transform: { note: "checks whether Observe settings are due for a refresh when the page is shown" }
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"visibilitychange",
|
|
87
|
+
onVisibilityChange
|
|
88
|
+
),
|
|
89
|
+
listenWindow(
|
|
90
|
+
{
|
|
91
|
+
key: "page:lifecycle.pageshow",
|
|
92
|
+
title: "Returning to the page",
|
|
93
|
+
accessNote: "pageshow event on window, its persisted flag",
|
|
94
|
+
group: "Page lifecycle",
|
|
95
|
+
origin: "observe-external",
|
|
96
|
+
fields: [
|
|
97
|
+
{
|
|
98
|
+
path: "event.persisted",
|
|
99
|
+
label: "Page restored from the browser's back/forward cache",
|
|
100
|
+
exposure: "presence",
|
|
101
|
+
transform: { note: "checks whether Observe settings are due for a refresh" }
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"pageshow",
|
|
106
|
+
(event) => {
|
|
107
|
+
if (event.persisted) this.refresh();
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
/** Fetches when the cached config is stale; adopts a fresh config another tab stored meanwhile. */
|
|
113
|
+
refresh() {
|
|
114
|
+
try {
|
|
115
|
+
if (this.stopped || this.fetching) return;
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
const record = loadConfigRecord(this.storage);
|
|
118
|
+
const { fetchedAt, expiresAt } = record ?? {};
|
|
119
|
+
if (record?.endpoint === this.url && typeof fetchedAt === "number" && typeof expiresAt === "number" && fetchedAt > 0 && expiresAt - fetchedAt <= CONFIG_MAX_TTL_MS && fetchedAt <= now && now < expiresAt) {
|
|
120
|
+
if (record.config.version !== this.appliedVersion) {
|
|
121
|
+
this.appliedVersion = record.config.version;
|
|
122
|
+
this.onConfig?.(record.config);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const sinceAttempt = this.lastAttemptAt === void 0 ? Infinity : now - this.lastAttemptAt;
|
|
127
|
+
if (sinceAttempt >= 0 && sinceAttempt < CONFIG_MIN_ATTEMPT_GAP_MS) return;
|
|
128
|
+
this.lastAttemptAt = now;
|
|
31
129
|
clearTimeout(this.retryTimer);
|
|
32
|
-
|
|
130
|
+
void this.fetchConfig();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
this.logger.error("Unexpected error in SDK config refresh", error);
|
|
33
133
|
}
|
|
134
|
+
}
|
|
135
|
+
async fetchConfig(attempt = 0) {
|
|
136
|
+
if (this.stopped || this.fetching) return;
|
|
34
137
|
this.fetching = true;
|
|
35
138
|
const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
|
|
36
139
|
this.controller = controller;
|
|
@@ -51,6 +154,18 @@ var ConfigManager = class {
|
|
|
51
154
|
label: "Whether the server asked to retry later",
|
|
52
155
|
exposure: "presence"
|
|
53
156
|
},
|
|
157
|
+
{
|
|
158
|
+
path: "response.header.cache-control",
|
|
159
|
+
label: "How long the server allows reusing the settings",
|
|
160
|
+
exposure: "computed",
|
|
161
|
+
transform: { note: "max-age, reduced to the settings' expiry time" }
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
path: "response.header.age",
|
|
165
|
+
label: "How long the settings were already stored on the way",
|
|
166
|
+
exposure: "computed",
|
|
167
|
+
transform: { note: "subtracted from max-age for the settings' expiry time" }
|
|
168
|
+
},
|
|
54
169
|
{
|
|
55
170
|
path: "response.body",
|
|
56
171
|
label: "Observe settings",
|
|
@@ -62,7 +177,9 @@ var ConfigManager = class {
|
|
|
62
177
|
this.url,
|
|
63
178
|
{
|
|
64
179
|
method: "GET",
|
|
65
|
-
cache
|
|
180
|
+
// Revalidates the browser's HTTP cache entry (If-None-Match once the server sends an ETag)
|
|
181
|
+
// without a custom header, which would add a CORS preflight.
|
|
182
|
+
cache: "no-cache",
|
|
66
183
|
...controller ? { signal: controller.signal } : {}
|
|
67
184
|
}
|
|
68
185
|
);
|
|
@@ -75,7 +192,15 @@ var ConfigManager = class {
|
|
|
75
192
|
const config = parseReliabilityConfig(raw);
|
|
76
193
|
if (!config) throw new Error("Invalid SDK config response");
|
|
77
194
|
if (this.stopped || controller?.signal.aborted) return;
|
|
78
|
-
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
cacheConfigRecord(this.storage, {
|
|
197
|
+
endpoint: this.url,
|
|
198
|
+
fetchedAt: now,
|
|
199
|
+
expiresAt: now + configTtlMs(response.headers),
|
|
200
|
+
config
|
|
201
|
+
});
|
|
202
|
+
if (loadConfigRecord(this.storage)) this.storage.removeItem(CONFIG_STORAGE_KEY);
|
|
203
|
+
this.appliedVersion = config.version;
|
|
79
204
|
this.onConfig?.(config);
|
|
80
205
|
} catch (error) {
|
|
81
206
|
if (!this.stopped) {
|
|
@@ -94,7 +219,7 @@ var ConfigManager = class {
|
|
|
94
219
|
}
|
|
95
220
|
destroy() {
|
|
96
221
|
this.stopped = true;
|
|
97
|
-
|
|
222
|
+
this.removers.splice(0).forEach((remove) => remove());
|
|
98
223
|
clearTimeout(this.requestTimer);
|
|
99
224
|
clearTimeout(this.retryTimer);
|
|
100
225
|
this.controller?.abort();
|
|
@@ -102,7 +227,11 @@ var ConfigManager = class {
|
|
|
102
227
|
}
|
|
103
228
|
};
|
|
104
229
|
export {
|
|
230
|
+
CONFIG_DEFAULT_TTL_MS,
|
|
105
231
|
CONFIG_FETCH_TIMEOUT_MS,
|
|
106
|
-
|
|
107
|
-
|
|
232
|
+
CONFIG_MAX_TTL_MS,
|
|
233
|
+
CONFIG_MIN_ATTEMPT_GAP_MS,
|
|
234
|
+
CONFIG_MIN_TTL_MS,
|
|
235
|
+
ConfigManager,
|
|
236
|
+
configTtlMs
|
|
108
237
|
};
|
package/dist/analysis/config.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
2
|
var CONFIG_STORAGE_KEY = "cbo_sdk_config";
|
|
3
|
+
var CONFIG_CACHE_STORAGE_KEY = "cbo_sdk_config_cache";
|
|
3
4
|
var CONFIG_REQUEST_HEADER = "X-Corbado-Observe-Config";
|
|
4
5
|
var DEFAULT_FLUSH_INTERVAL_MS = 500;
|
|
5
6
|
var DEFAULT_SEQ_LOCK_TIMEOUT_MS = 50;
|
|
@@ -184,23 +185,32 @@ function resolveFixedDataConfig(raw) {
|
|
|
184
185
|
const value = raw;
|
|
185
186
|
const pmd = value.pmd;
|
|
186
187
|
const pmdExplicit = typeof pmd === "boolean";
|
|
188
|
+
const crossOrigin = value.crossOriginSession;
|
|
189
|
+
const cookieDomain = crossOrigin && typeof crossOrigin === "object" && typeof crossOrigin.cookieDomain === "string" ? crossOrigin.cookieDomain : void 0;
|
|
187
190
|
return {
|
|
188
191
|
pmd: pmdExplicit ? pmd : false,
|
|
189
192
|
pmdExplicit,
|
|
190
|
-
privateMode: asBoolean(value.privateMode, true)
|
|
193
|
+
privateMode: asBoolean(value.privateMode, true),
|
|
194
|
+
...cookieDomain !== void 0 && { crossOriginSession: { cookieDomain } }
|
|
191
195
|
};
|
|
192
196
|
} catch {
|
|
193
197
|
return void 0;
|
|
194
198
|
}
|
|
195
199
|
}
|
|
200
|
+
function loadConfigRecord(storage) {
|
|
201
|
+
const record = storage.getItem(CONFIG_CACHE_STORAGE_KEY);
|
|
202
|
+
const config = record && typeof record === "object" ? parseReliabilityConfig(record.config) : null;
|
|
203
|
+
return config ? { ...record, config } : null;
|
|
204
|
+
}
|
|
196
205
|
function loadCachedConfig(storage) {
|
|
197
|
-
return parseReliabilityConfig(storage.getItem(CONFIG_STORAGE_KEY));
|
|
206
|
+
return loadConfigRecord(storage)?.config ?? parseReliabilityConfig(storage.getItem(CONFIG_STORAGE_KEY));
|
|
198
207
|
}
|
|
199
|
-
function
|
|
200
|
-
storage.setItem(
|
|
208
|
+
function cacheConfigRecord(storage, record) {
|
|
209
|
+
storage.setItem(CONFIG_CACHE_STORAGE_KEY, record);
|
|
201
210
|
}
|
|
202
211
|
export {
|
|
203
212
|
CONFIG_BOUNDS,
|
|
213
|
+
CONFIG_CACHE_STORAGE_KEY,
|
|
204
214
|
CONFIG_REQUEST_HEADER,
|
|
205
215
|
CONFIG_STORAGE_KEY,
|
|
206
216
|
DEFAULT_DEVICE_INFO_COLLECTOR_TIMEOUT_MS,
|
|
@@ -208,8 +218,9 @@ export {
|
|
|
208
218
|
DEFAULT_RELIABILITY_CONFIG,
|
|
209
219
|
DEFAULT_SEQ_LOCK_TIMEOUT_MS,
|
|
210
220
|
DEFAULT_SESSION_INACTIVITY_MS,
|
|
211
|
-
|
|
221
|
+
cacheConfigRecord,
|
|
212
222
|
loadCachedConfig,
|
|
223
|
+
loadConfigRecord,
|
|
213
224
|
parseReliabilityConfig,
|
|
214
225
|
resolveFixedDataConfig,
|
|
215
226
|
resolveReliabilityConfig
|