@corbado/observe 1.8.0 → 1.9.1

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 (43) hide show
  1. package/dist/analysis/client.mjs +82 -56
  2. package/dist/analysis/config-manager.mjs +172 -18
  3. package/dist/analysis/config.mjs +12 -4
  4. package/dist/analysis/device-info-manager.mjs +16 -1
  5. package/dist/analysis/device.mjs +88 -10
  6. package/dist/analysis/handoff.mjs +7 -0
  7. package/dist/analysis/low-events/grouped-input-tracker.mjs +148 -33
  8. package/dist/analysis/low-events/input-tracker.mjs +138 -59
  9. package/dist/analysis/low-events/shared-window-observer.mjs +148 -77
  10. package/dist/analysis/low-events/viewport-batcher.mjs +3 -3
  11. package/dist/analysis/queue.mjs +75 -8
  12. package/dist/analysis/{low-events/input-paste-observer.mjs → readers/input-paste.mjs} +2 -2
  13. package/dist/analysis/readers/input.mjs +61 -0
  14. package/dist/analysis/readers/loader-global.mjs +15 -0
  15. package/dist/analysis/readers/network.mjs +14 -0
  16. package/dist/analysis/readers/page.mjs +30 -0
  17. package/dist/analysis/readers/storage.mjs +28 -0
  18. package/dist/analysis/{utils → readers}/web-lock.mjs +6 -2
  19. package/dist/analysis/readers/window-events.mjs +48 -0
  20. package/dist/analysis/storage/index.mjs +61 -9
  21. package/dist/analysis/store/event-store.mjs +34 -16
  22. package/dist/analysis/transports/fetch.mjs +45 -3
  23. package/dist/analysis/utils/email-link-operation-full.mjs +22 -2
  24. package/dist/analysis/utils/email-otp-operation-full.mjs +0 -1
  25. package/dist/analysis/utils/form-operation-full.mjs +0 -1
  26. package/dist/analysis/utils/operation-full-provide-identifier-with-cui.mjs +6 -6
  27. package/dist/analysis/utils/passkey-enrollment-operation-full.mjs +9 -6
  28. package/dist/analysis/utils/passkey-login-operation-full.mjs +10 -5
  29. package/dist/analysis/utils/password-login-operation-full.mjs +8 -4
  30. package/dist/analysis/utils/password-manager-sampler.mjs +44 -4
  31. package/dist/analysis/utils/sms-otp-operation-full.mjs +0 -1
  32. package/dist/analysis/utils/totp-operation-full.mjs +0 -1
  33. package/dist/analysis/version.mjs +1 -1
  34. package/dist/cdn/script.global.js +4 -4
  35. package/dist/index.d.mts +151 -26
  36. package/dist/index.d.ts +151 -26
  37. package/dist/index.js +2 -2
  38. package/dist/index.mjs +2 -2
  39. package/package.json +5 -3
  40. package/tools/reader-meta-text.d.mts +2 -0
  41. package/tools/reader-meta-text.mjs +26 -0
  42. package/tools/strip-reader-meta.d.mts +33 -0
  43. package/tools/strip-reader-meta.mjs +131 -0
@@ -1,6 +1,8 @@
1
1
  // src/low-events/grouped-input-tracker.ts
2
2
  import { InputChangeRecorder } from "./input-change-recorder.mjs";
3
- import { InputPasteObserver } from "./input-paste-observer.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";
4
6
  var LowEventGroupedInputTracker = class {
5
7
  constructor(logger, fields, sink) {
6
8
  this.logger = logger;
@@ -13,6 +15,27 @@ var LowEventGroupedInputTracker = class {
13
15
  sink.reserveCaptureSeq
14
16
  );
15
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
+ },
16
39
  this.inputs,
17
40
  logger,
18
41
  (ts, _inserted, captureSeq) => {
@@ -32,35 +55,115 @@ var LowEventGroupedInputTracker = class {
32
55
  );
33
56
  this.unregisterInputs = sink.registerInputs?.(this.inputs, () => this.flushPending());
34
57
  this.capturePrefill();
58
+ const guard = (_name, run) => this.safe(run);
35
59
  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
- });
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
+ );
57
123
  }
58
124
  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)) {
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
+ )) {
64
167
  this.emit("focus");
65
168
  }
66
169
  }
@@ -78,7 +181,24 @@ var LowEventGroupedInputTracker = class {
78
181
  return this.inputs.some((field) => field === target);
79
182
  }
80
183
  length() {
81
- return this.inputs.reduce((total, field) => total + field.value.length, 0);
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
+ );
82
202
  }
83
203
  safe(action) {
84
204
  try {
@@ -87,11 +207,6 @@ var LowEventGroupedInputTracker = class {
87
207
  this.logger.error("Failed to observe grouped input", error);
88
208
  }
89
209
  }
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
210
  cancelChange() {
96
211
  if (this.changeTimer !== null) clearTimeout(this.changeTimer);
97
212
  this.changeTimer = null;
@@ -1,12 +1,31 @@
1
1
  // src/low-events/input-tracker.ts
2
2
  import { InputChangeRecorder } from "./input-change-recorder.mjs";
3
- import { InputPasteObserver } from "./input-paste-observer.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";
4
6
  import { readPasswordManagerDomSignals } from "../readers/password-manager-dom.mjs";
5
7
  var MOUNT_PROBE_DELAY_MS = 500;
6
8
  var FOCUS_PROBE_DELAY_MS = 250;
7
9
  function fieldContentLength(field) {
8
- if (field.type === "checkbox" || field.type === "radio") return field.checked ? 1 : 0;
9
- return field.value.length;
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
+ );
10
29
  }
11
30
  var LowEventInputTracker = class {
12
31
  constructor(logger, inputField, sink) {
@@ -20,6 +39,27 @@ var LowEventInputTracker = class {
20
39
  sink.reserveCaptureSeq
21
40
  );
22
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
+ },
23
63
  [inputField],
24
64
  logger,
25
65
  (ts, _inserted, captureSeq) => {
@@ -36,27 +76,13 @@ var LowEventInputTracker = class {
36
76
  },
37
77
  sink.reserveCaptureSeq
38
78
  );
39
- this.boundHandlers = {
40
- focus: this.wrapHandler("focus", () => this.handleFocus()),
41
- blur: this.wrapHandler("blur", () => this.handleBlur()),
42
- pointerdown: this.wrapHandler("pointerdown", () => this.enqueueNonInputEvent("pointerdown")),
43
- pointerup: this.wrapHandler("pointerup", () => this.enqueueNonInputEvent("pointerup")),
44
- click: this.wrapHandler("click", () => this.enqueueNonInputEvent("click")),
45
- input: (event) => {
46
- try {
47
- this.handleInput(event);
48
- } catch (error) {
49
- this.logger.error("Failed to observe input", error);
50
- }
51
- },
52
- keyup: this.wrapKeyboardHandler("keyup", (event) => this.handleKeyUp(event)),
53
- pageHide: this.wrapHandler("pagehide", () => this.flushForTeardown())
54
- };
55
79
  this.unregisterInputs = sink.registerInputs?.([inputField], () => this.flushPending());
56
80
  this.capturePrefill();
57
81
  this.attach();
58
82
  }
59
83
  unregisterInputs;
84
+ removeFieldListeners;
85
+ removePageHide;
60
86
  changes;
61
87
  pasteObserver;
62
88
  emittedSignals = /* @__PURE__ */ new Set();
@@ -64,7 +90,6 @@ var LowEventInputTracker = class {
64
90
  mountProbeTimer = null;
65
91
  focusProbeTimer = null;
66
92
  destroyed = false;
67
- boundHandlers;
68
93
  destroy() {
69
94
  if (this.destroyed) {
70
95
  return;
@@ -89,18 +114,93 @@ var LowEventInputTracker = class {
89
114
  }
90
115
  attach() {
91
116
  const input = this.inputField;
92
- input.addEventListener("focus", this.boundHandlers.focus);
93
- input.addEventListener("blur", this.boundHandlers.blur);
94
- input.addEventListener("pointerdown", this.boundHandlers.pointerdown);
95
- input.addEventListener("pointerup", this.boundHandlers.pointerup);
96
- input.addEventListener("click", this.boundHandlers.click);
97
- input.addEventListener("input", this.boundHandlers.input);
98
- input.addEventListener("keyup", this.boundHandlers.keyup);
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
+ );
99
164
  if (typeof window !== "undefined") {
100
- window.addEventListener("pagehide", this.boundHandlers.pageHide);
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
+ );
101
184
  }
102
185
  if (typeof document !== "undefined") {
103
- if (document.activeElement === input) {
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
+ )) {
104
204
  this.handleFocus();
105
205
  }
106
206
  }
@@ -109,16 +209,9 @@ var LowEventInputTracker = class {
109
209
  }
110
210
  }
111
211
  detach() {
112
- const input = this.inputField;
113
- input.removeEventListener("focus", this.boundHandlers.focus);
114
- input.removeEventListener("blur", this.boundHandlers.blur);
115
- input.removeEventListener("pointerdown", this.boundHandlers.pointerdown);
116
- input.removeEventListener("pointerup", this.boundHandlers.pointerup);
117
- input.removeEventListener("click", this.boundHandlers.click);
118
- input.removeEventListener("input", this.boundHandlers.input);
119
- input.removeEventListener("keyup", this.boundHandlers.keyup);
212
+ this.removeFieldListeners?.();
120
213
  if (typeof window !== "undefined") {
121
- window.removeEventListener("pagehide", this.boundHandlers.pageHide);
214
+ this.removePageHide?.();
122
215
  }
123
216
  }
124
217
  pushLow = (low) => {
@@ -129,9 +222,14 @@ var LowEventInputTracker = class {
129
222
  if (typeof __CBO_DATA_PMD__ === "boolean" ? __CBO_DATA_PMD__ : this.sink.passwordManagerDomScan?.() !== false) {
130
223
  const matches = readPasswordManagerDomSignals({
131
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",
132
229
  fields: [
133
230
  {
134
231
  path: "signals[]",
232
+ label: "Password manager traces found",
135
233
  exposure: "enum",
136
234
  transform: { values: ["pa-1", "pa-2", "pa-3", "pa-4", "pa-5", "pa-6"] }
137
235
  }
@@ -179,19 +277,9 @@ var LowEventInputTracker = class {
179
277
  this.pasteObserver.flush();
180
278
  this.enqueueNonInputEvent("blur");
181
279
  }
182
- handleInput(event) {
280
+ handleInput(event, composing) {
183
281
  if (this.pasteObserver.handlesInput(event)) return;
184
- this.changes.record(
185
- fieldContentLength(this.inputField),
186
- Date.now(),
187
- event.isComposing || event.inputType === "insertCompositionText"
188
- );
189
- }
190
- handleKeyUp(event) {
191
- if (event.key !== "Escape") {
192
- return;
193
- }
194
- this.enqueueNonInputEvent("keyup-escape");
282
+ this.changes.record(fieldContentLength(this.inputField), Date.now(), composing);
195
283
  }
196
284
  flushForTeardown() {
197
285
  this.pasteObserver.flush();
@@ -207,15 +295,6 @@ var LowEventInputTracker = class {
207
295
  }
208
296
  };
209
297
  }
210
- wrapKeyboardHandler(name, fn) {
211
- return (event) => {
212
- try {
213
- fn(event);
214
- } catch (error) {
215
- this.logger.error(`Failed to handle low event "${name}"`, error);
216
- }
217
- };
218
- }
219
298
  };
220
299
  export {
221
300
  LowEventInputTracker,