@corbado/observe 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/client.mjs +1851 -0
- package/dist/analysis/config-manager.mjs +83 -0
- package/dist/analysis/config.mjs +216 -0
- package/dist/analysis/device-info-manager.mjs +153 -0
- package/dist/analysis/device.mjs +562 -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 +133 -0
- package/dist/analysis/low-events/input-change-recorder.mjs +200 -0
- package/dist/analysis/low-events/input-paste-observer.mjs +91 -0
- package/dist/analysis/low-events/input-tracker.mjs +223 -0
- package/dist/analysis/low-events/otp-input-binding.mjs +67 -0
- package/dist/analysis/low-events/shared-window-observer.mjs +225 -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 +887 -0
- package/dist/analysis/readers/device.mjs +73 -0
- package/dist/analysis/readers/meta.mjs +18 -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/storage/index.mjs +197 -0
- package/dist/analysis/storage/migration.mjs +56 -0
- package/dist/analysis/store/event-store.mjs +114 -0
- package/dist/analysis/transport.mjs +0 -0
- package/dist/analysis/transports/fetch.mjs +108 -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 +142 -0
- package/dist/analysis/utils/email-otp-operation-full.mjs +39 -0
- package/dist/analysis/utils/form-operation-full.mjs +81 -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 +32 -0
- package/dist/analysis/utils/passkey-login-operation-full.mjs +34 -0
- package/dist/analysis/utils/password-enrollment-operation-full.mjs +27 -0
- package/dist/analysis/utils/password-login-operation-full.mjs +61 -0
- package/dist/analysis/utils/password-manager-sampler.mjs +130 -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 +39 -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 +35 -0
- package/dist/analysis/utils/trusted-device-operation-full.mjs +100 -0
- package/dist/analysis/utils/web-lock.mjs +52 -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 +156 -4
- package/dist/index.d.ts +156 -4
- package/dist/index.js +4 -4
- package/dist/index.mjs +4 -4
- package/package.json +7 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/logger.ts
|
|
2
|
+
function createLogger(options) {
|
|
3
|
+
const debugEnabled = options.debug;
|
|
4
|
+
let telemetrySink;
|
|
5
|
+
const withPrefix = (level, message) => `[Corbado Observe SDK ${level}] ${message}`;
|
|
6
|
+
return {
|
|
7
|
+
reportTelemetry(level, message) {
|
|
8
|
+
if (!telemetrySink) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
telemetrySink(level, message);
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
setTelemetrySink(sink) {
|
|
17
|
+
telemetrySink = sink;
|
|
18
|
+
},
|
|
19
|
+
debug(message, ...args) {
|
|
20
|
+
if (!debugEnabled) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log(withPrefix("DEBUG", message), ...args);
|
|
24
|
+
},
|
|
25
|
+
info(message, ...args) {
|
|
26
|
+
if (!debugEnabled) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(withPrefix("INFO", message), ...args);
|
|
30
|
+
},
|
|
31
|
+
warn(message, ...args) {
|
|
32
|
+
if (!debugEnabled) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.warn(withPrefix("WARN", message), ...args);
|
|
36
|
+
},
|
|
37
|
+
error(message, ...args) {
|
|
38
|
+
if (!debugEnabled) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.error(withPrefix("ERROR", message), ...args);
|
|
42
|
+
},
|
|
43
|
+
critical(message, ...args) {
|
|
44
|
+
console.error(withPrefix("CRITICAL", message), ...args);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
createLogger
|
|
50
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/low-events/capture-sequence.ts
|
|
2
|
+
var captureSequence = 0;
|
|
3
|
+
function reserveCaptureSeq() {
|
|
4
|
+
if (captureSequence === 4294967295) {
|
|
5
|
+
throw new Error("Low-event capture sequence exhausted");
|
|
6
|
+
}
|
|
7
|
+
return ++captureSequence;
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
reserveCaptureSeq
|
|
11
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/low-events/ceremony-initiator.ts
|
|
2
|
+
function inferCeremonyInitiator(operation, spec) {
|
|
3
|
+
if (operation === "passkey-login") {
|
|
4
|
+
switch (spec) {
|
|
5
|
+
case "passkey-known-identifier":
|
|
6
|
+
case "passkey-no-identifier":
|
|
7
|
+
return "passkey-button";
|
|
8
|
+
case "passkey-known-identifier-auto":
|
|
9
|
+
case "passkey-no-identifier-auto":
|
|
10
|
+
case "passkey-immediate":
|
|
11
|
+
return "automatic";
|
|
12
|
+
}
|
|
13
|
+
} else if (operation === "passkey-enrollment") {
|
|
14
|
+
switch (spec) {
|
|
15
|
+
case "manual":
|
|
16
|
+
return "passkey-button";
|
|
17
|
+
case "auto":
|
|
18
|
+
case "conditional":
|
|
19
|
+
return "automatic";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
inferCeremonyInitiator
|
|
26
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/low-events/form-input-binding.ts
|
|
2
|
+
import { LowEventInputTracker } from "./input-tracker.mjs";
|
|
3
|
+
var FormInputBinding = class {
|
|
4
|
+
constructor(logger, sink) {
|
|
5
|
+
this.logger = logger;
|
|
6
|
+
this.sink = sink;
|
|
7
|
+
}
|
|
8
|
+
trackers = /* @__PURE__ */ new Map();
|
|
9
|
+
destroyed = false;
|
|
10
|
+
/** Bind exactly these fields; an empty list detaches every field. */
|
|
11
|
+
setFields(fields) {
|
|
12
|
+
if (this.destroyed) return;
|
|
13
|
+
const next = new Set(fields);
|
|
14
|
+
for (const [field, tracker] of this.trackers) {
|
|
15
|
+
if (next.has(field)) continue;
|
|
16
|
+
this.safe(() => tracker.destroy());
|
|
17
|
+
this.trackers.delete(field);
|
|
18
|
+
}
|
|
19
|
+
for (const field of next) {
|
|
20
|
+
if (this.trackers.has(field)) continue;
|
|
21
|
+
this.safe(() => this.trackers.set(field, new LowEventInputTracker(this.logger, field, this.sink())));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
capturePrefill(newCapture = false) {
|
|
25
|
+
for (const tracker of this.trackers.values()) this.safe(() => tracker.capturePrefill(newCapture));
|
|
26
|
+
}
|
|
27
|
+
flushPending() {
|
|
28
|
+
for (const tracker of this.trackers.values()) this.safe(() => tracker.flushPending());
|
|
29
|
+
}
|
|
30
|
+
destroy() {
|
|
31
|
+
if (this.destroyed) return;
|
|
32
|
+
this.destroyed = true;
|
|
33
|
+
for (const tracker of this.trackers.values()) this.safe(() => tracker.destroy());
|
|
34
|
+
this.trackers.clear();
|
|
35
|
+
}
|
|
36
|
+
safe(run) {
|
|
37
|
+
try {
|
|
38
|
+
run();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
this.logger.error("Form input binding failed", error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export {
|
|
45
|
+
FormInputBinding
|
|
46
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// src/low-events/grouped-input-tracker.ts
|
|
2
|
+
import { InputChangeRecorder } from "./input-change-recorder.mjs";
|
|
3
|
+
import { InputPasteObserver } from "./input-paste-observer.mjs";
|
|
4
|
+
var LowEventGroupedInputTracker = class {
|
|
5
|
+
constructor(logger, fields, sink) {
|
|
6
|
+
this.logger = logger;
|
|
7
|
+
this.sink = sink;
|
|
8
|
+
this.inputs = [...new Set(fields)];
|
|
9
|
+
this.changes = new InputChangeRecorder(
|
|
10
|
+
this.length(),
|
|
11
|
+
(low) => sink.enqueueLowEvent(low),
|
|
12
|
+
logger,
|
|
13
|
+
sink.reserveCaptureSeq
|
|
14
|
+
);
|
|
15
|
+
this.paste = new InputPasteObserver(
|
|
16
|
+
this.inputs,
|
|
17
|
+
logger,
|
|
18
|
+
(ts, _inserted, captureSeq) => {
|
|
19
|
+
this.cancelChange();
|
|
20
|
+
this.changes.flush();
|
|
21
|
+
this.changes.markInputObserved();
|
|
22
|
+
const oldLength = this.changes.previousLength;
|
|
23
|
+
this.changes.previousLength = this.length();
|
|
24
|
+
sink.enqueueLowEvent({
|
|
25
|
+
...captureSeq === void 0 ? {} : { captureSeq },
|
|
26
|
+
lowType: "input-paste",
|
|
27
|
+
ts,
|
|
28
|
+
inputEffect: this.length() === 0 ? "clear" : oldLength === 0 ? "fill" : "modify"
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
sink.reserveCaptureSeq
|
|
32
|
+
);
|
|
33
|
+
this.unregisterInputs = sink.registerInputs?.(this.inputs, () => this.flushPending());
|
|
34
|
+
this.capturePrefill();
|
|
35
|
+
for (const input of this.inputs) {
|
|
36
|
+
this.listen(input, "input", (event) => {
|
|
37
|
+
if (this.paste.handlesInput(event)) return;
|
|
38
|
+
this.composing ||= event.isComposing || event.inputType === "insertCompositionText";
|
|
39
|
+
if (this.changeTimer !== null) return;
|
|
40
|
+
this.changeTime = Date.now();
|
|
41
|
+
this.changeSeq = sink.reserveCaptureSeq?.();
|
|
42
|
+
this.changeTimer = setTimeout(() => this.safe(() => this.flushChanges()), 0);
|
|
43
|
+
});
|
|
44
|
+
this.listen(input, "focus", (event) => {
|
|
45
|
+
if (this.contains(event.relatedTarget)) return;
|
|
46
|
+
this.emit("focus");
|
|
47
|
+
});
|
|
48
|
+
this.listen(input, "blur", (event) => {
|
|
49
|
+
if (this.contains(event.relatedTarget)) return;
|
|
50
|
+
this.flushPending();
|
|
51
|
+
this.emit("blur");
|
|
52
|
+
});
|
|
53
|
+
for (const type of ["click", "pointerdown", "pointerup"]) this.listen(input, type, () => this.emit(type));
|
|
54
|
+
this.listen(input, "keyup", (event) => {
|
|
55
|
+
if (event.key === "Escape") this.emit("keyup-escape");
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (typeof window !== "undefined")
|
|
59
|
+
this.listen(window, "pagehide", () => {
|
|
60
|
+
this.flushPending();
|
|
61
|
+
sink.flushKeepalive();
|
|
62
|
+
});
|
|
63
|
+
if (typeof document !== "undefined" && this.contains(document.activeElement)) {
|
|
64
|
+
this.emit("focus");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
inputs;
|
|
68
|
+
changes;
|
|
69
|
+
paste;
|
|
70
|
+
unregisterInputs;
|
|
71
|
+
listeners = [];
|
|
72
|
+
changeTimer = null;
|
|
73
|
+
changeTime = 0;
|
|
74
|
+
changeSeq;
|
|
75
|
+
composing = false;
|
|
76
|
+
destroyed = false;
|
|
77
|
+
contains(target) {
|
|
78
|
+
return this.inputs.some((field) => field === target);
|
|
79
|
+
}
|
|
80
|
+
length() {
|
|
81
|
+
return this.inputs.reduce((total, field) => total + field.value.length, 0);
|
|
82
|
+
}
|
|
83
|
+
safe(action) {
|
|
84
|
+
try {
|
|
85
|
+
action();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.logger.error("Failed to observe grouped input", error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
listen(target, type, handler) {
|
|
91
|
+
const wrapped = (event) => this.safe(() => handler(event));
|
|
92
|
+
target.addEventListener(type, wrapped);
|
|
93
|
+
this.listeners.push(() => target.removeEventListener(type, wrapped));
|
|
94
|
+
}
|
|
95
|
+
cancelChange() {
|
|
96
|
+
if (this.changeTimer !== null) clearTimeout(this.changeTimer);
|
|
97
|
+
this.changeTimer = null;
|
|
98
|
+
}
|
|
99
|
+
flushChanges() {
|
|
100
|
+
if (this.changeTimer === null) return;
|
|
101
|
+
this.cancelChange();
|
|
102
|
+
if (!this.paste.pending) this.changes.record(this.length(), this.changeTime, this.composing, this.changeSeq);
|
|
103
|
+
this.composing = false;
|
|
104
|
+
}
|
|
105
|
+
emit(lowType) {
|
|
106
|
+
this.flushChanges();
|
|
107
|
+
this.changes.flush();
|
|
108
|
+
this.sink.enqueueLowEvent({ lowType, ts: Date.now() });
|
|
109
|
+
}
|
|
110
|
+
capturePrefill(newCapture = false) {
|
|
111
|
+
if (this.destroyed) return;
|
|
112
|
+
this.flushPending();
|
|
113
|
+
this.changes.capturePrefill(this.length(), newCapture);
|
|
114
|
+
}
|
|
115
|
+
flushPending() {
|
|
116
|
+
this.paste.flush();
|
|
117
|
+
this.flushChanges();
|
|
118
|
+
this.changes.flush();
|
|
119
|
+
}
|
|
120
|
+
destroy() {
|
|
121
|
+
if (this.destroyed) return;
|
|
122
|
+
this.destroyed = true;
|
|
123
|
+
this.safe(() => {
|
|
124
|
+
this.flushPending();
|
|
125
|
+
this.paste.destroy();
|
|
126
|
+
this.unregisterInputs?.();
|
|
127
|
+
for (const remove of this.listeners) remove();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
export {
|
|
132
|
+
LowEventGroupedInputTracker
|
|
133
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// src/low-events/input-change-recorder.ts
|
|
2
|
+
var INPUT_IDLE_MS = 3e3;
|
|
3
|
+
var BIG_INPUT_DELTA_THRESHOLD = 3;
|
|
4
|
+
var AUTOFILL_STREAK_LENGTH = 5;
|
|
5
|
+
var AUTOFILL_FAST_INTERVAL_MS = 80;
|
|
6
|
+
var AUTOFILL_UNIFORM_SPREAD_MS = 25;
|
|
7
|
+
var InputChangeRecorder = class {
|
|
8
|
+
constructor(previousLength, pushLow, logger, reserveCaptureSeq) {
|
|
9
|
+
this.previousLength = previousLength;
|
|
10
|
+
this.pushLow = pushLow;
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
this.reserveCaptureSeq = reserveCaptureSeq;
|
|
13
|
+
}
|
|
14
|
+
prefillSettled = false;
|
|
15
|
+
inputBatch = null;
|
|
16
|
+
currentEffect = "modify";
|
|
17
|
+
ringEffect = "modify";
|
|
18
|
+
autofillTimestamps = [];
|
|
19
|
+
autofillStreak = null;
|
|
20
|
+
idleTimer = null;
|
|
21
|
+
markInputObserved() {
|
|
22
|
+
this.prefillSettled = true;
|
|
23
|
+
}
|
|
24
|
+
capturePrefill(length, newCapture = false) {
|
|
25
|
+
if (newCapture) this.prefillSettled = false;
|
|
26
|
+
if (this.prefillSettled) return;
|
|
27
|
+
this.previousLength = length;
|
|
28
|
+
if (length === 0) return;
|
|
29
|
+
this.prefillSettled = true;
|
|
30
|
+
this.pushLow({ lowType: "input-prefill", ts: Date.now(), initiator: "unknown" });
|
|
31
|
+
}
|
|
32
|
+
flush() {
|
|
33
|
+
this.cancelIdle();
|
|
34
|
+
this.flushInputBatch();
|
|
35
|
+
this.flushAutofillStreak();
|
|
36
|
+
}
|
|
37
|
+
cancelIdle() {
|
|
38
|
+
if (this.idleTimer !== null) clearTimeout(this.idleTimer);
|
|
39
|
+
this.idleTimer = null;
|
|
40
|
+
}
|
|
41
|
+
scheduleIdle() {
|
|
42
|
+
this.cancelIdle();
|
|
43
|
+
this.idleTimer = setTimeout(() => {
|
|
44
|
+
this.idleTimer = null;
|
|
45
|
+
try {
|
|
46
|
+
this.flushInputBatch(true);
|
|
47
|
+
this.flushAutofillStreak();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this.logger.error("Failed to flush idle input", error);
|
|
50
|
+
}
|
|
51
|
+
}, INPUT_IDLE_MS);
|
|
52
|
+
}
|
|
53
|
+
extendInputBatch(timestamp, effect = this.currentEffect, captureSeq) {
|
|
54
|
+
const batch = this.inputBatch;
|
|
55
|
+
if (!batch) {
|
|
56
|
+
this.inputBatch = { firstTimestamp: timestamp, lastTimestamp: timestamp, effect, captureSeq };
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
batch.lastTimestamp = timestamp;
|
|
60
|
+
}
|
|
61
|
+
drainAutofillRingToBatch() {
|
|
62
|
+
for (const ts of this.autofillTimestamps) {
|
|
63
|
+
this.extendInputBatch(ts.ts, this.ringEffect, ts.captureSeq);
|
|
64
|
+
}
|
|
65
|
+
this.autofillTimestamps.length = 0;
|
|
66
|
+
}
|
|
67
|
+
flushInputBatch(idle = false) {
|
|
68
|
+
this.drainAutofillRingToBatch();
|
|
69
|
+
const batch = this.inputBatch;
|
|
70
|
+
if (!batch) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.pushLow({
|
|
74
|
+
...batch.captureSeq === void 0 ? {} : { captureSeq: batch.captureSeq },
|
|
75
|
+
lowType: idle ? "input-idle" : "input",
|
|
76
|
+
ts: batch.firstTimestamp,
|
|
77
|
+
durationMs: batch.lastTimestamp - batch.firstTimestamp,
|
|
78
|
+
inputEffect: batch.effect
|
|
79
|
+
});
|
|
80
|
+
this.inputBatch = null;
|
|
81
|
+
}
|
|
82
|
+
flushAutofillStreak() {
|
|
83
|
+
const streak = this.autofillStreak;
|
|
84
|
+
if (!streak) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.pushLow({
|
|
88
|
+
...streak.captureSeq === void 0 ? {} : { captureSeq: streak.captureSeq },
|
|
89
|
+
lowType: "big-input-add",
|
|
90
|
+
ts: streak.firstTimestamp,
|
|
91
|
+
durationMs: streak.lastTimestamp - streak.firstTimestamp,
|
|
92
|
+
inputEffect: streak.effect
|
|
93
|
+
});
|
|
94
|
+
this.autofillStreak = null;
|
|
95
|
+
}
|
|
96
|
+
detectAutofillStreak() {
|
|
97
|
+
const ring = this.autofillTimestamps;
|
|
98
|
+
if (ring.length < AUTOFILL_STREAK_LENGTH) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
let minInterval = Infinity;
|
|
102
|
+
let maxInterval = -Infinity;
|
|
103
|
+
for (let i = 1; i < ring.length; i++) {
|
|
104
|
+
const interval = ring[i].ts - ring[i - 1].ts;
|
|
105
|
+
if (interval > AUTOFILL_FAST_INTERVAL_MS) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (interval < minInterval) {
|
|
109
|
+
minInterval = interval;
|
|
110
|
+
}
|
|
111
|
+
if (interval > maxInterval) {
|
|
112
|
+
maxInterval = interval;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return maxInterval - minInterval <= AUTOFILL_UNIFORM_SPREAD_MS;
|
|
116
|
+
}
|
|
117
|
+
record(newLength, timestamp = Date.now(), composing = false, captureSeq = this.reserveCaptureSeq?.()) {
|
|
118
|
+
this.markInputObserved();
|
|
119
|
+
this.scheduleIdle();
|
|
120
|
+
const oldLength = this.previousLength;
|
|
121
|
+
this.currentEffect = newLength === 0 ? "clear" : oldLength === 0 ? "fill" : "modify";
|
|
122
|
+
const delta = newLength - oldLength;
|
|
123
|
+
this.previousLength = newLength;
|
|
124
|
+
if (newLength === 0 && oldLength > 0) {
|
|
125
|
+
this.flush();
|
|
126
|
+
this.pushLow({
|
|
127
|
+
...captureSeq === void 0 ? {} : { captureSeq },
|
|
128
|
+
lowType: "input-clear",
|
|
129
|
+
ts: timestamp,
|
|
130
|
+
inputEffect: "clear"
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (composing) {
|
|
135
|
+
this.flushAutofillStreak();
|
|
136
|
+
this.drainAutofillRingToBatch();
|
|
137
|
+
this.extendInputBatch(timestamp, this.currentEffect, captureSeq);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (delta >= BIG_INPUT_DELTA_THRESHOLD) {
|
|
141
|
+
this.flushInputBatch();
|
|
142
|
+
this.flushAutofillStreak();
|
|
143
|
+
this.pushLow({
|
|
144
|
+
...captureSeq === void 0 ? {} : { captureSeq },
|
|
145
|
+
lowType: "big-input-add",
|
|
146
|
+
ts: timestamp,
|
|
147
|
+
inputEffect: this.currentEffect
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (delta <= -BIG_INPUT_DELTA_THRESHOLD) {
|
|
152
|
+
this.flushInputBatch();
|
|
153
|
+
this.flushAutofillStreak();
|
|
154
|
+
this.pushLow({
|
|
155
|
+
...captureSeq === void 0 ? {} : { captureSeq },
|
|
156
|
+
lowType: "big-input-rem",
|
|
157
|
+
ts: timestamp,
|
|
158
|
+
inputEffect: this.currentEffect
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const activeStreak = this.autofillStreak;
|
|
163
|
+
if (activeStreak) {
|
|
164
|
+
if (delta === 1 && timestamp - activeStreak.lastTimestamp <= AUTOFILL_FAST_INTERVAL_MS) {
|
|
165
|
+
activeStreak.lastTimestamp = timestamp;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.flushAutofillStreak();
|
|
169
|
+
}
|
|
170
|
+
if (delta !== 1) {
|
|
171
|
+
this.drainAutofillRingToBatch();
|
|
172
|
+
this.extendInputBatch(timestamp, this.currentEffect, captureSeq);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const ring = this.autofillTimestamps;
|
|
176
|
+
if (ring.length === 0) this.ringEffect = this.currentEffect;
|
|
177
|
+
ring.push({ ts: timestamp, captureSeq });
|
|
178
|
+
if (ring.length > AUTOFILL_STREAK_LENGTH) {
|
|
179
|
+
const graduated = ring.shift();
|
|
180
|
+
this.extendInputBatch(graduated.ts, this.ringEffect, graduated.captureSeq);
|
|
181
|
+
this.ringEffect = "modify";
|
|
182
|
+
}
|
|
183
|
+
if (!this.detectAutofillStreak()) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const streakStart = ring[0];
|
|
187
|
+
const streakLast = ring[ring.length - 1];
|
|
188
|
+
ring.length = 0;
|
|
189
|
+
this.flushInputBatch();
|
|
190
|
+
this.autofillStreak = {
|
|
191
|
+
firstTimestamp: streakStart.ts,
|
|
192
|
+
captureSeq: streakStart.captureSeq,
|
|
193
|
+
lastTimestamp: streakLast.ts,
|
|
194
|
+
effect: this.ringEffect
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
export {
|
|
199
|
+
InputChangeRecorder
|
|
200
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/low-events/input-paste-observer.ts
|
|
2
|
+
var InputPasteObserver = class {
|
|
3
|
+
constructor(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
|
+
};
|