@corbado/observe 1.7.2 → 1.9.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 (76) hide show
  1. package/dist/analysis/client.mjs +1877 -0
  2. package/dist/analysis/config-manager.mjs +108 -0
  3. package/dist/analysis/config.mjs +216 -0
  4. package/dist/analysis/device-info-manager.mjs +168 -0
  5. package/dist/analysis/device.mjs +640 -0
  6. package/dist/analysis/handoff.mjs +7 -0
  7. package/dist/analysis/index.mjs +118 -0
  8. package/dist/analysis/logger.mjs +50 -0
  9. package/dist/analysis/low-events/capture-sequence.mjs +11 -0
  10. package/dist/analysis/low-events/ceremony-initiator.mjs +26 -0
  11. package/dist/analysis/low-events/form-input-binding.mjs +46 -0
  12. package/dist/analysis/low-events/grouped-input-tracker.mjs +248 -0
  13. package/dist/analysis/low-events/input-change-recorder.mjs +200 -0
  14. package/dist/analysis/low-events/input-tracker.mjs +302 -0
  15. package/dist/analysis/low-events/otp-input-binding.mjs +67 -0
  16. package/dist/analysis/low-events/shared-window-observer.mjs +296 -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 +954 -0
  21. package/dist/analysis/readers/device.mjs +73 -0
  22. package/dist/analysis/readers/input-paste.mjs +91 -0
  23. package/dist/analysis/readers/input.mjs +61 -0
  24. package/dist/analysis/readers/loader-global.mjs +15 -0
  25. package/dist/analysis/readers/meta.mjs +18 -0
  26. package/dist/analysis/readers/network.mjs +14 -0
  27. package/dist/analysis/readers/page.mjs +30 -0
  28. package/dist/analysis/readers/password-manager-dom.mjs +112 -0
  29. package/dist/analysis/readers/password-manager-extensions.mjs +247 -0
  30. package/dist/analysis/readers/private-mode.mjs +9 -0
  31. package/dist/analysis/readers/storage.mjs +28 -0
  32. package/dist/analysis/readers/web-lock.mjs +56 -0
  33. package/dist/analysis/readers/window-events.mjs +48 -0
  34. package/dist/analysis/storage/index.mjs +249 -0
  35. package/dist/analysis/storage/migration.mjs +56 -0
  36. package/dist/analysis/store/event-store.mjs +132 -0
  37. package/dist/analysis/transport.mjs +0 -0
  38. package/dist/analysis/transports/fetch.mjs +150 -0
  39. package/dist/analysis/types.mjs +25 -0
  40. package/dist/analysis/utils/appconfirmation-operation-full.mjs +19 -0
  41. package/dist/analysis/utils/blocked-user-agents.mjs +37 -0
  42. package/dist/analysis/utils/captcha-operation-full.mjs +17 -0
  43. package/dist/analysis/utils/email-link-continuity.mjs +50 -0
  44. package/dist/analysis/utils/email-link-operation-full.mjs +162 -0
  45. package/dist/analysis/utils/email-otp-operation-full.mjs +38 -0
  46. package/dist/analysis/utils/form-operation-full.mjs +80 -0
  47. package/dist/analysis/utils/key-operation-full.mjs +38 -0
  48. package/dist/analysis/utils/normalize-error.mjs +29 -0
  49. package/dist/analysis/utils/operation-full-provide-identifier-with-cui.mjs +46 -0
  50. package/dist/analysis/utils/operation-full.mjs +235 -0
  51. package/dist/analysis/utils/passkey-deletion-operation-full.mjs +17 -0
  52. package/dist/analysis/utils/passkey-enrollment-operation-full.mjs +35 -0
  53. package/dist/analysis/utils/passkey-login-operation-full.mjs +39 -0
  54. package/dist/analysis/utils/password-enrollment-operation-full.mjs +27 -0
  55. package/dist/analysis/utils/password-login-operation-full.mjs +65 -0
  56. package/dist/analysis/utils/password-manager-sampler.mjs +170 -0
  57. package/dist/analysis/utils/provide-data-operation-full.mjs +28 -0
  58. package/dist/analysis/utils/serialize-raw-error.mjs +205 -0
  59. package/dist/analysis/utils/sms-otp-operation-full.mjs +38 -0
  60. package/dist/analysis/utils/social-operation-login.mjs +17 -0
  61. package/dist/analysis/utils/source-path.mjs +39 -0
  62. package/dist/analysis/utils/sso-operation-login.mjs +37 -0
  63. package/dist/analysis/utils/totp-operation-full.mjs +34 -0
  64. package/dist/analysis/utils/trusted-device-operation-full.mjs +100 -0
  65. package/dist/analysis/utils/webauthn-payloads.mjs +101 -0
  66. package/dist/analysis/version.mjs +7 -0
  67. package/dist/cdn/script.global.js +4 -4
  68. package/dist/index.d.mts +285 -21
  69. package/dist/index.d.ts +285 -21
  70. package/dist/index.js +4 -4
  71. package/dist/index.mjs +4 -4
  72. package/package.json +10 -3
  73. package/tools/reader-meta-text.d.mts +2 -0
  74. package/tools/reader-meta-text.mjs +26 -0
  75. package/tools/strip-reader-meta.d.mts +33 -0
  76. package/tools/strip-reader-meta.mjs +131 -0
@@ -0,0 +1,46 @@
1
+ // src/utils/operation-full-provide-identifier-with-cui.ts
2
+ import { LowEventInputTracker } from "../low-events/input-tracker.mjs";
3
+ import { OperationFull } from "./operation-full.mjs";
4
+ import { acceptHandoffs } from "../handoff.mjs";
5
+ var OperationFullProvideIdentifierWithCUI = class extends OperationFull {
6
+ lowEventTracker;
7
+ cui;
8
+ provideIdentifier;
9
+ constructor(tracker, config) {
10
+ super(tracker, "provide-identifier");
11
+ this.cui = {
12
+ trigger: (data, options) => {
13
+ this.trigger(data, options);
14
+ },
15
+ // getOptions only fetches assertion options; it is not a user interaction on its own.
16
+ getOptions: acceptHandoffs(
17
+ this.defineStep("cui-get-options", true)
18
+ ),
19
+ ceremony: acceptHandoffs(
20
+ this.defineStep("cui-ceremony")
21
+ ),
22
+ postResponse: acceptHandoffs(this.defineStep("cui-post-response"))
23
+ };
24
+ this.provideIdentifier = {
25
+ trigger: (data, options) => {
26
+ this.trigger(data, options);
27
+ },
28
+ clientValidation: this.defineStep("pi-client-validation"),
29
+ postResponse: this.defineStep("pi-post-response")
30
+ };
31
+ void this.subflowStart({ explicitSpecType: config?.explicitSpecType });
32
+ if (!config?.inputHtmlField) {
33
+ return;
34
+ }
35
+ this.lowEventTracker = this.createLowCollector(
36
+ () => new LowEventInputTracker(tracker.getLogger(), config.inputHtmlField, this.lowEventSink("input", "identifier"))
37
+ );
38
+ this.registerLowFlusher(() => this.lowEventTracker?.flushPending());
39
+ }
40
+ destroy() {
41
+ this.lowEventTracker?.destroy();
42
+ }
43
+ };
44
+ export {
45
+ OperationFullProvideIdentifierWithCUI
46
+ };
@@ -0,0 +1,235 @@
1
+ // src/utils/operation-full.ts
2
+ import { reserveCaptureSeq } from "../low-events/capture-sequence.mjs";
3
+ import { inferCeremonyInitiator } from "../low-events/ceremony-initiator.mjs";
4
+ import { v7 as uuidv7 } from "uuid";
5
+ import { normalizeError } from "./normalize-error.mjs";
6
+ import { serializeRawError } from "./serialize-raw-error.mjs";
7
+ var CEREMONY_LOW_EVENT_TAIL_MS = 1500;
8
+ var OperationFull = class {
9
+ constructor(tracker, operationName) {
10
+ this.tracker = tracker;
11
+ this.operationName = operationName;
12
+ }
13
+ startReference;
14
+ captureSpecType;
15
+ captureSessionId;
16
+ lowFlushers = [];
17
+ lowPrefillCapturers = [];
18
+ lowEventSink(sourceKind, fieldRole) {
19
+ return {
20
+ reserveCaptureSeq,
21
+ enqueueLowEvent: (low) => this.tracker.enqueueLowEvent(
22
+ {
23
+ ...low,
24
+ captureSeq: low.captureSeq ?? reserveCaptureSeq(),
25
+ subflowStartEventId: this.startReference,
26
+ sourceKind,
27
+ fieldRole,
28
+ initiator: low.initiator ?? (fieldRole ? `input-${fieldRole}` : "unknown")
29
+ },
30
+ this.captureSessionId
31
+ ),
32
+ flushKeepalive: () => this.tracker.lowEventFlushKeepalive(),
33
+ passwordManagerDomScan: () => this.tracker.isPasswordManagerDomScanEnabled(),
34
+ registerInputs: (fields, flush) => this.tracker.getWindowObserver().registerInputs(
35
+ fields,
36
+ () => ({
37
+ subflowStartEventId: this.startReference,
38
+ fieldRole,
39
+ initiator: fieldRole ? `input-${fieldRole}` : "unknown",
40
+ sessionId: this.captureSessionId
41
+ }),
42
+ flush
43
+ ),
44
+ beginCeremony: (initiator) => this.tracker.getWindowObserver().beginCeremony({
45
+ subflowStartEventId: this.startReference,
46
+ initiator: initiator ?? "unknown",
47
+ sessionId: this.captureSessionId
48
+ })
49
+ };
50
+ }
51
+ createLowCollector(create) {
52
+ try {
53
+ const collector = create();
54
+ this.lowPrefillCapturers.push((newCapture) => collector.capturePrefill(newCapture));
55
+ return collector;
56
+ } catch (error) {
57
+ this.tracker.getLogger().error("Failed to attach low-event collector", error);
58
+ return void 0;
59
+ }
60
+ }
61
+ registerLowFlusher(flush) {
62
+ this.lowFlushers.push(flush);
63
+ }
64
+ captureOptions(options, reference = this.startReference, sessionId = this.captureSessionId, initiator) {
65
+ if (!reference && !initiator) return options;
66
+ const context = {};
67
+ if (reference) {
68
+ context.subflowStartEventId = options?.subflowStartEventId ?? reference;
69
+ context.captureSessionId = options?.captureSessionId ?? sessionId;
70
+ }
71
+ if (initiator) context.initiator = options?.initiator ?? initiator;
72
+ return Object.defineProperties(
73
+ {},
74
+ {
75
+ ...Object.getOwnPropertyDescriptors(options ?? {}),
76
+ ...Object.getOwnPropertyDescriptors(context)
77
+ }
78
+ );
79
+ }
80
+ /** Anchor (subflowStartEventId) of the current start; undefined before the first start. */
81
+ get captureReference() {
82
+ return this.startReference;
83
+ }
84
+ /** Session the current start was tracked under. */
85
+ get captureSession() {
86
+ return this.captureSessionId;
87
+ }
88
+ /**
89
+ * Continue an attempt started elsewhere (e.g. on another page of the same session) without
90
+ * emitting a subflow_started: later steps and low events carry the given anchor and session.
91
+ */
92
+ adoptCapture(reference, sessionId, specType) {
93
+ this.startReference = reference;
94
+ this.captureSessionId = sessionId;
95
+ this.captureSpecType = specType;
96
+ }
97
+ /**
98
+ * Emits an additional subflow_started; construction already emits the initial start.
99
+ * `data` supports SubflowTrigger fields (`actor`, `explicitSpecType`, `ignoreAsInteraction`)
100
+ * plus free-form extras; see SubflowTrigger for programmatic interaction suppression.
101
+ */
102
+ subflowStart(data, options) {
103
+ try {
104
+ for (const flush of this.lowFlushers) {
105
+ try {
106
+ flush();
107
+ } catch (error) {
108
+ this.tracker.getLogger().error("Failed to flush previous low-event context", error);
109
+ }
110
+ }
111
+ this.captureSpecType = data?.explicitSpecType;
112
+ this.startReference = uuidv7();
113
+ this.captureSessionId = this.tracker.getSessionId();
114
+ this.tracker.trackSubflowStarted(this.operationName, data, this.captureOptions(options), this.startReference);
115
+ this.captureInputPrefills(true);
116
+ } catch (error) {
117
+ this.tracker.getLogger().error("Failed to start low-event capture context", error);
118
+ }
119
+ }
120
+ /**
121
+ * Capture values initialized by the application after constructing this operation, without
122
+ * dispatching DOM input events. Call immediately after initialization, before user editing.
123
+ * Each field/group emits at most one prefill per subflow start; values are never recorded.
124
+ */
125
+ capturePrefill() {
126
+ this.captureInputPrefills(false);
127
+ }
128
+ captureInputPrefills(newCapture) {
129
+ for (const capture of this.lowPrefillCapturers) {
130
+ try {
131
+ capture(newCapture);
132
+ } catch (error) {
133
+ this.tracker.getLogger().error("Failed to capture input prefill", error);
134
+ }
135
+ }
136
+ }
137
+ /**
138
+ * @deprecated `subflow_trigger` will be removed in the next major version.
139
+ */
140
+ trigger(data, options) {
141
+ void this.tracker.trackSubflowTrigger(this.operationName, data, this.captureOptions(options));
142
+ }
143
+ customStep(stepName) {
144
+ return this.defineStep(stepName);
145
+ }
146
+ subflowStepStarted(stepName, data, options, ignoreAsInteraction) {
147
+ this.captureSpecType = data?.explicitSpecType ?? this.captureSpecType;
148
+ void this.tracker.trackSubflowStepStarted(
149
+ this.operationName,
150
+ stepName,
151
+ data,
152
+ this.captureOptions(options),
153
+ ignoreAsInteraction
154
+ );
155
+ }
156
+ subflowStepFinished(stepName, data, options) {
157
+ if (!options?.subflowStartEventId || options.subflowStartEventId === this.startReference) {
158
+ this.captureSpecType = data?.explicitSpecType ?? this.captureSpecType;
159
+ }
160
+ void this.tracker.trackSubflowStepFinished(this.operationName, stepName, data, this.captureOptions(options));
161
+ }
162
+ subflowStepError(stepName, data, options) {
163
+ let payload = data;
164
+ try {
165
+ const rawError = this.tracker.getSdkConfig().rawErrors ? options?.rawError : void 0;
166
+ if (rawError !== void 0) payload = { ...data, rawError: serializeRawError(rawError, options?.rawErrorLimits) };
167
+ } catch {
168
+ }
169
+ void this.tracker.trackSubflowStepError(this.operationName, stepName, payload, this.captureOptions(options));
170
+ }
171
+ /**
172
+ * Decorate a (ceremony) step so window low-event collection follows its lifecycle: `start` arms
173
+ * the tracker, `finished`/`error`/`errorTyped` schedule disarming after
174
+ * {@link CEREMONY_LOW_EVENT_TAIL_MS}. A `start` during the tail (ceremony retry) keeps the
175
+ * tracker armed.
176
+ */
177
+ withWindowLowEvents(step, windowTracker) {
178
+ let ceremonyReference;
179
+ const settle = (options) => {
180
+ if (options?.subflowStartEventId && options.subflowStartEventId !== ceremonyReference) return;
181
+ windowTracker.scheduleDisarm(CEREMONY_LOW_EVENT_TAIL_MS);
182
+ };
183
+ const inner = step;
184
+ return {
185
+ ...step,
186
+ start: (data, options) => {
187
+ ceremonyReference = options?.subflowStartEventId ?? this.startReference;
188
+ const spec = data?.explicitSpecType ?? this.captureSpecType;
189
+ const initiator = options?.initiator ?? inferCeremonyInitiator(this.operationName, spec);
190
+ windowTracker.arm(initiator, spec === "passkey-cui");
191
+ inner.start(data, initiator ? { ...options, initiator } : options);
192
+ },
193
+ finished: (data, options) => {
194
+ inner.finished(data, options);
195
+ settle(options);
196
+ },
197
+ error: (error, options) => {
198
+ inner.error(error, options);
199
+ settle(options);
200
+ },
201
+ errorTyped: (error, options) => {
202
+ inner.errorTyped?.(error, options);
203
+ settle(options);
204
+ }
205
+ };
206
+ }
207
+ defineStep(stepName, ignoreAsInteractionOnStart) {
208
+ let stepReference;
209
+ let stepSessionId;
210
+ let stepInitiator;
211
+ const stepOptions = (options) => this.captureOptions(options, stepReference, stepSessionId, stepInitiator);
212
+ const step = {
213
+ start: (data, options) => {
214
+ stepReference = this.startReference;
215
+ stepSessionId = this.captureSessionId;
216
+ stepInitiator = options?.initiator;
217
+ this.subflowStepStarted(stepName, data, stepOptions(options), ignoreAsInteractionOnStart);
218
+ },
219
+ finished: (data, options) => {
220
+ this.subflowStepFinished(stepName, data, stepOptions(options));
221
+ },
222
+ error: (error, options) => {
223
+ this.subflowStepError(stepName, { error: normalizeError(error) }, stepOptions(options));
224
+ },
225
+ errorTyped: (error, options) => {
226
+ this.subflowStepError(stepName, error, stepOptions(options));
227
+ }
228
+ };
229
+ return step;
230
+ }
231
+ };
232
+ export {
233
+ CEREMONY_LOW_EVENT_TAIL_MS,
234
+ OperationFull
235
+ };
@@ -0,0 +1,17 @@
1
+ // src/utils/passkey-deletion-operation-full.ts
2
+ import { OperationFull } from "./operation-full.mjs";
3
+ var PasskeyDeletionOperationFull = class extends OperationFull {
4
+ /** Optional confirmation dialog: start when it opens, finish when the user confirms. */
5
+ userConfirmation;
6
+ /** The deletion request: finish only when the passkey was actually deleted. */
7
+ deletion;
8
+ constructor(tracker, _config = {}) {
9
+ super(tracker, "passkey-deletion");
10
+ this.userConfirmation = this.defineStep("user-confirmation");
11
+ this.deletion = this.defineStep("deletion");
12
+ void this.subflowStart({});
13
+ }
14
+ };
15
+ export {
16
+ PasskeyDeletionOperationFull
17
+ };
@@ -0,0 +1,35 @@
1
+ // src/utils/passkey-enrollment-operation-full.ts
2
+ import { LowEventWindowTracker } from "../low-events/window-tracker.mjs";
3
+ import { OperationFull } from "./operation-full.mjs";
4
+ import { acceptHandoffs } from "../handoff.mjs";
5
+ var PasskeyEnrollmentOperationFull = class extends OperationFull {
6
+ getOptions;
7
+ ceremony;
8
+ postResponse;
9
+ lowEventTracker;
10
+ constructor(tracker, config) {
11
+ super(tracker, "passkey-enrollment");
12
+ this.lowEventTracker = new LowEventWindowTracker(tracker.getLogger(), this.lowEventSink("ceremony"));
13
+ this.registerLowFlusher(() => this.lowEventTracker.disarm());
14
+ this.getOptions = acceptHandoffs(
15
+ this.defineStep("get-options")
16
+ );
17
+ this.ceremony = acceptHandoffs(
18
+ this.withWindowLowEvents(
19
+ this.defineStep("ceremony"),
20
+ this.lowEventTracker
21
+ )
22
+ );
23
+ this.postResponse = acceptHandoffs(this.defineStep("post-response"));
24
+ void this.subflowStart({
25
+ explicitSpecType: config?.explicitSpecType,
26
+ ...config?.ignoreAsInteraction ? { ignoreAsInteraction: true } : {}
27
+ });
28
+ }
29
+ destroy() {
30
+ this.lowEventTracker.destroy();
31
+ }
32
+ };
33
+ export {
34
+ PasskeyEnrollmentOperationFull
35
+ };
@@ -0,0 +1,39 @@
1
+ // src/utils/passkey-login-operation-full.ts
2
+ import { LowEventWindowTracker } from "../low-events/window-tracker.mjs";
3
+ import { OperationFull } from "./operation-full.mjs";
4
+ import { acceptHandoffs } from "../handoff.mjs";
5
+ var PasskeyLoginOperationFull = class extends OperationFull {
6
+ getOptions;
7
+ ceremony;
8
+ postResponse;
9
+ lowEventTracker;
10
+ constructor(tracker, config) {
11
+ super(tracker, "passkey-login");
12
+ this.lowEventTracker = new LowEventWindowTracker(tracker.getLogger(), this.lowEventSink("ceremony"));
13
+ this.registerLowFlusher(() => this.lowEventTracker.disarm());
14
+ this.getOptions = acceptHandoffs(
15
+ this.defineStep("get-options")
16
+ );
17
+ this.ceremony = acceptHandoffs(
18
+ this.withWindowLowEvents(
19
+ this.defineStep("ceremony"),
20
+ this.lowEventTracker
21
+ )
22
+ );
23
+ this.postResponse = acceptHandoffs(this.defineStep("post-response"));
24
+ void this.subflowStart({ explicitSpecType: config?.explicitSpecType });
25
+ }
26
+ /**
27
+ * @deprecated `subflow_trigger` is not supported for passkey-login and will be removed in the
28
+ * next major version.
29
+ */
30
+ trigger(data, options) {
31
+ super.trigger(data, options);
32
+ }
33
+ destroy() {
34
+ this.lowEventTracker.destroy();
35
+ }
36
+ };
37
+ export {
38
+ PasskeyLoginOperationFull
39
+ };
@@ -0,0 +1,27 @@
1
+ // src/utils/password-enrollment-operation-full.ts
2
+ import { LowEventInputTracker } from "../low-events/input-tracker.mjs";
3
+ import { OperationFull } from "./operation-full.mjs";
4
+ var PasswordEnrollmentOperationFull = class extends OperationFull {
5
+ clientValidation;
6
+ postResponse;
7
+ lowEventTracker;
8
+ constructor(tracker, config) {
9
+ super(tracker, "password-enrollment");
10
+ this.clientValidation = this.defineStep("client-validation");
11
+ this.postResponse = this.defineStep("post-response");
12
+ void this.subflowStart({ explicitSpecType: config?.explicitSpecType });
13
+ if (!config?.inputHtmlField) {
14
+ return;
15
+ }
16
+ this.lowEventTracker = this.createLowCollector(
17
+ () => new LowEventInputTracker(tracker.getLogger(), config.inputHtmlField, this.lowEventSink("input", "password"))
18
+ );
19
+ this.registerLowFlusher(() => this.lowEventTracker?.flushPending());
20
+ }
21
+ destroy() {
22
+ this.lowEventTracker?.destroy();
23
+ }
24
+ };
25
+ export {
26
+ PasswordEnrollmentOperationFull
27
+ };
@@ -0,0 +1,65 @@
1
+ // src/utils/password-login-operation-full.ts
2
+ import { LowEventInputTracker } from "../low-events/input-tracker.mjs";
3
+ import { OperationFull } from "./operation-full.mjs";
4
+ import { acceptHandoffs } from "../handoff.mjs";
5
+ var PasswordLoginOperationFull = class extends OperationFull {
6
+ clientValidation;
7
+ postResponse;
8
+ /**
9
+ * Conditional-UI passkey ceremony that runs while the password form is mounted. The three steps
10
+ * are emitted under the same `password-login` subflow so a CUI passkey login on this screen is
11
+ * attributed to password-login rather than a separate subflow.
12
+ */
13
+ cui;
14
+ lowEventTracker;
15
+ identifierLowEventTracker;
16
+ constructor(tracker, config) {
17
+ super(tracker, "password-login");
18
+ this.clientValidation = this.defineStep("client-validation");
19
+ this.postResponse = this.defineStep("post-response");
20
+ this.cui = {
21
+ // getOptions only fetches assertion options; it is not a user interaction on its own.
22
+ getOptions: acceptHandoffs(
23
+ this.defineStep("cui-get-options", true)
24
+ ),
25
+ ceremony: acceptHandoffs(
26
+ this.defineStep(
27
+ "cui-ceremony"
28
+ )
29
+ ),
30
+ postResponse: this.defineStep("cui-post-response")
31
+ };
32
+ void this.subflowStart({ explicitSpecType: config?.explicitSpecType });
33
+ if (config?.identifierInputHtmlField) {
34
+ this.identifierLowEventTracker = this.createLowCollector(
35
+ () => new LowEventInputTracker(
36
+ tracker.getLogger(),
37
+ config.identifierInputHtmlField,
38
+ this.lowEventSink("input", "identifier")
39
+ )
40
+ );
41
+ this.registerLowFlusher(() => this.identifierLowEventTracker?.flushPending());
42
+ }
43
+ if (!config?.inputHtmlField) {
44
+ return;
45
+ }
46
+ this.lowEventTracker = this.createLowCollector(
47
+ () => new LowEventInputTracker(tracker.getLogger(), config.inputHtmlField, this.lowEventSink("input", "password"))
48
+ );
49
+ this.registerLowFlusher(() => this.lowEventTracker?.flushPending());
50
+ }
51
+ /**
52
+ * @deprecated `subflow_trigger` is not supported for password-login and will be removed in the
53
+ * next major version.
54
+ */
55
+ trigger(data, options) {
56
+ super.trigger(data, options);
57
+ }
58
+ destroy() {
59
+ this.lowEventTracker?.destroy();
60
+ this.identifierLowEventTracker?.destroy();
61
+ }
62
+ };
63
+ export {
64
+ PasswordLoginOperationFull
65
+ };
@@ -0,0 +1,170 @@
1
+ // src/utils/password-manager-sampler.ts
2
+ import { listenWindow, readDocumentComplete } from "../readers/page.mjs";
3
+ import {
4
+ readPasswordManagerExtensions
5
+ } from "../readers/password-manager-extensions.mjs";
6
+ var LOAD_RESAMPLE_DELAY_MS = 3e3;
7
+ var INPUT_FOCUS_SAMPLE_DELAY_MS = 500;
8
+ var MAX_FOCUS_SAMPLES = 6;
9
+ var IDLE_TIMEOUT_MS = 1e3;
10
+ var PasswordManagerSampler = class {
11
+ constructor(logger, isEnabled, windowObserver) {
12
+ this.logger = logger;
13
+ this.isEnabled = isEnabled;
14
+ if (typeof window === "undefined" || typeof document === "undefined") return;
15
+ this.sample();
16
+ if (readDocumentComplete({
17
+ key: "page:lifecycle.ready-state",
18
+ title: "Page load state",
19
+ accessNote: "document.readyState",
20
+ group: "Page lifecycle",
21
+ origin: "observe-external",
22
+ fields: [
23
+ {
24
+ path: "readyState",
25
+ label: "Whether the page has finished loading",
26
+ exposure: "enum",
27
+ transform: { values: ["complete", "other"] }
28
+ }
29
+ ]
30
+ })) {
31
+ this.sampleAfter(LOAD_RESAMPLE_DELAY_MS);
32
+ } else {
33
+ const onLoad = () => {
34
+ this.sampleWhenIdle();
35
+ this.sampleAfter(LOAD_RESAMPLE_DELAY_MS);
36
+ };
37
+ this.removers.push(
38
+ listenWindow(
39
+ {
40
+ key: "page:lifecycle.load",
41
+ title: "Page load",
42
+ accessNote: "load event on window",
43
+ group: "Page lifecycle",
44
+ origin: "observe-external",
45
+ fields: [
46
+ {
47
+ path: "event.load",
48
+ label: "Page finished loading",
49
+ exposure: "presence",
50
+ transform: { note: "schedules a password manager sample" }
51
+ }
52
+ ]
53
+ },
54
+ "load",
55
+ onLoad,
56
+ { once: true }
57
+ )
58
+ );
59
+ }
60
+ const removeFocus = windowObserver.onInputFocus((field) => {
61
+ if (!this.isEnabled() || this.sampledFields.has(field)) return;
62
+ this.sampledFields.add(field);
63
+ if (++this.focusSamples >= MAX_FOCUS_SAMPLES) removeFocus();
64
+ this.sampleAfter(INPUT_FOCUS_SAMPLE_DELAY_MS);
65
+ });
66
+ this.removers.push(removeFocus);
67
+ }
68
+ detected = /* @__PURE__ */ new Map();
69
+ timers = /* @__PURE__ */ new Set();
70
+ idleCallbacks = /* @__PURE__ */ new Set();
71
+ /** Registered fields whose first focus already scheduled a sample (weak, so removed forms are not retained). */
72
+ sampledFields = /* @__PURE__ */ new WeakSet();
73
+ focusSamples = 0;
74
+ removers = [];
75
+ destroyed = false;
76
+ /** Per session, how many managers the last reported set held; the set only grows, so a count suffices. */
77
+ reportedCounts = /* @__PURE__ */ new Map();
78
+ /** Managers detected on this page so far, in detection order. */
79
+ getDetected() {
80
+ return [...this.detected.values()];
81
+ }
82
+ /**
83
+ * The full set of detected manager names when it grew since the last report for `sessionId`, otherwise
84
+ * undefined. Each session's first event after a detection carries the set (also after `resetSession()`),
85
+ * later events only when a sample added a manager. Signed-in upgrades are not reported, so they never
86
+ * count as growth. A lost event that carried the set is not repaired by later events of the same page.
87
+ */
88
+ takeReport(sessionId) {
89
+ if (!this.isEnabled() || this.detected.size <= (this.reportedCounts.get(sessionId) ?? 0)) return void 0;
90
+ this.reportedCounts.set(sessionId, this.detected.size);
91
+ return this.getDetected().map(({ name }) => ({ name }));
92
+ }
93
+ destroy() {
94
+ this.destroyed = true;
95
+ for (const remove of this.removers) remove();
96
+ for (const timer of this.timers) clearTimeout(timer);
97
+ this.timers.clear();
98
+ for (const handle of this.idleCallbacks) cancelIdleCallback(handle);
99
+ this.idleCallbacks.clear();
100
+ }
101
+ sampleAfter(delayMs) {
102
+ const timer = setTimeout(() => {
103
+ this.timers.delete(timer);
104
+ this.sampleWhenIdle();
105
+ }, delayMs);
106
+ this.timers.add(timer);
107
+ }
108
+ sampleWhenIdle() {
109
+ if (typeof requestIdleCallback !== "function") {
110
+ this.sample();
111
+ return;
112
+ }
113
+ const handle = requestIdleCallback(
114
+ () => {
115
+ this.idleCallbacks.delete(handle);
116
+ this.sample();
117
+ },
118
+ { timeout: IDLE_TIMEOUT_MS }
119
+ );
120
+ this.idleCallbacks.add(handle);
121
+ }
122
+ sample() {
123
+ if (this.destroyed || !this.isEnabled()) return;
124
+ try {
125
+ const found = readPasswordManagerExtensions({
126
+ key: "browser-api:password-managers.extensions",
127
+ title: "Installed password manager extensions",
128
+ accessNote: "globals and DOM elements that known password manager extensions inject",
129
+ group: "Password manager detection",
130
+ origin: "observe-external",
131
+ fields: [
132
+ {
133
+ path: "result[].name",
134
+ label: "Names of known password managers found",
135
+ exposure: "enum",
136
+ transform: {
137
+ values: [
138
+ "1password",
139
+ "bitwarden",
140
+ "dashlane",
141
+ "kaspersky",
142
+ "keeper",
143
+ "lastpass",
144
+ "nordpass",
145
+ "protonpass"
146
+ ]
147
+ }
148
+ },
149
+ // Read to merge samples, never reported (takeReport keeps only the name).
150
+ { path: "result[].signedIn", label: "Whether the password manager is signed in", exposure: "none" }
151
+ ],
152
+ dataConfig: "pmd"
153
+ });
154
+ for (const extension of found) {
155
+ const known = this.detected.get(extension.name);
156
+ if (!known || extension.signedIn && !known.signedIn) {
157
+ this.detected.set(extension.name, extension);
158
+ }
159
+ }
160
+ } catch (error) {
161
+ this.logger.error("Failed to detect password manager extensions", error);
162
+ }
163
+ }
164
+ };
165
+ export {
166
+ INPUT_FOCUS_SAMPLE_DELAY_MS,
167
+ LOAD_RESAMPLE_DELAY_MS,
168
+ MAX_FOCUS_SAMPLES,
169
+ PasswordManagerSampler
170
+ };
@@ -0,0 +1,28 @@
1
+ // src/utils/provide-data-operation-full.ts
2
+ import { LowEventInputTracker } from "../low-events/input-tracker.mjs";
3
+ import { OperationFull } from "./operation-full.mjs";
4
+ var ProvideDataOperationFull = class extends OperationFull {
5
+ clientValidation;
6
+ postResponse;
7
+ lowEventTracker;
8
+ constructor(tracker, config = {}) {
9
+ super(tracker, "provide-data");
10
+ this.clientValidation = this.defineStep("client-validation");
11
+ this.postResponse = this.defineStep("post-response");
12
+ const { inputHtmlField, ...startData } = config;
13
+ void this.subflowStart(startData);
14
+ if (!inputHtmlField) {
15
+ return;
16
+ }
17
+ this.lowEventTracker = this.createLowCollector(
18
+ () => new LowEventInputTracker(tracker.getLogger(), inputHtmlField, this.lowEventSink("input", "data"))
19
+ );
20
+ this.registerLowFlusher(() => this.lowEventTracker?.flushPending());
21
+ }
22
+ destroy() {
23
+ this.lowEventTracker?.destroy();
24
+ }
25
+ };
26
+ export {
27
+ ProvideDataOperationFull
28
+ };