@corbado/observe 1.8.0 → 1.9.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.
Files changed (43) hide show
  1. package/dist/analysis/client.mjs +82 -56
  2. package/dist/analysis/config-manager.mjs +172 -18
  3. package/dist/analysis/config.mjs +12 -4
  4. package/dist/analysis/device-info-manager.mjs +16 -1
  5. package/dist/analysis/device.mjs +88 -10
  6. package/dist/analysis/handoff.mjs +7 -0
  7. package/dist/analysis/low-events/grouped-input-tracker.mjs +148 -33
  8. package/dist/analysis/low-events/input-tracker.mjs +138 -59
  9. package/dist/analysis/low-events/shared-window-observer.mjs +148 -77
  10. package/dist/analysis/low-events/viewport-batcher.mjs +3 -3
  11. package/dist/analysis/queue.mjs +75 -8
  12. package/dist/analysis/{low-events/input-paste-observer.mjs → readers/input-paste.mjs} +2 -2
  13. package/dist/analysis/readers/input.mjs +61 -0
  14. package/dist/analysis/readers/loader-global.mjs +15 -0
  15. package/dist/analysis/readers/network.mjs +14 -0
  16. package/dist/analysis/readers/page.mjs +30 -0
  17. package/dist/analysis/readers/storage.mjs +28 -0
  18. package/dist/analysis/{utils → readers}/web-lock.mjs +6 -2
  19. package/dist/analysis/readers/window-events.mjs +48 -0
  20. package/dist/analysis/storage/index.mjs +61 -9
  21. package/dist/analysis/store/event-store.mjs +34 -16
  22. package/dist/analysis/transports/fetch.mjs +45 -3
  23. package/dist/analysis/utils/email-link-operation-full.mjs +22 -2
  24. package/dist/analysis/utils/email-otp-operation-full.mjs +0 -1
  25. package/dist/analysis/utils/form-operation-full.mjs +0 -1
  26. package/dist/analysis/utils/operation-full-provide-identifier-with-cui.mjs +6 -6
  27. package/dist/analysis/utils/passkey-enrollment-operation-full.mjs +9 -6
  28. package/dist/analysis/utils/passkey-login-operation-full.mjs +10 -5
  29. package/dist/analysis/utils/password-login-operation-full.mjs +8 -4
  30. package/dist/analysis/utils/password-manager-sampler.mjs +44 -4
  31. package/dist/analysis/utils/sms-otp-operation-full.mjs +0 -1
  32. package/dist/analysis/utils/totp-operation-full.mjs +0 -1
  33. package/dist/analysis/version.mjs +1 -1
  34. package/dist/cdn/script.global.js +4 -4
  35. package/dist/index.d.mts +151 -26
  36. package/dist/index.d.ts +151 -26
  37. package/dist/index.js +2 -2
  38. package/dist/index.mjs +2 -2
  39. package/package.json +5 -3
  40. package/tools/reader-meta-text.d.mts +2 -0
  41. package/tools/reader-meta-text.mjs +26 -0
  42. package/tools/strip-reader-meta.d.mts +33 -0
  43. package/tools/strip-reader-meta.mjs +131 -0
@@ -4,7 +4,8 @@ import {
4
4
  KeyRegistrationOperationFull
5
5
  } from "./utils/key-operation-full.mjs";
6
6
  import { reserveCaptureSeq } from "./low-events/capture-sequence.mjs";
7
- import { applySourcePathQuery } from "./utils/source-path.mjs";
7
+ import { readSourcePath } from "./readers/page.mjs";
8
+ import { readUserAgent } from "./readers/device.mjs";
8
9
  import { SharedWindowObserver, getCapturePageId } from "./low-events/shared-window-observer.mjs";
9
10
  import {
10
11
  AuthEventName
@@ -33,7 +34,7 @@ import { PasskeyLoginOperationFull } from "./utils/passkey-login-operation-full.
33
34
  import { EmailOtpOperationFull } from "./utils/email-otp-operation-full.mjs";
34
35
  import { SmsOtpOperationFull } from "./utils/sms-otp-operation-full.mjs";
35
36
  import { isBlockedUserAgent } from "./utils/blocked-user-agents.mjs";
36
- import { withWebLock } from "./utils/web-lock.mjs";
37
+ import { hasWebLocks, withWebLock } from "./readers/web-lock.mjs";
37
38
  import { SDK_NAME, SDK_VERSION } from "./version.mjs";
38
39
  import {
39
40
  EmailLinkContinuationOperation,
@@ -507,7 +508,23 @@ var CorbadoTracker = class _CorbadoTracker {
507
508
  if (typeof navigator === "undefined") {
508
509
  return false;
509
510
  }
510
- return isBlockedUserAgent(navigator.userAgent);
511
+ return isBlockedUserAgent(
512
+ readUserAgent({
513
+ key: "browser-api:sdk.blocked-user-agent",
514
+ title: "User agent check for crawlers and bots",
515
+ accessNote: "navigator.userAgent, matched against a list of crawler and bot patterns",
516
+ group: "Observe SDK state",
517
+ origin: "observe-external",
518
+ fields: [
519
+ {
520
+ path: "result",
521
+ label: "Whether the browser looks like a bot",
522
+ exposure: "computed",
523
+ transform: { note: "matched against the blocked user-agent list; only the boolean is used (tracking off)" }
524
+ }
525
+ ]
526
+ })
527
+ );
511
528
  }
512
529
  resolveSessionId() {
513
530
  try {
@@ -678,27 +695,74 @@ var CorbadoTracker = class _CorbadoTracker {
678
695
  return current;
679
696
  };
680
697
  const timeoutMs = this.config.seqLockTimeoutMs;
681
- if (timeoutMs === 0 || typeof navigator === "undefined" || !navigator.locks?.request) {
698
+ if (timeoutMs === 0 || !hasWebLocks({
699
+ key: "browser-api:sdk.web-locks",
700
+ title: "Coordination of Observe data across open tabs",
701
+ accessNote: "Web Locks API (navigator.locks) on Observe's own lock names",
702
+ group: "Observe SDK state",
703
+ origin: "observe-internal",
704
+ fields: [
705
+ {
706
+ path: "available",
707
+ label: "Whether the browser supports tab coordination",
708
+ exposure: "enum",
709
+ transform: { values: [true, false] }
710
+ }
711
+ ]
712
+ })) {
682
713
  return allocate();
683
714
  }
684
- return withWebLock(this.seqLockName, allocate, {
685
- timeoutMs,
686
- onTimeout: () => {
687
- this.logger.warn(`Sequence Web Lock acquisition timed out after ${timeoutMs}ms; using fallback`);
688
- this.reportTelemetry("info", `seq web lock timeout; using fallback, ${timeoutMs}ms`);
715
+ return withWebLock(
716
+ {
717
+ key: "browser-api:sdk.web-locks",
718
+ title: "Coordination of Observe data across open tabs",
719
+ accessNote: "Web Locks API (navigator.locks) on Observe's own lock names",
720
+ group: "Observe SDK state",
721
+ origin: "observe-internal",
722
+ fields: [
723
+ {
724
+ path: "result",
725
+ label: "Coordination between open tabs",
726
+ exposure: "none",
727
+ transform: { note: "own cbo_* lock names; serializes the SDK's storage updates across tabs" }
728
+ }
729
+ ]
730
+ },
731
+ this.seqLockName,
732
+ allocate,
733
+ {
734
+ timeoutMs,
735
+ onTimeout: () => {
736
+ this.logger.warn(`Sequence Web Lock acquisition timed out after ${timeoutMs}ms; using fallback`);
737
+ this.reportTelemetry("info", `seq web lock timeout; using fallback, ${timeoutMs}ms`);
738
+ }
689
739
  }
690
- });
740
+ );
691
741
  }
692
742
  resolveTrackingSourcePath() {
693
743
  if (typeof window === "undefined") {
694
744
  return void 0;
695
745
  }
696
- const search = applySourcePathQuery(
697
- window.location.search,
746
+ return readSourcePath(
747
+ {
748
+ key: "page:location.source-path",
749
+ title: "Page path and allowed query parameters",
750
+ accessNote: "location.pathname and location.search; the query is reduced by the sourcePathQuery policy inside the reader (trackingSourcePath)",
751
+ group: "Page",
752
+ origin: "observe-external",
753
+ fields: [
754
+ { path: "url.path", label: "Page path", exposure: "raw" },
755
+ {
756
+ path: "url.query",
757
+ label: "Page address parameters (allowed ones only)",
758
+ exposure: "redacted",
759
+ transform: { note: "sourcePathQuery 'allowlist': values of non-allowlisted keys become '*'; 'all': raw" }
760
+ }
761
+ ]
762
+ },
698
763
  this.config.sourcePathQuery,
699
764
  this.config.sourcePathQueryAllow
700
765
  );
701
- return `${window.location.pathname}${search}`;
702
766
  }
703
767
  /**
704
768
  * Per-tab id for event meta, cached after the first successful resolution: the id is
@@ -1535,56 +1599,18 @@ var CorbadoTracker = class _CorbadoTracker {
1535
1599
  smsOtpOperationFull(config) {
1536
1600
  return new SmsOtpOperationFull(this, config);
1537
1601
  }
1538
- /**
1539
- * Track a code from an authenticator app (TOTP) through its verification.
1540
- *
1541
- * @param config - Optional code input field(s) and spec type (`totp-login` or `totp-enrollment`).
1542
- * @remarks
1543
- * There is no send step: the code comes from the user's authenticator, not from the application.
1544
- * Report the verification request on postResponse. Bind the code input to capture typing and
1545
- * autofill; use setInputFields when the input is rendered after the operation starts.
1546
- * @example
1547
- * ```ts
1548
- * const totp = tracker.totpOperationFull({ explicitSpecType: "totp-login", inputHtmlField: codeInput });
1549
- * totp.postResponse.start({});
1550
- * totp.postResponse.finished({});
1551
- * ```
1552
- */
1553
1602
  totpOperationFull(config) {
1554
1603
  return new TotpOperationFull(this, config);
1555
1604
  }
1556
1605
  provideIdentifierOperationFull(config) {
1557
- return new OperationFullProvideIdentifierWithCUI(this, config);
1606
+ return new OperationFullProvideIdentifierWithCUI(
1607
+ this,
1608
+ config
1609
+ );
1558
1610
  }
1559
1611
  provideDataOperationFull(config) {
1560
1612
  return new ProvideDataOperationFull(this, config);
1561
1613
  }
1562
- /**
1563
- * Track one form: a page, step or modal where the user provides one coherent set of data and submits
1564
- * it, typically as one step of a custom process flow such as an onboarding.
1565
- *
1566
- * @param config - The form's stable `name` (required); optionally the names of its `fields`, the
1567
- * `inputHtmlFields` to collect interaction signals from, and `startedAt` when the form became
1568
- * visible earlier than this call.
1569
- * @remarks
1570
- * Create the operation when the form is shown; one operation per visit, so going back to a form
1571
- * creates a new one with the same name. Report `submit` when the application accepted the form, or
1572
- * `skip` when the user left it forward without submitting (e.g. "Do it later"). A form that ends with
1573
- * neither counts as abandoned; one whose last submit failed counts as failed. Without a name the form
1574
- * is not counted. Send field names only, never values. A form spread over several pages stays one
1575
- * operation: bind each page's inputs with `setInputFields`.
1576
- * @example
1577
- * ```ts
1578
- * const form = tracker.formOperationFull({ name: "personal-details", fields: ["firstName", "lastName"] });
1579
- * form.submit.start({});
1580
- * try {
1581
- * await savePersonalDetails(values);
1582
- * form.submit.finished({});
1583
- * } catch (error) {
1584
- * form.submit.error(error);
1585
- * }
1586
- * ```
1587
- */
1588
1614
  formOperationFull(config) {
1589
1615
  return new FormOperationFull(this, config);
1590
1616
  }
@@ -1829,7 +1855,7 @@ var PROBE_PAGE_ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx
1829
1855
  function randomProbePageId() {
1830
1856
  const bytes = new Uint8Array(16);
1831
1857
  try {
1832
- globalThis.crypto.getRandomValues(bytes);
1858
+ crypto.getRandomValues(bytes);
1833
1859
  } catch {
1834
1860
  for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
1835
1861
  }
@@ -1,46 +1,188 @@
1
1
  // src/config-manager.ts
2
- import { cacheConfig, loadCachedConfig, parseReliabilityConfig } from "./config.mjs";
2
+ import {
3
+ cacheConfigRecord,
4
+ CONFIG_STORAGE_KEY,
5
+ loadCachedConfig,
6
+ loadConfigRecord,
7
+ parseReliabilityConfig
8
+ } from "./config.mjs";
9
+ import { sendFetch } from "./readers/network.mjs";
10
+ import { listenWindow, readDocumentHidden } from "./readers/page.mjs";
3
11
  var CONFIG_FETCH_TIMEOUT_MS = 10 * 1e3;
4
- var CONFIG_REFRESH_INTERVAL_MS = 10 * 60 * 1e3;
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;
5
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
+ }
6
24
  var ConfigManager = class {
7
25
  constructor(url, storage, logger) {
8
26
  this.url = url;
9
27
  this.storage = storage;
10
28
  this.logger = logger;
11
29
  this.initialConfig = loadCachedConfig(storage) ?? void 0;
30
+ this.appliedVersion = this.initialConfig?.version;
12
31
  }
13
32
  initialConfig;
14
- refreshTimer;
15
33
  requestTimer;
16
34
  retryTimer;
17
35
  controller;
36
+ removers = [];
18
37
  fetching = false;
19
38
  stopped = false;
39
+ lastAttemptAt;
40
+ /** Version last handed to (or initially applied by) the tracker. */
41
+ appliedVersion;
20
42
  onConfig;
21
43
  start(onConfig) {
22
44
  if (this.stopped || this.onConfig) return;
23
45
  this.onConfig = onConfig;
24
- void this.fetchConfig();
46
+ this.listen();
47
+ this.refresh();
25
48
  }
26
- async fetchConfig(attempt = 0) {
27
- if (this.stopped || this.fetching) return;
28
- if (attempt === 0) {
29
- clearTimeout(this.refreshTimer);
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;
30
129
  clearTimeout(this.retryTimer);
31
- this.refreshTimer = setTimeout(() => void this.fetchConfig(), CONFIG_REFRESH_INTERVAL_MS);
130
+ void this.fetchConfig();
131
+ } catch (error) {
132
+ this.logger.error("Unexpected error in SDK config refresh", error);
32
133
  }
134
+ }
135
+ async fetchConfig(attempt = 0) {
136
+ if (this.stopped || this.fetching) return;
33
137
  this.fetching = true;
34
138
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
35
139
  this.controller = controller;
36
140
  this.requestTimer = controller ? setTimeout(() => controller.abort(), CONFIG_FETCH_TIMEOUT_MS) : void 0;
37
141
  let retryable = true;
38
142
  try {
39
- const response = await fetch(this.url, {
40
- method: "GET",
41
- cache: "no-store",
42
- ...controller ? { signal: controller.signal } : {}
43
- });
143
+ const response = await sendFetch(
144
+ {
145
+ key: "network:observe.sdk-config",
146
+ title: "Observe settings download",
147
+ accessNote: "fetch to the Observe API; the response is the SDK reliability config",
148
+ group: "Event delivery",
149
+ origin: "observe-internal",
150
+ fields: [
151
+ { path: "response.status", label: "Response code", exposure: "raw" },
152
+ {
153
+ path: "response.header.retry-after",
154
+ label: "Whether the server asked to retry later",
155
+ exposure: "presence"
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
+ },
169
+ {
170
+ path: "response.body",
171
+ label: "Observe settings",
172
+ exposure: "raw",
173
+ transform: { note: "SDK reliability config" }
174
+ }
175
+ ]
176
+ },
177
+ this.url,
178
+ {
179
+ method: "GET",
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",
183
+ ...controller ? { signal: controller.signal } : {}
184
+ }
185
+ );
44
186
  if (!response.ok) {
45
187
  retryable = (response.status === 408 || response.status === 429 || response.status >= 500) && !response.headers.has("Retry-After");
46
188
  throw new Error(`SDK config request failed (${response.status})`);
@@ -50,7 +192,15 @@ var ConfigManager = class {
50
192
  const config = parseReliabilityConfig(raw);
51
193
  if (!config) throw new Error("Invalid SDK config response");
52
194
  if (this.stopped || controller?.signal.aborted) return;
53
- cacheConfig(this.storage, config);
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;
54
204
  this.onConfig?.(config);
55
205
  } catch (error) {
56
206
  if (!this.stopped) {
@@ -69,7 +219,7 @@ var ConfigManager = class {
69
219
  }
70
220
  destroy() {
71
221
  this.stopped = true;
72
- clearTimeout(this.refreshTimer);
222
+ this.removers.splice(0).forEach((remove) => remove());
73
223
  clearTimeout(this.requestTimer);
74
224
  clearTimeout(this.retryTimer);
75
225
  this.controller?.abort();
@@ -77,7 +227,11 @@ var ConfigManager = class {
77
227
  }
78
228
  };
79
229
  export {
230
+ CONFIG_DEFAULT_TTL_MS,
80
231
  CONFIG_FETCH_TIMEOUT_MS,
81
- CONFIG_REFRESH_INTERVAL_MS,
82
- ConfigManager
232
+ CONFIG_MAX_TTL_MS,
233
+ CONFIG_MIN_ATTEMPT_GAP_MS,
234
+ CONFIG_MIN_TTL_MS,
235
+ ConfigManager,
236
+ configTtlMs
83
237
  };
@@ -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;
@@ -193,14 +194,20 @@ function resolveFixedDataConfig(raw) {
193
194
  return void 0;
194
195
  }
195
196
  }
197
+ function loadConfigRecord(storage) {
198
+ const record = storage.getItem(CONFIG_CACHE_STORAGE_KEY);
199
+ const config = record && typeof record === "object" ? parseReliabilityConfig(record.config) : null;
200
+ return config ? { ...record, config } : null;
201
+ }
196
202
  function loadCachedConfig(storage) {
197
- return parseReliabilityConfig(storage.getItem(CONFIG_STORAGE_KEY));
203
+ return loadConfigRecord(storage)?.config ?? parseReliabilityConfig(storage.getItem(CONFIG_STORAGE_KEY));
198
204
  }
199
- function cacheConfig(storage, config) {
200
- storage.setItem(CONFIG_STORAGE_KEY, config);
205
+ function cacheConfigRecord(storage, record) {
206
+ storage.setItem(CONFIG_CACHE_STORAGE_KEY, record);
201
207
  }
202
208
  export {
203
209
  CONFIG_BOUNDS,
210
+ CONFIG_CACHE_STORAGE_KEY,
204
211
  CONFIG_REQUEST_HEADER,
205
212
  CONFIG_STORAGE_KEY,
206
213
  DEFAULT_DEVICE_INFO_COLLECTOR_TIMEOUT_MS,
@@ -208,8 +215,9 @@ export {
208
215
  DEFAULT_RELIABILITY_CONFIG,
209
216
  DEFAULT_SEQ_LOCK_TIMEOUT_MS,
210
217
  DEFAULT_SESSION_INACTIVITY_MS,
211
- cacheConfig,
218
+ cacheConfigRecord,
212
219
  loadCachedConfig,
220
+ loadConfigRecord,
213
221
  parseReliabilityConfig,
214
222
  resolveFixedDataConfig,
215
223
  resolveReliabilityConfig
@@ -1,6 +1,21 @@
1
1
  // src/device-info-manager.ts
2
+ import { readDocumentHidden } from "./readers/page.mjs";
2
3
  function isDocumentHidden() {
3
- return typeof document !== "undefined" && document.visibilityState === "hidden";
4
+ return typeof document !== "undefined" && readDocumentHidden({
5
+ key: "page:lifecycle.visibility",
6
+ title: "Page visibility",
7
+ accessNote: "document.visibilityState",
8
+ group: "Page lifecycle",
9
+ origin: "observe-external",
10
+ fields: [
11
+ {
12
+ path: "visibilityState",
13
+ label: "Whether the page is hidden",
14
+ exposure: "enum",
15
+ transform: { values: ["hidden", "other"] }
16
+ }
17
+ ]
18
+ });
4
19
  }
5
20
  var DeviceInfoManager = class {
6
21
  constructor(logger, source, refreshInterval, attachInterval, diagnostics, onUndelivered) {
@@ -180,7 +180,18 @@ var DeviceCollector = class {
180
180
  "navigator.bluetooth.getAvailability",
181
181
  () => readBluetoothAvailability({
182
182
  key: "browser-api:device.bluetooth-availability",
183
- fields: [{ path: "result", exposure: "enum", transform: { values: [true, false] } }]
183
+ title: "Bluetooth availability",
184
+ accessNote: "navigator.bluetooth.getAvailability()",
185
+ group: "Device information",
186
+ origin: "observe-external",
187
+ fields: [
188
+ {
189
+ path: "result",
190
+ label: "Whether Bluetooth is available",
191
+ exposure: "enum",
192
+ transform: { values: [true, false] }
193
+ }
194
+ ]
184
195
  })
185
196
  ),
186
197
  this.startField(
@@ -188,9 +199,14 @@ var DeviceCollector = class {
188
199
  "PublicKeyCredential.getClientCapabilities",
189
200
  () => readClientCapabilities({
190
201
  key: "browser-api:device.client-capabilities",
202
+ title: "Passkey features supported by the browser",
203
+ accessNote: "PublicKeyCredential.getClientCapabilities(), every returned key",
204
+ group: "Device information",
205
+ origin: "observe-external",
191
206
  fields: [
192
207
  {
193
208
  path: "result.<capability>",
209
+ label: "Supported passkey features",
194
210
  exposure: "enum",
195
211
  transform: { values: ["true", "false"], note: "open set of capability names, kept as reported" }
196
212
  }
@@ -202,7 +218,18 @@ var DeviceCollector = class {
202
218
  "PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable",
203
219
  () => readUserVerifyingPlatformAuthenticatorAvailable({
204
220
  key: "browser-api:device.uvpaa",
205
- fields: [{ path: "result", exposure: "enum", transform: { values: [true, false] } }]
221
+ title: "Built-in passkey support on the device",
222
+ accessNote: "PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()",
223
+ group: "Device information",
224
+ origin: "observe-external",
225
+ fields: [
226
+ {
227
+ path: "result",
228
+ label: "Whether the device has a built-in passkey authenticator",
229
+ exposure: "enum",
230
+ transform: { values: [true, false] }
231
+ }
232
+ ]
206
233
  })
207
234
  ),
208
235
  this.startField(
@@ -210,7 +237,18 @@ var DeviceCollector = class {
210
237
  "PublicKeyCredential.isConditionalMediationAvailable",
211
238
  () => readConditionalMediationAvailable({
212
239
  key: "browser-api:device.conditional-mediation",
213
- fields: [{ path: "result", exposure: "enum", transform: { values: [true, false] } }]
240
+ title: "Passkey autofill availability",
241
+ accessNote: "PublicKeyCredential.isConditionalMediationAvailable()",
242
+ group: "Device information",
243
+ origin: "observe-external",
244
+ fields: [
245
+ {
246
+ path: "result",
247
+ label: "Whether passkey autofill is available",
248
+ exposure: "enum",
249
+ transform: { values: [true, false] }
250
+ }
251
+ ]
214
252
  })
215
253
  ),
216
254
  this.startField(
@@ -218,11 +256,20 @@ var DeviceCollector = class {
218
256
  "navigator.userAgentData.getHighEntropyValues",
219
257
  () => readJavaScriptHighEntropy({
220
258
  key: "browser-api:device.ua-high-entropy",
259
+ title: "Device platform, version and model reported by the browser",
260
+ accessNote: "User-Agent Client Hints: navigator.userAgentData.getHighEntropyValues()",
261
+ group: "Device information",
262
+ origin: "observe-external",
221
263
  fields: [
222
- { path: "result.platform", exposure: "raw" },
223
- { path: "result.mobile", exposure: "enum", transform: { values: [true, false] } },
224
- { path: "result.platformVersion", exposure: "raw" },
225
- { path: "result.model", exposure: "raw" }
264
+ { path: "result.platform", label: "Operating system", exposure: "raw" },
265
+ {
266
+ path: "result.mobile",
267
+ label: "Whether the device is a mobile device",
268
+ exposure: "enum",
269
+ transform: { values: [true, false] }
270
+ },
271
+ { path: "result.platformVersion", label: "Operating system version", exposure: "raw" },
272
+ { path: "result.model", label: "Device model", exposure: "raw" }
226
273
  ]
227
274
  })
228
275
  ),
@@ -231,7 +278,18 @@ var DeviceCollector = class {
231
278
  "navigator.webdriver",
232
279
  () => readWebdriver({
233
280
  key: "browser-api:device.webdriver",
234
- fields: [{ path: "result", exposure: "enum", transform: { values: [true, false] } }]
281
+ title: "Browser automation flag",
282
+ accessNote: "navigator.webdriver",
283
+ group: "Device information",
284
+ origin: "observe-external",
285
+ fields: [
286
+ {
287
+ path: "result",
288
+ label: "Whether the browser is controlled by automation",
289
+ exposure: "enum",
290
+ transform: { values: [true, false] }
291
+ }
292
+ ]
235
293
  })
236
294
  ),
237
295
  this.startField(
@@ -239,7 +297,11 @@ var DeviceCollector = class {
239
297
  "navigator.maxTouchPoints",
240
298
  () => readMaxTouchPoints({
241
299
  key: "browser-api:device.max-touch-points",
242
- fields: [{ path: "result", exposure: "raw" }]
300
+ title: "Touch screen support",
301
+ accessNote: "navigator.maxTouchPoints",
302
+ group: "Device information",
303
+ origin: "observe-external",
304
+ fields: [{ path: "result", label: "Number of touch points of the screen", exposure: "raw" }]
243
305
  })
244
306
  )
245
307
  ];
@@ -377,7 +439,18 @@ var DeviceCollector = class {
377
439
  source,
378
440
  () => typeof __CBO_DATA_PRIVATE_MODE__ === "boolean" && !__CBO_DATA_PRIVATE_MODE__ ? void 0 : readPrivateMode({
379
441
  key: "browser-api:device.private-mode",
380
- fields: [{ path: "result.isPrivate", exposure: "enum", transform: { values: [true, false] } }],
442
+ title: "Private browsing mode",
443
+ accessNote: "detectincognitojs library (IndexedDB, storage quota and browser-specific probes)",
444
+ group: "Device information",
445
+ origin: "observe-external",
446
+ fields: [
447
+ {
448
+ path: "result.isPrivate",
449
+ label: "Whether private browsing is on",
450
+ exposure: "enum",
451
+ transform: { values: [true, false] }
452
+ }
453
+ ],
381
454
  dataConfig: "privateMode"
382
455
  })
383
456
  );
@@ -456,9 +529,14 @@ var DeviceCollector = class {
456
529
  static getUserAgent() {
457
530
  return readUserAgent({
458
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",
459
536
  fields: [
460
537
  {
461
538
  path: "result",
539
+ label: "Browser and operating system",
462
540
  exposure: "class",
463
541
  transform: {
464
542
  classes: [
@@ -0,0 +1,7 @@
1
+ // src/handoff.ts
2
+ function acceptHandoffs(step) {
3
+ return step;
4
+ }
5
+ export {
6
+ acceptHandoffs
7
+ };