@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,73 @@
|
|
|
1
|
+
// src/readers/device.ts
|
|
2
|
+
function readUserAgent(_meta) {
|
|
3
|
+
if (typeof navigator === "undefined") {
|
|
4
|
+
return "";
|
|
5
|
+
}
|
|
6
|
+
return navigator.userAgent ?? "";
|
|
7
|
+
}
|
|
8
|
+
async function readBluetoothAvailability(_meta) {
|
|
9
|
+
if (typeof navigator === "undefined" || !navigator.bluetooth) {
|
|
10
|
+
return void 0;
|
|
11
|
+
}
|
|
12
|
+
return await navigator.bluetooth.getAvailability();
|
|
13
|
+
}
|
|
14
|
+
async function readUserVerifyingPlatformAuthenticatorAvailable(_meta) {
|
|
15
|
+
if (typeof PublicKeyCredential === "undefined" || !PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable) {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
19
|
+
}
|
|
20
|
+
async function readConditionalMediationAvailable(_meta) {
|
|
21
|
+
if (typeof PublicKeyCredential === "undefined" || !PublicKeyCredential.isConditionalMediationAvailable) {
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
return await PublicKeyCredential.isConditionalMediationAvailable();
|
|
25
|
+
}
|
|
26
|
+
async function readClientCapabilities(_meta) {
|
|
27
|
+
if (typeof PublicKeyCredential === "undefined" || !PublicKeyCredential.getClientCapabilities) {
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
const rawClientCapabilities = await PublicKeyCredential.getClientCapabilities();
|
|
31
|
+
return Object.fromEntries(Object.entries(rawClientCapabilities).map(([key, value]) => [key, value.toString()]));
|
|
32
|
+
}
|
|
33
|
+
async function readJavaScriptHighEntropy(_meta) {
|
|
34
|
+
if (typeof navigator === "undefined" || !navigator.userAgentData) {
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
const ua = await navigator.userAgentData.getHighEntropyValues(["platformVersion", "model"]);
|
|
38
|
+
const platform = ua.platform;
|
|
39
|
+
const mobile = ua.mobile;
|
|
40
|
+
const platformVersion = ua.platformVersion;
|
|
41
|
+
const model = ua.model || void 0;
|
|
42
|
+
if (!platform || mobile === void 0 || !platformVersion) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
platform,
|
|
47
|
+
mobile,
|
|
48
|
+
platformVersion,
|
|
49
|
+
model
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function readWebdriver(_meta) {
|
|
53
|
+
if (typeof navigator === "undefined") {
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
return navigator.webdriver;
|
|
57
|
+
}
|
|
58
|
+
function readMaxTouchPoints(_meta) {
|
|
59
|
+
if (typeof navigator === "undefined" || typeof navigator.maxTouchPoints !== "number") {
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
62
|
+
return navigator.maxTouchPoints;
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
readBluetoothAvailability,
|
|
66
|
+
readClientCapabilities,
|
|
67
|
+
readConditionalMediationAvailable,
|
|
68
|
+
readJavaScriptHighEntropy,
|
|
69
|
+
readMaxTouchPoints,
|
|
70
|
+
readUserAgent,
|
|
71
|
+
readUserVerifyingPlatformAuthenticatorAvailable,
|
|
72
|
+
readWebdriver
|
|
73
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/readers/input-paste.ts
|
|
2
|
+
var InputPasteObserver = class {
|
|
3
|
+
constructor(_meta, inputs, logger, changed, reserveCaptureSeq) {
|
|
4
|
+
this.inputs = inputs;
|
|
5
|
+
this.logger = logger;
|
|
6
|
+
this.changed = changed;
|
|
7
|
+
this.reserveCaptureSeq = reserveCaptureSeq;
|
|
8
|
+
for (const input of inputs) {
|
|
9
|
+
input.addEventListener("paste", this.onPaste, true);
|
|
10
|
+
input.addEventListener("beforeinput", this.onBeforeInput, true);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
before = null;
|
|
14
|
+
timestamp = 0;
|
|
15
|
+
captureSeq;
|
|
16
|
+
pasteEvent;
|
|
17
|
+
timer = null;
|
|
18
|
+
onPaste = (event) => this.begin(event);
|
|
19
|
+
onBeforeInput = (event) => {
|
|
20
|
+
if (event.inputType === "insertFromPaste") this.begin(event);
|
|
21
|
+
else this.flush();
|
|
22
|
+
};
|
|
23
|
+
/** An unrelated input event invalidates pending paste provenance before recording its change. */
|
|
24
|
+
handlesInput(event) {
|
|
25
|
+
if (!this.before) return false;
|
|
26
|
+
const inPasteDispatch = this.pasteEvent?.type === "paste" && (this.pasteEvent.eventPhase ?? 0) > 0;
|
|
27
|
+
if (event.inputType === "insertFromPaste" || inPasteDispatch) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
this.clear();
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
get pending() {
|
|
34
|
+
return this.before !== null;
|
|
35
|
+
}
|
|
36
|
+
begin(event) {
|
|
37
|
+
if (!event.isTrusted || this.before) return;
|
|
38
|
+
try {
|
|
39
|
+
this.before = this.inputs.map((input) => input.value);
|
|
40
|
+
this.pasteEvent = event;
|
|
41
|
+
this.timestamp = Date.now();
|
|
42
|
+
this.captureSeq = this.reserveCaptureSeq?.();
|
|
43
|
+
this.timer = setTimeout(() => this.flush(), 0);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
this.logger.error("Failed to observe paste", error);
|
|
46
|
+
this.clear();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
flush() {
|
|
50
|
+
const before = this.before;
|
|
51
|
+
if (!before) return;
|
|
52
|
+
const timestamp = this.timestamp;
|
|
53
|
+
const captureSeq = this.captureSeq;
|
|
54
|
+
this.clear();
|
|
55
|
+
try {
|
|
56
|
+
let inserted = 0;
|
|
57
|
+
let changed = false;
|
|
58
|
+
this.inputs.forEach((input, index) => {
|
|
59
|
+
const old = before[index];
|
|
60
|
+
const value = input.value;
|
|
61
|
+
if (old === value) return;
|
|
62
|
+
changed = true;
|
|
63
|
+
let start = 0;
|
|
64
|
+
while (start < old.length && start < value.length && old[start] === value[start]) start++;
|
|
65
|
+
let end = 0;
|
|
66
|
+
while (end < old.length - start && end < value.length - start && old[old.length - end - 1] === value[value.length - end - 1])
|
|
67
|
+
end++;
|
|
68
|
+
inserted += value.length - start - end;
|
|
69
|
+
});
|
|
70
|
+
if (changed) this.changed(timestamp, inserted, captureSeq);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
this.logger.error("Failed to finish paste observation", error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
clear() {
|
|
76
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
77
|
+
this.timer = null;
|
|
78
|
+
this.before = null;
|
|
79
|
+
this.pasteEvent = void 0;
|
|
80
|
+
}
|
|
81
|
+
destroy() {
|
|
82
|
+
this.flush();
|
|
83
|
+
for (const input of this.inputs) {
|
|
84
|
+
input.removeEventListener("paste", this.onPaste, true);
|
|
85
|
+
input.removeEventListener("beforeinput", this.onBeforeInput, true);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
export {
|
|
90
|
+
InputPasteObserver
|
|
91
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/readers/input.ts
|
|
2
|
+
var FIELD_EVENTS = ["focus", "blur", "pointerdown", "pointerup", "click", "input", "keyup"];
|
|
3
|
+
function observeInputField(_meta, field, handlers, guard, order = FIELD_EVENTS) {
|
|
4
|
+
const listeners = [];
|
|
5
|
+
for (const type of order) {
|
|
6
|
+
const listener = reduceFieldEvent(type, handlers, guard);
|
|
7
|
+
if (listener) listeners.push([type, listener]);
|
|
8
|
+
}
|
|
9
|
+
for (const [type, listener] of listeners) field.addEventListener(type, listener);
|
|
10
|
+
return () => {
|
|
11
|
+
for (const [type, listener] of listeners) field.removeEventListener(type, listener);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function reduceFieldEvent(type, handlers, guard) {
|
|
15
|
+
switch (type) {
|
|
16
|
+
case "focus":
|
|
17
|
+
case "blur": {
|
|
18
|
+
const handler = handlers[type];
|
|
19
|
+
return handler && ((event) => guard(type, () => handler(event.relatedTarget)));
|
|
20
|
+
}
|
|
21
|
+
case "pointerdown":
|
|
22
|
+
case "pointerup":
|
|
23
|
+
case "click": {
|
|
24
|
+
const handler = handlers[type];
|
|
25
|
+
return handler && (() => guard(type, () => handler()));
|
|
26
|
+
}
|
|
27
|
+
case "input": {
|
|
28
|
+
const handler = handlers.input;
|
|
29
|
+
return handler && ((event) => guard(
|
|
30
|
+
"input",
|
|
31
|
+
() => handler(
|
|
32
|
+
event.isComposing || event.inputType === "insertCompositionText",
|
|
33
|
+
event
|
|
34
|
+
)
|
|
35
|
+
));
|
|
36
|
+
}
|
|
37
|
+
case "keyup": {
|
|
38
|
+
const handler = handlers.keyupEscape;
|
|
39
|
+
return handler && ((event) => guard("keyup", () => {
|
|
40
|
+
if (event.key === "Escape") handler();
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function readInputContentLength(_meta, field) {
|
|
46
|
+
if (field.type === "checkbox" || field.type === "radio") return field.checked ? 1 : 0;
|
|
47
|
+
return field.value.length;
|
|
48
|
+
}
|
|
49
|
+
function readInputGroupValueLength(_meta, fields) {
|
|
50
|
+
return fields.reduce((total, field) => total + field.value.length, 0);
|
|
51
|
+
}
|
|
52
|
+
function readFocusedField(_meta, fields) {
|
|
53
|
+
const active = document.activeElement;
|
|
54
|
+
return fields.find((field) => field === active);
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
observeInputField,
|
|
58
|
+
readFocusedField,
|
|
59
|
+
readInputContentLength,
|
|
60
|
+
readInputGroupValueLength
|
|
61
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/readers/loader-global.ts
|
|
2
|
+
function drainLoaderQueue(_meta, name, replay) {
|
|
3
|
+
const global = window.Corbado;
|
|
4
|
+
if (global?.[name]) {
|
|
5
|
+
replay(global[name]);
|
|
6
|
+
delete window.Corbado[name];
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function installLoaderGlobal(_meta, api) {
|
|
10
|
+
window.Corbado = api;
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
drainLoaderQueue,
|
|
14
|
+
installLoaderGlobal
|
|
15
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/readers/network.ts
|
|
2
|
+
function sendFetch(_meta, url, init) {
|
|
3
|
+
return fetch(url, init);
|
|
4
|
+
}
|
|
5
|
+
function beaconSender(_meta) {
|
|
6
|
+
if (typeof navigator?.sendBeacon !== "function") {
|
|
7
|
+
return void 0;
|
|
8
|
+
}
|
|
9
|
+
return (url, data) => navigator.sendBeacon(url, data);
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
beaconSender,
|
|
13
|
+
sendFetch
|
|
14
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/readers/page.ts
|
|
2
|
+
import { applySourcePathQuery } from "../utils/source-path.mjs";
|
|
3
|
+
function listenWindow(_meta, type, handler, options) {
|
|
4
|
+
if (options === void 0) {
|
|
5
|
+
window.addEventListener(type, handler);
|
|
6
|
+
} else {
|
|
7
|
+
window.addEventListener(type, handler, options);
|
|
8
|
+
}
|
|
9
|
+
return () => window.removeEventListener(type, handler);
|
|
10
|
+
}
|
|
11
|
+
function readDocumentHidden(_meta) {
|
|
12
|
+
return document.visibilityState === "hidden";
|
|
13
|
+
}
|
|
14
|
+
function readDocumentComplete(_meta) {
|
|
15
|
+
return document.readyState === "complete";
|
|
16
|
+
}
|
|
17
|
+
function readPageUrl(_meta) {
|
|
18
|
+
return window.location.href;
|
|
19
|
+
}
|
|
20
|
+
function readSourcePath(_meta, mode, allow) {
|
|
21
|
+
const search = applySourcePathQuery(window.location.search, mode, allow);
|
|
22
|
+
return `${window.location.pathname}${search}`;
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
listenWindow,
|
|
26
|
+
readDocumentComplete,
|
|
27
|
+
readDocumentHidden,
|
|
28
|
+
readPageUrl,
|
|
29
|
+
readSourcePath
|
|
30
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// src/readers/password-manager-dom.ts
|
|
2
|
+
var SIGNAL_TAG_QUERIES = [
|
|
3
|
+
{ query: "com-1password-button", eventType: "pa-1" },
|
|
4
|
+
{ query: "com-1password-menu", eventType: "pa-2" },
|
|
5
|
+
{ query: "com-1password-notification", eventType: "pa-3" }
|
|
6
|
+
];
|
|
7
|
+
var SIGNAL_ATTR_SELECTOR = "[data-lastpass-root], [data-lp], [data-lpignore], [data-lpfieldtype]";
|
|
8
|
+
var SIGNAL_ATTR_EVENT_TYPE = "pa-4";
|
|
9
|
+
var POPOVER_MANUAL_SELECTOR = '[popover="manual"]';
|
|
10
|
+
var PROFILE_A_MIN_STYLE_LENGTH = 400;
|
|
11
|
+
var PROFILE_A_MIN_INITIAL_IMPORTANT = 20;
|
|
12
|
+
var PROFILE_A_EVENT_TYPE = "pa-5";
|
|
13
|
+
var PROFILE_B_EVENT_TYPE = "pa-6";
|
|
14
|
+
var safeQueryAll = (root, selector) => {
|
|
15
|
+
try {
|
|
16
|
+
return Array.from(root.querySelectorAll(selector));
|
|
17
|
+
} catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var isPopoverProfileA = (el) => {
|
|
22
|
+
if (el.getAttribute("popover") !== "manual") {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const style = el.getAttribute("style") ?? "";
|
|
26
|
+
if (style.length < PROFILE_A_MIN_STYLE_LENGTH) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
if (!/z-index:\s*2147483647\b/i.test(style)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (!/position:\s*fixed\b/i.test(style)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (!/display:\s*block\b/i.test(style)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const initialImportantHits = style.match(/initial\s*!important/gi);
|
|
39
|
+
if ((initialImportantHits?.length ?? 0) < PROFILE_A_MIN_INITIAL_IMPORTANT) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
};
|
|
44
|
+
var isPopoverProfileB = (el) => {
|
|
45
|
+
if (isPopoverProfileA(el)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (el.getAttribute("popover") !== "manual") {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const style = el.getAttribute("style") ?? "";
|
|
52
|
+
if (!/z-index:\s*2147483647\b/i.test(style)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (!/position:\s*fixed\b/i.test(style)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (!/backdrop-filter:\s*[^;]*blur\(\s*20px\s*\)/i.test(style)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (!/background-color:\s*rgba\(\s*35\s*,\s*35\s*,\s*35\s*,/i.test(style)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
var forEachScanRoot = (cb) => {
|
|
67
|
+
cb(document);
|
|
68
|
+
for (const host of Array.from(document.querySelectorAll("*"))) {
|
|
69
|
+
const shadowRoot = host.shadowRoot;
|
|
70
|
+
if (shadowRoot) {
|
|
71
|
+
cb(shadowRoot);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var scanInputEnvSignals = () => {
|
|
76
|
+
const matched = /* @__PURE__ */ new Set();
|
|
77
|
+
if (typeof document === "undefined") {
|
|
78
|
+
return matched;
|
|
79
|
+
}
|
|
80
|
+
forEachScanRoot((root) => {
|
|
81
|
+
for (const entry of SIGNAL_TAG_QUERIES) {
|
|
82
|
+
if (matched.has(entry.eventType)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (safeQueryAll(root, entry.query).length > 0) {
|
|
86
|
+
matched.add(entry.eventType);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!matched.has(SIGNAL_ATTR_EVENT_TYPE) && safeQueryAll(root, SIGNAL_ATTR_SELECTOR).length > 0) {
|
|
90
|
+
matched.add(SIGNAL_ATTR_EVENT_TYPE);
|
|
91
|
+
}
|
|
92
|
+
if (!matched.has(PROFILE_A_EVENT_TYPE) || !matched.has(PROFILE_B_EVENT_TYPE)) {
|
|
93
|
+
for (const el of safeQueryAll(root, POPOVER_MANUAL_SELECTOR)) {
|
|
94
|
+
if (!matched.has(PROFILE_A_EVENT_TYPE) && isPopoverProfileA(el)) {
|
|
95
|
+
matched.add(PROFILE_A_EVENT_TYPE);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!matched.has(PROFILE_B_EVENT_TYPE) && isPopoverProfileB(el)) {
|
|
99
|
+
matched.add(PROFILE_B_EVENT_TYPE);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return matched;
|
|
105
|
+
};
|
|
106
|
+
function readPasswordManagerDomSignals(_meta) {
|
|
107
|
+
return scanInputEnvSignals();
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
readPasswordManagerDomSignals,
|
|
111
|
+
scanInputEnvSignals
|
|
112
|
+
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// src/readers/password-manager-extensions.ts
|
|
2
|
+
var ONEPASSWORD_SCRIPT = "/inline/injected/webauthn-listeners.js";
|
|
3
|
+
var DASHLANE_STACK = /\/dashlane-(?:shadow-dom|top-layer)-page-script\.js/;
|
|
4
|
+
var KASPERSKY_SCRIPT = /-extension:\/\/[^/]+\/content-scripts\/passkey_proxy\/index\.js$/;
|
|
5
|
+
var KEEPER_STACK = /-extension:\/\/[^/]+\/content_scripts\/shadow-dom\/shadowAttachNotifier\.js:/;
|
|
6
|
+
var NORDPASS_STACK = /-extension:\/\/[^/]+\/js\/nordpass-script\.js:/;
|
|
7
|
+
var NORDPASS_PASSKEYS_KEY = /-extension:\/\/[^/]+\/js\/passkeys\.js:/;
|
|
8
|
+
var KEEPER_SOURCE = /["'`](?:keeper:shadow-root-attached|webauthn-(?:get|create)-request)["'`]/;
|
|
9
|
+
var PROTONPASS_SOURCE = /Intercept cancelled for `(?:get|create)`|["'`](?:conditionalMediationAvailable|verifyingPlatformAuthenticatorAvailable)["'`]/;
|
|
10
|
+
var sentryDebugIdKeys = () => Object.keys(window._sentryDebugIds ?? {});
|
|
11
|
+
var stackOf = (fn) => {
|
|
12
|
+
try {
|
|
13
|
+
fn();
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return String(error?.stack ?? "");
|
|
16
|
+
}
|
|
17
|
+
return "";
|
|
18
|
+
};
|
|
19
|
+
var attachShadowStack = () => stackOf(() => Element.prototype.attachShadow.call(null, { mode: "open" }));
|
|
20
|
+
var has1PasswordAccessor = () => {
|
|
21
|
+
const hook = Object.getOwnPropertyDescriptor(navigator.credentials ?? {}, "get");
|
|
22
|
+
if (!hook?.get || !hook.set || hook.configurable) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const marker = () => void 0;
|
|
26
|
+
const read = hook.get.call({ _get: marker });
|
|
27
|
+
return read === marker || typeof read === "function" && read === hook.get.call({}) && !Function.prototype.toString.call(read).includes("[native code]");
|
|
28
|
+
};
|
|
29
|
+
var credentialSlots = (...keys) => {
|
|
30
|
+
const credentials = navigator.credentials;
|
|
31
|
+
return credentials ? keys.map((key) => Object.getOwnPropertyDescriptor(credentials, key)?.value) : [];
|
|
32
|
+
};
|
|
33
|
+
var hasBitwardenOverride = () => [navigator.credentials?.get, ...credentialSlots("get", "_get")].some(
|
|
34
|
+
(method) => typeof method === "function" && method.name === "getWebAuthnCredential"
|
|
35
|
+
);
|
|
36
|
+
var probeBitwardenKeyframes = () => {
|
|
37
|
+
const root = document.documentElement;
|
|
38
|
+
const sheets = document.adoptedStyleSheets;
|
|
39
|
+
if (!root || typeof root.getAnimations !== "function" || typeof sheets?.push !== "function") {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (root.getAnimations().length > 0) return false;
|
|
43
|
+
const sheet = new CSSStyleSheet();
|
|
44
|
+
sheet.replaceSync(":where(:root){animation:bitwardenfill 1s 1000s paused}");
|
|
45
|
+
sheets.push(sheet);
|
|
46
|
+
try {
|
|
47
|
+
const found = root.getAnimations().filter((animation) => animation.animationName === "bitwardenfill");
|
|
48
|
+
found.forEach((animation) => animation.cancel());
|
|
49
|
+
return found.length > 0;
|
|
50
|
+
} finally {
|
|
51
|
+
const index = sheets.indexOf(sheet);
|
|
52
|
+
if (index >= 0) sheets.splice(index, 1);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var bitwardenKeyframes;
|
|
56
|
+
var hasBitwardenKeyframes = () => bitwardenKeyframes ??= probeBitwardenKeyframes();
|
|
57
|
+
var hasDashlaneInstanceTrap = () => {
|
|
58
|
+
const credential = typeof PublicKeyCredential === "undefined" ? void 0 : PublicKeyCredential;
|
|
59
|
+
if (typeof credential !== "function" || !Object.prototype.hasOwnProperty.call(credential, Symbol.hasInstance)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const result = credential[Symbol.hasInstance]({});
|
|
63
|
+
if (!(result instanceof Promise)) return false;
|
|
64
|
+
result.catch(() => void 0);
|
|
65
|
+
return true;
|
|
66
|
+
};
|
|
67
|
+
var nordPassObserverStack = () => {
|
|
68
|
+
const target = {
|
|
69
|
+
get nodeType() {
|
|
70
|
+
throw new Error("probe");
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return stackOf(() => MutationObserver.prototype.observe.call(null, target));
|
|
74
|
+
};
|
|
75
|
+
var hasNordPassObserverProxy = () => {
|
|
76
|
+
const sentinel = new Error("probe");
|
|
77
|
+
const read = [];
|
|
78
|
+
const target = {
|
|
79
|
+
get nodeType() {
|
|
80
|
+
read.push("nodeType");
|
|
81
|
+
return 1;
|
|
82
|
+
},
|
|
83
|
+
get ownerDocument() {
|
|
84
|
+
read.push("ownerDocument");
|
|
85
|
+
throw sentinel;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
MutationObserver.prototype.observe.call(null, target);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return error === sentinel && read.join() === "nodeType,ownerDocument";
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
};
|
|
95
|
+
var hasKeeperOverride = () => {
|
|
96
|
+
const methods = [
|
|
97
|
+
Element.prototype.attachShadow,
|
|
98
|
+
navigator.credentials?.get,
|
|
99
|
+
navigator.credentials?.create,
|
|
100
|
+
...credentialSlots("get", "_get", "create")
|
|
101
|
+
];
|
|
102
|
+
return methods.some((m) => typeof m === "function" && KEEPER_SOURCE.test(Function.prototype.toString.call(m)));
|
|
103
|
+
};
|
|
104
|
+
var hasLastPassRoot = () => {
|
|
105
|
+
let root = document;
|
|
106
|
+
while (root) {
|
|
107
|
+
if (root.querySelector("[data-lastpass-icon-root], [data-lastpass-root]")) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
root = root.activeElement?.shadowRoot;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
};
|
|
114
|
+
var hasProtonPassOverride = () => {
|
|
115
|
+
const credential = typeof PublicKeyCredential === "undefined" ? void 0 : PublicKeyCredential;
|
|
116
|
+
const methods = [
|
|
117
|
+
navigator.credentials?.get,
|
|
118
|
+
...credentialSlots("get", "_get"),
|
|
119
|
+
credential?.isConditionalMediationAvailable,
|
|
120
|
+
credential?._isConditionalMediationAvailable,
|
|
121
|
+
credential?.isUserVerifyingPlatformAuthenticatorAvailable,
|
|
122
|
+
credential?._isUserVerifyingPlatformAuthenticatorAvailable
|
|
123
|
+
];
|
|
124
|
+
return methods.some((m) => typeof m === "function" && PROTONPASS_SOURCE.test(Function.prototype.toString.call(m)));
|
|
125
|
+
};
|
|
126
|
+
var PROBES = [
|
|
127
|
+
{
|
|
128
|
+
name: "1password",
|
|
129
|
+
signal: "in-field button",
|
|
130
|
+
signedIn: true,
|
|
131
|
+
// Renders only for a signed-in account (locked or not), while a login field is focused.
|
|
132
|
+
test: () => document.querySelector("com-1password-button") !== null
|
|
133
|
+
},
|
|
134
|
+
{ name: "1password", signal: "credentials.get accessor", test: has1PasswordAccessor },
|
|
135
|
+
{
|
|
136
|
+
name: "1password",
|
|
137
|
+
signal: "Sentry debug id",
|
|
138
|
+
// Sentry debug-id keys are stack strings naming the injected script.
|
|
139
|
+
test: () => sentryDebugIdKeys().some((key) => key.includes(ONEPASSWORD_SCRIPT))
|
|
140
|
+
},
|
|
141
|
+
{ name: "bitwarden", signal: "passkey override", signedIn: true, test: hasBitwardenOverride },
|
|
142
|
+
{ name: "bitwarden", signal: "autofill keyframes", test: hasBitwardenKeyframes },
|
|
143
|
+
{
|
|
144
|
+
name: "dashlane",
|
|
145
|
+
signal: "injected script tag",
|
|
146
|
+
// Edge injects its page scripts as tags (https only); they are in the DOM before they execute.
|
|
147
|
+
test: () => document.querySelector('script[src*="-extension://"][src*="/dashlane-"][src$="-page-script.js"]') !== null
|
|
148
|
+
},
|
|
149
|
+
{ name: "dashlane", signal: "PublicKeyCredential hasInstance trap", test: hasDashlaneInstanceTrap },
|
|
150
|
+
{
|
|
151
|
+
name: "dashlane",
|
|
152
|
+
signal: "page script stack",
|
|
153
|
+
// The shadow-dom and top-layer page scripts (http too) wrap these methods; a null receiver throws inside them.
|
|
154
|
+
test: () => DASHLANE_STACK.test(attachShadowStack()) || typeof HTMLDialogElement !== "undefined" && DASHLANE_STACK.test(
|
|
155
|
+
stackOf(() => HTMLDialogElement.prototype.showModal.call(null))
|
|
156
|
+
)
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "kaspersky",
|
|
160
|
+
signal: "passkey proxy flag",
|
|
161
|
+
// Set in every https frame at document_start, with or without its desktop app.
|
|
162
|
+
test: () => window.wasKPMKeyPassProxyInited === true
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "kaspersky",
|
|
166
|
+
signal: "injected script tag",
|
|
167
|
+
// Firefox injects the proxy as a script tag, which is in the DOM before it runs.
|
|
168
|
+
test: () => KASPERSKY_SCRIPT.test(
|
|
169
|
+
document.querySelector('script[src*="/content-scripts/passkey_proxy/"]')?.src ?? ""
|
|
170
|
+
)
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "keeper",
|
|
174
|
+
signal: "attachShadow stack",
|
|
175
|
+
// Wrapped at document_start; a null receiver throws inside the wrapper before it notifies.
|
|
176
|
+
test: () => KEEPER_STACK.test(attachShadowStack())
|
|
177
|
+
},
|
|
178
|
+
{ name: "keeper", signal: "override source", test: hasKeeperOverride },
|
|
179
|
+
{
|
|
180
|
+
name: "keeper",
|
|
181
|
+
signal: "field lock",
|
|
182
|
+
// Tagged on every login field it decorates, signed in or not, shortly after load.
|
|
183
|
+
test: () => document.querySelector("[data-keeper-lock-id]") !== null
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "lastpass",
|
|
187
|
+
signal: "passkey override",
|
|
188
|
+
signedIn: true,
|
|
189
|
+
// Registered at document_start (https) while the vault is unlocked and its passkey support is enabled.
|
|
190
|
+
test: () => typeof window.__nativeCredentialsGet === "function"
|
|
191
|
+
},
|
|
192
|
+
// Otherwise nothing runs in the page until it decorates a login field, shortly after load.
|
|
193
|
+
{ name: "lastpass", signal: "icon root", test: hasLastPassRoot },
|
|
194
|
+
{ name: "nordpass", signal: "observer proxy stack", test: () => NORDPASS_STACK.test(nordPassObserverStack()) },
|
|
195
|
+
{ name: "nordpass", signal: "observer read order", test: hasNordPassObserverProxy },
|
|
196
|
+
{
|
|
197
|
+
name: "nordpass",
|
|
198
|
+
signal: "passkeys Sentry debug id",
|
|
199
|
+
test: () => sentryDebugIdKeys().some((key) => NORDPASS_PASSKEYS_KEY.test(key))
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: "nordpass",
|
|
203
|
+
signal: "Sentry debug id",
|
|
204
|
+
test: () => sentryDebugIdKeys().some((key) => NORDPASS_STACK.test(key))
|
|
205
|
+
},
|
|
206
|
+
{ name: "protonpass", signal: "WebAuthn override source", test: hasProtonPassOverride },
|
|
207
|
+
{
|
|
208
|
+
name: "protonpass",
|
|
209
|
+
signal: "field icon",
|
|
210
|
+
// Plain http, frames and later wrappers: its root and field-icon elements once its client script runs.
|
|
211
|
+
test: () => document.querySelector("[data-protonpass-role]") !== null
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
function probePasswordManagerExtensions() {
|
|
215
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
return PROBES.map(({ name, signal, signedIn, test }) => {
|
|
219
|
+
try {
|
|
220
|
+
return { name, signal, signedIn, matched: test() };
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return { name, signal, signedIn, matched: false, error };
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function detectPasswordManagerExtensions() {
|
|
227
|
+
const detected = /* @__PURE__ */ new Map();
|
|
228
|
+
for (const { name, signedIn, matched } of probePasswordManagerExtensions()) {
|
|
229
|
+
if (!matched) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const extension = detected.get(name) ?? { name };
|
|
233
|
+
if (signedIn) {
|
|
234
|
+
extension.signedIn = true;
|
|
235
|
+
}
|
|
236
|
+
detected.set(name, extension);
|
|
237
|
+
}
|
|
238
|
+
return [...detected.values()];
|
|
239
|
+
}
|
|
240
|
+
function readPasswordManagerExtensions(_meta) {
|
|
241
|
+
return detectPasswordManagerExtensions();
|
|
242
|
+
}
|
|
243
|
+
export {
|
|
244
|
+
detectPasswordManagerExtensions,
|
|
245
|
+
probePasswordManagerExtensions,
|
|
246
|
+
readPasswordManagerExtensions
|
|
247
|
+
};
|