@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,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
+ };
@@ -0,0 +1,56 @@
1
+ // src/readers/web-lock.ts
2
+ function withWebLock(_meta, name, fn, options = {}) {
3
+ const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
4
+ if (locks?.request) {
5
+ const timeoutMs = options.timeoutMs;
6
+ const controller = timeoutMs !== void 0 && typeof AbortController !== "undefined" ? new AbortController() : null;
7
+ if (timeoutMs !== void 0 && !controller) {
8
+ return Promise.resolve().then(fn);
9
+ }
10
+ try {
11
+ let ran = false;
12
+ let timer;
13
+ const clearAcquisitionTimer = () => {
14
+ if (timer !== void 0) {
15
+ clearTimeout(timer);
16
+ timer = void 0;
17
+ }
18
+ };
19
+ const callback = () => {
20
+ ran = true;
21
+ clearAcquisitionTimer();
22
+ return fn();
23
+ };
24
+ const granted = controller ? locks.request(name, { signal: controller.signal }, callback) : locks.request(name, callback);
25
+ if (controller && timeoutMs !== void 0 && !ran) {
26
+ timer = setTimeout(() => {
27
+ timer = void 0;
28
+ controller.abort();
29
+ try {
30
+ options.onTimeout?.();
31
+ } catch {
32
+ }
33
+ }, timeoutMs);
34
+ }
35
+ return granted.catch((error) => {
36
+ if (ran) {
37
+ throw error;
38
+ }
39
+ return fn();
40
+ }).finally(clearAcquisitionTimer);
41
+ } catch {
42
+ }
43
+ }
44
+ try {
45
+ return Promise.resolve(fn());
46
+ } catch (error) {
47
+ return Promise.reject(error);
48
+ }
49
+ }
50
+ function hasWebLocks(_meta) {
51
+ return typeof navigator !== "undefined" && !!navigator.locks?.request;
52
+ }
53
+ export {
54
+ hasWebLocks,
55
+ withWebLock
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
+ };
@@ -0,0 +1,249 @@
1
+ // src/storage/index.ts
2
+ import { cookieJar, localStorageArea, sessionStorageArea } from "../readers/storage.mjs";
3
+ function namespacedKey(key, namespace) {
4
+ return `${key}_${namespace}`;
5
+ }
6
+ var NamespacedStorage = class {
7
+ constructor(engine, namespace) {
8
+ this.engine = engine;
9
+ this.namespace = namespace;
10
+ }
11
+ getItem(key) {
12
+ return this.engine.getItem(namespacedKey(key, this.namespace));
13
+ }
14
+ setItem(key, value) {
15
+ this.engine.setItem(namespacedKey(key, this.namespace), value);
16
+ }
17
+ removeItem(key) {
18
+ this.engine.removeItem(namespacedKey(key, this.namespace));
19
+ }
20
+ };
21
+ var LocalStorage = class {
22
+ constructor(logger) {
23
+ this.logger = logger;
24
+ }
25
+ area = localStorageArea({
26
+ key: "storage:sdk.local-storage",
27
+ title: "Observe data saved in the browser",
28
+ accessNote: "localStorage, Observe's own cbo_ keys",
29
+ group: "Observe SDK state",
30
+ origin: "observe-internal",
31
+ fields: [
32
+ {
33
+ path: "value",
34
+ label: "Observe's own saved data",
35
+ exposure: "none",
36
+ transform: {
37
+ note: "own cbo_* keys only (session, sequence, outbox, config cache, experiments, client env, debug); written and read back by the SDK"
38
+ }
39
+ }
40
+ ]
41
+ });
42
+ /**
43
+ * Error situations:
44
+ * - localStorage is not available: return null
45
+ * - localStorage.getItem throws: catch exception, log error and return null
46
+ * - JSON.parse throws: catch exception, log error and return null
47
+ */
48
+ getItem(key) {
49
+ try {
50
+ if (typeof localStorage === "undefined") return null;
51
+ const item = this.area.getItem(key);
52
+ if (item) {
53
+ return JSON.parse(item);
54
+ }
55
+ return null;
56
+ } catch (error) {
57
+ this.logger.error(`LocalStorage.getItem failed for key "${key}"`, error);
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * Error situations:
63
+ * - localStorage is not available: return without writing
64
+ * - localStorage.setItem throws: catch exception and log error
65
+ * - JSON.stringify throws: catch exception and log error
66
+ */
67
+ setItem(key, value) {
68
+ try {
69
+ if (typeof localStorage === "undefined") return;
70
+ this.area.setItem(key, JSON.stringify(value));
71
+ } catch (error) {
72
+ this.logger.error(`LocalStorage.setItem failed for key "${key}"`, error);
73
+ this.logger.reportTelemetry?.("error", `localStorage.setItem failed: ${key}`);
74
+ }
75
+ }
76
+ /**
77
+ * Error situations:
78
+ * - localStorage is not available: return without writing
79
+ * - localStorage.removeItem throws: catch exception and log error
80
+ */
81
+ removeItem(key) {
82
+ try {
83
+ if (typeof localStorage === "undefined") return;
84
+ this.area.removeItem(key);
85
+ } catch (error) {
86
+ this.logger.error(`LocalStorage.removeItem failed for key "${key}"`, error);
87
+ }
88
+ }
89
+ };
90
+ var CookieStorage = class {
91
+ constructor(logger, domain) {
92
+ this.logger = logger;
93
+ this.domain = domain;
94
+ }
95
+ jar = cookieJar({
96
+ key: "storage:sdk.cookie",
97
+ title: "Observe data saved in cookies",
98
+ accessNote: "document.cookie, Observe's own cbo_ cookies",
99
+ group: "Observe SDK state",
100
+ origin: "observe-internal",
101
+ fields: [
102
+ {
103
+ path: "value",
104
+ label: "Observe's own cookies",
105
+ exposure: "none",
106
+ transform: {
107
+ note: "own cbo_* cookies only (cookie storage mode); the cookie string is scanned for the requested key"
108
+ }
109
+ }
110
+ ]
111
+ });
112
+ /**
113
+ * Error situations:
114
+ * - document is not available: return null
115
+ * - document.cookie read throws: catch exception, log error and return null
116
+ * - decodeURIComponent throws: catch exception, log error and return null
117
+ * - JSON.parse throws: catch exception, log error and return null
118
+ */
119
+ getItem(key) {
120
+ try {
121
+ if (typeof document === "undefined") return null;
122
+ const name = key + "=";
123
+ const parts = this.jar.read().split(";");
124
+ for (let i = 0; i < parts.length; i++) {
125
+ const part = parts[i].trim();
126
+ if (part.indexOf(name) === 0) {
127
+ const rawValue = part.substring(name.length);
128
+ return JSON.parse(decodeURIComponent(rawValue));
129
+ }
130
+ }
131
+ return null;
132
+ } catch (error) {
133
+ this.logger.error(`CookieStorage.getItem failed for key "${key}"`, error);
134
+ return null;
135
+ }
136
+ }
137
+ /**
138
+ * Error situations:
139
+ * - document is not available: return without writing
140
+ * - document.cookie write throws: catch exception and log error
141
+ * - JSON.stringify throws: catch exception and log error
142
+ */
143
+ setItem(key, value) {
144
+ try {
145
+ if (typeof document === "undefined") return;
146
+ let cookieString = `${key}=${encodeURIComponent(JSON.stringify(value))}; path=/; SameSite=Lax`;
147
+ if (this.domain) {
148
+ cookieString += `; domain=${this.domain}`;
149
+ }
150
+ this.jar.write(cookieString);
151
+ } catch (error) {
152
+ this.logger.error(`CookieStorage.setItem failed for key "${key}"`, error);
153
+ this.logger.reportTelemetry?.("error", `cookie.setItem failed: ${key}`);
154
+ }
155
+ }
156
+ /**
157
+ * Error situations:
158
+ * - document is not available: return without writing
159
+ * - document.cookie write throws: catch exception and log error
160
+ */
161
+ removeItem(key) {
162
+ try {
163
+ if (typeof document === "undefined") return;
164
+ let cookieString = `${key}=; Max-Age=-99999999; path=/`;
165
+ if (this.domain) {
166
+ cookieString += `; domain=${this.domain}`;
167
+ }
168
+ this.jar.write(cookieString);
169
+ } catch (error) {
170
+ this.logger.error(`CookieStorage.removeItem failed for key "${key}"`, error);
171
+ }
172
+ }
173
+ };
174
+ var SessionStorage = class {
175
+ constructor(logger) {
176
+ this.logger = logger;
177
+ }
178
+ area = sessionStorageArea({
179
+ key: "storage:sdk.session-storage",
180
+ title: "Observe data saved for the current tab",
181
+ accessNote: "sessionStorage, Observe's own cbo_ keys",
182
+ group: "Observe SDK state",
183
+ origin: "observe-internal",
184
+ fields: [
185
+ {
186
+ path: "value",
187
+ label: "Observe's own data for this tab",
188
+ exposure: "none",
189
+ transform: {
190
+ note: "own cbo_* keys only (per-tab session, sequence and tab id); written and read back by the SDK"
191
+ }
192
+ }
193
+ ]
194
+ });
195
+ /**
196
+ * Error situations:
197
+ * - sessionStorage is not available: return null
198
+ * - sessionStorage.getItem throws: catch exception, log error and return null
199
+ * - JSON.parse throws: catch exception, log error and return null
200
+ */
201
+ getItem(key) {
202
+ try {
203
+ if (typeof sessionStorage === "undefined") return null;
204
+ const item = this.area.getItem(key);
205
+ if (item) {
206
+ return JSON.parse(item);
207
+ }
208
+ return null;
209
+ } catch (error) {
210
+ this.logger.error(`SessionStorage.getItem failed for key "${key}"`, error);
211
+ return null;
212
+ }
213
+ }
214
+ /**
215
+ * Error situations:
216
+ * - sessionStorage is not available: return without writing
217
+ * - sessionStorage.setItem throws: catch exception and log error
218
+ * - JSON.stringify throws: catch exception and log error
219
+ */
220
+ setItem(key, value) {
221
+ try {
222
+ if (typeof sessionStorage === "undefined") return;
223
+ this.area.setItem(key, JSON.stringify(value));
224
+ } catch (error) {
225
+ this.logger.error(`SessionStorage.setItem failed for key "${key}"`, error);
226
+ this.logger.reportTelemetry?.("error", `sessionStorage.setItem failed: ${key}`);
227
+ }
228
+ }
229
+ /**
230
+ * Error situations:
231
+ * - sessionStorage is not available: return without writing
232
+ * - sessionStorage.removeItem throws: catch exception and log error
233
+ */
234
+ removeItem(key) {
235
+ try {
236
+ if (typeof sessionStorage === "undefined") return;
237
+ this.area.removeItem(key);
238
+ } catch (error) {
239
+ this.logger.error(`SessionStorage.removeItem failed for key "${key}"`, error);
240
+ }
241
+ }
242
+ };
243
+ export {
244
+ CookieStorage,
245
+ LocalStorage,
246
+ NamespacedStorage,
247
+ SessionStorage,
248
+ namespacedKey
249
+ };
@@ -0,0 +1,56 @@
1
+ // src/storage/migration.ts
2
+ import { namespacedKey } from "./index.mjs";
3
+ var STORAGE_SCHEMA_VERSION = 1;
4
+ var STORAGE_SCHEMA_KEY = "cbo_storage_schema";
5
+ var LOCAL_KEYS = [
6
+ "cbo_seq",
7
+ "cbo_sdk_config",
8
+ "cbo_experiments",
9
+ "cbo_client_env_handle",
10
+ "cbo_outbox",
11
+ "cbo_visibility_change_debug",
12
+ "cbo_pagehide_debug",
13
+ "cbo_session"
14
+ ];
15
+ var SESSION_KEYS = ["cbo_session_id", "cbo_tab_id"];
16
+ var COOKIE_KEYS = ["cbo_client_env_handle"];
17
+ function writeVerified(storage, key, value) {
18
+ storage.setItem(key, value);
19
+ return JSON.stringify(storage.getItem(key)) === JSON.stringify(value);
20
+ }
21
+ function migrateArea(storage, projectId, keys) {
22
+ const schema = storage.getItem(STORAGE_SCHEMA_KEY);
23
+ if (schema !== null) {
24
+ if (schema.version !== STORAGE_SCHEMA_VERSION || schema.projectId !== projectId) return false;
25
+ if (schema.complete === true) return true;
26
+ if (schema.complete !== false) return false;
27
+ } else {
28
+ if (keys.some((key) => storage.getItem(namespacedKey(key, projectId)) !== null)) {
29
+ return writeVerified(storage, STORAGE_SCHEMA_KEY, { version: STORAGE_SCHEMA_VERSION, projectId, complete: true });
30
+ }
31
+ if (!writeVerified(storage, STORAGE_SCHEMA_KEY, { version: STORAGE_SCHEMA_VERSION, projectId, complete: false })) {
32
+ return false;
33
+ }
34
+ }
35
+ for (const key of keys) {
36
+ const target = namespacedKey(key, projectId);
37
+ if (storage.getItem(target) !== null) continue;
38
+ const legacy = storage.getItem(key);
39
+ if (legacy !== null && !writeVerified(storage, target, legacy)) return false;
40
+ }
41
+ return writeVerified(storage, STORAGE_SCHEMA_KEY, { version: STORAGE_SCHEMA_VERSION, projectId, complete: true });
42
+ }
43
+ function migrateLegacyStorage(projectId, local, session, cookie, logger) {
44
+ try {
45
+ if (!migrateArea(local, projectId, LOCAL_KEYS)) return;
46
+ migrateArea(session, projectId, SESSION_KEYS);
47
+ if (cookie) migrateArea(cookie, projectId, COOKIE_KEYS);
48
+ } catch (error) {
49
+ logger.error("Failed to migrate legacy Observe storage", error);
50
+ }
51
+ }
52
+ export {
53
+ STORAGE_SCHEMA_KEY,
54
+ STORAGE_SCHEMA_VERSION,
55
+ migrateLegacyStorage
56
+ };
@@ -0,0 +1,132 @@
1
+ // src/store/event-store.ts
2
+ import { withWebLock } from "../readers/web-lock.mjs";
3
+ var OUTBOX_STORAGE_KEY = "cbo_outbox";
4
+ var OUTBOX_LOCK_NAME = "cbo_outbox";
5
+ var DEFAULT_MAX_ENTRIES = 500;
6
+ var DEFAULT_MAX_SERIALIZED_LENGTH = 512 * 1024;
7
+ function isOutboxEntry(value) {
8
+ if (!value || typeof value !== "object") {
9
+ return false;
10
+ }
11
+ const entry = value;
12
+ return typeof entry.id === "string" && entry.id !== "" && typeof entry.sessionId === "string" && typeof entry.flushAfter === "number" && Number.isFinite(entry.flushAfter) && typeof entry.attempts === "number" && Number.isFinite(entry.attempts) && typeof entry.event === "object" && entry.event !== null;
13
+ }
14
+ var EventStore = class {
15
+ constructor(storage, logger, maxEntries = DEFAULT_MAX_ENTRIES, maxSerializedLength = DEFAULT_MAX_SERIALIZED_LENGTH, lockName = OUTBOX_LOCK_NAME, storageKey = OUTBOX_STORAGE_KEY) {
16
+ this.storage = storage;
17
+ this.logger = logger;
18
+ this.maxEntries = maxEntries;
19
+ this.maxSerializedLength = maxSerializedLength;
20
+ this.lockName = lockName;
21
+ this.storageKey = storageKey;
22
+ this.entries = this.read();
23
+ }
24
+ entries = [];
25
+ /** Malformed entries are dropped on every read; report only once per instance to avoid log spam. */
26
+ reportedMalformed = false;
27
+ read() {
28
+ const raw = this.storage.getItem(this.storageKey);
29
+ if (!Array.isArray(raw)) {
30
+ return [];
31
+ }
32
+ const valid = raw.filter(isOutboxEntry);
33
+ if (valid.length < raw.length && !this.reportedMalformed) {
34
+ this.reportedMalformed = true;
35
+ this.logger.error(`EventStore dropped ${raw.length - valid.length} malformed outbox entries`);
36
+ this.logger.reportTelemetry?.("error", "event store dropped malformed outbox entries");
37
+ }
38
+ return valid;
39
+ }
40
+ mutate(fn) {
41
+ return withWebLock(
42
+ {
43
+ key: "browser-api:sdk.web-locks",
44
+ title: "Coordination of Observe data across open tabs",
45
+ accessNote: "Web Locks API (navigator.locks) on Observe's own lock names",
46
+ group: "Observe SDK state",
47
+ origin: "observe-internal",
48
+ fields: [
49
+ {
50
+ path: "result",
51
+ label: "Coordination between open tabs",
52
+ exposure: "none",
53
+ transform: { note: "own cbo_* lock names; serializes the SDK's storage updates across tabs" }
54
+ }
55
+ ]
56
+ },
57
+ this.lockName,
58
+ () => {
59
+ try {
60
+ let next = fn(this.read());
61
+ if (next.length > this.maxEntries) {
62
+ const evicted = next.length - this.maxEntries;
63
+ next = next.slice(next.length - this.maxEntries);
64
+ this.logger.error(`EventStore evicted ${evicted} oldest entries (cap ${this.maxEntries})`);
65
+ this.logger.reportTelemetry?.("error", "event store evicted entries (cap)");
66
+ }
67
+ next = this.enforceSizeBudget(next);
68
+ this.entries = next;
69
+ this.storage.setItem(this.storageKey, next);
70
+ } catch (error) {
71
+ this.logger.error("Failed to mutate event store", error);
72
+ this.logger.reportTelemetry?.("error", "failed to mutate event store");
73
+ }
74
+ }
75
+ );
76
+ }
77
+ /** Evict oldest entries until the serialized outbox fits {@link maxSerializedLength}. */
78
+ enforceSizeBudget(next) {
79
+ const sizes = next.map((entry) => JSON.stringify(entry).length);
80
+ let total = sizes.reduce((sum, size) => sum + size, next.length + 1);
81
+ let evict = 0;
82
+ while (total > this.maxSerializedLength && evict < next.length) {
83
+ total -= sizes[evict] + 1;
84
+ evict++;
85
+ }
86
+ if (evict === 0) {
87
+ return next;
88
+ }
89
+ this.logger.error(`EventStore evicted ${evict} oldest entries (size budget ${this.maxSerializedLength})`);
90
+ this.logger.reportTelemetry?.("error", "event store evicted entries (size budget)");
91
+ return next.slice(evict);
92
+ }
93
+ /** Append an entry. No-op if an entry with the same id already exists. */
94
+ add(entry) {
95
+ return this.mutate((current) => current.some((e) => e.id === entry.id) ? current : [...current, entry]);
96
+ }
97
+ /** Re-read from storage and return all entries (memory mirror refreshed). */
98
+ all() {
99
+ this.entries = this.read();
100
+ return this.entries;
101
+ }
102
+ /** Entries whose `flushAfter` has elapsed, in insertion order. */
103
+ getDue(now) {
104
+ return this.all().filter((entry) => entry.flushAfter <= now);
105
+ }
106
+ /** Remove entries by id (acknowledged or permanently dropped). */
107
+ remove(ids) {
108
+ if (ids.length === 0) {
109
+ return Promise.resolve();
110
+ }
111
+ const idSet = new Set(ids);
112
+ return this.mutate((current) => current.filter((entry) => !idSet.has(entry.id)));
113
+ }
114
+ /** Replace existing entries (matched by id) with updated copies, e.g. after bumping backoff. */
115
+ update(updated) {
116
+ if (updated.length === 0) {
117
+ return Promise.resolve();
118
+ }
119
+ const byId = new Map(updated.map((entry) => [entry.id, entry]));
120
+ return this.mutate((current) => current.map((entry) => byId.get(entry.id) ?? entry));
121
+ }
122
+ size() {
123
+ return this.all().length;
124
+ }
125
+ };
126
+ export {
127
+ DEFAULT_MAX_ENTRIES,
128
+ DEFAULT_MAX_SERIALIZED_LENGTH,
129
+ EventStore,
130
+ OUTBOX_LOCK_NAME,
131
+ OUTBOX_STORAGE_KEY
132
+ };
File without changes