@corbado/observe 1.7.2 → 1.8.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 (65) hide show
  1. package/dist/analysis/client.mjs +1851 -0
  2. package/dist/analysis/config-manager.mjs +83 -0
  3. package/dist/analysis/config.mjs +216 -0
  4. package/dist/analysis/device-info-manager.mjs +153 -0
  5. package/dist/analysis/device.mjs +562 -0
  6. package/dist/analysis/index.mjs +118 -0
  7. package/dist/analysis/logger.mjs +50 -0
  8. package/dist/analysis/low-events/capture-sequence.mjs +11 -0
  9. package/dist/analysis/low-events/ceremony-initiator.mjs +26 -0
  10. package/dist/analysis/low-events/form-input-binding.mjs +46 -0
  11. package/dist/analysis/low-events/grouped-input-tracker.mjs +133 -0
  12. package/dist/analysis/low-events/input-change-recorder.mjs +200 -0
  13. package/dist/analysis/low-events/input-paste-observer.mjs +91 -0
  14. package/dist/analysis/low-events/input-tracker.mjs +223 -0
  15. package/dist/analysis/low-events/otp-input-binding.mjs +67 -0
  16. package/dist/analysis/low-events/shared-window-observer.mjs +225 -0
  17. package/dist/analysis/low-events/sink.mjs +0 -0
  18. package/dist/analysis/low-events/viewport-batcher.mjs +42 -0
  19. package/dist/analysis/low-events/window-tracker.mjs +40 -0
  20. package/dist/analysis/queue.mjs +887 -0
  21. package/dist/analysis/readers/device.mjs +73 -0
  22. package/dist/analysis/readers/meta.mjs +18 -0
  23. package/dist/analysis/readers/password-manager-dom.mjs +112 -0
  24. package/dist/analysis/readers/password-manager-extensions.mjs +247 -0
  25. package/dist/analysis/readers/private-mode.mjs +9 -0
  26. package/dist/analysis/storage/index.mjs +197 -0
  27. package/dist/analysis/storage/migration.mjs +56 -0
  28. package/dist/analysis/store/event-store.mjs +114 -0
  29. package/dist/analysis/transport.mjs +0 -0
  30. package/dist/analysis/transports/fetch.mjs +108 -0
  31. package/dist/analysis/types.mjs +25 -0
  32. package/dist/analysis/utils/appconfirmation-operation-full.mjs +19 -0
  33. package/dist/analysis/utils/blocked-user-agents.mjs +37 -0
  34. package/dist/analysis/utils/captcha-operation-full.mjs +17 -0
  35. package/dist/analysis/utils/email-link-continuity.mjs +50 -0
  36. package/dist/analysis/utils/email-link-operation-full.mjs +142 -0
  37. package/dist/analysis/utils/email-otp-operation-full.mjs +39 -0
  38. package/dist/analysis/utils/form-operation-full.mjs +81 -0
  39. package/dist/analysis/utils/key-operation-full.mjs +38 -0
  40. package/dist/analysis/utils/normalize-error.mjs +29 -0
  41. package/dist/analysis/utils/operation-full-provide-identifier-with-cui.mjs +46 -0
  42. package/dist/analysis/utils/operation-full.mjs +235 -0
  43. package/dist/analysis/utils/passkey-deletion-operation-full.mjs +17 -0
  44. package/dist/analysis/utils/passkey-enrollment-operation-full.mjs +32 -0
  45. package/dist/analysis/utils/passkey-login-operation-full.mjs +34 -0
  46. package/dist/analysis/utils/password-enrollment-operation-full.mjs +27 -0
  47. package/dist/analysis/utils/password-login-operation-full.mjs +61 -0
  48. package/dist/analysis/utils/password-manager-sampler.mjs +130 -0
  49. package/dist/analysis/utils/provide-data-operation-full.mjs +28 -0
  50. package/dist/analysis/utils/serialize-raw-error.mjs +205 -0
  51. package/dist/analysis/utils/sms-otp-operation-full.mjs +39 -0
  52. package/dist/analysis/utils/social-operation-login.mjs +17 -0
  53. package/dist/analysis/utils/source-path.mjs +39 -0
  54. package/dist/analysis/utils/sso-operation-login.mjs +37 -0
  55. package/dist/analysis/utils/totp-operation-full.mjs +35 -0
  56. package/dist/analysis/utils/trusted-device-operation-full.mjs +100 -0
  57. package/dist/analysis/utils/web-lock.mjs +52 -0
  58. package/dist/analysis/utils/webauthn-payloads.mjs +101 -0
  59. package/dist/analysis/version.mjs +7 -0
  60. package/dist/cdn/script.global.js +4 -4
  61. package/dist/index.d.mts +156 -4
  62. package/dist/index.d.ts +156 -4
  63. package/dist/index.js +4 -4
  64. package/dist/index.mjs +4 -4
  65. package/package.json +7 -2
@@ -0,0 +1,562 @@
1
+ // src/device.ts
2
+ import { normalizeError } from "./utils/normalize-error.mjs";
3
+ import { v7 as uuidv7 } from "uuid";
4
+ import {
5
+ readBluetoothAvailability,
6
+ readClientCapabilities,
7
+ readConditionalMediationAvailable,
8
+ readJavaScriptHighEntropy,
9
+ readMaxTouchPoints,
10
+ readUserAgent,
11
+ readUserVerifyingPlatformAuthenticatorAvailable,
12
+ readWebdriver
13
+ } from "./readers/device.mjs";
14
+ import { readPrivateMode } from "./readers/private-mode.mjs";
15
+ var DEVICE_INFO_FAST_TIMEOUT_MS = 300;
16
+ var DEVICE_INFO_LATE_TIMEOUT_MS = 6e3;
17
+ var PRIVATE_MODE_CACHE_KEY = "cbo_pm";
18
+ var PRIVATE_MODE_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
19
+ var PRIVATE_MODE_DETECTOR_VERSION = "detectincognitojs@1.9.0";
20
+ var DEFAULT_SETTINGS = {
21
+ timing: false
22
+ };
23
+ var TAB_ID_KEY = "cbo_tab_id";
24
+ var TIMING_FIELD_CODES = {
25
+ bluetoothAvailable: "bt",
26
+ clientCapabilities: "cc",
27
+ userVerifyingPlatformAuthenticatorAvailable: "uvpaa",
28
+ conditionalMediationAvailable: "cm",
29
+ javaScriptHighEntropy: "he",
30
+ privateMode: "pm",
31
+ webdriver: "wd",
32
+ maxTouchPoints: "mtp"
33
+ };
34
+ function nowMs() {
35
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
36
+ }
37
+ function timeoutError(field, source, timeoutMs) {
38
+ return {
39
+ field,
40
+ source,
41
+ error: { name: "TimeoutError", message: `Device info collection timed out after ${timeoutMs}ms` }
42
+ };
43
+ }
44
+ async function settlesWithin(promise, timeoutMs) {
45
+ let timer;
46
+ try {
47
+ return await Promise.race([
48
+ promise.then(
49
+ () => true,
50
+ () => true
51
+ ),
52
+ new Promise((resolve) => {
53
+ timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
54
+ })
55
+ ]);
56
+ } finally {
57
+ if (timer) {
58
+ clearTimeout(timer);
59
+ }
60
+ }
61
+ }
62
+ var DeviceCollector = class {
63
+ constructor(logger, storage, storageSource = "localstorage", sessionStorage, settings = () => DEFAULT_SETTINGS, emitTelemetry, privateModeEnabled = true) {
64
+ this.logger = logger;
65
+ this.storage = storage;
66
+ this.storageSource = storageSource;
67
+ this.sessionStorage = sessionStorage;
68
+ this.settings = settings;
69
+ this.emitTelemetry = emitTelemetry;
70
+ this.privateModeEnabled = privateModeEnabled;
71
+ }
72
+ /** privateMode known on this page (from the cache or a detection), reused by every refresh. */
73
+ pagePrivateMode;
74
+ /** Detection runs at most once per page: a refresh never reruns the IndexedDB benchmark. */
75
+ privateModeDetectionStarted = false;
76
+ /** Last known value per fast field, so a refresh's fast snapshot does not drop a slow-but-stable field. */
77
+ lastKnown = {};
78
+ /**
79
+ * Collect device info in two steps and hand each snapshot to `publish`:
80
+ * 1. The fast snapshot: every fast field, published once all of them settled or
81
+ * {@link DEVICE_INFO_FAST_TIMEOUT_MS} passed. An uncached privateMode is detected once per page,
82
+ * started together with the fast fields; the fast snapshot includes privateMode when it is known
83
+ * already (cache, an earlier detection on this page) or the detection finished in time.
84
+ * 2. At most one update: fields and a detection still running keep going until
85
+ * {@link DEVICE_INFO_LATE_TIMEOUT_MS} after the collection start. Published when that work is done.
86
+ *
87
+ * Resolves once the run is complete. On a refresh (`refresh: true`), fast fields that did not
88
+ * settle in time fall back to their last known value in the fast snapshot.
89
+ *
90
+ * Error situations:
91
+ * - Any thrown or rejected error while collecting an individual field: log "Failed to collect device info field", set the field to undefined, and add an entry to collectionErrors
92
+ * - data.bluetoothAvailable is undefined when navigator is not available or navigator.bluetooth is falsy
93
+ * - data.userVerifyingPlatformAuthenticatorAvailable is undefined when PublicKeyCredential is not available or isUserVerifyingPlatformAuthenticatorAvailable is missing
94
+ * - data.conditionalMediationAvailable is undefined when PublicKeyCredential is not available or isConditionalMediationAvailable is missing
95
+ * - data.clientCapabilities is undefined when PublicKeyCredential is not available or getClientCapabilities is missing
96
+ * - data.javaScriptHighEntropy is undefined when navigator is not available or navigator.userAgentData is falsy, or when high-entropy values omit platform, mobile, or platformVersion
97
+ * - data.webdriver is undefined when navigator is not available
98
+ * - data.maxTouchPoints is undefined when navigator is not available or navigator.maxTouchPoints is not a number
99
+ * - A field (or the privateMode detection) that does not settle within the late deadline: a TimeoutError entry "Device info collection timed out after 6000ms" in collectionErrors of the update
100
+ * - The privateMode cache is unreadable, malformed, expired or from another detector version: detect instead; a failing cache write is logged and ignored
101
+ * - The settings provider throwing: log "Failed to resolve device info collector settings" and collect with the built-in defaults
102
+ * - `publish` throwing: log "Failed to publish device info" and continue
103
+ * - Any other unexpected error: log "Failed to collect device info" and publish nothing further
104
+ */
105
+ async collect(publish, options = {}) {
106
+ try {
107
+ const settings = this.resolveSettings();
108
+ const startedAt = nowMs();
109
+ const clientEnvHandleEntry = this.getClientEnvHandle();
110
+ const base = {
111
+ type: "web",
112
+ clientEnvHandle: clientEnvHandleEntry.value,
113
+ clientEnvHandleMeta: {
114
+ timestamp: clientEnvHandleEntry.timestamp,
115
+ source: clientEnvHandleEntry.source
116
+ },
117
+ tabId: this.getTabId()
118
+ };
119
+ const privateModeOn = typeof __CBO_DATA_PRIVATE_MODE__ === "boolean" ? __CBO_DATA_PRIVATE_MODE__ : this.privateModeEnabled;
120
+ const privateMode = privateModeOn ? this.knownPrivateMode(settings.timing) : void 0;
121
+ let detected;
122
+ const detection = privateModeOn && privateMode === void 0 && !this.privateModeDetectionStarted ? this.detectPrivateMode(settings.timing).then((result) => {
123
+ detected = result;
124
+ }) : void 0;
125
+ const tasks = this.startFastFields();
126
+ await settlesWithin(Promise.all(tasks.map((task) => task.promise)), DEVICE_INFO_FAST_TIMEOUT_MS);
127
+ const fastMs = nowMs() - startedAt;
128
+ const fallback = options.refresh ? this.lastKnown : {};
129
+ if (settings.timing) {
130
+ this.reportFastTiming(fastMs, tasks);
131
+ }
132
+ const detectionErrors = () => detected?.error ? [detected.error] : [];
133
+ this.safePublish(
134
+ publish,
135
+ this.buildInfo(base, tasks, detected ? detected.value : privateMode, fallback, detectionErrors()),
136
+ "fast"
137
+ );
138
+ const late = tasks.filter((task) => !task.settled);
139
+ const detectionPending = detection !== void 0 && detected === void 0;
140
+ if (late.length === 0 && !detectionPending) {
141
+ return;
142
+ }
143
+ const lateStartedAt = nowMs();
144
+ await Promise.all([
145
+ settlesWithin(
146
+ Promise.all(late.map((task) => task.promise)),
147
+ DEVICE_INFO_LATE_TIMEOUT_MS - (lateStartedAt - startedAt)
148
+ ),
149
+ detection
150
+ ]);
151
+ const lateErrors = late.filter((task) => !task.settled).map((task) => timeoutError(task.field, task.source, DEVICE_INFO_LATE_TIMEOUT_MS));
152
+ lateErrors.push(...detectionErrors());
153
+ if (settings.timing && late.length > 0) {
154
+ this.reportLateTiming(nowMs() - startedAt, late);
155
+ }
156
+ this.safePublish(
157
+ publish,
158
+ this.buildInfo(base, tasks, detected ? detected.value : privateMode, fallback, lateErrors),
159
+ "update"
160
+ );
161
+ } catch (error) {
162
+ this.logger.error("Failed to collect device info", error);
163
+ }
164
+ }
165
+ /**
166
+ * Collect a complete snapshot: runs {@link collect} to the end and resolves with its last published
167
+ * snapshot (the update when there was one), or an empty object when nothing could be collected.
168
+ */
169
+ async getInfo() {
170
+ let latest;
171
+ await this.collect((info) => {
172
+ latest = info;
173
+ });
174
+ return latest ?? {};
175
+ }
176
+ startFastFields() {
177
+ return [
178
+ this.startField(
179
+ "bluetoothAvailable",
180
+ "navigator.bluetooth.getAvailability",
181
+ () => readBluetoothAvailability({
182
+ key: "browser-api:device.bluetooth-availability",
183
+ fields: [{ path: "result", exposure: "enum", transform: { values: [true, false] } }]
184
+ })
185
+ ),
186
+ this.startField(
187
+ "clientCapabilities",
188
+ "PublicKeyCredential.getClientCapabilities",
189
+ () => readClientCapabilities({
190
+ key: "browser-api:device.client-capabilities",
191
+ fields: [
192
+ {
193
+ path: "result.<capability>",
194
+ exposure: "enum",
195
+ transform: { values: ["true", "false"], note: "open set of capability names, kept as reported" }
196
+ }
197
+ ]
198
+ })
199
+ ),
200
+ this.startField(
201
+ "userVerifyingPlatformAuthenticatorAvailable",
202
+ "PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable",
203
+ () => readUserVerifyingPlatformAuthenticatorAvailable({
204
+ key: "browser-api:device.uvpaa",
205
+ fields: [{ path: "result", exposure: "enum", transform: { values: [true, false] } }]
206
+ })
207
+ ),
208
+ this.startField(
209
+ "conditionalMediationAvailable",
210
+ "PublicKeyCredential.isConditionalMediationAvailable",
211
+ () => readConditionalMediationAvailable({
212
+ key: "browser-api:device.conditional-mediation",
213
+ fields: [{ path: "result", exposure: "enum", transform: { values: [true, false] } }]
214
+ })
215
+ ),
216
+ this.startField(
217
+ "javaScriptHighEntropy",
218
+ "navigator.userAgentData.getHighEntropyValues",
219
+ () => readJavaScriptHighEntropy({
220
+ key: "browser-api:device.ua-high-entropy",
221
+ 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" }
226
+ ]
227
+ })
228
+ ),
229
+ this.startField(
230
+ "webdriver",
231
+ "navigator.webdriver",
232
+ () => readWebdriver({
233
+ key: "browser-api:device.webdriver",
234
+ fields: [{ path: "result", exposure: "enum", transform: { values: [true, false] } }]
235
+ })
236
+ ),
237
+ this.startField(
238
+ "maxTouchPoints",
239
+ "navigator.maxTouchPoints",
240
+ () => readMaxTouchPoints({
241
+ key: "browser-api:device.max-touch-points",
242
+ fields: [{ path: "result", exposure: "raw" }]
243
+ })
244
+ )
245
+ ];
246
+ }
247
+ /** Start one field; its promise never rejects, a failure becomes the field's collection error. */
248
+ startField(field, source, collect) {
249
+ const startedAt = nowMs();
250
+ const task = { field, source };
251
+ task.promise = Promise.resolve().then(collect).then(
252
+ (value) => ({ field, value, durationMs: nowMs() - startedAt }),
253
+ (error) => {
254
+ this.logger.error(`Failed to collect device info field "${field}"`, error);
255
+ return {
256
+ field,
257
+ durationMs: nowMs() - startedAt,
258
+ error: { field, source, error: normalizeError(error) ?? { message: "Unknown error" } }
259
+ };
260
+ }
261
+ ).then((result) => {
262
+ task.settled = result;
263
+ return result;
264
+ });
265
+ return task;
266
+ }
267
+ buildInfo(base, tasks, privateMode, fallback, extraErrors) {
268
+ const data = {};
269
+ const collectionErrors = [];
270
+ for (const task of tasks) {
271
+ if (task.settled) {
272
+ if (task.settled.error) {
273
+ collectionErrors.push(task.settled.error);
274
+ }
275
+ data[task.field] = task.settled.value;
276
+ this.remember(task.field, task.settled.value);
277
+ } else {
278
+ data[task.field] = fallback[task.field];
279
+ }
280
+ }
281
+ data.privateMode = privateMode;
282
+ collectionErrors.push(...extraErrors);
283
+ const deviceInfo = {
284
+ ...base,
285
+ data: {
286
+ bluetoothAvailable: data.bluetoothAvailable,
287
+ javaScriptHighEntropy: data.javaScriptHighEntropy,
288
+ privateMode: data.privateMode,
289
+ webdriver: data.webdriver,
290
+ conditionalMediationAvailable: data.conditionalMediationAvailable,
291
+ userVerifyingPlatformAuthenticatorAvailable: data.userVerifyingPlatformAuthenticatorAvailable,
292
+ clientCapabilities: data.clientCapabilities,
293
+ maxTouchPoints: data.maxTouchPoints
294
+ }
295
+ };
296
+ if (collectionErrors.length > 0) {
297
+ deviceInfo.collectionErrors = collectionErrors;
298
+ }
299
+ return deviceInfo;
300
+ }
301
+ remember(field, value) {
302
+ if (value !== void 0) {
303
+ this.lastKnown[field] = value;
304
+ }
305
+ }
306
+ safePublish(publish, info, phase) {
307
+ try {
308
+ publish(info, phase);
309
+ } catch (error) {
310
+ this.logger.error("Failed to publish device info", error);
311
+ }
312
+ }
313
+ /** privateMode as already known on this page, else from a valid cache entry (which then sticks for the page). */
314
+ knownPrivateMode(timing) {
315
+ if (this.pagePrivateMode !== void 0) {
316
+ return this.pagePrivateMode;
317
+ }
318
+ const cached = this.readPrivateModeCache();
319
+ if (cached) {
320
+ this.pagePrivateMode = cached.value;
321
+ if (timing) {
322
+ this.reportTelemetry(`device info privateMode from cache, age ${Math.round((Date.now() - cached.ts) / 1e3)}s`);
323
+ }
324
+ }
325
+ return this.pagePrivateMode;
326
+ }
327
+ /**
328
+ * The cache lives in localStorage only: in cookie storage mode the private-mode flag would be sent
329
+ * to the host's servers with every request, so it is not cached there (detection runs per page).
330
+ */
331
+ canCachePrivateMode() {
332
+ return this.storageSource === "localstorage";
333
+ }
334
+ readPrivateModeCache() {
335
+ if (!this.canCachePrivateMode()) {
336
+ return void 0;
337
+ }
338
+ try {
339
+ const entry = this.storage.getItem(PRIVATE_MODE_CACHE_KEY);
340
+ if (!entry || typeof entry !== "object" || typeof entry.value !== "boolean" || typeof entry.ts !== "number" || entry.dv !== PRIVATE_MODE_DETECTOR_VERSION) {
341
+ return void 0;
342
+ }
343
+ const age = Date.now() - entry.ts;
344
+ if (age < 0 || age >= PRIVATE_MODE_CACHE_TTL_MS) {
345
+ return void 0;
346
+ }
347
+ return entry;
348
+ } catch (error) {
349
+ this.logger.debug("Failed to read cached privateMode", error);
350
+ return void 0;
351
+ }
352
+ }
353
+ writePrivateModeCache(value) {
354
+ if (!this.canCachePrivateMode()) {
355
+ return;
356
+ }
357
+ try {
358
+ this.storage.setItem(PRIVATE_MODE_CACHE_KEY, {
359
+ value,
360
+ ts: Date.now(),
361
+ dv: PRIVATE_MODE_DETECTOR_VERSION
362
+ });
363
+ } catch (error) {
364
+ this.logger.error("Failed to cache privateMode", error);
365
+ }
366
+ }
367
+ /**
368
+ * Run the privateMode detection once for this page, bounded by the late deadline. A definite result
369
+ * is cached and reused by every later collection; a timeout or failure yields a collection error.
370
+ */
371
+ async detectPrivateMode(timing) {
372
+ this.privateModeDetectionStarted = true;
373
+ const source = "detectIncognito";
374
+ const startedAt = nowMs();
375
+ const task = this.startField(
376
+ "privateMode",
377
+ source,
378
+ () => typeof __CBO_DATA_PRIVATE_MODE__ === "boolean" && !__CBO_DATA_PRIVATE_MODE__ ? void 0 : readPrivateMode({
379
+ key: "browser-api:device.private-mode",
380
+ fields: [{ path: "result.isPrivate", exposure: "enum", transform: { values: [true, false] } }],
381
+ dataConfig: "privateMode"
382
+ })
383
+ );
384
+ const settled = await settlesWithin(task.promise, DEVICE_INFO_LATE_TIMEOUT_MS);
385
+ if (!settled || !task.settled) {
386
+ if (timing) {
387
+ this.reportTelemetry(`device info privateMode timed out after ${DEVICE_INFO_LATE_TIMEOUT_MS}ms`);
388
+ }
389
+ return { error: timeoutError("privateMode", source, DEVICE_INFO_LATE_TIMEOUT_MS) };
390
+ }
391
+ if (timing) {
392
+ this.reportTelemetry(`device info privateMode detected in ${Math.round(nowMs() - startedAt)}ms`);
393
+ }
394
+ if (task.settled.error) {
395
+ return { error: task.settled.error };
396
+ }
397
+ const value = task.settled.value;
398
+ if (typeof value !== "boolean") {
399
+ return { value: void 0 };
400
+ }
401
+ this.pagePrivateMode = value;
402
+ this.writePrivateModeCache(value);
403
+ return { value };
404
+ }
405
+ /**
406
+ * Resolve the settings for one collection run. A broken provider must never stop collection, so
407
+ * anything thrown degrades to the built-in defaults.
408
+ */
409
+ resolveSettings() {
410
+ try {
411
+ return { timing: this.settings()?.timing === true };
412
+ } catch (error) {
413
+ this.logger.error("Failed to resolve device info collector settings", error);
414
+ return DEFAULT_SETTINGS;
415
+ }
416
+ }
417
+ /**
418
+ * Emit the fast snapshot's durations as an info telemetry entry: the total wall time until the fast
419
+ * snapshot plus every fast field's own time, in definition order so two entries can be compared field
420
+ * by field. Per-field values use the short codes from `TIMING_FIELD_CODES` and carry no unit — the
421
+ * unit is stated once on the total, and all values are milliseconds. A field still running at the
422
+ * fast deadline reports the time it had run so far (it is reported again by the late entry).
423
+ */
424
+ reportFastTiming(totalMs, tasks) {
425
+ const perField = tasks.map((task) => `${TIMING_FIELD_CODES[task.field]}=${Math.round(task.settled?.durationMs ?? totalMs)}`).join(",");
426
+ this.reportTelemetry(`device info collected in ${Math.round(totalMs)}ms (${perField})`);
427
+ }
428
+ /** Emit the late fields' outcome: their duration, or `timeout` when they missed the late deadline. */
429
+ reportLateTiming(totalMs, late) {
430
+ const perField = late.map(
431
+ (task) => `${TIMING_FIELD_CODES[task.field]}=${task.settled ? Math.round(task.settled.durationMs) : "timeout"}`
432
+ ).join(",");
433
+ this.reportTelemetry(`device info late fields done in ${Math.round(totalMs)}ms (${perField})`);
434
+ }
435
+ reportTelemetry(message) {
436
+ if (!this.emitTelemetry) {
437
+ return;
438
+ }
439
+ try {
440
+ this.emitTelemetry("info", message);
441
+ } catch (error) {
442
+ this.logger.error("Failed to report device info timing", error);
443
+ }
444
+ }
445
+ /**
446
+ * Error situations:
447
+ * - navigator is not available: user agent is treated as empty, osName and browserName are "other"
448
+ * - navigator is available but userAgent is null or undefined: same as above
449
+ */
450
+ static getOsAndBrowserInfo() {
451
+ return {
452
+ osName: this.getOsName(),
453
+ browserName: this.getBrowserName()
454
+ };
455
+ }
456
+ static getUserAgent() {
457
+ return readUserAgent({
458
+ key: "browser-api:device.user-agent",
459
+ fields: [
460
+ {
461
+ path: "result",
462
+ exposure: "class",
463
+ transform: {
464
+ classes: [
465
+ "iOS",
466
+ "Android",
467
+ "Windows",
468
+ "macOS",
469
+ "Linux",
470
+ "ChromeOS",
471
+ "Chrome",
472
+ "Firefox",
473
+ "Safari",
474
+ "Edge",
475
+ "Opera",
476
+ "IE",
477
+ "other"
478
+ ]
479
+ }
480
+ }
481
+ ]
482
+ });
483
+ }
484
+ static getOsName() {
485
+ const ua = this.getUserAgent();
486
+ if (ua.includes("iOS")) {
487
+ return "iOS";
488
+ } else if (ua.includes("Android")) {
489
+ return "Android";
490
+ } else if (ua.includes("Windows")) {
491
+ return "Windows";
492
+ } else if (ua.includes("Macintosh")) {
493
+ return "macOS";
494
+ } else if (ua.includes("Linux")) {
495
+ return "Linux";
496
+ } else if (ua.includes("ChromeOS")) {
497
+ return "ChromeOS";
498
+ }
499
+ return "other";
500
+ }
501
+ static getBrowserName() {
502
+ const ua = this.getUserAgent();
503
+ if (ua.includes("Chrome")) {
504
+ return "Chrome";
505
+ } else if (ua.includes("Firefox")) {
506
+ return "Firefox";
507
+ } else if (ua.includes("Safari")) {
508
+ return "Safari";
509
+ } else if (ua.includes("Edge")) {
510
+ return "Edge";
511
+ } else if (ua.includes("Opera")) {
512
+ return "Opera";
513
+ } else if (ua.includes("IE")) {
514
+ return "IE";
515
+ }
516
+ return "other";
517
+ }
518
+ getClientEnvHandle() {
519
+ const existingClientEnvHandle = this.storage.getItem("cbo_client_env_handle");
520
+ if (existingClientEnvHandle) {
521
+ return existingClientEnvHandle;
522
+ }
523
+ const newClientEnvHandleValue = uuidv7();
524
+ const newClientEnvHandle = {
525
+ timestamp: Date.now(),
526
+ source: this.storageSource,
527
+ value: newClientEnvHandleValue
528
+ };
529
+ this.storage.setItem("cbo_client_env_handle", newClientEnvHandle);
530
+ return newClientEnvHandle;
531
+ }
532
+ /**
533
+ * Resolve a per-tab id from sessionStorage (unique per browsing context). Returns undefined when no
534
+ * session storage is available so two tabs sharing one localStorage-backed session can be distinguished.
535
+ *
536
+ * Public because the tracker also stamps this id onto every event's meta at track() time (see
537
+ * `EventMeta.tabId`); both callers share the same sessionStorage-backed id.
538
+ */
539
+ getTabId() {
540
+ if (!this.sessionStorage) {
541
+ return void 0;
542
+ }
543
+ const existing = this.sessionStorage.getItem(TAB_ID_KEY);
544
+ if (existing) {
545
+ return existing;
546
+ }
547
+ const tabId = uuidv7();
548
+ this.sessionStorage.setItem(TAB_ID_KEY, tabId);
549
+ if (this.sessionStorage.getItem(TAB_ID_KEY) !== tabId) {
550
+ return void 0;
551
+ }
552
+ return tabId;
553
+ }
554
+ };
555
+ export {
556
+ DEVICE_INFO_FAST_TIMEOUT_MS,
557
+ DEVICE_INFO_LATE_TIMEOUT_MS,
558
+ DeviceCollector,
559
+ PRIVATE_MODE_CACHE_KEY,
560
+ PRIVATE_MODE_CACHE_TTL_MS,
561
+ PRIVATE_MODE_DETECTOR_VERSION
562
+ };
@@ -0,0 +1,118 @@
1
+ // src/index.ts
2
+ import { CorbadoTracker } from "./client.mjs";
3
+ import { SDK_NAME, SDK_VERSION } from "./version.mjs";
4
+ export * from "./types.mjs";
5
+ export * from "./client.mjs";
6
+ export * from "./transport.mjs";
7
+ export * from "./queue.mjs";
8
+ export * from "./config.mjs";
9
+ export * from "./store/event-store.mjs";
10
+ export * from "./storage/index.mjs";
11
+ export * from "./logger.mjs";
12
+ export * from "./utils/operation-full.mjs";
13
+ export * from "./utils/key-operation-full.mjs";
14
+ export * from "./utils/serialize-raw-error.mjs";
15
+ export * from "./utils/trusted-device-operation-full.mjs";
16
+ export * from "./utils/operation-full-provide-identifier-with-cui.mjs";
17
+ export * from "./utils/provide-data-operation-full.mjs";
18
+ export * from "./utils/form-operation-full.mjs";
19
+ export * from "./utils/passkey-login-operation-full.mjs";
20
+ export * from "./utils/email-otp-operation-full.mjs";
21
+ export * from "./utils/sms-otp-operation-full.mjs";
22
+ export * from "./utils/totp-operation-full.mjs";
23
+ export * from "./utils/email-link-operation-full.mjs";
24
+ export * from "./utils/social-operation-login.mjs";
25
+ export * from "./utils/sso-operation-login.mjs";
26
+ export * from "./utils/passkey-enrollment-operation-full.mjs";
27
+ export * from "./utils/passkey-deletion-operation-full.mjs";
28
+ export * from "./utils/password-login-operation-full.mjs";
29
+ export * from "./utils/password-enrollment-operation-full.mjs";
30
+ export * from "./utils/appconfirmation-operation-full.mjs";
31
+ export * from "./utils/captcha-operation-full.mjs";
32
+ import {
33
+ detectPasswordManagerExtensions
34
+ } from "./readers/password-manager-extensions.mjs";
35
+ import {
36
+ READER_EXPOSURES
37
+ } from "./readers/meta.mjs";
38
+ import {
39
+ sanitizeCreationOptions,
40
+ sanitizeCreationResponse,
41
+ sanitizeRequestOptions,
42
+ sanitizeAssertionResponse
43
+ } from "./utils/webauthn-payloads.mjs";
44
+ var currentTracker;
45
+ function init(options) {
46
+ const tracker = new CorbadoTracker(options);
47
+ currentTracker = tracker;
48
+ return tracker;
49
+ }
50
+ function getTracker() {
51
+ return currentTracker;
52
+ }
53
+ function resetSession() {
54
+ return currentTracker?.resetSession();
55
+ }
56
+ 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);
67
+ }
68
+ function setExperiments(assignments) {
69
+ currentTracker?.setExperiments(assignments);
70
+ }
71
+ function clearExperiment(key) {
72
+ currentTracker?.clearExperiment(key);
73
+ }
74
+ function clearExperiments() {
75
+ currentTracker?.clearExperiments();
76
+ }
77
+ function getExperiments() {
78
+ return currentTracker?.getExperiments() ?? {};
79
+ }
80
+ 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);
88
+ }
89
+ async function destroy() {
90
+ if (!currentTracker) return;
91
+ await currentTracker.destroy();
92
+ currentTracker = void 0;
93
+ }
94
+ export {
95
+ READER_EXPOSURES,
96
+ SDK_NAME,
97
+ SDK_VERSION,
98
+ clearExperiment,
99
+ clearExperiments,
100
+ destroy,
101
+ detectPasswordManagerExtensions,
102
+ getExperiments,
103
+ getSessionId,
104
+ getTracker,
105
+ init,
106
+ logError,
107
+ logInfo,
108
+ resetSession,
109
+ sanitizeAssertionResponse,
110
+ sanitizeCreationOptions,
111
+ sanitizeCreationResponse,
112
+ sanitizeRequestOptions,
113
+ setExperiment,
114
+ setExperiments,
115
+ setFlowTags,
116
+ setUser,
117
+ telemetry
118
+ };