@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
@@ -2,64 +2,124 @@
2
2
  import { reserveCaptureSeq } from "./capture-sequence.mjs";
3
3
  import { v7 as uuidv7 } from "uuid";
4
4
  import { createViewportBatcher } from "./viewport-batcher.mjs";
5
+ import { observeWindowLowEvents, readActiveFocus, readFocusedChain } from "../readers/window-events.mjs";
5
6
  var documentPageId;
6
7
  var getCapturePageId = () => documentPageId ??= uuidv7();
7
- var focusedElements = () => {
8
- const chain = [];
9
- for (let active = document.activeElement; active; active = active.shadowRoot?.activeElement ?? null) {
10
- chain.push(active);
11
- }
12
- return chain;
13
- };
14
8
  var SharedWindowObserver = class {
15
9
  constructor(logger, enqueue, flushKeepalive) {
16
10
  this.logger = logger;
17
11
  this.enqueue = enqueue;
18
12
  this.flushKeepalive = flushKeepalive;
19
13
  if (typeof window === "undefined" || typeof document === "undefined") return;
20
- this.listen(window, "blur", () => {
21
- this.flushInputs();
22
- this.bracket = this.context();
23
- this.emit({ lowType: "window-blur", ts: Date.now() }, this.bracket);
24
- });
25
- this.listen(window, "focus", () => {
26
- this.flushInputs();
27
- this.emit({ lowType: "window-focus", ts: Date.now() }, this.bracket ?? this.context());
28
- this.bracket = void 0;
29
- });
30
- this.listen(document, "visibilitychange", () => {
31
- this.flushInputs();
32
- this.emit({
33
- lowType: `document-visibilitychange-${document.visibilityState === "hidden" ? "hidden" : "visible"}`,
34
- ts: Date.now()
35
- });
36
- });
37
- this.listen(document, "focusout", (event) => {
38
- const registration = this.findInput(event.target);
39
- this.lastBlur = registration && !event.relatedTarget && Date.now() - this.unrelatedPointerAt > 250 ? { registration, ts: Date.now() } : void 0;
40
- });
41
- this.listen(document, "focusin", (event) => {
42
- this.lastBlur = void 0;
43
- const field = this.inputFocusListeners.size > 0 && event.composedPath().find((target) => this.findInput(target));
44
- if (field) this.notifyInputFocus(field);
45
- });
46
- this.listen(document, "pointerdown", (event) => {
47
- if (!this.findInput(event.target)) {
48
- this.lastBlur = void 0;
49
- this.unrelatedPointerAt = Date.now();
50
- } else this.unrelatedPointerAt = -Infinity;
51
- });
52
- if (window.visualViewport) {
53
- this.listen(window.visualViewport, "resize", () => this.extendViewport("resize"));
54
- this.listen(window.visualViewport, "scroll", () => this.extendViewport("scroll"));
55
- }
56
- this.listen(window, "pagehide", () => {
57
- this.flushInputs();
58
- this.resize.flush();
59
- this.scroll.flush();
60
- this.flushKeepalive();
61
- });
62
- this.observeNavigation();
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
+ );
63
123
  }
64
124
  inputs = /* @__PURE__ */ new Set();
65
125
  inputFocusListeners = /* @__PURE__ */ new Set();
@@ -86,7 +146,21 @@ var SharedWindowObserver = class {
86
146
  const registration = { fields, context, flush };
87
147
  this.inputs.add(registration);
88
148
  if (this.inputFocusListeners.size > 0 && typeof document !== "undefined") {
89
- const focused = focusedElements();
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
+ });
90
164
  const field = fields.find((candidate) => focused.includes(candidate));
91
165
  if (field) this.notifyInputFocus(field);
92
166
  }
@@ -138,10 +212,31 @@ var SharedWindowObserver = class {
138
212
  const active = [...this.ceremonies].filter((item) => !item.settled);
139
213
  if (active.length === 1) return active[0].context;
140
214
  if (active.length > 1) return void 0;
141
- const input = this.findInput(document.activeElement);
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);
142
237
  if (input) return input.context();
143
238
  const previous = this.lastBlur;
144
- if (previous && Date.now() - previous.ts <= 250 && (document.activeElement === document.body || !document.activeElement)) {
239
+ if (previous && Date.now() - previous.ts <= 250 && focus.idle) {
145
240
  return previous.registration.context();
146
241
  }
147
242
  return void 0;
@@ -174,17 +269,6 @@ var SharedWindowObserver = class {
174
269
  sessionId
175
270
  );
176
271
  }
177
- listen(target, name, handler) {
178
- const safe = (event) => {
179
- try {
180
- handler(event);
181
- } catch (error) {
182
- this.logger.error(`Failed to observe window ${name}`, error);
183
- }
184
- };
185
- target.addEventListener(name, safe);
186
- this.removers.push(() => target.removeEventListener(name, safe));
187
- }
188
272
  extendViewport(kind) {
189
273
  const context = this.context();
190
274
  const previous = this.viewportContext[kind];
@@ -193,19 +277,6 @@ var SharedWindowObserver = class {
193
277
  this.viewportContext[kind] = context;
194
278
  this[kind].extend();
195
279
  }
196
- observeNavigation() {
197
- const navigation = window.navigation;
198
- if (!navigation) return;
199
- this.listen(navigation, "currententrychange", (event) => {
200
- const change = event;
201
- const from = change.from?.index;
202
- const to = navigation.currentEntry?.index;
203
- if (change.navigationType === "traverse" && typeof from === "number" && typeof to === "number" && from >= 0 && to >= 0 && to < from) {
204
- this.flushInputs();
205
- this.emit({ lowType: "window-navigate-back", ts: Date.now() });
206
- }
207
- });
208
- }
209
280
  destroy() {
210
281
  if (this.destroyed) return;
211
282
  this.flushInputs();
@@ -7,7 +7,7 @@ var createViewportBatcher = (pushLow, eventType, reserveCaptureSeq) => {
7
7
  let timeout = null;
8
8
  const flush = () => {
9
9
  if (timeout !== null) {
10
- window.clearTimeout(timeout);
10
+ clearTimeout(timeout);
11
11
  timeout = null;
12
12
  }
13
13
  const ts = startTimestamp;
@@ -31,9 +31,9 @@ var createViewportBatcher = (pushLow, eventType, reserveCaptureSeq) => {
31
31
  captureSeq = sequence;
32
32
  }
33
33
  if (timeout !== null) {
34
- window.clearTimeout(timeout);
34
+ clearTimeout(timeout);
35
35
  }
36
- timeout = window.setTimeout(flush, VIEWPORT_BATCH_TIMEOUT_MS);
36
+ timeout = setTimeout(flush, VIEWPORT_BATCH_TIMEOUT_MS);
37
37
  };
38
38
  return { extend, flush };
39
39
  };
@@ -3,6 +3,7 @@ import { v7 as uuidv7 } from "uuid";
3
3
  import { AuthEventName } from "./types.mjs";
4
4
  import { CONFIG_REQUEST_HEADER, DEFAULT_RELIABILITY_CONFIG } from "./config.mjs";
5
5
  import { EventStore } from "./store/event-store.mjs";
6
+ import { listenWindow, readDocumentHidden, readPageUrl } from "./readers/page.mjs";
6
7
  var PROBE_QUEUE_MAX_ITEMS = 500;
7
8
  var PROBE_QUEUE_MAX_BYTES = 512 * 1024;
8
9
  var PROBE_BATCH_MAX_ITEMS = 200;
@@ -96,6 +97,8 @@ var RequestQueue = class {
96
97
  dirtySinceSyncFlush = false;
97
98
  onVisibilityChange = null;
98
99
  onPageHide = null;
100
+ removeVisibilityChange = null;
101
+ removePageHide = null;
99
102
  batchSize;
100
103
  flushIntervalOverride;
101
104
  debug;
@@ -743,7 +746,21 @@ var RequestQueue = class {
743
746
  this.onVisibilityChange = () => {
744
747
  try {
745
748
  this.writeLifecycleDebugOutput("cbo_visibility_change_debug");
746
- if (document.visibilityState === "hidden" && this.config.tvc) {
749
+ if (readDocumentHidden({
750
+ key: "page:lifecycle.visibility",
751
+ title: "Page visibility",
752
+ accessNote: "document.visibilityState",
753
+ group: "Page lifecycle",
754
+ origin: "observe-external",
755
+ fields: [
756
+ {
757
+ path: "visibilityState",
758
+ label: "Whether the page is hidden",
759
+ exposure: "enum",
760
+ transform: { values: ["hidden", "other"] }
761
+ }
762
+ ]
763
+ }) && this.config.tvc) {
747
764
  this.flushSync("visibility-change");
748
765
  }
749
766
  } catch (error) {
@@ -760,8 +777,44 @@ var RequestQueue = class {
760
777
  this.logger.error("Unexpected error in queue flushSync", error);
761
778
  }
762
779
  };
763
- window.addEventListener("visibilitychange", this.onVisibilityChange);
764
- window.addEventListener("pagehide", this.onPageHide);
780
+ this.removeVisibilityChange = listenWindow(
781
+ {
782
+ key: "page:lifecycle.visibilitychange",
783
+ title: "Page visibility changes",
784
+ accessNote: "visibilitychange event on document",
785
+ group: "Page lifecycle",
786
+ origin: "observe-external",
787
+ fields: [
788
+ {
789
+ path: "event.visibilitychange",
790
+ label: "Page hidden or shown again",
791
+ exposure: "presence",
792
+ transform: { note: "flushes pending events when the page is hidden" }
793
+ }
794
+ ]
795
+ },
796
+ "visibilitychange",
797
+ this.onVisibilityChange
798
+ );
799
+ this.removePageHide = listenWindow(
800
+ {
801
+ key: "page:lifecycle.pagehide",
802
+ title: "Leaving the page",
803
+ accessNote: "pagehide event on window",
804
+ group: "Page lifecycle",
805
+ origin: "observe-external",
806
+ fields: [
807
+ {
808
+ path: "event.pagehide",
809
+ label: "Leaving the page",
810
+ exposure: "presence",
811
+ transform: { note: "flushes pending events on unload" }
812
+ }
813
+ ]
814
+ },
815
+ "pagehide",
816
+ this.onPageHide
817
+ );
765
818
  }
766
819
  writeLifecycleDebugOutput(key) {
767
820
  if (!this.debug || !this.storage) {
@@ -769,7 +822,21 @@ var RequestQueue = class {
769
822
  }
770
823
  const output = {
771
824
  last_executed: Date.now(),
772
- url: window.location.href
825
+ url: readPageUrl({
826
+ key: "page:location.url",
827
+ title: "Page address (debug output only)",
828
+ accessNote: "location.href, only written to the debug log",
829
+ group: "Page",
830
+ origin: "observe-external",
831
+ fields: [
832
+ {
833
+ path: "url",
834
+ label: "Full page address",
835
+ exposure: "raw",
836
+ transform: { note: "debug mode only; written to own localStorage, never sent" }
837
+ }
838
+ ]
839
+ })
773
840
  };
774
841
  this.storage.setItem(key, output);
775
842
  }
@@ -862,11 +929,11 @@ var RequestQueue = class {
862
929
  this.clearTimer();
863
930
  this.dropProbes(this.probeQueue.slice());
864
931
  if (typeof window !== "undefined") {
865
- if (this.onVisibilityChange) {
866
- window.removeEventListener("visibilitychange", this.onVisibilityChange);
932
+ if (this.removeVisibilityChange) {
933
+ this.removeVisibilityChange();
867
934
  }
868
- if (this.onPageHide) {
869
- window.removeEventListener("pagehide", this.onPageHide);
935
+ if (this.removePageHide) {
936
+ this.removePageHide();
870
937
  }
871
938
  }
872
939
  } catch (error) {
@@ -1,6 +1,6 @@
1
- // src/low-events/input-paste-observer.ts
1
+ // src/readers/input-paste.ts
2
2
  var InputPasteObserver = class {
3
- constructor(inputs, logger, changed, reserveCaptureSeq) {
3
+ constructor(_meta, inputs, logger, changed, reserveCaptureSeq) {
4
4
  this.inputs = inputs;
5
5
  this.logger = logger;
6
6
  this.changed = changed;
@@ -0,0 +1,61 @@
1
+ // src/readers/input.ts
2
+ var FIELD_EVENTS = ["focus", "blur", "pointerdown", "pointerup", "click", "input", "keyup"];
3
+ function observeInputField(_meta, field, handlers, guard, order = FIELD_EVENTS) {
4
+ const listeners = [];
5
+ for (const type of order) {
6
+ const listener = reduceFieldEvent(type, handlers, guard);
7
+ if (listener) listeners.push([type, listener]);
8
+ }
9
+ for (const [type, listener] of listeners) field.addEventListener(type, listener);
10
+ return () => {
11
+ for (const [type, listener] of listeners) field.removeEventListener(type, listener);
12
+ };
13
+ }
14
+ function reduceFieldEvent(type, handlers, guard) {
15
+ switch (type) {
16
+ case "focus":
17
+ case "blur": {
18
+ const handler = handlers[type];
19
+ return handler && ((event) => guard(type, () => handler(event.relatedTarget)));
20
+ }
21
+ case "pointerdown":
22
+ case "pointerup":
23
+ case "click": {
24
+ const handler = handlers[type];
25
+ return handler && (() => guard(type, () => handler()));
26
+ }
27
+ case "input": {
28
+ const handler = handlers.input;
29
+ return handler && ((event) => guard(
30
+ "input",
31
+ () => handler(
32
+ event.isComposing || event.inputType === "insertCompositionText",
33
+ event
34
+ )
35
+ ));
36
+ }
37
+ case "keyup": {
38
+ const handler = handlers.keyupEscape;
39
+ return handler && ((event) => guard("keyup", () => {
40
+ if (event.key === "Escape") handler();
41
+ }));
42
+ }
43
+ }
44
+ }
45
+ function readInputContentLength(_meta, field) {
46
+ if (field.type === "checkbox" || field.type === "radio") return field.checked ? 1 : 0;
47
+ return field.value.length;
48
+ }
49
+ function readInputGroupValueLength(_meta, fields) {
50
+ return fields.reduce((total, field) => total + field.value.length, 0);
51
+ }
52
+ function readFocusedField(_meta, fields) {
53
+ const active = document.activeElement;
54
+ return fields.find((field) => field === active);
55
+ }
56
+ export {
57
+ observeInputField,
58
+ readFocusedField,
59
+ readInputContentLength,
60
+ readInputGroupValueLength
61
+ };
@@ -0,0 +1,15 @@
1
+ // src/readers/loader-global.ts
2
+ function drainLoaderQueue(_meta, name, replay) {
3
+ const global = window.Corbado;
4
+ if (global?.[name]) {
5
+ replay(global[name]);
6
+ delete window.Corbado[name];
7
+ }
8
+ }
9
+ function installLoaderGlobal(_meta, api) {
10
+ window.Corbado = api;
11
+ }
12
+ export {
13
+ drainLoaderQueue,
14
+ installLoaderGlobal
15
+ };
@@ -0,0 +1,14 @@
1
+ // src/readers/network.ts
2
+ function sendFetch(_meta, url, init) {
3
+ return fetch(url, init);
4
+ }
5
+ function beaconSender(_meta) {
6
+ if (typeof navigator?.sendBeacon !== "function") {
7
+ return void 0;
8
+ }
9
+ return (url, data) => navigator.sendBeacon(url, data);
10
+ }
11
+ export {
12
+ beaconSender,
13
+ sendFetch
14
+ };
@@ -0,0 +1,30 @@
1
+ // src/readers/page.ts
2
+ import { applySourcePathQuery } from "../utils/source-path.mjs";
3
+ function listenWindow(_meta, type, handler, options) {
4
+ if (options === void 0) {
5
+ window.addEventListener(type, handler);
6
+ } else {
7
+ window.addEventListener(type, handler, options);
8
+ }
9
+ return () => window.removeEventListener(type, handler);
10
+ }
11
+ function readDocumentHidden(_meta) {
12
+ return document.visibilityState === "hidden";
13
+ }
14
+ function readDocumentComplete(_meta) {
15
+ return document.readyState === "complete";
16
+ }
17
+ function readPageUrl(_meta) {
18
+ return window.location.href;
19
+ }
20
+ function readSourcePath(_meta, mode, allow) {
21
+ const search = applySourcePathQuery(window.location.search, mode, allow);
22
+ return `${window.location.pathname}${search}`;
23
+ }
24
+ export {
25
+ listenWindow,
26
+ readDocumentComplete,
27
+ readDocumentHidden,
28
+ readPageUrl,
29
+ readSourcePath
30
+ };
@@ -0,0 +1,28 @@
1
+ // src/readers/storage.ts
2
+ function localStorageArea(_meta) {
3
+ return {
4
+ getItem: (key) => localStorage.getItem(key),
5
+ setItem: (key, value) => localStorage.setItem(key, value),
6
+ removeItem: (key) => localStorage.removeItem(key)
7
+ };
8
+ }
9
+ function sessionStorageArea(_meta) {
10
+ return {
11
+ getItem: (key) => sessionStorage.getItem(key),
12
+ setItem: (key, value) => sessionStorage.setItem(key, value),
13
+ removeItem: (key) => sessionStorage.removeItem(key)
14
+ };
15
+ }
16
+ function cookieJar(_meta) {
17
+ return {
18
+ read: () => document.cookie,
19
+ write: (cookie) => {
20
+ document.cookie = cookie;
21
+ }
22
+ };
23
+ }
24
+ export {
25
+ cookieJar,
26
+ localStorageArea,
27
+ sessionStorageArea
28
+ };
@@ -1,5 +1,5 @@
1
- // src/utils/web-lock.ts
2
- function withWebLock(name, fn, options = {}) {
1
+ // src/readers/web-lock.ts
2
+ function withWebLock(_meta, name, fn, options = {}) {
3
3
  const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
4
4
  if (locks?.request) {
5
5
  const timeoutMs = options.timeoutMs;
@@ -47,6 +47,10 @@ function withWebLock(name, fn, options = {}) {
47
47
  return Promise.reject(error);
48
48
  }
49
49
  }
50
+ function hasWebLocks(_meta) {
51
+ return typeof navigator !== "undefined" && !!navigator.locks?.request;
52
+ }
50
53
  export {
54
+ hasWebLocks,
51
55
  withWebLock
52
56
  };
@@ -0,0 +1,48 @@
1
+ // src/readers/window-events.ts
2
+ function observeWindowLowEvents(_meta, handlers, guard) {
3
+ const removers = [];
4
+ const listen = (target, name, handler) => {
5
+ const safe = (event) => guard(name, () => handler(event));
6
+ target.addEventListener(name, safe);
7
+ removers.push(() => target.removeEventListener(name, safe));
8
+ };
9
+ listen(window, "blur", () => handlers.blur());
10
+ listen(window, "focus", () => handlers.focus());
11
+ listen(document, "visibilitychange", () => handlers.visibilityChange(document.visibilityState === "hidden"));
12
+ listen(document, "focusout", (event) => handlers.focusOut(event.target, !!event.relatedTarget));
13
+ listen(document, "focusin", (event) => handlers.focusIn(() => event.composedPath()));
14
+ listen(document, "pointerdown", (event) => handlers.pointerDown(event.target));
15
+ if (window.visualViewport) {
16
+ listen(window.visualViewport, "resize", () => handlers.viewportResize());
17
+ listen(window.visualViewport, "scroll", () => handlers.viewportScroll());
18
+ }
19
+ listen(window, "pagehide", () => handlers.pageHide());
20
+ const navigation = window.navigation;
21
+ if (navigation) {
22
+ listen(navigation, "currententrychange", (event) => {
23
+ const change = event;
24
+ const from = change.from?.index;
25
+ const to = navigation.currentEntry?.index;
26
+ if (change.navigationType === "traverse" && typeof from === "number" && typeof to === "number" && from >= 0 && to >= 0 && to < from) {
27
+ handlers.navigateBack();
28
+ }
29
+ });
30
+ }
31
+ return removers;
32
+ }
33
+ function readFocusedChain(_meta) {
34
+ const chain = [];
35
+ for (let active = document.activeElement; active; active = active.shadowRoot?.activeElement ?? null) {
36
+ chain.push(active);
37
+ }
38
+ return chain;
39
+ }
40
+ function readActiveFocus(_meta) {
41
+ const active = document.activeElement;
42
+ return { active, idle: active === document.body || !active };
43
+ }
44
+ export {
45
+ observeWindowLowEvents,
46
+ readActiveFocus,
47
+ readFocusedChain
48
+ };