@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.
- package/dist/analysis/client.mjs +1877 -0
- package/dist/analysis/config-manager.mjs +108 -0
- package/dist/analysis/config.mjs +216 -0
- package/dist/analysis/device-info-manager.mjs +168 -0
- package/dist/analysis/device.mjs +640 -0
- package/dist/analysis/handoff.mjs +7 -0
- package/dist/analysis/index.mjs +118 -0
- package/dist/analysis/logger.mjs +50 -0
- package/dist/analysis/low-events/capture-sequence.mjs +11 -0
- package/dist/analysis/low-events/ceremony-initiator.mjs +26 -0
- package/dist/analysis/low-events/form-input-binding.mjs +46 -0
- package/dist/analysis/low-events/grouped-input-tracker.mjs +248 -0
- package/dist/analysis/low-events/input-change-recorder.mjs +200 -0
- package/dist/analysis/low-events/input-tracker.mjs +302 -0
- package/dist/analysis/low-events/otp-input-binding.mjs +67 -0
- package/dist/analysis/low-events/shared-window-observer.mjs +296 -0
- package/dist/analysis/low-events/sink.mjs +0 -0
- package/dist/analysis/low-events/viewport-batcher.mjs +42 -0
- package/dist/analysis/low-events/window-tracker.mjs +40 -0
- package/dist/analysis/queue.mjs +954 -0
- package/dist/analysis/readers/device.mjs +73 -0
- package/dist/analysis/readers/input-paste.mjs +91 -0
- package/dist/analysis/readers/input.mjs +61 -0
- package/dist/analysis/readers/loader-global.mjs +15 -0
- package/dist/analysis/readers/meta.mjs +18 -0
- package/dist/analysis/readers/network.mjs +14 -0
- package/dist/analysis/readers/page.mjs +30 -0
- package/dist/analysis/readers/password-manager-dom.mjs +112 -0
- package/dist/analysis/readers/password-manager-extensions.mjs +247 -0
- package/dist/analysis/readers/private-mode.mjs +9 -0
- package/dist/analysis/readers/storage.mjs +28 -0
- package/dist/analysis/readers/web-lock.mjs +56 -0
- package/dist/analysis/readers/window-events.mjs +48 -0
- package/dist/analysis/storage/index.mjs +249 -0
- package/dist/analysis/storage/migration.mjs +56 -0
- package/dist/analysis/store/event-store.mjs +132 -0
- package/dist/analysis/transport.mjs +0 -0
- package/dist/analysis/transports/fetch.mjs +150 -0
- package/dist/analysis/types.mjs +25 -0
- package/dist/analysis/utils/appconfirmation-operation-full.mjs +19 -0
- package/dist/analysis/utils/blocked-user-agents.mjs +37 -0
- package/dist/analysis/utils/captcha-operation-full.mjs +17 -0
- package/dist/analysis/utils/email-link-continuity.mjs +50 -0
- package/dist/analysis/utils/email-link-operation-full.mjs +162 -0
- package/dist/analysis/utils/email-otp-operation-full.mjs +38 -0
- package/dist/analysis/utils/form-operation-full.mjs +80 -0
- package/dist/analysis/utils/key-operation-full.mjs +38 -0
- package/dist/analysis/utils/normalize-error.mjs +29 -0
- package/dist/analysis/utils/operation-full-provide-identifier-with-cui.mjs +46 -0
- package/dist/analysis/utils/operation-full.mjs +235 -0
- package/dist/analysis/utils/passkey-deletion-operation-full.mjs +17 -0
- package/dist/analysis/utils/passkey-enrollment-operation-full.mjs +35 -0
- package/dist/analysis/utils/passkey-login-operation-full.mjs +39 -0
- package/dist/analysis/utils/password-enrollment-operation-full.mjs +27 -0
- package/dist/analysis/utils/password-login-operation-full.mjs +65 -0
- package/dist/analysis/utils/password-manager-sampler.mjs +170 -0
- package/dist/analysis/utils/provide-data-operation-full.mjs +28 -0
- package/dist/analysis/utils/serialize-raw-error.mjs +205 -0
- package/dist/analysis/utils/sms-otp-operation-full.mjs +38 -0
- package/dist/analysis/utils/social-operation-login.mjs +17 -0
- package/dist/analysis/utils/source-path.mjs +39 -0
- package/dist/analysis/utils/sso-operation-login.mjs +37 -0
- package/dist/analysis/utils/totp-operation-full.mjs +34 -0
- package/dist/analysis/utils/trusted-device-operation-full.mjs +100 -0
- package/dist/analysis/utils/webauthn-payloads.mjs +101 -0
- package/dist/analysis/version.mjs +7 -0
- package/dist/cdn/script.global.js +4 -4
- package/dist/index.d.mts +285 -21
- package/dist/index.d.ts +285 -21
- package/dist/index.js +4 -4
- package/dist/index.mjs +4 -4
- package/package.json +10 -3
- package/tools/reader-meta-text.d.mts +2 -0
- package/tools/reader-meta-text.mjs +26 -0
- package/tools/strip-reader-meta.d.mts +33 -0
- 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
|
+
};
|