@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,118 @@
1
+ // src/index.ts
2
+ import { CorbadoTracker } from "./client.mjs";
3
+ import { SDK_NAME, SDK_VERSION } from "./version.mjs";
4
+ export * from "./types.mjs";
5
+ export * from "./client.mjs";
6
+ export * from "./transport.mjs";
7
+ export * from "./queue.mjs";
8
+ export * from "./config.mjs";
9
+ export * from "./store/event-store.mjs";
10
+ export * from "./storage/index.mjs";
11
+ export * from "./logger.mjs";
12
+ export * from "./utils/operation-full.mjs";
13
+ export * from "./utils/key-operation-full.mjs";
14
+ export * from "./utils/serialize-raw-error.mjs";
15
+ export * from "./utils/trusted-device-operation-full.mjs";
16
+ export * from "./utils/operation-full-provide-identifier-with-cui.mjs";
17
+ export * from "./utils/provide-data-operation-full.mjs";
18
+ export * from "./utils/form-operation-full.mjs";
19
+ export * from "./utils/passkey-login-operation-full.mjs";
20
+ export * from "./utils/email-otp-operation-full.mjs";
21
+ export * from "./utils/sms-otp-operation-full.mjs";
22
+ export * from "./utils/totp-operation-full.mjs";
23
+ export * from "./utils/email-link-operation-full.mjs";
24
+ export * from "./utils/social-operation-login.mjs";
25
+ export * from "./utils/sso-operation-login.mjs";
26
+ export * from "./utils/passkey-enrollment-operation-full.mjs";
27
+ export * from "./utils/passkey-deletion-operation-full.mjs";
28
+ export * from "./utils/password-login-operation-full.mjs";
29
+ export * from "./utils/password-enrollment-operation-full.mjs";
30
+ export * from "./utils/appconfirmation-operation-full.mjs";
31
+ export * from "./utils/captcha-operation-full.mjs";
32
+ import {
33
+ detectPasswordManagerExtensions
34
+ } from "./readers/password-manager-extensions.mjs";
35
+ import {
36
+ READER_EXPOSURES
37
+ } from "./readers/meta.mjs";
38
+ import {
39
+ sanitizeCreationOptions,
40
+ sanitizeCreationResponse,
41
+ sanitizeRequestOptions,
42
+ sanitizeAssertionResponse
43
+ } from "./utils/webauthn-payloads.mjs";
44
+ var currentTracker;
45
+ function init(options) {
46
+ const tracker = new CorbadoTracker(options);
47
+ currentTracker = tracker;
48
+ return tracker;
49
+ }
50
+ function getTracker() {
51
+ return currentTracker;
52
+ }
53
+ function resetSession() {
54
+ return currentTracker?.resetSession();
55
+ }
56
+ function getSessionId() {
57
+ return currentTracker?.getSessionId();
58
+ }
59
+ function setUser(user) {
60
+ currentTracker?.setUser(user);
61
+ }
62
+ function setFlowTags(tags, options) {
63
+ currentTracker?.setFlowTags(tags, options);
64
+ }
65
+ function setExperiment(key, variant) {
66
+ currentTracker?.setExperiment(key, variant);
67
+ }
68
+ function setExperiments(assignments) {
69
+ currentTracker?.setExperiments(assignments);
70
+ }
71
+ function clearExperiment(key) {
72
+ currentTracker?.clearExperiment(key);
73
+ }
74
+ function clearExperiments() {
75
+ currentTracker?.clearExperiments();
76
+ }
77
+ function getExperiments() {
78
+ return currentTracker?.getExperiments() ?? {};
79
+ }
80
+ function telemetry(level, message) {
81
+ currentTracker?.telemetry(level, message);
82
+ }
83
+ function logInfo(message) {
84
+ currentTracker?.logInfo(message);
85
+ }
86
+ function logError(message) {
87
+ currentTracker?.logError(message);
88
+ }
89
+ async function destroy() {
90
+ if (!currentTracker) return;
91
+ await currentTracker.destroy();
92
+ currentTracker = void 0;
93
+ }
94
+ export {
95
+ READER_EXPOSURES,
96
+ SDK_NAME,
97
+ SDK_VERSION,
98
+ clearExperiment,
99
+ clearExperiments,
100
+ destroy,
101
+ detectPasswordManagerExtensions,
102
+ getExperiments,
103
+ getSessionId,
104
+ getTracker,
105
+ init,
106
+ logError,
107
+ logInfo,
108
+ resetSession,
109
+ sanitizeAssertionResponse,
110
+ sanitizeCreationOptions,
111
+ sanitizeCreationResponse,
112
+ sanitizeRequestOptions,
113
+ setExperiment,
114
+ setExperiments,
115
+ setFlowTags,
116
+ setUser,
117
+ telemetry
118
+ };
@@ -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,248 @@
1
+ // src/low-events/grouped-input-tracker.ts
2
+ import { InputChangeRecorder } from "./input-change-recorder.mjs";
3
+ import { InputPasteObserver } from "../readers/input-paste.mjs";
4
+ import { observeInputField, readFocusedField, readInputGroupValueLength } from "../readers/input.mjs";
5
+ import { listenWindow } from "../readers/page.mjs";
6
+ var LowEventGroupedInputTracker = class {
7
+ constructor(logger, fields, sink) {
8
+ this.logger = logger;
9
+ this.sink = sink;
10
+ this.inputs = [...new Set(fields)];
11
+ this.changes = new InputChangeRecorder(
12
+ this.length(),
13
+ (low) => sink.enqueueLowEvent(low),
14
+ logger,
15
+ sink.reserveCaptureSeq
16
+ );
17
+ this.paste = new InputPasteObserver(
18
+ {
19
+ key: "dom:input.paste",
20
+ title: "Paste into tracked input fields",
21
+ accessNote: "paste events on the input elements the integration passes to Observe; pasted values never leave the reader",
22
+ group: "Input interaction",
23
+ origin: "observe-external",
24
+ fields: [
25
+ {
26
+ path: "event.paste",
27
+ label: "Paste into the field",
28
+ exposure: "presence",
29
+ transform: { note: "trusted paste events only" }
30
+ },
31
+ {
32
+ path: "value",
33
+ label: "Length of the pasted text",
34
+ exposure: "size",
35
+ transform: { note: "inserted length only; values are compared in memory and discarded" }
36
+ }
37
+ ]
38
+ },
39
+ this.inputs,
40
+ logger,
41
+ (ts, _inserted, captureSeq) => {
42
+ this.cancelChange();
43
+ this.changes.flush();
44
+ this.changes.markInputObserved();
45
+ const oldLength = this.changes.previousLength;
46
+ this.changes.previousLength = this.length();
47
+ sink.enqueueLowEvent({
48
+ ...captureSeq === void 0 ? {} : { captureSeq },
49
+ lowType: "input-paste",
50
+ ts,
51
+ inputEffect: this.length() === 0 ? "clear" : oldLength === 0 ? "fill" : "modify"
52
+ });
53
+ },
54
+ sink.reserveCaptureSeq
55
+ );
56
+ this.unregisterInputs = sink.registerInputs?.(this.inputs, () => this.flushPending());
57
+ this.capturePrefill();
58
+ const guard = (_name, run) => this.safe(run);
59
+ for (const input of this.inputs) {
60
+ this.listeners.push(
61
+ observeInputField(
62
+ {
63
+ key: "dom:input.events",
64
+ title: "Interaction with tracked input fields",
65
+ accessNote: "focus, blur, pointerdown, pointerup, click, input and keyup events on the input elements the integration passes to Observe, reduced to booleans and identities",
66
+ group: "Input interaction",
67
+ origin: "observe-external",
68
+ fields: [
69
+ { path: "event.focus", label: "Field selected", exposure: "presence" },
70
+ {
71
+ path: "event.focus.relatedTarget",
72
+ label: "Whether focus moved between fields of the same group",
73
+ exposure: "computed",
74
+ transform: { note: "only whether focus moves within the group" }
75
+ },
76
+ { path: "event.blur", label: "Field left", exposure: "presence" },
77
+ { path: "event.pointerdown", label: "Pointer pressed on the field", exposure: "presence" },
78
+ { path: "event.pointerup", label: "Pointer released on the field", exposure: "presence" },
79
+ { path: "event.click", label: "Click on the field", exposure: "presence" },
80
+ { path: "event.input", label: "Typing in the field", exposure: "presence" },
81
+ {
82
+ path: "event.input.isComposing",
83
+ label: "Whether text is being composed with an input method",
84
+ exposure: "enum",
85
+ transform: { values: [true, false] }
86
+ },
87
+ {
88
+ path: "event.keyup.key",
89
+ label: "Escape key pressed",
90
+ exposure: "enum",
91
+ transform: { values: ["Escape", "other"] }
92
+ }
93
+ ]
94
+ },
95
+ input,
96
+ {
97
+ input: (composing, event) => {
98
+ if (this.paste.handlesInput(event)) return;
99
+ this.composing ||= composing;
100
+ if (this.changeTimer !== null) return;
101
+ this.changeTime = Date.now();
102
+ this.changeSeq = sink.reserveCaptureSeq?.();
103
+ this.changeTimer = setTimeout(() => this.safe(() => this.flushChanges()), 0);
104
+ },
105
+ focus: (relatedTarget) => {
106
+ if (this.contains(relatedTarget)) return;
107
+ this.emit("focus");
108
+ },
109
+ blur: (relatedTarget) => {
110
+ if (this.contains(relatedTarget)) return;
111
+ this.flushPending();
112
+ this.emit("blur");
113
+ },
114
+ click: () => this.emit("click"),
115
+ pointerdown: () => this.emit("pointerdown"),
116
+ pointerup: () => this.emit("pointerup"),
117
+ keyupEscape: () => this.emit("keyup-escape")
118
+ },
119
+ guard,
120
+ ["input", "focus", "blur", "click", "pointerdown", "pointerup", "keyup"]
121
+ )
122
+ );
123
+ }
124
+ if (typeof window !== "undefined")
125
+ this.listeners.push(
126
+ listenWindow(
127
+ {
128
+ key: "page:lifecycle.pagehide",
129
+ title: "Leaving the page",
130
+ accessNote: "pagehide event on window",
131
+ group: "Page lifecycle",
132
+ origin: "observe-external",
133
+ fields: [
134
+ {
135
+ path: "event.pagehide",
136
+ label: "Leaving the page",
137
+ exposure: "presence",
138
+ transform: { note: "flushes pending input low events on unload" }
139
+ }
140
+ ]
141
+ },
142
+ "pagehide",
143
+ (_event) => this.safe(() => {
144
+ this.flushPending();
145
+ sink.flushKeepalive();
146
+ })
147
+ )
148
+ );
149
+ if (typeof document !== "undefined" && readFocusedField(
150
+ {
151
+ key: "dom:input.focused",
152
+ title: "Whether a tracked input field has focus",
153
+ accessNote: "document.activeElement compared with the passed input elements",
154
+ group: "Input interaction",
155
+ origin: "observe-external",
156
+ fields: [
157
+ {
158
+ path: "focused",
159
+ label: "Whether the field is selected",
160
+ exposure: "enum",
161
+ transform: { values: [true, false] }
162
+ }
163
+ ]
164
+ },
165
+ this.inputs
166
+ )) {
167
+ this.emit("focus");
168
+ }
169
+ }
170
+ inputs;
171
+ changes;
172
+ paste;
173
+ unregisterInputs;
174
+ listeners = [];
175
+ changeTimer = null;
176
+ changeTime = 0;
177
+ changeSeq;
178
+ composing = false;
179
+ destroyed = false;
180
+ contains(target) {
181
+ return this.inputs.some((field) => field === target);
182
+ }
183
+ length() {
184
+ return readInputGroupValueLength(
185
+ {
186
+ key: "dom:input.content",
187
+ title: "Length of the text in tracked input fields",
188
+ accessNote: "input value length only; the value itself does not leave the reader",
189
+ group: "Input interaction",
190
+ origin: "observe-external",
191
+ fields: [
192
+ {
193
+ path: "value.length",
194
+ label: "Length of the entered text",
195
+ exposure: "size",
196
+ transform: { note: "summed over the fields of a group" }
197
+ }
198
+ ]
199
+ },
200
+ this.inputs
201
+ );
202
+ }
203
+ safe(action) {
204
+ try {
205
+ action();
206
+ } catch (error) {
207
+ this.logger.error("Failed to observe grouped input", error);
208
+ }
209
+ }
210
+ cancelChange() {
211
+ if (this.changeTimer !== null) clearTimeout(this.changeTimer);
212
+ this.changeTimer = null;
213
+ }
214
+ flushChanges() {
215
+ if (this.changeTimer === null) return;
216
+ this.cancelChange();
217
+ if (!this.paste.pending) this.changes.record(this.length(), this.changeTime, this.composing, this.changeSeq);
218
+ this.composing = false;
219
+ }
220
+ emit(lowType) {
221
+ this.flushChanges();
222
+ this.changes.flush();
223
+ this.sink.enqueueLowEvent({ lowType, ts: Date.now() });
224
+ }
225
+ capturePrefill(newCapture = false) {
226
+ if (this.destroyed) return;
227
+ this.flushPending();
228
+ this.changes.capturePrefill(this.length(), newCapture);
229
+ }
230
+ flushPending() {
231
+ this.paste.flush();
232
+ this.flushChanges();
233
+ this.changes.flush();
234
+ }
235
+ destroy() {
236
+ if (this.destroyed) return;
237
+ this.destroyed = true;
238
+ this.safe(() => {
239
+ this.flushPending();
240
+ this.paste.destroy();
241
+ this.unregisterInputs?.();
242
+ for (const remove of this.listeners) remove();
243
+ });
244
+ }
245
+ };
246
+ export {
247
+ LowEventGroupedInputTracker
248
+ };
@@ -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
+ };