@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,302 @@
1
+ // src/low-events/input-tracker.ts
2
+ import { InputChangeRecorder } from "./input-change-recorder.mjs";
3
+ import { InputPasteObserver } from "../readers/input-paste.mjs";
4
+ import { observeInputField, readFocusedField, readInputContentLength } from "../readers/input.mjs";
5
+ import { listenWindow } from "../readers/page.mjs";
6
+ import { readPasswordManagerDomSignals } from "../readers/password-manager-dom.mjs";
7
+ var MOUNT_PROBE_DELAY_MS = 500;
8
+ var FOCUS_PROBE_DELAY_MS = 250;
9
+ function fieldContentLength(field) {
10
+ return readInputContentLength(
11
+ {
12
+ key: "dom:input.content",
13
+ title: "Length of the text in tracked input fields",
14
+ accessNote: "input value length only; the value itself does not leave the reader",
15
+ group: "Input interaction",
16
+ origin: "observe-external",
17
+ fields: [
18
+ { path: "value.length", label: "Length of the entered text", exposure: "size" },
19
+ {
20
+ path: "checked",
21
+ label: "Whether a checkbox or option is selected",
22
+ exposure: "enum",
23
+ transform: { values: [true, false], note: "checkbox and radio only" }
24
+ }
25
+ ]
26
+ },
27
+ field
28
+ );
29
+ }
30
+ var LowEventInputTracker = class {
31
+ constructor(logger, inputField, sink) {
32
+ this.logger = logger;
33
+ this.inputField = inputField;
34
+ this.sink = sink;
35
+ this.changes = new InputChangeRecorder(
36
+ fieldContentLength(inputField),
37
+ this.pushLow,
38
+ logger,
39
+ sink.reserveCaptureSeq
40
+ );
41
+ this.pasteObserver = new InputPasteObserver(
42
+ {
43
+ key: "dom:input.paste",
44
+ title: "Paste into tracked input fields",
45
+ accessNote: "paste events on the input elements the integration passes to Observe; pasted values never leave the reader",
46
+ group: "Input interaction",
47
+ origin: "observe-external",
48
+ fields: [
49
+ {
50
+ path: "event.paste",
51
+ label: "Paste into the field",
52
+ exposure: "presence",
53
+ transform: { note: "trusted paste events only" }
54
+ },
55
+ {
56
+ path: "value",
57
+ label: "Length of the pasted text",
58
+ exposure: "size",
59
+ transform: { note: "inserted length only; values are compared in memory and discarded" }
60
+ }
61
+ ]
62
+ },
63
+ [inputField],
64
+ logger,
65
+ (ts, _inserted, captureSeq) => {
66
+ this.changes.flush();
67
+ this.changes.markInputObserved();
68
+ const oldLength = this.changes.previousLength;
69
+ this.changes.previousLength = fieldContentLength(inputField);
70
+ this.pushLow({
71
+ ...captureSeq === void 0 ? {} : { captureSeq },
72
+ lowType: "input-paste",
73
+ ts,
74
+ inputEffect: fieldContentLength(inputField) === 0 ? "clear" : oldLength === 0 ? "fill" : "modify"
75
+ });
76
+ },
77
+ sink.reserveCaptureSeq
78
+ );
79
+ this.unregisterInputs = sink.registerInputs?.([inputField], () => this.flushPending());
80
+ this.capturePrefill();
81
+ this.attach();
82
+ }
83
+ unregisterInputs;
84
+ removeFieldListeners;
85
+ removePageHide;
86
+ changes;
87
+ pasteObserver;
88
+ emittedSignals = /* @__PURE__ */ new Set();
89
+ firstFocusHandled = false;
90
+ mountProbeTimer = null;
91
+ focusProbeTimer = null;
92
+ destroyed = false;
93
+ destroy() {
94
+ if (this.destroyed) {
95
+ return;
96
+ }
97
+ this.destroyed = true;
98
+ try {
99
+ if (this.mountProbeTimer !== null) {
100
+ clearTimeout(this.mountProbeTimer);
101
+ this.mountProbeTimer = null;
102
+ }
103
+ if (this.focusProbeTimer !== null) {
104
+ clearTimeout(this.focusProbeTimer);
105
+ this.focusProbeTimer = null;
106
+ }
107
+ this.changes.flush();
108
+ this.unregisterInputs?.();
109
+ this.pasteObserver.destroy();
110
+ this.detach();
111
+ } catch (error) {
112
+ this.logger.error("Failed to destroy LowEventInputTracker", error);
113
+ }
114
+ }
115
+ attach() {
116
+ const input = this.inputField;
117
+ this.removeFieldListeners = observeInputField(
118
+ {
119
+ key: "dom:input.events",
120
+ title: "Interaction with tracked input fields",
121
+ accessNote: "focus, blur, pointerdown, pointerup, click, input and keyup events on the input elements the integration passes to Observe, reduced to booleans and identities",
122
+ group: "Input interaction",
123
+ origin: "observe-external",
124
+ fields: [
125
+ { path: "event.focus", label: "Field selected", exposure: "presence" },
126
+ { path: "event.blur", label: "Field left", exposure: "presence" },
127
+ { path: "event.pointerdown", label: "Pointer pressed on the field", exposure: "presence" },
128
+ { path: "event.pointerup", label: "Pointer released on the field", exposure: "presence" },
129
+ { path: "event.click", label: "Click on the field", exposure: "presence" },
130
+ { path: "event.input", label: "Typing in the field", exposure: "presence" },
131
+ {
132
+ path: "event.input.isComposing",
133
+ label: "Whether text is being composed with an input method",
134
+ exposure: "enum",
135
+ transform: { values: [true, false] }
136
+ },
137
+ {
138
+ path: "event.keyup.key",
139
+ label: "Escape key pressed",
140
+ exposure: "enum",
141
+ transform: { values: ["Escape", "other"] }
142
+ }
143
+ ]
144
+ },
145
+ input,
146
+ {
147
+ focus: () => this.handleFocus(),
148
+ blur: () => this.handleBlur(),
149
+ pointerdown: () => this.enqueueNonInputEvent("pointerdown"),
150
+ pointerup: () => this.enqueueNonInputEvent("pointerup"),
151
+ click: () => this.enqueueNonInputEvent("click"),
152
+ input: (composing, event) => this.handleInput(event, composing),
153
+ keyupEscape: () => this.enqueueNonInputEvent("keyup-escape")
154
+ },
155
+ (name, run) => {
156
+ try {
157
+ run();
158
+ } catch (error) {
159
+ if (name === "input") this.logger.error("Failed to observe input", error);
160
+ else this.logger.error(`Failed to handle low event "${name}"`, error);
161
+ }
162
+ }
163
+ );
164
+ if (typeof window !== "undefined") {
165
+ this.removePageHide = listenWindow(
166
+ {
167
+ key: "page:lifecycle.pagehide",
168
+ title: "Leaving the page",
169
+ accessNote: "pagehide event on window",
170
+ group: "Page lifecycle",
171
+ origin: "observe-external",
172
+ fields: [
173
+ {
174
+ path: "event.pagehide",
175
+ label: "Leaving the page",
176
+ exposure: "presence",
177
+ transform: { note: "flushes pending input low events on unload" }
178
+ }
179
+ ]
180
+ },
181
+ "pagehide",
182
+ this.wrapHandler("pagehide", () => this.flushForTeardown())
183
+ );
184
+ }
185
+ if (typeof document !== "undefined") {
186
+ if (readFocusedField(
187
+ {
188
+ key: "dom:input.focused",
189
+ title: "Whether a tracked input field has focus",
190
+ accessNote: "document.activeElement compared with the passed input elements",
191
+ group: "Input interaction",
192
+ origin: "observe-external",
193
+ fields: [
194
+ {
195
+ path: "focused",
196
+ label: "Whether the field is selected",
197
+ exposure: "enum",
198
+ transform: { values: [true, false] }
199
+ }
200
+ ]
201
+ },
202
+ [input]
203
+ )) {
204
+ this.handleFocus();
205
+ }
206
+ }
207
+ if (typeof window !== "undefined") {
208
+ this.mountProbeTimer = setTimeout(() => this.runEnvProbe(), MOUNT_PROBE_DELAY_MS);
209
+ }
210
+ }
211
+ detach() {
212
+ this.removeFieldListeners?.();
213
+ if (typeof window !== "undefined") {
214
+ this.removePageHide?.();
215
+ }
216
+ }
217
+ pushLow = (low) => {
218
+ this.sink.enqueueLowEvent(low);
219
+ };
220
+ runEnvProbe() {
221
+ try {
222
+ if (typeof __CBO_DATA_PMD__ === "boolean" ? __CBO_DATA_PMD__ : this.sink.passwordManagerDomScan?.() !== false) {
223
+ const matches = readPasswordManagerDomSignals({
224
+ key: "dom:password-managers.dom-signals",
225
+ title: "Password manager traces in the page",
226
+ accessNote: "DOM markers that password manager extensions insert (attributes, elements, styles)",
227
+ group: "Password manager detection",
228
+ origin: "observe-external",
229
+ fields: [
230
+ {
231
+ path: "signals[]",
232
+ label: "Password manager traces found",
233
+ exposure: "enum",
234
+ transform: { values: ["pa-1", "pa-2", "pa-3", "pa-4", "pa-5", "pa-6"] }
235
+ }
236
+ ],
237
+ dataConfig: "pmd"
238
+ });
239
+ const now = Date.now();
240
+ for (const eventType of matches) {
241
+ if (this.emittedSignals.has(eventType)) {
242
+ continue;
243
+ }
244
+ this.emittedSignals.add(eventType);
245
+ this.pushLow({ lowType: eventType, ts: now });
246
+ }
247
+ }
248
+ } catch (error) {
249
+ this.logger.error("Failed to run input env probe", error);
250
+ }
251
+ }
252
+ scheduleFocusProbe() {
253
+ if (this.firstFocusHandled) {
254
+ return;
255
+ }
256
+ this.firstFocusHandled = true;
257
+ this.focusProbeTimer = setTimeout(() => this.runEnvProbe(), FOCUS_PROBE_DELAY_MS);
258
+ }
259
+ capturePrefill(newCapture = false) {
260
+ if (this.destroyed) return;
261
+ this.flushPending();
262
+ this.changes.capturePrefill(fieldContentLength(this.inputField), newCapture);
263
+ }
264
+ flushPending() {
265
+ this.pasteObserver.flush();
266
+ this.changes.flush();
267
+ }
268
+ enqueueNonInputEvent(eventType) {
269
+ this.changes.flush();
270
+ this.pushLow({ lowType: eventType, ts: Date.now() });
271
+ }
272
+ handleFocus() {
273
+ this.scheduleFocusProbe();
274
+ this.enqueueNonInputEvent("focus");
275
+ }
276
+ handleBlur() {
277
+ this.pasteObserver.flush();
278
+ this.enqueueNonInputEvent("blur");
279
+ }
280
+ handleInput(event, composing) {
281
+ if (this.pasteObserver.handlesInput(event)) return;
282
+ this.changes.record(fieldContentLength(this.inputField), Date.now(), composing);
283
+ }
284
+ flushForTeardown() {
285
+ this.pasteObserver.flush();
286
+ this.changes.flush();
287
+ this.sink.flushKeepalive();
288
+ }
289
+ wrapHandler(name, fn) {
290
+ return () => {
291
+ try {
292
+ fn();
293
+ } catch (error) {
294
+ this.logger.error(`Failed to handle low event "${name}"`, error);
295
+ }
296
+ };
297
+ }
298
+ };
299
+ export {
300
+ LowEventInputTracker,
301
+ fieldContentLength
302
+ };
@@ -0,0 +1,67 @@
1
+ // src/low-events/otp-input-binding.ts
2
+ import { LowEventInputTracker } from "./input-tracker.mjs";
3
+ import { LowEventGroupedInputTracker } from "./grouped-input-tracker.mjs";
4
+ var OTPInputBinding = class {
5
+ constructor(logger, inputSink, groupSink) {
6
+ this.logger = logger;
7
+ this.inputSink = inputSink;
8
+ this.groupSink = groupSink;
9
+ }
10
+ collector;
11
+ fields = [];
12
+ grouped = false;
13
+ destroyed = false;
14
+ prefillClaimed = false;
15
+ setFields(fields, grouped = true) {
16
+ try {
17
+ if (this.destroyed) return;
18
+ const next = [...new Set(fields)];
19
+ if (grouped === this.grouped && next.length === this.fields.length && next.every((field, i) => field === this.fields[i]))
20
+ return;
21
+ this.collector?.destroy();
22
+ this.collector = void 0;
23
+ this.fields = [];
24
+ this.grouped = grouped;
25
+ if (next.length === 0) return;
26
+ const sink = grouped ? this.groupSink : this.inputSink;
27
+ const filteredSink = { ...sink, enqueueLowEvent: (low) => this.enqueue(sink, low) };
28
+ this.collector = grouped ? new LowEventGroupedInputTracker(this.logger, next, filteredSink) : new LowEventInputTracker(this.logger, next[0], filteredSink);
29
+ this.fields = next;
30
+ } catch (error) {
31
+ this.logger.error("Failed to bind OTP inputs", error);
32
+ }
33
+ }
34
+ enqueue(sink, low) {
35
+ if (low.lowType === "input-prefill") {
36
+ if (this.prefillClaimed) return;
37
+ this.prefillClaimed = true;
38
+ } else if ([
39
+ "input",
40
+ "input-idle",
41
+ "input-clear",
42
+ "input-paste",
43
+ "big-input-paste",
44
+ "big-input-add",
45
+ "big-input-rem"
46
+ ].includes(low.lowType)) {
47
+ this.prefillClaimed = true;
48
+ }
49
+ sink.enqueueLowEvent(low);
50
+ }
51
+ capturePrefill(newCapture = false) {
52
+ if (newCapture) this.prefillClaimed = false;
53
+ this.collector?.capturePrefill(newCapture);
54
+ }
55
+ flushPending() {
56
+ this.collector?.flushPending();
57
+ }
58
+ destroy() {
59
+ this.destroyed = true;
60
+ this.collector?.destroy();
61
+ this.collector = void 0;
62
+ this.fields = [];
63
+ }
64
+ };
65
+ export {
66
+ OTPInputBinding
67
+ };
@@ -0,0 +1,296 @@
1
+ // src/low-events/shared-window-observer.ts
2
+ import { reserveCaptureSeq } from "./capture-sequence.mjs";
3
+ import { v7 as uuidv7 } from "uuid";
4
+ import { createViewportBatcher } from "./viewport-batcher.mjs";
5
+ import { observeWindowLowEvents, readActiveFocus, readFocusedChain } from "../readers/window-events.mjs";
6
+ var documentPageId;
7
+ var getCapturePageId = () => documentPageId ??= uuidv7();
8
+ var SharedWindowObserver = class {
9
+ constructor(logger, enqueue, flushKeepalive) {
10
+ this.logger = logger;
11
+ this.enqueue = enqueue;
12
+ this.flushKeepalive = flushKeepalive;
13
+ if (typeof window === "undefined" || typeof document === "undefined") return;
14
+ this.removers = observeWindowLowEvents(
15
+ {
16
+ key: "page:window.low-events",
17
+ title: "Window focus, visibility, size and back navigation",
18
+ accessNote: "window blur/focus, document visibilitychange/focusin/focusout/pointerdown, visualViewport resize/scroll, pagehide, Navigation API currententrychange (traverse)",
19
+ group: "Window interaction",
20
+ origin: "observe-external",
21
+ fields: [
22
+ { path: "event.blur", label: "Window left", exposure: "presence" },
23
+ { path: "event.focus", label: "Window selected", exposure: "presence" },
24
+ {
25
+ path: "event.visibilitychange",
26
+ label: "Page hidden or shown again",
27
+ exposure: "enum",
28
+ transform: { values: ["hidden", "visible"] }
29
+ },
30
+ {
31
+ path: "event.focusout",
32
+ label: "Whether a tracked field lost focus",
33
+ exposure: "computed",
34
+ transform: { note: "only whether a handed-over input lost focus to nothing" }
35
+ },
36
+ {
37
+ path: "event.focusin",
38
+ label: "Which tracked field gained focus",
39
+ exposure: "computed",
40
+ transform: { note: "only which handed-over input gained focus" }
41
+ },
42
+ {
43
+ path: "event.pointerdown",
44
+ label: "Whether a tracked field was pressed",
45
+ exposure: "computed",
46
+ transform: { note: "only whether the pointer went to a handed-over input" }
47
+ },
48
+ {
49
+ path: "event.visualviewport.resize",
50
+ label: "Duration of zooming or resizing",
51
+ exposure: "size",
52
+ transform: { note: "batched duration only" }
53
+ },
54
+ {
55
+ path: "event.visualviewport.scroll",
56
+ label: "Duration of scrolling",
57
+ exposure: "size",
58
+ transform: { note: "batched duration only" }
59
+ },
60
+ { path: "event.pagehide", label: "Leaving the page", exposure: "presence" },
61
+ {
62
+ path: "event.navigation.back",
63
+ label: "Going back within the page",
64
+ exposure: "enum",
65
+ transform: { values: [true, false], note: "committed same-document traverse to an earlier entry" }
66
+ }
67
+ ]
68
+ },
69
+ {
70
+ blur: () => {
71
+ this.flushInputs();
72
+ this.bracket = this.context();
73
+ this.emit({ lowType: "window-blur", ts: Date.now() }, this.bracket);
74
+ },
75
+ focus: () => {
76
+ this.flushInputs();
77
+ this.emit({ lowType: "window-focus", ts: Date.now() }, this.bracket ?? this.context());
78
+ this.bracket = void 0;
79
+ },
80
+ visibilityChange: (hidden) => {
81
+ this.flushInputs();
82
+ this.emit({
83
+ lowType: `document-visibilitychange-${hidden ? "hidden" : "visible"}`,
84
+ ts: Date.now()
85
+ });
86
+ },
87
+ focusOut: (target, hasRelatedTarget) => {
88
+ const registration = this.findInput(target);
89
+ this.lastBlur = registration && !hasRelatedTarget && Date.now() - this.unrelatedPointerAt > 250 ? { registration, ts: Date.now() } : void 0;
90
+ },
91
+ focusIn: (composedPath) => {
92
+ this.lastBlur = void 0;
93
+ const field = this.inputFocusListeners.size > 0 && composedPath().find((target) => this.findInput(target));
94
+ if (field) this.notifyInputFocus(field);
95
+ },
96
+ pointerDown: (target) => {
97
+ if (!this.findInput(target)) {
98
+ this.lastBlur = void 0;
99
+ this.unrelatedPointerAt = Date.now();
100
+ } else this.unrelatedPointerAt = -Infinity;
101
+ },
102
+ viewportResize: () => this.extendViewport("resize"),
103
+ viewportScroll: () => this.extendViewport("scroll"),
104
+ pageHide: () => {
105
+ this.flushInputs();
106
+ this.resize.flush();
107
+ this.scroll.flush();
108
+ this.flushKeepalive();
109
+ },
110
+ navigateBack: () => {
111
+ this.flushInputs();
112
+ this.emit({ lowType: "window-navigate-back", ts: Date.now() });
113
+ }
114
+ },
115
+ (name, run) => {
116
+ try {
117
+ run();
118
+ } catch (error) {
119
+ this.logger.error(`Failed to observe window ${name}`, error);
120
+ }
121
+ }
122
+ );
123
+ }
124
+ inputs = /* @__PURE__ */ new Set();
125
+ inputFocusListeners = /* @__PURE__ */ new Set();
126
+ ceremonies = /* @__PURE__ */ new Set();
127
+ removers = [];
128
+ lastBlur;
129
+ bracket;
130
+ unrelatedPointerAt = -Infinity;
131
+ destroyed = false;
132
+ viewportContext = {};
133
+ resize = createViewportBatcher(
134
+ (low) => this.emit(low, this.viewportContext.resize),
135
+ "visualviewport-resize",
136
+ reserveCaptureSeq
137
+ );
138
+ scroll = createViewportBatcher(
139
+ (low) => this.emit(low, this.viewportContext.scroll),
140
+ "visualviewport-scroll",
141
+ reserveCaptureSeq
142
+ );
143
+ registerInputs(fields, context, flush) {
144
+ if (this.destroyed) return () => {
145
+ };
146
+ const registration = { fields, context, flush };
147
+ this.inputs.add(registration);
148
+ if (this.inputFocusListeners.size > 0 && typeof document !== "undefined") {
149
+ const focused = readFocusedChain({
150
+ key: "dom:window.focused-element",
151
+ title: "Which tracked input field has focus",
152
+ accessNote: "document.activeElement and the focus event path, matched against the passed input elements",
153
+ group: "Window interaction",
154
+ origin: "observe-external",
155
+ fields: [
156
+ {
157
+ path: "activeElement",
158
+ label: "Which tracked field is selected",
159
+ exposure: "computed",
160
+ transform: { note: "identity compared against handed-over inputs only" }
161
+ }
162
+ ]
163
+ });
164
+ const field = fields.find((candidate) => focused.includes(candidate));
165
+ if (field) this.notifyInputFocus(field);
166
+ }
167
+ return () => {
168
+ this.inputs.delete(registration);
169
+ if (this.lastBlur?.registration === registration) this.lastBlur = void 0;
170
+ };
171
+ }
172
+ beginCeremony(context) {
173
+ const registration = { context, settled: false };
174
+ if (!this.destroyed) {
175
+ this.flushInputs();
176
+ this.ceremonies.add(registration);
177
+ }
178
+ const destroy = () => {
179
+ if (registration.timer) clearTimeout(registration.timer);
180
+ this.ceremonies.delete(registration);
181
+ if (this.bracket === registration.context) this.bracket = void 0;
182
+ };
183
+ return {
184
+ settle: (tailMs) => {
185
+ registration.settled = true;
186
+ if (registration.timer) clearTimeout(registration.timer);
187
+ registration.timer = setTimeout(destroy, tailMs);
188
+ },
189
+ destroy
190
+ };
191
+ }
192
+ /**
193
+ * Calls `listener` with the field whenever a registered input gains focus, including one already focused when
194
+ * registered.
195
+ */
196
+ onInputFocus(listener) {
197
+ if (this.destroyed) return () => {
198
+ };
199
+ this.inputFocusListeners.add(listener);
200
+ return () => this.inputFocusListeners.delete(listener);
201
+ }
202
+ notifyInputFocus(field) {
203
+ for (const listener of this.inputFocusListeners) {
204
+ try {
205
+ listener(field);
206
+ } catch (error) {
207
+ this.logger.error("Failed to notify registered input focus", error);
208
+ }
209
+ }
210
+ }
211
+ context() {
212
+ const active = [...this.ceremonies].filter((item) => !item.settled);
213
+ if (active.length === 1) return active[0].context;
214
+ if (active.length > 1) return void 0;
215
+ const focus = readActiveFocus({
216
+ key: "dom:window.focused-element",
217
+ title: "Which tracked input field has focus",
218
+ accessNote: "document.activeElement and the focus event path, matched against the passed input elements",
219
+ group: "Window interaction",
220
+ origin: "observe-external",
221
+ fields: [
222
+ {
223
+ path: "activeElement",
224
+ label: "Which tracked field is selected",
225
+ exposure: "computed",
226
+ transform: { note: "identity compared against handed-over inputs only" }
227
+ },
228
+ {
229
+ path: "activeElement.idle",
230
+ label: "Whether nothing is selected",
231
+ exposure: "enum",
232
+ transform: { values: [true, false], note: "nothing focused (body or none)" }
233
+ }
234
+ ]
235
+ });
236
+ const input = this.findInput(focus.active);
237
+ if (input) return input.context();
238
+ const previous = this.lastBlur;
239
+ if (previous && Date.now() - previous.ts <= 250 && focus.idle) {
240
+ return previous.registration.context();
241
+ }
242
+ return void 0;
243
+ }
244
+ findInput(target) {
245
+ return [...this.inputs].find((registration) => registration.fields.some((field) => field === target));
246
+ }
247
+ /** Flush all registered input batches before a high-level lifecycle boundary. */
248
+ flushInputs() {
249
+ for (const input of this.inputs) {
250
+ try {
251
+ input.flush();
252
+ } catch (error) {
253
+ this.logger.error("Failed to flush window input context", error);
254
+ }
255
+ }
256
+ }
257
+ emit(low, context) {
258
+ if (this.destroyed) return;
259
+ const { sessionId, ...capture } = context ?? {};
260
+ this.enqueue(
261
+ {
262
+ ...low,
263
+ ...capture,
264
+ captureSeq: low.captureSeq ?? reserveCaptureSeq(),
265
+ sourceKind: "window",
266
+ captureVersion: 1,
267
+ pageId: getCapturePageId()
268
+ },
269
+ sessionId
270
+ );
271
+ }
272
+ extendViewport(kind) {
273
+ const context = this.context();
274
+ const previous = this.viewportContext[kind];
275
+ if (previous?.subflowStartEventId !== context?.subflowStartEventId || previous?.fieldRole !== context?.fieldRole || previous?.sessionId !== context?.sessionId)
276
+ this[kind].flush();
277
+ this.viewportContext[kind] = context;
278
+ this[kind].extend();
279
+ }
280
+ destroy() {
281
+ if (this.destroyed) return;
282
+ this.flushInputs();
283
+ this.resize.flush();
284
+ this.scroll.flush();
285
+ this.destroyed = true;
286
+ for (const remove of this.removers) remove();
287
+ for (const ceremony of this.ceremonies) if (ceremony.timer) clearTimeout(ceremony.timer);
288
+ this.ceremonies.clear();
289
+ this.inputs.clear();
290
+ this.inputFocusListeners.clear();
291
+ }
292
+ };
293
+ export {
294
+ SharedWindowObserver,
295
+ getCapturePageId
296
+ };
File without changes