@sagepilot-ai/web-events-addon 0.0.0-stage → 0.2.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.
package/dist/index.js ADDED
@@ -0,0 +1,2764 @@
1
+ // ../sdk-installation-client/src/index.ts
2
+ var SagepilotInstallationError = class extends Error {
3
+ constructor(code, message, options = {}) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "SagepilotInstallationError";
7
+ this.status = options.status;
8
+ this.cause = options.cause;
9
+ }
10
+ };
11
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
+ var VISITOR_ID = /^[A-Za-z0-9_-]{1,128}$/;
13
+ var TERMINAL = /* @__PURE__ */ new Set(["admitted", "duplicate", "disabled", "invalid", "retryable"]);
14
+ function safeURL(value) {
15
+ const url = new URL(value);
16
+ if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]", "10.0.2.2"].includes(url.hostname))) {
17
+ throw new Error("SDK endpoints require HTTPS and cannot contain credentials, queries or fragments.");
18
+ }
19
+ return url;
20
+ }
21
+ function validateSagepilotInstallationReceipt(value, requestedIDs) {
22
+ const results = value?.results;
23
+ if (!Array.isArray(results)) throw new Error("SDK admission receipt is missing.");
24
+ const requested = new Set(requestedIDs), seen = /* @__PURE__ */ new Set();
25
+ if (requested.size !== requestedIDs.length) throw new Error("SDK batch contains duplicate observation IDs.");
26
+ for (const item of results) {
27
+ if (!item || !requested.has(item.event_id) || seen.has(item.event_id) || !TERMINAL.has(item.status)) {
28
+ throw new Error("SDK admission receipt does not match the requested observations.");
29
+ }
30
+ seen.add(item.event_id);
31
+ }
32
+ return { results };
33
+ }
34
+ var SagepilotInstallationClient = class {
35
+ /** Validates the public installation binding without treating it as customer authentication. */
36
+ constructor(options) {
37
+ this.options = options;
38
+ this.current = null;
39
+ this.anonymousId = null;
40
+ this.pending = null;
41
+ this.startup = null;
42
+ this.generation = 0;
43
+ this.uploadRevision = 0;
44
+ this.transitioning = false;
45
+ this.owners = /* @__PURE__ */ new Map();
46
+ this.requests = /* @__PURE__ */ new Set();
47
+ const parts = options.key.split(":");
48
+ if (parts.length !== 2 || !parts.every((value) => UUID.test(value))) throw new Error("SDK key must be workspace_id:installation_id UUIDs.");
49
+ this.workspaceId = parts[0].toLowerCase();
50
+ this.installationId = parts[1].toLowerCase();
51
+ const host = safeURL(options.apiHost ?? "https://api.sagepilot.ai");
52
+ if (host.pathname !== "/") throw new Error("SDK apiHost must be a gateway origin.");
53
+ this.apiHost = host.origin;
54
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
55
+ this.storageKey = `sagepilot:sdk-session:${this.workspaceId}:${this.installationId}`;
56
+ }
57
+ /** Returns a copy of the active anonymous session without allowing mutation of capture ownership. */
58
+ session() {
59
+ return this.current ? { ...this.current } : null;
60
+ }
61
+ /** The merchant route owns the cookie path and forwards only the visitor cookie. */
62
+ get visitorCookiePath() {
63
+ return `/sagepilot/visitor/${this.workspaceId}/${this.installationId}`;
64
+ }
65
+ /** Resolves browser continuity on the storefront without sending an upload token or contact data. */
66
+ async syncVisitorCookie(anonymousId, analyticsAllowed, popupCompleted) {
67
+ if (this.options.platform !== "web" || typeof location === "undefined") throw new Error("Visitor cookies require a website.");
68
+ const endpoint = safeURL(location.origin + this.visitorCookiePath);
69
+ const value = await this.request(endpoint.href, {
70
+ method: "POST",
71
+ mode: "same-origin",
72
+ credentials: "same-origin",
73
+ body: JSON.stringify({ analytics_allowed: analyticsAllowed, anonymous_id: anonymousId, popup_completed: popupCompleted })
74
+ }, 2500);
75
+ if (!value || value.anonymous_id !== null && (typeof value.anonymous_id !== "string" || !VISITOR_ID.test(value.anonymous_id)) || typeof value.popup_completed !== "boolean" || typeof value.restored !== "boolean") {
76
+ throw new Error("Invalid SDK visitor cookie response.");
77
+ }
78
+ return value;
79
+ }
80
+ /** Reports whether this runtime still holds matching capture authority for a queued owner. */
81
+ hasCaptureOwner(owner) {
82
+ const session = this.owners.get(owner.session_id) ?? this.current;
83
+ return !!session && session.session_id === owner.session_id && session.customer_id === owner.customer_id;
84
+ }
85
+ /** Restores only the requested browser/device owner, then obtains a fresh server-authorized session. */
86
+ async start(anonymousId) {
87
+ if (!anonymousId.trim() || anonymousId.length > 128) throw new Error("SDK anonymous ID must contain 1\u2013128 characters.");
88
+ if (this.anonymousId && this.anonymousId !== anonymousId) throw new Error("Destroy this SDK client before changing its anonymous owner.");
89
+ this.anonymousId = anonymousId;
90
+ if (this.startup) return this.startup;
91
+ const generation = this.generation;
92
+ const startup = (async () => {
93
+ if (!this.current && this.options.storage) {
94
+ let restored = null;
95
+ try {
96
+ const raw = await this.options.storage.getItem(this.storageKey);
97
+ if (raw && raw.length <= 16384) restored = this.validateSession(JSON.parse(raw));
98
+ const previous = await this.options.storage.getItem(this.storageKey + ":owners");
99
+ if (previous && previous.length <= 65536) {
100
+ const entries = JSON.parse(previous);
101
+ if (Array.isArray(entries) && entries.length <= 4) for (const entry of entries) {
102
+ const owner = this.validateSession(entry);
103
+ this.owners.set(owner.session_id, owner);
104
+ }
105
+ }
106
+ } catch {
107
+ }
108
+ if (generation !== this.generation) throw new Error("SDK session restoration was cancelled.");
109
+ this.current = restored;
110
+ }
111
+ if (generation !== this.generation) throw new Error("SDK session startup was cancelled.");
112
+ return this.refresh(true);
113
+ })().finally(() => {
114
+ if (this.startup === startup) this.startup = null;
115
+ });
116
+ this.startup = startup;
117
+ return startup;
118
+ }
119
+ /** Renews the owner or, when allowed for web capture, starts fresh after an expired browser session. */
120
+ async refresh(recoverAnonymous = false) {
121
+ if (this.pending) return this.pending;
122
+ if (!this.anonymousId) throw new Error("Start the installation client before renewing it.");
123
+ const generation = this.generation, prior = this.current;
124
+ const pending = (async () => {
125
+ let renewing = prior;
126
+ let value;
127
+ try {
128
+ value = await this.requestSession(prior?.session_token);
129
+ } catch (error) {
130
+ if (!recoverAnonymous || !prior || this.options.platform !== "web" || generation !== this.generation || !(error instanceof SagepilotInstallationError) || error.status !== 401) throw error;
131
+ renewing = null;
132
+ value = await this.requestSession();
133
+ }
134
+ const next = this.validateSession(value);
135
+ if (generation !== this.generation) throw new Error("SDK session startup was cancelled.");
136
+ if (renewing && (next.session_id !== renewing.session_id || next.customer_id !== renewing.customer_id) || !renewing && (next.customer_id !== null || next.session_id === prior?.session_id)) {
137
+ throw new Error("SDK session renewal changed capture ownership.");
138
+ }
139
+ if (prior && !renewing) this.owners.delete(prior.session_id);
140
+ this.current = next;
141
+ this.remember(next);
142
+ try {
143
+ await this.options.storage?.setItem(this.storageKey, JSON.stringify(next));
144
+ } catch {
145
+ }
146
+ return { ...next };
147
+ })().finally(() => {
148
+ if (this.pending === pending) this.pending = null;
149
+ });
150
+ this.pending = pending;
151
+ return pending;
152
+ }
153
+ /** Requests policy for this browser or device with the original token when renewing. */
154
+ requestSession(token) {
155
+ return this.request(`${this.apiHost}/platform-api/v1/sdk/sessions`, {
156
+ method: "POST",
157
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
158
+ body: JSON.stringify({
159
+ key: `${this.workspaceId}:${this.installationId}`,
160
+ anonymous_id: this.anonymousId,
161
+ platform: this.options.platform,
162
+ ...this.options.appId ? { app_id: this.options.appId } : {}
163
+ })
164
+ });
165
+ }
166
+ /** Supplies a short-lived native upload authorization only for the original capture owner. */
167
+ async authorize(owner) {
168
+ let session = this.owners.get(owner.session_id) ?? this.current;
169
+ if (!session || owner.customer_id !== session.customer_id || owner.session_id !== session.session_id) throw new Error("SDK capture ownership changed.");
170
+ if (Date.parse(session.expires_at) - Date.now() < 6e4) {
171
+ if (session.session_id === this.current?.session_id) session = await this.refresh();
172
+ else {
173
+ const original = session;
174
+ session = this.validateSession(await this.requestSession(session.session_token));
175
+ if (session.session_id !== original.session_id || session.customer_id !== original.customer_id) throw new Error("Historical capture ownership changed.");
176
+ this.remember(session);
177
+ }
178
+ }
179
+ if (!session.collection_enabled || owner.session_id !== session.session_id || owner.customer_id !== session.customer_id) throw new Error("SDK collection is disabled or its owner changed.");
180
+ return { url: session.ingestion_url, headers: {
181
+ Authorization: `Bearer ${session.session_token}`,
182
+ "Content-Type": "application/json",
183
+ "X-Sagepilot-Sdk-Name": this.options.sdk.name,
184
+ "X-Sagepilot-Sdk-Version": this.options.sdk.version
185
+ } };
186
+ }
187
+ /** Posts one bounded immutable batch; collectors own backoff and retain missing/retryable receipts. */
188
+ async upload(observations, owner) {
189
+ if (!observations.length || observations.length > 100) throw new Error("SDK batches require 1\u2013100 observations.");
190
+ for (const item of observations) {
191
+ if (item.owner.session_id !== owner.session_id || item.owner.customer_id !== owner.customer_id) throw new Error("SDK batch mixes capture ownership.");
192
+ }
193
+ const revision = this.uploadRevision;
194
+ const auth = await this.authorize(owner);
195
+ if (revision !== this.uploadRevision) throw new Error("SDK upload was cancelled.");
196
+ const body = JSON.stringify({ schema_version: 1, observations });
197
+ if (new TextEncoder().encode(body).byteLength > 128 * 1024) throw new Error("SDK batch exceeds the upload limit.");
198
+ return validateSagepilotInstallationReceipt(await this.request(auth.url, { method: "POST", headers: auth.headers, body }), observations.map((item) => item.event_id));
199
+ }
200
+ /** Submits explicit popup contacts independently from ordinary observation payloads. */
201
+ async submitContact(input) {
202
+ if (!UUID.test(input.submission_id) || !Number.isSafeInteger(input.popup_revision) || input.popup_revision < 1 || typeof input.marketing_consent !== "boolean") throw new Error("Invalid popup submission.");
203
+ const result = await this.authenticated("/contact", input);
204
+ if (!result || !["recorded", "verification_required"].includes(result.status) || result.status === "verification_required" && (!result.challenge_id || !UUID.test(result.challenge_id) || !Number.isFinite(Date.parse(result.expires_at ?? "")))) throw new Error("Invalid popup submission response.");
205
+ return result;
206
+ }
207
+ /** Adopts customer authority only from the server's completed verification operation. */
208
+ async verifyContact(challengeId, code) {
209
+ if (!UUID.test(challengeId) || !/^\d{6}$/.test(code)) throw new Error("Enter the six-digit verification code.");
210
+ return this.transition("/contact/verify", { challenge_id: challengeId, code }, true);
211
+ }
212
+ /** Revokes the current identified grant and establishes a fresh anonymous capture owner. */
213
+ async logout() {
214
+ return this.transition("/logout", {}, false);
215
+ }
216
+ /** Keeps a bounded credential cache separate from observations so old anonymous batches retain their owner. */
217
+ remember(session) {
218
+ this.owners.set(session.session_id, { ...session });
219
+ while (this.owners.size > 4) this.owners.delete(this.owners.keys().next().value);
220
+ try {
221
+ void Promise.resolve(this.options.storage?.setItem(this.storageKey + ":owners", JSON.stringify([...this.owners.values()]))).catch(() => {
222
+ });
223
+ } catch {
224
+ }
225
+ }
226
+ /** Uses the current scoped credential for identity operations without attaching it to event records. */
227
+ async authenticated(path, body) {
228
+ const prior = this.current;
229
+ if (!prior) throw new Error("Start the installation client first.");
230
+ if (Date.parse(prior.expires_at) - Date.now() < 6e4) await this.refresh();
231
+ if (this.current?.session_id !== prior.session_id) throw new Error("SDK identity session changed.");
232
+ return this.request(`${this.apiHost}/platform-api/v1/sdk${path}`, { method: "POST", headers: { Authorization: `Bearer ${this.current.session_token}` }, body: JSON.stringify(body) });
233
+ }
234
+ /** Serializes explicit ownership transitions and rejects unrelated or stale server responses. */
235
+ async transition(path, body, identified) {
236
+ if (this.transitioning) throw new Error("An SDK identity operation is already running.");
237
+ this.transitioning = true;
238
+ const generation = this.generation, prior = this.current;
239
+ try {
240
+ const next = this.validateSession(await this.authenticated(path, body));
241
+ if (generation !== this.generation || !prior || next.session_id === prior.session_id || (identified ? next.customer_id === null : next.customer_id !== null)) throw new Error("Invalid SDK identity transition.");
242
+ this.generation++;
243
+ this.cancelPending();
244
+ this.pending = null;
245
+ if (path === "/logout") this.owners.delete(prior.session_id);
246
+ else this.remember(prior);
247
+ this.current = next;
248
+ this.remember(next);
249
+ try {
250
+ await this.options.storage?.setItem(this.storageKey, JSON.stringify(next));
251
+ } catch {
252
+ }
253
+ return { ...next };
254
+ } finally {
255
+ this.transitioning = false;
256
+ }
257
+ }
258
+ /** Cancels requests on permission/ownership boundaries while preserving the current session for safe renewal. */
259
+ cancelPending() {
260
+ this.uploadRevision++;
261
+ for (const controller of this.requests) controller.abort();
262
+ }
263
+ /** Fences in-flight session adoption and stops all outstanding requests. */
264
+ destroy() {
265
+ this.generation++;
266
+ this.cancelPending();
267
+ this.current = null;
268
+ this.anonymousId = null;
269
+ this.pending = null;
270
+ this.startup = null;
271
+ this.owners.clear();
272
+ }
273
+ /** Rejects foreign, identified or malformed session responses before credentials are used. */
274
+ validateSession(value) {
275
+ const session = value;
276
+ if (!session || session.workspace_id !== this.workspaceId || session.installation_id !== this.installationId || session.platform !== this.options.platform || session.anonymous_id !== this.anonymousId || session.customer_id !== null && (typeof session.customer_id !== "string" || !UUID.test(session.customer_id)) || !UUID.test(session.session_id) || typeof session.session_token !== "string" || session.session_token.length > 16384 || !session.session_token || !Number.isFinite(Date.parse(session.expires_at)) || typeof session.collection_enabled !== "boolean" || session.visitor_cookie_enabled !== void 0 && typeof session.visitor_cookie_enabled !== "boolean" || !Number.isSafeInteger(session.revision)) {
277
+ throw new Error("SDK session response has invalid scope or ownership.");
278
+ }
279
+ const url = safeURL(session.ingestion_url);
280
+ if (url.pathname !== "/events/v1/sdk/observations") throw new Error("SDK ingestion route is invalid.");
281
+ return { ...session };
282
+ }
283
+ /** Performs bounded SDK HTTP requests without retrying mutations or following credential redirects. */
284
+ async request(url, init, timeoutMs = 15e3) {
285
+ const controller = new AbortController();
286
+ this.requests.add(controller);
287
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
288
+ try {
289
+ const headers = new Headers(init.headers);
290
+ headers.set("Content-Type", "application/json");
291
+ headers.set("X-Sagepilot-Sdk-Name", this.options.sdk.name);
292
+ headers.set("X-Sagepilot-Sdk-Version", this.options.sdk.version);
293
+ const response = await this.fetchImpl(url, { ...init, headers, signal: controller.signal, redirect: "error", credentials: init.credentials ?? "omit" });
294
+ if (!response.ok) {
295
+ void response.body?.cancel().catch(() => {
296
+ });
297
+ throw new SagepilotInstallationError("invalid_response", `SDK request failed with HTTP ${response.status}.`, { status: response.status });
298
+ }
299
+ if (Number(response.headers.get("content-length") ?? 0) > 65536) throw new Error("SDK response exceeds limit.");
300
+ let text2 = "";
301
+ if (response.body?.getReader) {
302
+ const reader = response.body.getReader(), decoder = new TextDecoder();
303
+ let bytes = 0;
304
+ try {
305
+ for (; ; ) {
306
+ const part = await reader.read();
307
+ if (part.done) break;
308
+ bytes += part.value.byteLength;
309
+ if (bytes > 65536) {
310
+ await reader.cancel();
311
+ throw new Error("SDK response exceeds limit.");
312
+ }
313
+ text2 += decoder.decode(part.value, { stream: true });
314
+ }
315
+ text2 += decoder.decode();
316
+ } finally {
317
+ reader.releaseLock();
318
+ }
319
+ } else {
320
+ text2 = await response.text();
321
+ if (text2.length > 65536) throw new Error("SDK response exceeds limit.");
322
+ }
323
+ return JSON.parse(text2);
324
+ } catch (error) {
325
+ if (controller.signal.aborted || error instanceof TypeError) {
326
+ throw new SagepilotInstallationError("network_error", "SDK request could not reach the server.", { cause: error });
327
+ }
328
+ throw error;
329
+ } finally {
330
+ clearTimeout(timeout);
331
+ this.requests.delete(controller);
332
+ }
333
+ }
334
+ };
335
+ function createSagepilotInstallationClient(options) {
336
+ return new SagepilotInstallationClient(options);
337
+ }
338
+
339
+ // package.json
340
+ var name = "@sagepilot-ai/web-events-addon";
341
+ var version = "0.2.0";
342
+
343
+ // src/projection.ts
344
+ var sensitive = /password|passwd|secret|token|authorization|cookie|session|email|phone|contact|address|card|payment|cvv|ssn|dob|birth|customer|user|account|person|profile|name|description|message|content|text|search|query/i;
345
+ var compact = /^[A-Za-z0-9_.:-]{1,128}$/;
346
+ var routes = /* @__PURE__ */ new Map();
347
+ function project(value) {
348
+ const output = { fields: [], structure: [], limited: false };
349
+ let budget = 256;
350
+ function visit(input, path, depth) {
351
+ if (--budget < 0 || depth > 8 || output.fields.length >= 48) {
352
+ output.limited = true;
353
+ return;
354
+ }
355
+ if (input === null || typeof input === "boolean" || typeof input === "number") {
356
+ if (typeof input === "number" && (!Number.isFinite(input) || Math.abs(input) > Number.MAX_SAFE_INTEGER)) return;
357
+ output.fields.push({ path, type: input === null ? "null" : typeof input, value: input });
358
+ return;
359
+ }
360
+ if (typeof input === "string") {
361
+ const numericProductReference = /\.(?:product_id|variant_id|sku)$/.test(path) && /^\d{1,20}$/.test(input);
362
+ if (numericProductReference || compact.test(input) && !/^\+?\d{7,}$/.test(input) && !/^[A-Za-z0-9_-]{32,}$/.test(input)) output.fields.push({ path, type: "string", value: input });
363
+ else output.fields.push({ path, type: "string", omitted: "privacy_or_size" });
364
+ return;
365
+ }
366
+ if (Array.isArray(input)) {
367
+ output.structure.push({ path, type: "array" });
368
+ for (let index = 0; index < Math.min(input.length, 10); index++) visit(input[index], `${path}[${index}]`, depth + 1);
369
+ if (input.length > 10) output.limited = true;
370
+ return;
371
+ }
372
+ if (!input || typeof input !== "object" || Object.getPrototypeOf(input) !== Object.prototype) return;
373
+ output.structure.push({ path, type: "object" });
374
+ const keys = [];
375
+ for (const key in input) {
376
+ if (Object.prototype.hasOwnProperty.call(input, key)) keys.push(key);
377
+ if (keys.length > 32) break;
378
+ }
379
+ keys.sort();
380
+ if (keys.length > 32) output.limited = true;
381
+ for (const key of keys.slice(0, 32)) {
382
+ if (!/^[A-Za-z_][A-Za-z0-9_]{0,47}$/.test(key) || sensitive.test(key)) continue;
383
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
384
+ if (descriptor && "value" in descriptor) visit(descriptor.value, `${path}.${key}`, depth + 1);
385
+ }
386
+ }
387
+ visit(value, "$", 0);
388
+ return output;
389
+ }
390
+ function projectText(value) {
391
+ if (typeof value !== "string" || value.length > 65536 || !/^\s*[\[{]/.test(value.slice(0, 64))) return null;
392
+ try {
393
+ return project(JSON.parse(value));
394
+ } catch {
395
+ return null;
396
+ }
397
+ }
398
+ function routeFor(input) {
399
+ if (input.length > 2048) return Promise.resolve(null);
400
+ let url;
401
+ try {
402
+ url = new URL(input, globalThis.location?.href);
403
+ } catch {
404
+ return Promise.resolve(null);
405
+ }
406
+ if (!/^https?:$/.test(url.protocol) || url.username || url.password) return Promise.resolve(null);
407
+ const cacheKey = url.origin + url.pathname;
408
+ const existing = routes.get(cacheKey);
409
+ if (existing) return existing;
410
+ const fingerprint = (async () => {
411
+ if (!globalThis.crypto?.subtle) return null;
412
+ const segments = [];
413
+ for (const segment of url.pathname.split("/").filter(Boolean).slice(0, 12)) {
414
+ if (/^v\d{1,2}$/i.test(segment)) {
415
+ segments.push(segment.toLowerCase());
416
+ continue;
417
+ }
418
+ if (!/^[A-Za-z_-]{1,64}$/.test(segment)) {
419
+ segments.push(":id");
420
+ continue;
421
+ }
422
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(segment));
423
+ segments.push(":op_" + Array.from(new Uint8Array(digest)).slice(0, 8).map((byte) => byte.toString(16).padStart(2, "0")).join(""));
424
+ }
425
+ return url.origin + "/" + segments.join("/");
426
+ })().catch(() => null);
427
+ if (routes.size >= 128) routes.delete(routes.keys().next().value);
428
+ routes.set(cacheKey, fingerprint);
429
+ return fingerprint;
430
+ }
431
+
432
+ // src/product.ts
433
+ var privateKey = /pass|secret|token|auth|cookie|session|customer|buyer|user|account|profile|email|phone|mobile|contact|address|payment|credit|cvv|order|personal|customiz|monogram|option|review|message|comment|__proto__|constructor|prototype/i;
434
+ var identifier = /^(?:@?id|schema_product(?:ID|SKU)|productGroup(?:ID|SKU)|(?:product[_-]?)?sku|(?:product|catalog(?:ue)?|variant|item|article|part|model)[_-]?(?:id|code|number|ref)|mpn|gtin(?:8|12|13|14)?|upc|ean|isbn|reference|code)$/i;
435
+ var aliases = { product_id: /^(?:product[_-]?id|id)$/i, variant_id: /^(?:variant[_-]?id)$/i, sku: /^(?:product[_-]?)?sku$/i };
436
+ var publicField = /^(?:@type|@id|name|title|product_name|slug|id|productGroupID|product[_-]?(?:id|sku|url)|variant[_-]?id|sku|mpn|gtin\d*|url|image|images|image_url|imageUrl|offers|variants|hasVariant|price|priceValue|sale_price|price[_-]?Currency|currency|currencyId|currencyFormat|brand|category|color|size|material|availability|stockRemaining|available|selected|selectedVariantId)$/i;
437
+ function normalized(value) {
438
+ return typeof value === "string" ? value.normalize("NFKC").trim().toLowerCase().replace(/\s+/g, " ") : "";
439
+ }
440
+ function commerceAction(value) {
441
+ const label2 = value.normalize("NFKC").trim();
442
+ return label2.replace(/\s*[·|–—-]\s*(?:₹|\$|€|£|INR|USD|EUR|GBP)\s*(?:\d{1,8}|\d{1,3}(?:,\d{3}){1,2}|\d{1,2}(?:,\d{2}){1,2},\d{3})(?:\.\d{1,2})?\s*$/i, "").trim();
443
+ }
444
+ function sameVariant(left, right) {
445
+ return !!left && !!right && (left === right || left.replace(/^gid:\/\/shopify\/ProductVariant\/(\d+)$/, "$1") === right.replace(/^gid:\/\/shopify\/ProductVariant\/(\d+)$/, "$1"));
446
+ }
447
+ function nativeForms() {
448
+ const result = [], forms = [...document.querySelectorAll("form[action]")];
449
+ if (forms.length > 32) return null;
450
+ for (const form of forms) {
451
+ const action = publicURL(form.getAttribute("action"));
452
+ if (!action || new URL(action).origin !== location.origin || !/\/cart\/add\/?$/.test(new URL(action).pathname)) continue;
453
+ const inputs = [...form.querySelectorAll('input[type="hidden"][name="id"]')], products = [...form.querySelectorAll('input[type="hidden"][name="product-id"]')];
454
+ if (inputs.length !== 1 || products.length > 1) continue;
455
+ const variant = id(inputs[0].value ?? inputs[0].getAttribute("value"));
456
+ const product = id(products.length ? products[0].value ?? products[0].getAttribute("value") : form.closest("[data-product-id]")?.getAttribute("data-product-id"));
457
+ if (variant) result.push({ form, product, variant });
458
+ }
459
+ return result;
460
+ }
461
+ function labeledProductID() {
462
+ const labels = [...document.querySelectorAll("span,dt,th,label")], values = [];
463
+ if (labels.length > 1024) return;
464
+ for (const label2 of labels) {
465
+ const value = label2.nextElementSibling;
466
+ if (label2.childElementCount || normalized(label2.textContent) !== "product id:" || !value || value.childElementCount || [label2, value].some((node) => node.closest('header,footer,form,dialog,[role="dialog"],[hidden],[aria-hidden="true"],[data-sagepilot-popup],[itemprop="review"],[itemtype$="/Review"],[data-review-id]'))) continue;
467
+ const product = id(value.textContent?.trim());
468
+ if (product) values.push(product);
469
+ }
470
+ return values.length === 1 ? values[0] : void 0;
471
+ }
472
+ function publicURL(value, base = location.href, image = false) {
473
+ if (typeof value !== "string" || value.length > 1024) return;
474
+ try {
475
+ const url = new URL(value, base);
476
+ const keys = [...url.searchParams.keys()];
477
+ if (url.username || url.password || /[<>\x00-\x1f\x7f@]/.test(decodeURIComponent(url.href)) || new Set(keys).size !== keys.length || keys.some((key) => /token|signature|auth|credential|password|secret|policy|expires|x-amz|^sig$|^key$/i.test(key))) return;
478
+ if (image && /\/_next\/image$/.test(url.pathname) && keys.every((key) => key === "url" || key === "w" && /^\d{1,5}$/.test(url.searchParams.get(key)) || key === "q" && /^\d{1,3}$/.test(url.searchParams.get(key)) && Number(url.searchParams.get(key)) <= 100)) return publicURL(url.searchParams.get("url"), base, true);
479
+ if (image && keys.length) {
480
+ if (!/\.(?:jpe?g|png|webp|avif|gif|svg)$/i.test(url.pathname) || !keys.every((key) => {
481
+ const value2 = url.searchParams.get(key);
482
+ return /^(?:w|h|width|height)$/.test(key) ? /^\d{1,5}$/.test(value2) && Number(value2) > 0 : /^(?:q|quality)$/.test(key) ? /^\d{1,3}$/.test(value2) && Number(value2) > 0 && Number(value2) <= 100 : key === "v" ? /^\d{1,20}$/.test(value2) : key === "dpr" ? /^(?:[1-7](?:\.\d{1,2})?|0\.[1-9]\d?|8)$/.test(value2) : /^(?:f|format)$/.test(key) ? /^(?:jpe?g|png|webp|avif|gif|svg|xxs|xs|s|m|l|xl|xxl|u|xu)$/.test(value2) : key === "fit" ? /^(?:cover|contain|fill|inside|outside|max|crop)$/.test(value2) : key === "auto" ? /^(?:format|compress|format,compress|compress,format)$/.test(value2) : key === "crop" && /^(?:center|faces|top|bottom|left|right)$/.test(value2);
483
+ })) return;
484
+ url.search = "";
485
+ }
486
+ if (!image && keys.length && (keys.length > 1 || !keys.every((key) => /^(?:variant|variant_id)$/.test(key) && !!id(url.searchParams.get(key))))) return;
487
+ if (!image && /^(?:#content|#product|#variant)?$/.test(url.hash)) url.hash = "";
488
+ if ((url.protocol === "https:" || url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) && !url.username && !url.password && !url.hash && !/[<>\x00-\x1f\x7f@]/.test(decodeURIComponent(url.href))) return url.href;
489
+ } catch {
490
+ }
491
+ }
492
+ function documentURL() {
493
+ try {
494
+ const url = new URL(location.href);
495
+ for (const key of [...url.searchParams.keys()]) if (/^utm_(?:source|medium|campaign|term|content|id|source_platform|creative_format|marketing_tactic)$/.test(key)) url.searchParams.delete(key);
496
+ return publicURL(url.href);
497
+ } catch {
498
+ return;
499
+ }
500
+ }
501
+ function productSourceURL(value) {
502
+ const url = publicURL(value);
503
+ return url && !/(?:^|[/_.-])(?:customers?|buyers?|users?|accounts?|profiles?|orders?|checkout|auth|login|sessions?|private|tokens?|contacts?|addresses|payments?)(?=[/_.-]|$)/i.test(decodeURIComponent(new URL(url).pathname)) ? url : void 0;
504
+ }
505
+ function scalar(value) {
506
+ if (value === null || typeof value === "boolean") return value;
507
+ if (typeof value === "number") return Number.isFinite(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER ? value : void 0;
508
+ if (typeof value !== "string" || value.length > 256 || /[<>\x00-\x1f\x7f]|@|https?:|\+?\d[\d\s().-]{5,}\d|[A-Za-z0-9_-]{32,}|\b(?:bearer|sk_live_|sk_test_|ghp_)|personalized|personalised|monogram|recommended for|your name/i.test(value.normalize("NFKC"))) return;
509
+ return value;
510
+ }
511
+ function id(value) {
512
+ if (typeof value === "number") return Number.isSafeInteger(value) ? String(value) : void 0;
513
+ return typeof value === "string" && /^[A-Za-z0-9_:/+=.-]{1,128}$/.test(value) && !/^(?:bearer|sk_live_|sk_test_|ghp_|eyJ[A-Za-z0-9_-]+\.)/i.test(value) ? value : void 0;
514
+ }
515
+ function links(facts, base) {
516
+ const image = facts.image_url ?? facts.imageUrl ?? facts.image ?? facts.images, offers = Array.isArray(facts.offers) ? facts.offers : [facts.offers];
517
+ const assets = Array.isArray(image) ? image : image === void 0 ? [] : [image], rawImages = [];
518
+ let complete = assets.length <= 16;
519
+ for (const value of assets.slice(0, 16)) {
520
+ const uri = value && typeof value === "object" ? value.url ?? value.contentUrl : value;
521
+ if (typeof uri === "string" && /^(?:https?:\/\/|\/)/i.test(uri) && publicURL(uri, base, true)) rawImages.push(new URL(uri, base).href);
522
+ else complete = false;
523
+ }
524
+ const urls = offers.map((value) => value && typeof value === "object" ? publicURL(value.url, base) : void 0).filter(Boolean);
525
+ return {
526
+ rawImages: complete ? rawImages : void 0,
527
+ url: publicURL(facts.url ?? facts.product_url ?? facts.productUrl ?? (typeof facts["@id"] === "string" && /[/?]/.test(facts["@id"]) ? facts["@id"] : void 0), base) ?? (urls.length === offers.length && new Set(urls).size === 1 ? urls[0] : void 0),
528
+ sku: id(facts.sku ?? facts.productSku ?? facts.product_sku),
529
+ images: (Array.isArray(image) ? image.slice(0, 8) : [image]).map((value) => {
530
+ const uri = value && typeof value === "object" ? value.url ?? value.contentUrl : value;
531
+ return typeof uri === "string" && /^(?:https?:\/\/|\/)/i.test(uri) ? publicURL(uri, base, true) : void 0;
532
+ }).filter((value) => !!value)
533
+ };
534
+ }
535
+ function parseProductJSON(value, limit = 262144) {
536
+ if (value.length > limit || new TextEncoder().encode(value).length > limit) return null;
537
+ try {
538
+ return JSON.parse(value, (key, item, context) => {
539
+ if (identifier.test(key) && typeof item === "number") return context?.source && /^-?\d+$/.test(context.source) ? context.source : id(item);
540
+ return item;
541
+ });
542
+ } catch {
543
+ return null;
544
+ }
545
+ }
546
+ function flightRows(value) {
547
+ const bytes = new TextEncoder().encode(value), decoder = new TextDecoder(), rows = [];
548
+ let offset = 0;
549
+ while (offset < bytes.length) {
550
+ if (bytes[offset] === 10) {
551
+ offset++;
552
+ continue;
553
+ }
554
+ const colon = bytes.indexOf(58, offset);
555
+ if (colon < offset || colon - offset > 16 || !/^[\da-f]*$/i.test(decoder.decode(bytes.subarray(offset, colon)))) return [];
556
+ const start = colon + 1, comma = bytes.indexOf(44, start), tag = String.fromCharCode(bytes[start]);
557
+ if (/^[A-Za-z]$/.test(tag) && comma > start + 1 && comma - start < 16 && /^[\da-f]+$/i.test(decoder.decode(bytes.subarray(start + 1, comma)))) {
558
+ const size = parseInt(decoder.decode(bytes.subarray(start + 1, comma)), 16);
559
+ if (comma + 1 + size > bytes.length) return [];
560
+ offset = comma + 1 + size;
561
+ continue;
562
+ }
563
+ const newline = bytes.indexOf(10, start), end = newline < 0 ? bytes.length : newline;
564
+ if (bytes[start] === 91 || bytes[start] === 123) {
565
+ const row = parseProductJSON(decoder.decode(bytes.subarray(start, end)));
566
+ if (row === null) return [];
567
+ rows.push(row);
568
+ }
569
+ offset = end + 1;
570
+ }
571
+ return rows;
572
+ }
573
+ function indexedProducts(graph, source, title, schemas) {
574
+ if (!Array.isArray(graph) || graph.length > 8192 || !Array.isArray(graph[0]) || graph[0][0] !== "ShallowReactive") return [];
575
+ const values = graph;
576
+ function ref(index) {
577
+ return typeof index === "number" && Number.isInteger(index) && index >= 0 && index < values.length ? values[index] : void 0;
578
+ }
579
+ const tail = new URL(source).pathname.split("/").filter(Boolean).pop(), result = [];
580
+ for (const row of graph) {
581
+ if (!row || typeof row !== "object" || Array.isArray(row) || !Object.prototype.hasOwnProperty.call(row, "productVariants") || normalized(ref(row.name)) !== title) continue;
582
+ const variants = ref(row.productVariants);
583
+ if (!Array.isArray(variants) || variants.length > 16) return [];
584
+ for (const index of variants) {
585
+ const value = ref(index);
586
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
587
+ const variant = value;
588
+ const product_id = id(ref(row.id)), variant_id = id(ref(variant.id)), sku = id(ref(variant.sku)), name2 = ref(variant.name);
589
+ if (!product_id || !variant_id || !sku || variant_id !== tail || normalized(name2) !== title) continue;
590
+ const matching = schemas.filter((value2) => {
591
+ const offers = Array.isArray(value2.offers) ? value2.offers : [value2.offers], declared = [value2.url, value2.product_url, value2.productUrl, value2["@id"], ...offers.map((row2) => row2 && typeof row2 === "object" ? row2.url : void 0)].filter((uri) => uri !== void 0);
592
+ return declared.every((uri) => publicURL(uri, source) === source) && normalized(value2.name) === title && [variant_id, sku].includes(String(value2.sku));
593
+ });
594
+ if (matching.length !== 1) return [];
595
+ result.push({ product_id, variant_id, sku, name: name2, url: source, image: links(matching[0], source).images[0], schema_productSKU: String(matching[0].sku) });
596
+ }
597
+ }
598
+ return result.length === 1 ? result : [];
599
+ }
600
+ function project2(row, source, source_ref, variant) {
601
+ if (Array.isArray(row.offers) && row.offers.length > 16 || Array.isArray(variant?.offers) && variant.offers.length > 16) return null;
602
+ const identifiers = {}, variant_identifiers = {}, attributes = {};
603
+ let limited = false;
604
+ for (const [record, ids] of [[row, identifiers], ...variant ? [[variant, variant_identifiers]] : []]) {
605
+ const keys = Object.keys(record);
606
+ if (keys.length > 48) limited = true;
607
+ for (const key of keys.slice(0, 48)) {
608
+ if (!/^[A-Za-z_@][A-Za-z0-9_@-]{0,63}$/.test(key) || privateKey.test(key) || ["variants", "offers", "@type", "@context"].includes(key) || source !== "catalog_response" && !identifier.test(key) && !publicField.test(key)) continue;
609
+ if (record === row && variant && /^(?:image|images|image_url|imageUrl)$/.test(key) && links(variant, source_ref).images.length) continue;
610
+ const value = record[key];
611
+ if (identifier.test(key)) {
612
+ const exact = id(value), target = record === row && /^variant[_-]?(?:id|code|number|ref)$/i.test(key) ? variant_identifiers : ids;
613
+ if (exact !== void 0 && Object.keys(target).length < 32) target[key] = exact;
614
+ else if (exact !== void 0) limited = true;
615
+ continue;
616
+ }
617
+ if (Object.keys(attributes).length >= 32) {
618
+ limited = true;
619
+ continue;
620
+ }
621
+ if (Array.isArray(value)) {
622
+ if (value.length > 16) {
623
+ limited = true;
624
+ continue;
625
+ }
626
+ const projected = value.map(scalar);
627
+ if (projected.every((item) => item !== void 0)) attributes[key] = projected;
628
+ } else {
629
+ const projected = scalar(value);
630
+ if (projected !== void 0) attributes[key] = projected;
631
+ }
632
+ }
633
+ }
634
+ const snapshot = { metadata: {
635
+ resolution: "resolved",
636
+ identifiers,
637
+ variant_identifiers,
638
+ attributes,
639
+ provenance: { source, source_ref, match: "unresolved" },
640
+ limited
641
+ } };
642
+ for (const [field, pattern] of Object.entries(aliases)) {
643
+ const variantSKU = Object.keys(variant_identifiers).some((key) => /^sku$/i.test(key));
644
+ if (field === "sku" && variant && !variantSKU && variant_identifiers.id !== void 0 && !Object.entries(identifiers).some(([key, value]) => aliases.sku.test(key) && value === variant_identifiers.id)) continue;
645
+ const pool = field === "product_id" ? identifiers : field === "sku" ? variantSKU ? variant_identifiers : identifiers : { ...identifiers, ...variant_identifiers };
646
+ const values = Object.keys(pool).filter((key) => pattern.test(key)).map((key) => pool[key]);
647
+ if (field === "variant_id" && !values.length && variant_identifiers.id !== void 0) values.push(variant_identifiers.id);
648
+ const unique = [...new Set(values)];
649
+ if (unique.length > 1) return null;
650
+ if (unique.length) snapshot[field] = unique[0];
651
+ }
652
+ const facts = { ...row, ...variant }, name2 = scalar(row.name ?? row.title ?? row.product_name ?? facts.name ?? facts.title), offer = facts.offers;
653
+ const offers = (Array.isArray(offer) ? offer : [offer]).filter((value) => value && typeof value === "object");
654
+ if (offers.some((value) => Array.isArray(value.priceSpecification) && value.priceSpecification.length > 16)) return null;
655
+ if (typeof name2 === "string" && name2.length <= 160) snapshot.name = name2;
656
+ const specifications = offers.map((value) => {
657
+ const specs = (Array.isArray(value.priceSpecification) ? value.priceSpecification : [value.priceSpecification]).filter((spec) => spec && typeof spec === "object");
658
+ return specs.filter((spec) => !spec.priceType || /(?:^|\/)SalePrice$/.test(String(spec.priceType)));
659
+ });
660
+ const prices = offers.map((value, n) => value.price ?? (value.lowPrice !== void 0 && value.lowPrice === value.highPrice ? value.lowPrice : void 0) ?? (specifications[n].length && specifications[n].every((spec) => spec.price === specifications[n][0].price) ? specifications[n][0].price : void 0));
661
+ const money = facts.price && typeof facts.price === "object" && !Array.isArray(facts.price) ? facts.price : void 0;
662
+ const amount = facts.sale_price ?? facts.priceValue ?? (money ? /^[A-Z]{3}$/.test(String(money.currencyCode)) ? money.amount : void 0 : facts.price) ?? (prices.length && prices.every((value) => value === prices[0]) ? prices[0] : void 0);
663
+ if ((typeof amount === "number" || typeof amount === "string" && /^\d{1,10}(?:\.\d{1,4})?$/.test(amount)) && Number.isFinite(Number(amount)) && Number(amount) >= 0 && Number(amount) <= Number.MAX_SAFE_INTEGER) snapshot.price = Number(amount);
664
+ const currencies = offers.map((value, n) => value.priceCurrency ?? (specifications[n].length && specifications[n].every((spec) => spec.priceCurrency === specifications[n][0].priceCurrency) ? specifications[n][0].priceCurrency : void 0)).filter(Boolean);
665
+ const currency = facts.currency ?? facts.currencyId ?? facts.price_currency ?? facts.priceCurrency ?? money?.currencyCode ?? (new Set(currencies).size === 1 ? currencies[0] : void 0);
666
+ if (typeof currency === "string" && /^[A-Z]{3}$/.test(currency)) snapshot.currency = currency;
667
+ if (money && [facts.currency, facts.currencyId, facts.price_currency, facts.priceCurrency, ...currencies].some((value) => value !== void 0 && value !== money.currencyCode)) {
668
+ delete snapshot.price;
669
+ delete snapshot.currency;
670
+ }
671
+ const { url, images, rawImages } = links(facts, source_ref);
672
+ if (url) snapshot.url = url;
673
+ if (images.length) snapshot.image_url = images[0];
674
+ if (!Object.keys(identifiers).length && !Object.keys(variant_identifiers).length || typeof name2 !== "string" || snapshot.price === void 0 && !images.length) return null;
675
+ if (new TextEncoder().encode(JSON.stringify(snapshot)).length > 8192) return null;
676
+ return { snapshot, ref: crypto.randomUUID(), images, rawImages, url, sku: snapshot.sku, variant: snapshot.variant_id, expires: Date.now() + 9e5 };
677
+ }
678
+ function entryKey(value) {
679
+ const identity = value.snapshot.metadata.provenance.source === "public_hydration" && value.snapshot.product_id ? [value.snapshot.product_id, value.snapshot.variant_id, value.snapshot.sku] : [Object.entries(value.snapshot.metadata.identifiers).sort(), Object.entries(value.snapshot.metadata.variant_identifiers).sort()];
680
+ return JSON.stringify([identity, value.snapshot.metadata.provenance.source_ref, value.blocked && !Object.keys(value.snapshot.metadata.identifiers).length ? [value.sku, value.url, value.images, value.ids] : null]);
681
+ }
682
+ function createProductIndex(options) {
683
+ let records = [], limited = false, pageLimited = false, stopped = false, active = null, jsonPage = "", jsonPageTime = 0;
684
+ const generations = /* @__PURE__ */ new WeakMap(), startup = [];
685
+ const listeners = /* @__PURE__ */ new Set();
686
+ const timeout = setTimeout(() => {
687
+ if (startup.length || listeners.size) limited = true;
688
+ startup.length = 0;
689
+ for (const stop of [...listeners]) stop();
690
+ listeners.clear();
691
+ }, 1e4);
692
+ function pause() {
693
+ active = null;
694
+ startup.length = 0;
695
+ for (const stop of [...listeners]) stop();
696
+ }
697
+ function observe(value, source, kind = "catalog_response") {
698
+ const epoch = options.epoch(), source_ref = productSourceURL(source);
699
+ if (!epoch || stopped || !source_ref) return;
700
+ if (typeof value === "string") value = parseProductJSON(value);
701
+ const filename = new URL(source_ref).pathname.split("/").pop() ?? "";
702
+ const queue = [{ value, product: kind !== "json_ld" && /(?:^|[-_.])(?:products?|catalog)(?:[-_.]|$)/i.test(filename), depth: 0 }];
703
+ let visited = 0;
704
+ while (queue.length && visited++ < 512) {
705
+ const item = queue.shift();
706
+ if (item.depth > 5) {
707
+ limited = true;
708
+ continue;
709
+ }
710
+ if (Array.isArray(item.value)) {
711
+ for (const child of item.value.slice(0, 128)) queue.push({ value: child, product: item.product, depth: item.depth + 1 });
712
+ if (item.value.length > 128) limited = true;
713
+ continue;
714
+ }
715
+ if (!item.value || typeof item.value !== "object") continue;
716
+ const row = item.value, types = Array.isArray(row["@type"]) ? row["@type"] : [row["@type"]];
717
+ if (types.includes("ProductGroup") && Array.isArray(row.hasVariant)) {
718
+ if (row.hasVariant.length > 16) {
719
+ limited = true;
720
+ continue;
721
+ }
722
+ const { id: groupID, productID: groupProductID, hasVariant, ...group } = row;
723
+ const parent = Object.fromEntries(Object.entries(group).filter(([key]) => !aliases.product_id.test(key) && !aliases.sku.test(key) && !/^(?:url|@id|product[_-]?url)$/i.test(key)));
724
+ for (const child of hasVariant) if (child && typeof child === "object") queue.push({ value: { ...parent, ...child, url: links(child, source_ref).url, productGroupID: row.productGroupID ?? groupProductID ?? groupID, productGroupSKU: row.sku, "@type": "Product" }, product: true, depth: item.depth + 1, group: { url: links(row, source_ref).url, name: normalized(row.name) } });
725
+ continue;
726
+ }
727
+ const product = types.some((type) => ["Product", "IndividualProduct"].includes(String(type))) || item.product;
728
+ if (product && Object.keys(row).some((key) => identifier.test(key))) {
729
+ const variants = Array.isArray(row.variants) ? row.variants : [void 0];
730
+ if (variants.length > 16) {
731
+ limited = true;
732
+ continue;
733
+ }
734
+ if (variants.some((value2) => value2 !== void 0 && (!value2 || typeof value2 !== "object" || Array.isArray(value2)))) continue;
735
+ for (const variant of variants.slice(0, 16)) {
736
+ const selected = variant && typeof variant === "object" ? variant : void 0;
737
+ let entry = project2(row, kind, source_ref, selected);
738
+ if (!entry) {
739
+ const facts = { ...row, ...selected }, evidence = links(facts, source_ref);
740
+ const ids = Object.keys(facts).filter((key) => aliases.product_id.test(key)).map((key) => id(facts[key])).filter((value2) => value2 !== void 0);
741
+ if (!evidence.sku && !evidence.url && !evidence.images.length && !ids.length) continue;
742
+ entry = { ...evidence, ids, variant: id(selected?.variant_id ?? selected?.variantId ?? selected?.id ?? row.variant_id ?? row.variantId), ref: crypto.randomUUID(), blocked: true, expires: Date.now() + 9e5, snapshot: { metadata: { resolution: "ambiguous", identifiers: {}, variant_identifiers: {}, attributes: {}, provenance: { source: kind, source_ref, match: "unresolved" }, limited: false } } };
743
+ }
744
+ if (entry.snapshot.metadata.limited) limited = true;
745
+ entry.group = item.group;
746
+ const signature = entryKey(entry);
747
+ const previous = records.find((existing) => entryKey(existing) === signature);
748
+ if (["json_ld", "public_hydration"].includes(kind) && previous && !entry.blocked) {
749
+ const conflict = previous.blocked || ["price", "currency", "url"].some((key) => previous.snapshot[key] !== void 0 && entry.snapshot[key] !== void 0 && previous.snapshot[key] !== entry.snapshot[key]) || ["identifiers", "variant_identifiers"].some((key) => Object.entries(previous.snapshot.metadata[key]).some(([name2, value2]) => entry.snapshot.metadata[key][name2] !== void 0 && entry.snapshot.metadata[key][name2] !== value2));
750
+ const strong = previous.url ? previous : entry, weak = strong === entry ? previous : entry;
751
+ entry.snapshot = { ...weak.snapshot, ...strong.snapshot, metadata: { ...strong.snapshot.metadata, identifiers: { ...weak.snapshot.metadata.identifiers, ...strong.snapshot.metadata.identifiers }, variant_identifiers: { ...weak.snapshot.metadata.variant_identifiers, ...strong.snapshot.metadata.variant_identifiers }, attributes: { ...weak.snapshot.metadata.attributes, ...strong.snapshot.metadata.attributes } } };
752
+ entry.url = strong.url ?? weak.url;
753
+ entry.group = strong.group ?? weak.group;
754
+ entry.images = [.../* @__PURE__ */ new Set([...strong.images, ...weak.images])];
755
+ entry.rawImages = strong.rawImages && weak.rawImages ? [.../* @__PURE__ */ new Set([...strong.rawImages, ...weak.rawImages])] : void 0;
756
+ if (entry.images.length > 16 || (entry.rawImages?.length ?? 0) > 16 || Object.keys(entry.snapshot.metadata.attributes).length > 32 || Object.keys(entry.snapshot.metadata.identifiers).length > 32 || Object.keys(entry.snapshot.metadata.variant_identifiers).length > 32) limited = true;
757
+ if (conflict || limited) {
758
+ entry.blocked = true;
759
+ entry.snapshot.metadata.resolution = "ambiguous";
760
+ }
761
+ }
762
+ entry.ref = previous?.ref ?? entry.ref;
763
+ records = records.filter((existing) => entryKey(existing) !== signature && existing.expires > Date.now());
764
+ if (records.length >= 128) {
765
+ limited = true;
766
+ continue;
767
+ }
768
+ records.push(entry);
769
+ }
770
+ }
771
+ for (const key of ["products", "product", "catalog", "data", "@graph", "items", "itemListElement", "item", "mainEntity"]) if (row[key] !== void 0 && (key !== "items" || product)) queue.push({ value: row[key], product: product || ["products", "product", "catalog"].includes(key), depth: item.depth + 1 });
772
+ }
773
+ if (queue.length) limited = true;
774
+ }
775
+ function readPage() {
776
+ function incomplete() {
777
+ pageLimited = true;
778
+ jsonPage = "";
779
+ }
780
+ const currentDocument = documentURL(), source = currentDocument && productSourceURL(currentDocument), canonical = publicURL(document.querySelector('link[rel="canonical"]')?.getAttribute("href")) ?? source;
781
+ if (!source || !canonical) return;
782
+ const native = nativeForms();
783
+ if (!native) {
784
+ incomplete();
785
+ return;
786
+ }
787
+ const scripts = [];
788
+ let work = 0, hydrationWork = 0, schemaCount = 0, hydrationCount = 0, hydrationLimited = false, hash = 2166136261, second = 5381;
789
+ for (const script of document.scripts) {
790
+ if (script.src || script.type === "application/json" && privateKey.test(script.id)) continue;
791
+ const text2 = script.textContent ?? "";
792
+ if (script.type !== "application/ld+json" && script.type !== "application/json" && script.id !== "__NEXT_DATA__" && !text2.slice(0, 128).includes("self.__next_f")) continue;
793
+ if (script.type === "application/ld+json") {
794
+ if (++schemaCount > 256 || text2.length > 262144 || (work += new TextEncoder().encode(text2).length) > 1048576) {
795
+ incomplete();
796
+ return;
797
+ }
798
+ } else if (++hydrationCount > 256 || hydrationLimited || text2.length > 1048576 - hydrationWork || (hydrationWork += new TextEncoder().encode(text2).length) > 1048576) {
799
+ hydrationLimited = true;
800
+ continue;
801
+ }
802
+ scripts.push(script);
803
+ for (let n = 0; n < text2.length; n++) {
804
+ const code = text2.charCodeAt(n);
805
+ hash = Math.imul(hash ^ code, 16777619) >>> 0;
806
+ second = (Math.imul(second, 33) ^ code) >>> 0;
807
+ }
808
+ }
809
+ const signature = [source, canonical, document.querySelector("h1")?.textContent, document.readyState, scripts.length, hydrationLimited, hash, second, JSON.stringify(native.map(({ product, variant }) => [product, variant]))].join(":");
810
+ if (jsonPage === signature && Date.now() - jsonPageTime < 9e5) return;
811
+ pageLimited = false;
812
+ jsonPage = signature;
813
+ jsonPageTime = Date.now();
814
+ const previousRefs = new Map(records.filter((entry) => entry.expires > Date.now()).map((entry) => [entryKey(entry), entry.ref]));
815
+ records = records.filter((entry) => !["json_ld", "public_hydration"].includes(entry.snapshot.metadata.provenance.source) || entry.snapshot.metadata.provenance.source_ref !== source);
816
+ const schemas = [], roots = [], chunks = [], variantSources = [];
817
+ let bytes = 0, indexed, indexedCount = 0;
818
+ for (const script of scripts) {
819
+ const text2 = script.textContent ?? "";
820
+ if (script.type === "application/ld+json") {
821
+ if (new TextEncoder().encode(text2).length > 262144) {
822
+ incomplete();
823
+ return;
824
+ }
825
+ const pending2 = [parseProductJSON(text2)];
826
+ for (let n = 0; pending2.length && n < 256; n++) {
827
+ const value = pending2.shift();
828
+ if (Array.isArray(value)) {
829
+ if (value.length > 64) {
830
+ incomplete();
831
+ return;
832
+ }
833
+ pending2.push(...value);
834
+ continue;
835
+ }
836
+ if (!value || typeof value !== "object") continue;
837
+ const row = value, types = Array.isArray(row["@type"]) ? row["@type"] : [row["@type"]];
838
+ if (types.some((type) => ["Product", "ProductGroup", "IndividualProduct"].includes(String(type)))) schemas.push(row);
839
+ for (const key of ["@graph", "mainEntity", "itemListElement", "item"]) if (row[key]) pending2.push(row[key]);
840
+ }
841
+ if (pending2.length) {
842
+ incomplete();
843
+ return;
844
+ }
845
+ } else if (hydrationLimited) continue;
846
+ else if (script.type === "application/json" && script.id === "__NUXT_DATA__") {
847
+ bytes += new TextEncoder().encode(text2).length;
848
+ if (++indexedCount > 1) hydrationLimited = true;
849
+ try {
850
+ indexed = JSON.parse(text2);
851
+ } catch {
852
+ hydrationLimited = true;
853
+ }
854
+ } else if (script.type === "application/json" && script.id !== "__NEXT_DATA__") {
855
+ bytes += new TextEncoder().encode(text2).length;
856
+ const parsed = parseProductJSON(text2), variants = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" ? parsed.variants : void 0;
857
+ if (Array.isArray(variants) && variants.length <= 16 && variants.every((row) => row && typeof row === "object" && !Array.isArray(row) && id(row.id))) {
858
+ const root = parsed && !Array.isArray(parsed) && typeof parsed === "object" && parsed.handle === new URL(canonical).pathname.split("/").pop() ? parsed : void 0;
859
+ const declared = root ? [...new Set(Object.entries(root).filter(([key]) => aliases.product_id.test(key)).map(([, value]) => id(value)).filter((value) => value !== void 0))] : [];
860
+ if (declared.length > 1) {
861
+ pageLimited = true;
862
+ continue;
863
+ }
864
+ const parentIDs = variants.map((row) => id(row.featured_image?.product_id)), parent = root ? declared[0] : parentIDs.every((value) => value && value === parentIDs[0]) ? parentIDs[0] : void 0;
865
+ if (parent) variantSources.push({ rows: variants, parent });
866
+ }
867
+ } else if (script.id === "__NEXT_DATA__" && text2.length <= 1048576) {
868
+ bytes += new TextEncoder().encode(text2).length;
869
+ roots.push(parseProductJSON(text2, 1048576));
870
+ } else if (text2.includes("self.__next_f.push")) for (const match of text2.length <= 1048576 ? text2.matchAll(/self\.__next_f\.push\(\[1\s*,\s*("(?:[^"\\]|\\.)*")\s*\]\)/g) : []) {
871
+ try {
872
+ const chunk = JSON.parse(match[1]);
873
+ if (typeof chunk === "string") {
874
+ bytes += new TextEncoder().encode(chunk).length;
875
+ if (bytes <= 1048576) chunks.push(chunk);
876
+ }
877
+ } catch {
878
+ }
879
+ }
880
+ }
881
+ if (bytes > 1048576) hydrationLimited = true;
882
+ if (!hydrationLimited) roots.push(...flightRows(chunks.join("")));
883
+ else roots.length = 0;
884
+ const title = normalized(document.querySelector("h1")?.textContent), hydrated = [], typedVariants = /* @__PURE__ */ new Map();
885
+ const anchors = [...document.querySelectorAll("a[href]")].slice(0, 512).map((node) => publicURL(node.getAttribute("href"))).filter((value) => !!value);
886
+ const pending = roots.map((value) => ({ value, product: false, depth: 0 }));
887
+ let visited = 0;
888
+ while (pending.length && visited++ < 8192) {
889
+ const item = pending.shift();
890
+ if (Array.isArray(item.value)) {
891
+ if (item.value.length > 128) {
892
+ hydrationLimited = true;
893
+ continue;
894
+ }
895
+ for (const value of item.value) if (value && typeof value === "object") pending.push({ value, product: item.product, depth: item.depth + 1 });
896
+ continue;
897
+ }
898
+ if (!item.value || typeof item.value !== "object") continue;
899
+ const original = item.value, core = original.product;
900
+ const row = core && typeof core === "object" && !Array.isArray(core) && Array.isArray(original.variants) ? { ...core, variants: original.variants } : original, name2 = normalized(row.name ?? row.title ?? row.product_name);
901
+ const typedID = id(row.id);
902
+ if (typedID && /^gid:\/\/shopify\/ProductVariant\/\d+$/.test(typedID)) typedVariants.set(typedID, [...typedVariants.get(typedID) ?? [], row]);
903
+ const schema = schemas.filter((value) => normalized(value.name) === name2 && (links(value, source).url === canonical || name2 === title));
904
+ const segments = new URL(canonical).pathname.split("/").filter(Boolean).map(decodeURIComponent);
905
+ const slugValue = row.slug ?? row.handle ?? (schema.length === 1 && name2 === title ? row.url : void 0);
906
+ const slug = typeof slugValue === "string" && /^[a-z0-9-]{1,160}$/.test(slugValue) && segments[segments.length - 1] === slugValue;
907
+ const productLinks = typeof row.handle === "string" && /^[a-z0-9-]{1,160}$/.test(row.handle) && /^gid:\/\/shopify\/Product\/\d+$/.test(typedID ?? "") ? [...new Set(anchors.filter((value) => new URL(value).origin === location.origin && new URL(value).pathname.endsWith("/products/" + row.handle)))] : [];
908
+ const declaredID = Object.entries(row).some(([key, value]) => /^(?:id|product[_-]?id|sku)$/i.test(key) && id(value) !== void 0 && segments.includes(id(value)));
909
+ const publicFacts = [row.price, row.priceValue, row.sale_price, row.image, row.image_url].some((value) => value !== void 0) || Array.isArray(row.variants) && row.variants.length > 0;
910
+ if (name2 && publicFacts && (slug && (row.handle === void 0 || schema.length === 1) || productLinks.length === 1 || links(row, source).url === canonical || item.product && declaredID && schema.length === 1 && name2 === title) && (Object.keys(row).some((key) => identifier.test(key)) || Array.isArray(row.variants))) hydrated.push({ row, schema: schema.length === 1 ? schema[0] : void 0, url: productLinks.length === 1 ? productLinks[0] : void 0, structured: row !== original || /^gid:\/\/shopify\/Product\/\d+$/.test(typedID ?? "") });
911
+ for (const [key, value] of Object.entries(original)) if (!privateKey.test(key) && value && typeof value === "object") pending.push({ value, product: item.product || /product/i.test(key), depth: item.depth + 1 });
912
+ }
913
+ const consumed = /* @__PURE__ */ new Set();
914
+ const schemaVariants = schemas.flatMap((row) => Array.isArray(row.hasVariant) ? row.hasVariant : [row]).filter((row) => !!row && typeof row === "object" && !Array.isArray(row));
915
+ if (!pending.length && !hydrationLimited && bytes <= 1048576) for (const item of hydrated) {
916
+ const variantID = id(item.row.variantId), concrete = variantID && /^gid:\/\/shopify\/ProductVariant\/\d+$/.test(variantID) ? typedVariants.get(variantID) : void 0;
917
+ const concreteFacts = concrete?.map((row2) => JSON.stringify([row2.price, row2.image, row2.sku]));
918
+ if (concreteFacts && new Set(concreteFacts).size !== 1) {
919
+ pageLimited = true;
920
+ continue;
921
+ }
922
+ const row = concrete?.length ? { ...item.row, variants: [concrete[0]] } : item.row;
923
+ const safe = Object.fromEntries(Object.entries(row).filter(([key]) => (publicField.test(key) || identifier.test(key)) && !privateKey.test(key)));
924
+ if (typeof safe.price === "string" && /^\$[\da-f]+:/i.test(safe.price)) delete safe.price;
925
+ if (item.structured && (!safe.price || typeof safe.price !== "object")) delete safe.price;
926
+ if (![...links(safe, source).images].length) {
927
+ delete safe.image;
928
+ delete safe.images;
929
+ delete safe.image_url;
930
+ delete safe.imageUrl;
931
+ }
932
+ if (Array.isArray(safe.variants)) safe.variants = safe.variants.map((value) => {
933
+ if (!value || typeof value !== "object") return null;
934
+ const variant = Object.fromEntries(Object.entries(value).filter(([key]) => (publicField.test(key) || identifier.test(key)) && !privateKey.test(key))), money = variant.price;
935
+ const facts = schemaVariants.filter((row2) => sameVariant(id(variant.id ?? variant.variantId), links(row2, source).url && new URL(links(row2, source).url).searchParams.get("variant"))).map((row2) => project2(row2, "json_ld", source)?.snapshot).filter(Boolean);
936
+ if (item.structured && (!money || typeof money !== "object" || !/^[A-Z]{3}$/.test(String(money.currencyCode))) || money && typeof money === "object" && facts.some((fact) => fact.price !== void 0 && Number(money.amount) !== fact.price || fact.currency !== void 0 && money.currencyCode !== fact.currency)) for (const key of ["price", "currency", "priceCurrency", "price_currency", "offers"]) variant[key] = void 0;
937
+ return variant;
938
+ });
939
+ const schema = { ...item.schema };
940
+ if (item.row.special_price !== void 0) {
941
+ const declared = item.schema && project2(item.schema, "json_ld", source)?.snapshot;
942
+ delete safe.price;
943
+ if (declared?.price !== void 0 && declared.currency && Number(item.row.special_price) === declared.price) safe.sale_price = declared.price;
944
+ else for (const key of ["price", "offers", "currency", "currencyId", "priceCurrency", "price_currency"]) {
945
+ delete schema[key];
946
+ delete safe[key];
947
+ }
948
+ }
949
+ if (id(safe.id) && schema.productID !== void 0 && id(schema.productID) !== id(safe.id)) {
950
+ schema.schema_productID = schema.productID;
951
+ delete schema.productID;
952
+ }
953
+ const combined = { ...schema, ...safe, "@type": "Product", name: safe.name ?? safe.title ?? safe.product_name, url: links(safe, source).url ?? item.url ?? canonical };
954
+ observe({ products: [combined] }, source, "public_hydration");
955
+ if (item.schema) consumed.add(item.schema);
956
+ }
957
+ for (const row of schemas) if (!consumed.has(row)) observe(row, source, "json_ld");
958
+ if (!hydrationLimited && source === canonical) for (const row of indexedProducts(indexed, source, title, schemas)) {
959
+ observe({ products: [row] }, source, "public_hydration");
960
+ records = records.filter((entry) => entry.snapshot.metadata.provenance.source !== "json_ld" || entry.snapshot.metadata.provenance.source_ref !== source || entry.sku !== row.schema_productSKU || normalized(entry.snapshot.name) !== title || entry.url && entry.url !== source);
961
+ }
962
+ if (!pending.length && !hydrationLimited) for (const { rows: variants, parent: parentID } of variantSources) {
963
+ const bindings = native.filter((form) => form.product && variants.some((row) => sameVariant(id(row.id), form.variant)));
964
+ const parents = [...new Set(bindings.map((form) => form.product))];
965
+ if (!parents.length) continue;
966
+ if (parents.length !== 1 || parents[0] !== parentID) {
967
+ pageLimited = true;
968
+ continue;
969
+ }
970
+ for (const entry of records.filter((value) => ["json_ld", "public_hydration"].includes(value.snapshot.metadata.provenance.source) && value.snapshot.metadata.provenance.source_ref === source)) {
971
+ const selector = entry.url && new URL(entry.url).searchParams.get("variant"), matches = variants.filter((row2) => sameVariant(id(row2.id), selector));
972
+ if (!selector || matches.length !== 1 || !entry.url || new URL(entry.url).origin + new URL(entry.url).pathname !== new URL(canonical).origin + new URL(canonical).pathname) continue;
973
+ const row = matches[0], image = row.featured_image, parent = id(image?.product_id);
974
+ if (parent && parent !== parents[0] || row.sku !== void 0 && id(row.sku) !== entry.sku || entry.snapshot.product_id && entry.snapshot.product_id !== parents[0]) {
975
+ pageLimited = true;
976
+ continue;
977
+ }
978
+ entry.snapshot.product_id = parents[0];
979
+ entry.snapshot.variant_id = id(row.id);
980
+ entry.variant = id(row.id);
981
+ entry.snapshot.metadata.identifiers.product_id = parents[0];
982
+ entry.snapshot.metadata.variant_identifiers.variant_id = id(row.id);
983
+ entry.snapshot.metadata.provenance.source = "public_hydration";
984
+ }
985
+ }
986
+ for (const entry of records) entry.ref = previousRefs.get(entryKey(entry)) ?? entry.ref;
987
+ }
988
+ function consume(candidate) {
989
+ const xhr = candidate.ref.deref();
990
+ if (!xhr || stopped || !active || options.epoch() !== active || generations.get(xhr) !== candidate.generation) return;
991
+ if (xhr.readyState !== 4) return;
992
+ try {
993
+ if (xhr.status >= 200 && xhr.status < 300 && publicURL(xhr.responseURL) === candidate.url && /json/i.test(xhr.getResponseHeader("content-type") ?? "")) observe(xhr.responseType === "json" ? xhr.response : xhr.responseText, candidate.url);
994
+ } catch {
995
+ }
996
+ }
997
+ return {
998
+ observe,
999
+ /** Stores only safe request metadata and weak references before server authorization. */
1000
+ track(xhr, input, method) {
1001
+ const generation = (generations.get(xhr) ?? 0) + 1;
1002
+ generations.set(xhr, generation);
1003
+ const url = productSourceURL(input);
1004
+ if (active || stopped || !url || method.toUpperCase() !== "GET" || !/\/(?:products?|catalog)\.json$/i.test(new URL(url).pathname) || typeof WeakRef === "undefined") return;
1005
+ if (startup.length >= 32) {
1006
+ limited = true;
1007
+ return;
1008
+ }
1009
+ startup.push({ ref: new WeakRef(xhr), url, generation });
1010
+ },
1011
+ /** Activates after both gates, reading only still-live completed or subsequently completed XHRs. */
1012
+ activate() {
1013
+ const epoch = options.epoch();
1014
+ if (!epoch || stopped || active) return;
1015
+ active = epoch;
1016
+ for (const candidate of startup.splice(0)) {
1017
+ const xhr = candidate.ref.deref();
1018
+ if (!xhr) {
1019
+ limited = true;
1020
+ continue;
1021
+ }
1022
+ consume(candidate);
1023
+ if (xhr.readyState !== 4) {
1024
+ const stop = () => {
1025
+ candidate.ref.deref()?.removeEventListener("loadend", complete);
1026
+ listeners.delete(stop);
1027
+ };
1028
+ const complete = () => {
1029
+ consume(candidate);
1030
+ stop();
1031
+ };
1032
+ listeners.add(stop);
1033
+ xhr.addEventListener("loadend", complete, { once: true });
1034
+ }
1035
+ }
1036
+ },
1037
+ /** Resolves the actual target before considering current-page context; an off-page link never inherits its ancestor product. */
1038
+ resolve(root, observedPrice, observedCurrency, observedLabel) {
1039
+ const source_ref = location.origin + "/";
1040
+ const empty = (resolution) => ({ product_ref: crypto.randomUUID(), snapshot: { metadata: { resolution, identifiers: {}, variant_identifiers: {}, attributes: {}, provenance: { source: "dom", source_ref, match: "unresolved" }, limited: limited || pageLimited } } });
1041
+ if (stopped || !options.epoch() || root.closest("header,footer,[data-sagepilot-popup]")) return empty("unresolved");
1042
+ readPage();
1043
+ records = records.filter((entry) => entry.expires > Date.now());
1044
+ if (limited || pageLimited) return empty("ambiguous");
1045
+ const anchor = root.closest("a[href]"), targetURL = anchor ? publicURL(anchor.getAttribute("href")) : void 0;
1046
+ if (anchor && !targetURL) return empty("unresolved");
1047
+ const current = documentURL(), canonical = publicURL(document.querySelector('link[rel="canonical"]')?.getAttribute("href")) ?? current;
1048
+ if (!current) return empty("unresolved");
1049
+ const samePage = current && canonical && new URL(current).origin + new URL(current).pathname === new URL(canonical).origin + new URL(canonical).pathname;
1050
+ const currentSelector = current ? new URL(current).searchParams.get("variant") ?? new URL(current).searchParams.get("variant_id") : void 0;
1051
+ const clickedImage = /^img$/i.test(root.tagName) && root.getClientRects().length ? publicURL(root.currentSrc || root.getAttribute("src"), location.href, true) : void 0;
1052
+ const literalImage = clickedImage ? new URL(root.currentSrc || root.getAttribute("src"), location.href).href : void 0;
1053
+ const action = commerceAction(root.getAttribute("aria-label") ?? root.innerText ?? root.textContent ?? "");
1054
+ const primary = /^h1$/i.test(root.tagName) || root.matches("button,[role='button']") && /^(?:add(?: to (?:cart|bag|basket))?|buy(?: now)?|rent(?: now)?)[.!]?$/i.test(action);
1055
+ if (!anchor && primary && !samePage) return empty("unresolved");
1056
+ const form = root.form ?? root.closest("form"), privateForm = form && [...form.querySelectorAll("input:not([type='hidden']),textarea,select")].some((node) => node.tagName === "TEXTAREA" || !/^(?:quantity|qty|variant(?:_id)?|option(?:\[.*\])?|attribute_[A-Za-z0-9_]+)$/i.test(node.getAttribute("name") ?? ""));
1057
+ if (privateForm) return empty("unresolved");
1058
+ let pageContext = !anchor && primary && samePage && !privateForm && !root.closest("dialog,[role='dialog']");
1059
+ const images = /* @__PURE__ */ new Set(), urls = /* @__PURE__ */ new Set(), skus = /* @__PURE__ */ new Set(), variants = /* @__PURE__ */ new Set(), ids = /* @__PURE__ */ new Set();
1060
+ const bound = records.filter((entry) => entry.snapshot.metadata.provenance.source_ref === current && entry.url && new URL(entry.url).origin + new URL(entry.url).pathname === new URL(canonical).origin + new URL(canonical).pathname);
1061
+ const native = nativeForms(), owner = root.form ?? root.closest("form");
1062
+ if (!native) return empty("ambiguous");
1063
+ const own = native.find((value) => value.form === owner);
1064
+ const ownerAction = owner && publicURL(owner.getAttribute("action"));
1065
+ if (primary && ownerAction && new URL(ownerAction).origin === location.origin && /\/cart\/add\/?$/.test(new URL(ownerAction).pathname) && !own) return empty("ambiguous");
1066
+ const forms = native.filter((form2) => bound.some((entry) => sameVariant(entry.variant, form2.variant) || sameVariant(entry.url && new URL(entry.url).searchParams.get("variant"), form2.variant)));
1067
+ if (primary && own && !forms.includes(own)) return empty("unresolved");
1068
+ const checked = [...document.querySelectorAll('input[type="radio"]:checked')];
1069
+ if (checked.length > 32) return empty("ambiguous");
1070
+ const controls = checked.map((node) => id(node.value)).filter((value) => value && bound.some((entry) => entry.variant === value));
1071
+ const selections = [.../* @__PURE__ */ new Set([...(own ? forms.filter((value) => value === own) : forms).map((form2) => form2.variant), ...controls])];
1072
+ const nativeConflict = selections.length && (selections.length !== 1 || currentSelector && !sameVariant(selections[0], currentSelector));
1073
+ const nativeVariant = samePage && selections.length === 1 && (!currentSelector || sameVariant(selections[0], currentSelector)) ? selections[0] : void 0;
1074
+ let exactVariant = id(root.getAttribute("data-variant-id") ?? root.getAttribute("data-variant_id") ?? anchor?.getAttribute("data-variant-id"));
1075
+ const exactProduct = id(root.getAttribute("data-product-id") ?? root.getAttribute("data-product_id") ?? anchor?.getAttribute("data-product-id"));
1076
+ const exactSKU = id(root.getAttribute("data-sku") ?? anchor?.getAttribute("data-sku"));
1077
+ let card = root, direct = null;
1078
+ for (let depth = 0; card && depth < 6 && card !== document.body; depth++, card = card.parentElement) {
1079
+ const nodes2 = [card], walker = document.createTreeWalker(card, NodeFilter.SHOW_ELEMENT);
1080
+ let next;
1081
+ while ((next = walker.nextNode()) && nodes2.length <= 64) nodes2.push(next);
1082
+ if (nodes2.length > 64) break;
1083
+ if (!direct && card.matches("[data-product-id],[data-variant-id],[data-product_id],[data-variant_id]")) direct = card;
1084
+ for (const node of nodes2) {
1085
+ const image = publicURL(node.getAttribute("src") ?? void 0, location.href, true);
1086
+ if (image) images.add(image);
1087
+ for (const match of String(getComputedStyle(node).backgroundImage ?? "").matchAll(/url\(["']?([^"')]+)["']?\)/g)) {
1088
+ const value = publicURL(match[1], location.href, true);
1089
+ if (value) images.add(value);
1090
+ }
1091
+ const url = publicURL(node.getAttribute("href") ?? void 0);
1092
+ if (url) urls.add(url);
1093
+ const sku = id(node.getAttribute("data-sku") ?? (node.getAttribute("itemprop") === "sku" ? node.getAttribute("content") : null));
1094
+ if (sku) skus.add(sku);
1095
+ const variant = id(node.getAttribute("data-variant-id") ?? node.getAttribute("data-variant_id"));
1096
+ if (variant) variants.add(variant);
1097
+ const product = id(node.getAttribute("data-product-id") ?? node.getAttribute("data-product_id") ?? (node.getAttribute("itemprop") === "productID" ? node.getAttribute("content") : null));
1098
+ if (product) ids.add(product);
1099
+ }
1100
+ if (targetURL || images.size && urls.size) break;
1101
+ }
1102
+ const desired = targetURL ?? (urls.size === 1 ? [...urls][0] : void 0);
1103
+ if (primary && desired && new URL(desired).origin + new URL(desired).pathname !== new URL(canonical).origin + new URL(canonical).pathname) pageContext = false;
1104
+ if (pageContext && nativeConflict) return empty("ambiguous");
1105
+ exactVariant ?? (exactVariant = pageContext ? nativeVariant : void 0);
1106
+ const labelCandidate = pageContext && current === canonical && records.some((entry) => entry.snapshot.product_id === void 0 && entry.snapshot.metadata.provenance.source === "json_ld" && entry.snapshot.metadata.provenance.source_ref === current && entry.url === current && entry.sku && new URL(current).pathname.split("/").map(decodeURIComponent).includes(entry.sku));
1107
+ const labeled = labelCandidate ? labeledProductID() : void 0, productLabel = labeled && new URL(current).pathname.split("/").map(decodeURIComponent).includes(labeled) ? labeled : void 0;
1108
+ if (exactVariant) {
1109
+ variants.clear();
1110
+ variants.add(exactVariant);
1111
+ }
1112
+ if (exactProduct) {
1113
+ ids.clear();
1114
+ ids.add(exactProduct);
1115
+ }
1116
+ if (exactSKU) {
1117
+ skus.clear();
1118
+ skus.add(exactSKU);
1119
+ }
1120
+ const sharedImages = new Set([...images].filter((value) => records.filter((entry) => entry.images.includes(value)).length > 1));
1121
+ const pageSchema = records.filter((entry) => entry.snapshot.metadata.provenance.source === "json_ld" && entry.snapshot.metadata.provenance.source_ref === current);
1122
+ const uniqueLiteral = literalImage && pageSchema.every((entry) => entry.rawImages !== void 0) && pageSchema.filter((entry) => entry.rawImages?.includes(literalImage)).length === 1;
1123
+ const matches = [];
1124
+ for (const entry of records) {
1125
+ if (pageContext && ["json_ld", "public_hydration"].includes(entry.snapshot.metadata.provenance.source) && entry.snapshot.metadata.provenance.source_ref !== current) continue;
1126
+ const declaredVariantURL = entry.url ? new URL(entry.url).searchParams.get("variant") ?? new URL(entry.url).searchParams.get("variant_id") : void 0;
1127
+ const labelMatch = productLabel && entry.sku === productLabel && entry.url === current && entry.snapshot.metadata.provenance.source === "json_ld";
1128
+ if (labelMatch && entry.snapshot.product_id !== void 0 && entry.snapshot.product_id !== productLabel) return empty("ambiguous");
1129
+ if (exactVariant && !sameVariant(entry.variant ?? declaredVariantURL, exactVariant)) continue;
1130
+ const query = desired ? new URL(desired).searchParams : current ? new URL(current).searchParams : null;
1131
+ const selected = query?.get("variant") ?? query?.get("variant_id"), variantValues = Object.values(entry.snapshot.metadata.variant_identifiers);
1132
+ if (pageContext && currentSelector && ![entry.variant, entry.sku, declaredVariantURL, ...variantValues].some((value) => sameVariant(value, currentSelector))) continue;
1133
+ const exactURL = !!desired && (entry.url === desired || !!selected && entry.url?.split("?")[0] === desired.split("?")[0] && [entry.variant, entry.sku, ...variantValues].some((value) => sameVariant(value, selected)));
1134
+ const image = entry.images.find((value) => images.has(value) && !sharedImages.has(value)), token = entry.sku && new RegExp(`(?:^|[^A-Za-z0-9])${entry.sku.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:$|[^A-Za-z0-9])`);
1135
+ const skuImage = token ? [...images].find((value) => !sharedImages.has(value) && token.test(new URL(value).pathname)) : void 0;
1136
+ const title = normalized(entry.snapshot.name), titleOnPage = [...document.querySelectorAll("h1")].slice(0, 8).some((node) => node.getClientRects().length && normalized(node.textContent) === title);
1137
+ const groupOnPage = entry.group?.url === canonical && samePage && entry.snapshot.metadata.provenance.source_ref === current && [...document.querySelectorAll("h1")].slice(0, 8).some((node) => node.getClientRects().length && normalized(node.textContent) === entry.group?.name);
1138
+ const directImage = uniqueLiteral && !targetURL && samePage && pageSchema.includes(entry) && entry.rawImages?.includes(literalImage) && !sharedImages.has(clickedImage) && (!entry.url || entry.url.split("?")[0] === canonical.split("?")[0]) && (!currentSelector || [entry.variant, entry.sku, declaredVariantURL, ...variantValues].includes(currentSelector));
1139
+ const corroborated = title && title === normalized(observedLabel) && observedPrice !== void 0 && entry.snapshot.price === observedPrice;
1140
+ let match = exactURL ? "exact_url" : !targetURL && (entry.variant && variants.has(entry.variant) || exactVariant && sameVariant(entry.variant ?? declaredVariantURL, exactVariant)) ? "exact_variant" : labelMatch || !targetURL && typeof entry.snapshot.product_id === "string" && ids.has(entry.snapshot.product_id) ? "direct_id" : !targetURL && entry.sku && skus.has(entry.sku) ? "exact_sku" : skuImage && (!targetURL || !entry.url) ? "exact_sku" : directImage || image && (corroborated || entry.blocked || entry.url === current && titleOnPage || groupOnPage) && (!targetURL || !entry.url) ? "exact_image" : null;
1141
+ if (!match && pageContext && titleOnPage && (entry.snapshot.metadata.provenance.source === "catalog_response" || entry.snapshot.metadata.provenance.source_ref === current) && (entry.url === current || entry.url === canonical) && (!selected || [entry.variant, entry.sku, ...variantValues].some((value) => sameVariant(value, selected)))) match = "exact_url";
1142
+ if (match) matches.push({ entry, match, rank: exactURL ? 3 : match === "exact_url" ? 1 : 2, image: skuImage ?? image });
1143
+ }
1144
+ const rank = Math.max(0, ...matches.map((value) => value.rank)), best = matches.filter((value) => value.rank === rank);
1145
+ const directSelector = direct && id(direct.getAttribute("data-variant-id") ?? direct.getAttribute("data-variant_id") ?? direct.getAttribute("data-sku"));
1146
+ if (!best.length && direct && (!anchor || direct === anchor) && !/^h1$/i.test(root.tagName) && (!primary || samePage && (!currentSelector || currentSelector === directSelector)) && !root.closest("dialog,[role='dialog']")) {
1147
+ const identifiers = {}, variant_identifiers = {};
1148
+ const product = id(direct.getAttribute("data-product-id") ?? direct.getAttribute("data-product_id")), variant = id(direct.getAttribute("data-variant-id") ?? direct.getAttribute("data-variant_id"));
1149
+ if (product) identifiers.product_id = product;
1150
+ if (variant) variant_identifiers.variant_id = variant;
1151
+ if (product || variant) {
1152
+ const snapshot = { ...product ? { product_id: product } : {}, ...variant ? { variant_id: variant } : {}, metadata: { resolution: "resolved", identifiers, variant_identifiers, attributes: {}, provenance: { source: "dom_attribute", source_ref, match: "direct_id" }, limited: false } };
1153
+ const price2 = direct.getAttribute("data-price"), currency = direct.getAttribute("data-currency");
1154
+ if (price2 && /^\d{1,8}(?:\.\d{1,4})?$/.test(price2)) snapshot.price = Number(price2);
1155
+ if (currency && /^[A-Z]{3}$/.test(currency)) snapshot.currency = currency;
1156
+ const old = records.find((value) => value.snapshot.product_id === product && value.snapshot.variant_id === variant && value.snapshot.metadata.provenance.source === "dom_attribute");
1157
+ const entry = old ?? { snapshot, ref: crypto.randomUUID(), images: [], expires: Date.now() + 9e5 };
1158
+ if (!old && records.length < 128) records.push(entry);
1159
+ return { snapshot, product_ref: entry.ref };
1160
+ }
1161
+ }
1162
+ if (best.length !== 1 || best[0]?.entry.blocked) return empty(best.length || sharedImages.size ? "ambiguous" : "unresolved");
1163
+ const result = JSON.parse(JSON.stringify(best[0].entry.snapshot));
1164
+ if (productLabel && best[0].entry.sku === productLabel && best[0].entry.url === current && best[0].entry.snapshot.metadata.provenance.source === "json_ld") {
1165
+ result.product_id = productLabel;
1166
+ result.metadata.identifiers.product_id = productLabel;
1167
+ result.metadata.provenance.source = "dom";
1168
+ }
1169
+ if (exactVariant && !result.variant_id) {
1170
+ result.variant_id = exactVariant;
1171
+ result.metadata.variant_identifiers.variant_id = exactVariant;
1172
+ }
1173
+ const chosen = best[0].entry, groupID = chosen.snapshot.metadata.identifiers.productGroupID;
1174
+ if (pageContext && groupID && chosen.group?.url === canonical && chosen.snapshot.metadata.provenance.source_ref === current) {
1175
+ const selected = chosen.url && (new URL(chosen.url).searchParams.get("variant") ?? new URL(chosen.url).searchParams.get("variant_id")), parents = [...new Set(forms.filter((form2) => form2.product && sameVariant(form2.variant, selected)).map((form2) => form2.product))];
1176
+ if (parents.length > 1 || parents[0] === groupID && result.product_id !== void 0 && result.product_id !== parents[0]) return empty("ambiguous");
1177
+ if (parents[0] === groupID && result.product_id === void 0) {
1178
+ result.product_id = parents[0];
1179
+ result.metadata.identifiers.product_id = parents[0];
1180
+ result.metadata.provenance.source = "dom";
1181
+ }
1182
+ }
1183
+ if (typeof result.price === "number" && observedPrice !== void 0 && Math.abs(result.price - observedPrice) > 1e-9 || result.currency && observedCurrency && result.currency !== observedCurrency) return empty("unresolved");
1184
+ if (!result.image_url && best[0].image) result.image_url = best[0].image;
1185
+ result.metadata.provenance.match = best[0].match;
1186
+ return new TextEncoder().encode(JSON.stringify(result)).length <= 8192 ? { snapshot: result, product_ref: best[0].entry.ref } : empty("unresolved");
1187
+ },
1188
+ /** Clears every startup weak reference, deferred listener and retained product fact. */
1189
+ pause,
1190
+ destroy() {
1191
+ stopped = true;
1192
+ clearTimeout(timeout);
1193
+ pause();
1194
+ records = [];
1195
+ }
1196
+ };
1197
+ }
1198
+
1199
+ // src/dom.ts
1200
+ var imageSelector = "img[alt],div[alt],[role='img'][aria-label]";
1201
+ var privateRegion = "form,input,textarea,select,[contenteditable='true'],[data-sagepilot-popup],[hidden],[aria-hidden='true']";
1202
+ function nodes(root, limit) {
1203
+ const result = [root], walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
1204
+ let next;
1205
+ while (next = walker.nextNode()) {
1206
+ if (result.length >= limit) return null;
1207
+ result.push(next);
1208
+ }
1209
+ return result;
1210
+ }
1211
+ function text(root) {
1212
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1213
+ let result = "", next, count = 0;
1214
+ while (next = walker.nextNode()) {
1215
+ if (++count > 16 || (next.nodeValue?.length ?? 0) > 120) return null;
1216
+ const parent = next.parentElement;
1217
+ if (parent && (!parent.getClientRects().length || parent.closest("[aria-hidden='true']") || getComputedStyle(parent).visibility === "hidden")) continue;
1218
+ result += next.nodeValue ?? "";
1219
+ if (result.length > 120) return null;
1220
+ }
1221
+ return result.trim().replace(/\s+/g, " ");
1222
+ }
1223
+ function label(value) {
1224
+ value = value?.normalize("NFKC") ?? null;
1225
+ if (!value || value.length < 3 || value.length > 80 || /\d[\d\s().-]{5,}\d|@|https?:|\+\d|[A-Za-z0-9_-]{24,}|\b(?:your|my|account|profile|personalized|personalised|customized|customised|monogram|email|phone|address|password|token|customer|name)\b/i.test(value)) return null;
1226
+ return value.trim().toLowerCase().replace(/\s+/g, " ");
1227
+ }
1228
+ function price(value) {
1229
+ const match = value?.match(/^(?:rent at\s+)?(?:(USD|EUR|GBP|INR|CAD|AUD|JPY)\s*)?[$€£¥₹]?\s*(\d{1,8}(?:[.,]\d{1,2})?|(?:\d{1,3}(?:,\d{3})+|\d{1,3}(?:,\d{2})*,\d{3})(?:\.\d{1,2})?)\s*(USD|EUR|GBP|INR|CAD|AUD|JPY)?(?:\s*\/\s*(?:mo|month))?$/i);
1230
+ if (!match || !/[$€£¥₹]|\b(?:USD|EUR|GBP|INR|CAD|AUD|JPY)\b/i.test(value)) return null;
1231
+ if (match[1] && match[3] && match[1].toUpperCase() !== match[3].toUpperCase()) return null;
1232
+ const amount = Number(/,\d{3}(?:\.|$)/.test(match[2]) ? match[2].replace(/,/g, "") : match[2].replace(",", ".")), currency = match[1] ?? match[3];
1233
+ return Number.isFinite(amount) && amount <= Number.MAX_SAFE_INTEGER ? { price: amount, ...currency ? { currency: currency.toUpperCase() } : {} } : null;
1234
+ }
1235
+ function productAt(target) {
1236
+ let candidate = target;
1237
+ for (let depth = 0; candidate && depth < 6 && candidate !== document.body; depth++, candidate = candidate.parentElement) {
1238
+ if (candidate.closest(privateRegion)) return null;
1239
+ const children = nodes(candidate, 64);
1240
+ if (!children) return null;
1241
+ if (children.some((node) => node.matches("input,textarea,select,[contenteditable='true']"))) return null;
1242
+ const images = children.filter((node) => node.matches(imageSelector));
1243
+ if (images.length > 1) return null;
1244
+ if (images.length !== 1) continue;
1245
+ const name2 = label(images[0].getAttribute("alt") ?? images[0].getAttribute("aria-label"));
1246
+ if (!name2 || !children.some((node) => node !== images[0] && !node.childElementCount && node.getClientRects().length && getComputedStyle(node).visibility !== "hidden" && label(text(node)) === name2)) continue;
1247
+ const amounts = children.filter((node) => node.matches("p,span,[itemprop='price']") && !node.closest("del,s") && node.getClientRects().length && getComputedStyle(node).visibility !== "hidden").map((node) => price(text(node))).filter(Boolean);
1248
+ if (!amounts.length || new Set(amounts.map((amount) => `${amount.price}:${amount.currency ?? ""}`)).size > 1) continue;
1249
+ const siblings = candidate.parentElement?.children;
1250
+ if (siblings && siblings.length > 64) return null;
1251
+ for (const sibling of siblings ? [...siblings] : []) {
1252
+ if (sibling === candidate) continue;
1253
+ const entries = nodes(sibling, 64);
1254
+ if (!entries) return null;
1255
+ if (entries.some((node) => node.matches(imageSelector) && label(node.getAttribute("alt") ?? node.getAttribute("aria-label")) === name2)) return null;
1256
+ }
1257
+ return { node: candidate, label: name2, ...amounts[0] };
1258
+ }
1259
+ return null;
1260
+ }
1261
+ function quantity(product) {
1262
+ const values = (nodes(product.node, 64) ?? []).filter((node) => (!node.childElementCount || node.matches("p")) && node.getClientRects().length && getComputedStyle(node).visibility !== "hidden").map((node) => text(node)?.match(/^(?:.*\s)?(?:quantity|qty)\s*:\s*(\d{1,6})$/i)?.[1]).filter((value) => value !== void 0);
1263
+ const unique = [...new Set(values)];
1264
+ return unique.length === 1 ? Number(unique[0]) : null;
1265
+ }
1266
+ function createDOMCart(options) {
1267
+ const regions = /* @__PURE__ */ new WeakMap(), pending = /* @__PURE__ */ new Map();
1268
+ let stopped = false;
1269
+ function cart(wanted) {
1270
+ const markers = [], possible = document.querySelectorAll("h1,h2,h3,h4,h5,h6,span,[aria-label],aside,[role='dialog']");
1271
+ for (let index = 0; index < Math.min(possible.length, 128); index++) {
1272
+ const node2 = possible[index];
1273
+ if (!node2.childElementCount && /^(?:shopping )?(?:cart|bag)$/i.test(text(node2) ?? "")) markers.push(node2);
1274
+ }
1275
+ const candidates = /* @__PURE__ */ new Set();
1276
+ for (const marker of markers.slice(0, 8)) {
1277
+ let node2 = marker.parentElement;
1278
+ for (let depth = 0; node2 && node2 !== document.body && depth < 6; depth++, node2 = node2.parentElement) {
1279
+ const children = nodes(node2, 256);
1280
+ if (!children) break;
1281
+ if (children.some((child) => child.getClientRects().length && getComputedStyle(child).visibility !== "hidden" && (child.matches("[aria-busy='true'],[role='progressbar']") || !child.childElementCount && /^(?:loading|updating)[.\s…]*$/i.test(text(child) ?? "")))) break;
1282
+ const buttons = children.filter((child) => child.matches("button,[role='button']"));
1283
+ if (!buttons.some((button) => /^(?:proceed to )?checkout$/i.test(text(button) ?? ""))) continue;
1284
+ if (buttons.some((button) => /\badd\b.*\bcart\b/i.test(text(button) ?? "")) || node2.closest("[hidden],[aria-hidden='true'],[aria-busy='true']")) break;
1285
+ const style = getComputedStyle(node2);
1286
+ if (style.display === "none" || style.visibility === "hidden") break;
1287
+ candidates.add(node2);
1288
+ break;
1289
+ }
1290
+ }
1291
+ if (candidates.size !== 1) return null;
1292
+ const node = [...candidates][0], images = (nodes(node, 256) ?? []).filter((child) => child.matches(imageSelector));
1293
+ if (images.length > 16) return null;
1294
+ if (!images.length && !(nodes(node, 256) ?? []).some((child) => /^(?:(?:your|the|shopping)\s+)?(?:cart|bag)\s+(?:is\s+)?empty[.!:]?$|^add some products (?:in|to) (?:the|your) (?:cart|bag)\b/i.test(text(child) ?? ""))) return null;
1295
+ const seen = /* @__PURE__ */ new Set();
1296
+ let count = 0;
1297
+ for (const image of images) {
1298
+ const product = productAt(image), amount = product ? quantity(product) : null;
1299
+ if (!product || !node.contains(product.node) || amount === null || seen.has(product.label)) return null;
1300
+ seen.add(product.label);
1301
+ if (product.label === wanted) count = amount;
1302
+ }
1303
+ let ref = regions.get(node);
1304
+ if (!ref) {
1305
+ ref = crypto.randomUUID();
1306
+ regions.set(node, ref);
1307
+ }
1308
+ return { node, ref, quantity: count };
1309
+ }
1310
+ function remove(key) {
1311
+ const item = pending.get(key);
1312
+ if (item) clearTimeout(item.timer);
1313
+ pending.delete(key);
1314
+ if (!pending.size) observer.disconnect();
1315
+ }
1316
+ function finish(key) {
1317
+ const item = pending.get(key);
1318
+ if (!item) return;
1319
+ let after = null;
1320
+ try {
1321
+ after = cart(item.label);
1322
+ } catch {
1323
+ }
1324
+ remove(key);
1325
+ if (stopped || item.suppressed || !item.product.product_ref || options.epoch() !== item.epoch || options.networkVersion() !== item.network || performance.now() > item.deadline + 250 || performance.now() - item.changed < 300 || !after || after.node !== item.before.node || after.quantity <= item.before.quantity) return;
1326
+ options.emit("interaction", {
1327
+ action: "cart_change",
1328
+ confirmation: "dom_cart_quantity_increase",
1329
+ trigger_id: item.trigger,
1330
+ cart_region_ref: after.ref,
1331
+ before_quantity: item.before.quantity,
1332
+ after_quantity: after.quantity,
1333
+ product: { ...item.product, quantity: after.quantity - item.before.quantity }
1334
+ }, item.epoch, Date.now());
1335
+ }
1336
+ const observer = new MutationObserver((records) => {
1337
+ for (const item of pending.values()) if (records.some((record) => item.before.node.contains(record.target) || record.target.contains(item.before.node))) item.changed = performance.now();
1338
+ });
1339
+ return {
1340
+ /** Resolves the actual clicked product independently from the stricter cart-outcome reader. */
1341
+ click(target, epoch, trigger) {
1342
+ if (stopped) return {};
1343
+ const product = productAt(target), before = product ? cart(product.label) : null;
1344
+ const match = options.productSnapshot?.(target, before?.node.contains(target) ? void 0 : product?.price, product?.currency, product?.label);
1345
+ const resolved = match?.snapshot.metadata.resolution === "resolved";
1346
+ if (!product && !resolved) return {};
1347
+ const action = commerceAction((target.getAttribute("aria-label") ?? text(target) ?? "").slice(0, 256)).toLowerCase();
1348
+ const excluded = /\b(?:wishlist|compare|address|tag|subscribe|contact)\b/i.test(action + " " + (target.getAttribute("title") ?? "").slice(0, 256));
1349
+ const add = !excluded && /\badd\b.*\bcart\b/.test(action), increment = /^(?:\+|increase(?: quantity)?|increment(?: quantity)?)$/.test(action);
1350
+ const intent = resolved && !excluded && target.matches("button,[role='button']") && !target.closest("dialog,[role='dialog']") && /^(?:add|add to (?:cart|bag|basket))$/.test(action);
1351
+ const existing = product ? pending.get(product.label) : void 0;
1352
+ if (existing) existing.suppressed = true;
1353
+ const network = options.networkVersion();
1354
+ let watching;
1355
+ const sameProduct = !resolved || !product || typeof match?.snapshot.name !== "string" || label(match.snapshot.name) === product.label;
1356
+ if (product && sameProduct && before && !existing && pending.size < 3 && (add || increment && before.node.contains(target))) {
1357
+ watching = {
1358
+ label: product.label,
1359
+ epoch,
1360
+ trigger,
1361
+ before,
1362
+ network,
1363
+ changed: performance.now(),
1364
+ deadline: performance.now() + 2e3,
1365
+ suppressed: false,
1366
+ product: {},
1367
+ timer: setTimeout(() => finish(product.label), 2e3)
1368
+ };
1369
+ pending.set(product.label, watching);
1370
+ observer.observe(document.body, { childList: true, characterData: true, subtree: true });
1371
+ }
1372
+ const context = (product_ref) => {
1373
+ const snapshot = match?.snapshot, common = resolved ? snapshot : null;
1374
+ const evidence = { product_ref, ...!resolved && product ? { price: product.price, ...product.currency ? { currency: product.currency } : {} } : {}, ...snapshot ? { snapshot } : {} };
1375
+ for (const key of ["product_id", "variant_id", "sku", "price", "currency"]) if (common?.[key] !== void 0) evidence[key] = common[key];
1376
+ const amount = add || intent ? target.closest("[data-quantity]")?.getAttribute("data-quantity") : null;
1377
+ if (amount && /^\d{1,8}(?:\.\d{1,4})?$/.test(amount)) evidence.quantity = Number(amount);
1378
+ if (watching && product && !stopped && options.epoch() === epoch && pending.get(product.label) === watching)
1379
+ watching.product = { product_ref, ...add ? { price: product.price, ...product.currency ? { currency: product.currency } : {} } : {}, ...snapshot ? { snapshot } : {} };
1380
+ return { product: evidence, ...intent ? { intent: "add_to_cart" } : {} };
1381
+ };
1382
+ if (resolved) return context(match.product_ref);
1383
+ return crypto.subtle.digest("SHA-256", new TextEncoder().encode(location.origin + "\n" + product.label)).then((digest) => context("dom_" + [...new Uint8Array(digest)].slice(0, 16).map((byte) => byte.toString(16).padStart(2, "0")).join("")));
1384
+ },
1385
+ /** Fences delayed observations and clears all retained transient product context. */
1386
+ destroy() {
1387
+ stopped = true;
1388
+ observer.disconnect();
1389
+ for (const key of [...pending.keys()]) remove(key);
1390
+ }
1391
+ };
1392
+ }
1393
+
1394
+ // src/collectors.ts
1395
+ function observePage(options) {
1396
+ const originalPush = history.pushState, originalReplace = history.replaceState;
1397
+ let stopped = false, previous = "", current = "", currentPage = null, pendingClicks = 0, pendingRoutes = 0;
1398
+ const roles = /* @__PURE__ */ new Set(["button", "link", "checkbox", "radio", "textbox", "combobox", "menuitem", "tab", "option", "switch", "listbox", "slider"]);
1399
+ const dom = createDOMCart({ epoch: options.epoch, emit: options.emit, networkVersion: options.networkVersion ?? (() => 0), productSnapshot: options.productSnapshot });
1400
+ function navigation() {
1401
+ const epoch = options.epoch(), url = location.href;
1402
+ if (stopped || !epoch || url === current) return;
1403
+ const old = current;
1404
+ current = url;
1405
+ currentPage = null;
1406
+ if (pendingRoutes >= 128) {
1407
+ options.dropped();
1408
+ return;
1409
+ }
1410
+ pendingRoutes++;
1411
+ const time = Date.now();
1412
+ void Promise.all([routeFor(url), old ? routeFor(old) : Promise.resolve(null)]).then(([page, from]) => {
1413
+ if (!page || stopped || options.epoch() !== epoch) return;
1414
+ if (current === url) currentPage = page;
1415
+ previous = from ?? "";
1416
+ let match;
1417
+ try {
1418
+ const heading = current === url ? document.querySelector("main h1") ?? document.querySelector("h1") : null;
1419
+ match = heading ? options.productSnapshot?.(heading) : void 0;
1420
+ } catch {
1421
+ options.dropped();
1422
+ }
1423
+ options.emit("navigation", {
1424
+ action: "page_view",
1425
+ page,
1426
+ ...previous ? { previous_page: previous } : {},
1427
+ ...match?.snapshot.metadata.resolution === "resolved" ? { product: { product_ref: match.product_ref, snapshot: match.snapshot } } : {}
1428
+ }, epoch, time);
1429
+ }).catch(options.dropped).finally(() => {
1430
+ pendingRoutes--;
1431
+ });
1432
+ }
1433
+ function push(...args) {
1434
+ Reflect.apply(originalPush, this, args);
1435
+ navigation();
1436
+ }
1437
+ function replace(...args) {
1438
+ Reflect.apply(originalReplace, this, args);
1439
+ navigation();
1440
+ }
1441
+ function click(event) {
1442
+ const epoch = options.epoch();
1443
+ if (stopped || !epoch || !(event.target instanceof Element) || event.composedPath().some((node) => node instanceof Element && node.hasAttribute("data-sagepilot-popup"))) return;
1444
+ if (pendingClicks >= 128) {
1445
+ options.dropped();
1446
+ return;
1447
+ }
1448
+ try {
1449
+ const target = event.target.closest("button,a,input,select,textarea,[role='button']") ?? event.target;
1450
+ const path = [];
1451
+ let node = target;
1452
+ for (let depth = 0; node && depth < 6; depth++, node = node.parentElement) {
1453
+ const role = node.getAttribute("role"), type = node.getAttribute("type"), parent = node.parentElement;
1454
+ const sibling = parent && parent.children.length <= 64 ? Array.prototype.indexOf.call(parent.children, node) : -1;
1455
+ path.push({
1456
+ tag: node.tagName.toLowerCase(),
1457
+ sibling_index: sibling,
1458
+ ...role && roles.has(role) ? { role } : {},
1459
+ ...type && /^(button|submit|reset|checkbox|radio)$/.test(type) ? { type } : {}
1460
+ });
1461
+ }
1462
+ const time = Date.now(), targetTag = target.tagName.toLowerCase();
1463
+ const evidence = semanticEvidence(target);
1464
+ let product = {};
1465
+ try {
1466
+ product = dom.click(target, epoch, crypto.randomUUID());
1467
+ } catch {
1468
+ options.dropped();
1469
+ }
1470
+ const emit = (ordinary, page) => {
1471
+ if (!stopped && options.epoch() === epoch) options.emit("interaction", { action: "click", target_tag: targetTag, target_path: path, ...evidence, ...ordinary, ...page ? { page } : {} }, epoch, time);
1472
+ };
1473
+ pendingClicks++;
1474
+ if (typeof product.then !== "function") {
1475
+ try {
1476
+ emit(product, current === location.href ? currentPage : null);
1477
+ } finally {
1478
+ queueMicrotask(() => {
1479
+ pendingClicks--;
1480
+ });
1481
+ }
1482
+ } else void Promise.all([routeFor(location.href), Promise.resolve(product).catch(() => {
1483
+ options.dropped();
1484
+ return {};
1485
+ })]).then(([page, ordinary]) => emit(ordinary, page)).catch(options.dropped).finally(() => {
1486
+ pendingClicks--;
1487
+ });
1488
+ } catch {
1489
+ options.dropped();
1490
+ }
1491
+ }
1492
+ history.pushState = push;
1493
+ history.replaceState = replace;
1494
+ document.addEventListener("click", click, true);
1495
+ window.addEventListener("popstate", navigation);
1496
+ window.addEventListener("hashchange", navigation);
1497
+ navigation();
1498
+ return function stopPage() {
1499
+ stopped = true;
1500
+ dom.destroy();
1501
+ document.removeEventListener("click", click, true);
1502
+ window.removeEventListener("popstate", navigation);
1503
+ window.removeEventListener("hashchange", navigation);
1504
+ if (history.pushState === push) history.pushState = originalPush;
1505
+ if (history.replaceState === replace) history.replaceState = originalReplace;
1506
+ };
1507
+ }
1508
+ function semanticEvidence(target) {
1509
+ const hints = /* @__PURE__ */ new Set(["add", "cart", "remove", "checkout", "buy", "purchase", "wishlist", "product", "search", "subscribe", "submit"]);
1510
+ const label2 = (target.getAttribute("aria-label") ?? target.textContent ?? "").slice(0, 256).toLowerCase();
1511
+ const semanticTokens = [...new Set(label2.split(/[^a-z]+/).filter((token) => hints.has(token)))].sort();
1512
+ return { semantic_tokens: semanticTokens };
1513
+ }
1514
+
1515
+ // src/network.ts
1516
+ function observeNetwork(options) {
1517
+ const fetchOriginal = globalThis.fetch, proto = globalThis.Response?.prototype;
1518
+ const textOriginal = proto?.text, jsonOriginal = proto?.json;
1519
+ const xhr = globalThis.XMLHttpRequest?.prototype, openOriginal = xhr?.open, sendOriginal = xhr?.send;
1520
+ const responses = /* @__PURE__ */ new WeakMap(), requests = /* @__PURE__ */ new WeakMap();
1521
+ const active = /* @__PURE__ */ new Set(), xhrCleanups = /* @__PURE__ */ new Set();
1522
+ let stopped = false, insideFetch = false, pendingEmits = 0;
1523
+ function state(url, method, body) {
1524
+ const epoch = options.epoch();
1525
+ if (stopped || !epoch || active.size >= 128 || options.excluded(url)) return null;
1526
+ if (!/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/i.test(method)) return null;
1527
+ if (!/^(GET|HEAD|OPTIONS)$/i.test(method)) {
1528
+ try {
1529
+ options.mutation?.();
1530
+ } catch {
1531
+ options.dropped();
1532
+ }
1533
+ }
1534
+ const result = {
1535
+ id: crypto.randomUUID(),
1536
+ epoch,
1537
+ method: method.toUpperCase(),
1538
+ url,
1539
+ route: routeFor(url),
1540
+ start: performance.now(),
1541
+ request: projectText(body),
1542
+ status: 0,
1543
+ bodySeen: false
1544
+ };
1545
+ active.add(result);
1546
+ return result;
1547
+ }
1548
+ function emit(item, phase, outcome, response = null) {
1549
+ if (stopped || options.epoch() !== item.epoch) return;
1550
+ if (pendingEmits >= 128) {
1551
+ item.request = null;
1552
+ options.dropped();
1553
+ return;
1554
+ }
1555
+ pendingEmits++;
1556
+ const elapsed = Math.max(0, performance.now() - item.start), time = Date.now();
1557
+ const request = phase === "headers" ? item.request : null;
1558
+ if (phase === "headers") item.request = null;
1559
+ void item.route.then((route) => {
1560
+ if (!route || stopped || options.epoch() !== item.epoch) return;
1561
+ options.emit("network", {
1562
+ request_id: item.id,
1563
+ method: item.method,
1564
+ route,
1565
+ route_version: 2,
1566
+ status: item.status,
1567
+ duration_ms: Math.round(elapsed),
1568
+ outcome,
1569
+ phase,
1570
+ projection_version: 2,
1571
+ ...request ? { request } : {},
1572
+ ...response ? { response } : {}
1573
+ }, item.epoch, time);
1574
+ }).catch(options.dropped).finally(() => {
1575
+ pendingEmits--;
1576
+ });
1577
+ }
1578
+ function fetchObserved(...args) {
1579
+ let item = null;
1580
+ try {
1581
+ const input = args[0], init = args[1];
1582
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1583
+ item = state(url, init?.method ?? (input instanceof Request ? input.method : "GET"), init?.body);
1584
+ } catch {
1585
+ options.dropped();
1586
+ }
1587
+ let result;
1588
+ const previous = insideFetch;
1589
+ try {
1590
+ insideFetch = true;
1591
+ result = Reflect.apply(fetchOriginal, this, args);
1592
+ } catch (error) {
1593
+ if (item) {
1594
+ emit(item, "headers", "error");
1595
+ active.delete(item);
1596
+ }
1597
+ throw error;
1598
+ } finally {
1599
+ insideFetch = previous;
1600
+ }
1601
+ const captured = item;
1602
+ if (captured) void result.then((response) => {
1603
+ captured.status = response.status;
1604
+ emit(captured, "headers", "completed");
1605
+ if (!stopped && /(?:application|text)\/(?:[a-z0-9.+-]*\+)?json/i.test(response.headers.get("content-type") ?? "")) responses.set(response, captured);
1606
+ active.delete(captured);
1607
+ }, () => {
1608
+ emit(captured, "headers", "error");
1609
+ active.delete(captured);
1610
+ }).catch(options.dropped);
1611
+ return result;
1612
+ }
1613
+ function consume(response, original, isText) {
1614
+ const result = Reflect.apply(original, response, []), item = responses.get(response);
1615
+ if (item && !item.bodySeen) {
1616
+ item.bodySeen = true;
1617
+ responses.delete(response);
1618
+ void result.then((value) => {
1619
+ if (stopped || options.epoch() !== item.epoch) return;
1620
+ try {
1621
+ if (item.status >= 200 && item.status < 300) options.products?.observe(value, item.url);
1622
+ const projected = isText ? projectText(value) : project(value);
1623
+ emit(item, "body", "completed", projected);
1624
+ } catch {
1625
+ options.dropped();
1626
+ }
1627
+ }, () => {
1628
+ }).catch(options.dropped);
1629
+ }
1630
+ return result;
1631
+ }
1632
+ function text2() {
1633
+ return consume(this, textOriginal, true);
1634
+ }
1635
+ function json() {
1636
+ return consume(this, jsonOriginal, false);
1637
+ }
1638
+ function open(...args) {
1639
+ const prior = requests.get(this);
1640
+ const result = Reflect.apply(openOriginal, this, args);
1641
+ let url = null;
1642
+ try {
1643
+ url = String(args[1]);
1644
+ options.products?.track(this, url, args[0]);
1645
+ } catch {
1646
+ options.dropped();
1647
+ }
1648
+ if (prior) {
1649
+ prior.cleanup?.();
1650
+ active.delete(prior);
1651
+ requests.delete(this);
1652
+ }
1653
+ if (!insideFetch) {
1654
+ try {
1655
+ const item = url ? state(url, args[0], null) : null;
1656
+ if (item) requests.set(this, item);
1657
+ } catch {
1658
+ options.dropped();
1659
+ }
1660
+ }
1661
+ return result;
1662
+ }
1663
+ function send(body) {
1664
+ const item = requests.get(this);
1665
+ if (!item || item.cleanup || stopped || options.epoch() !== item.epoch) return Reflect.apply(sendOriginal, this, [body]);
1666
+ item.request = projectText(body);
1667
+ let outcome = "completed";
1668
+ const failed = (event) => {
1669
+ outcome = event.type;
1670
+ };
1671
+ const cleanup = () => {
1672
+ this.removeEventListener("loadend", complete);
1673
+ for (const type of ["error", "abort", "timeout"]) this.removeEventListener(type, failed);
1674
+ active.delete(item);
1675
+ requests.delete(this);
1676
+ xhrCleanups.delete(cleanup);
1677
+ item.cleanup = void 0;
1678
+ };
1679
+ const complete = () => {
1680
+ try {
1681
+ item.status = this.status;
1682
+ emit(item, "headers", outcome);
1683
+ if (outcome === "completed" && /(?:application|text)\/(?:[a-z0-9.+-]*\+)?json/i.test(this.getResponseHeader("content-type") ?? "")) {
1684
+ if (item.status >= 200 && item.status < 300) options.products?.observe(this.responseType === "json" ? this.response : this.responseType === "" || this.responseType === "text" ? this.responseText : null, item.url);
1685
+ const response = this.responseType === "json" ? project(this.response) : this.responseType === "" || this.responseType === "text" ? projectText(this.responseText) : null;
1686
+ if (response) emit(item, "body", outcome, response);
1687
+ }
1688
+ } catch {
1689
+ options.dropped();
1690
+ } finally {
1691
+ cleanup();
1692
+ }
1693
+ };
1694
+ item.cleanup = cleanup;
1695
+ xhrCleanups.add(cleanup);
1696
+ this.addEventListener("loadend", complete);
1697
+ for (const type of ["error", "abort", "timeout"]) this.addEventListener(type, failed);
1698
+ try {
1699
+ return Reflect.apply(sendOriginal, this, [body]);
1700
+ } catch (error) {
1701
+ cleanup();
1702
+ throw error;
1703
+ }
1704
+ }
1705
+ if (typeof fetchOriginal === "function") globalThis.fetch = fetchObserved;
1706
+ if (proto) proto.text = text2;
1707
+ if (proto) proto.json = json;
1708
+ if (xhr) {
1709
+ xhr.open = open;
1710
+ xhr.send = send;
1711
+ }
1712
+ return function stopNetwork() {
1713
+ stopped = true;
1714
+ for (const cleanup of [...xhrCleanups]) cleanup();
1715
+ active.clear();
1716
+ if (globalThis.fetch === fetchObserved) globalThis.fetch = fetchOriginal;
1717
+ if (proto?.text === text2) proto.text = textOriginal;
1718
+ if (proto?.json === json) proto.json = jsonOriginal;
1719
+ if (xhr?.open === open) xhr.open = openOriginal;
1720
+ if (xhr?.send === send) xhr.send = sendOriginal;
1721
+ };
1722
+ }
1723
+
1724
+ // src/storage.ts
1725
+ var JOURNAL_TTL_MS = 24 * 60 * 60 * 1e3;
1726
+ var MAX_RECORDS = 8192;
1727
+ var MAX_BYTES = 8 * 1024 * 1024;
1728
+ var BACKUP_RECORDS = 256;
1729
+ var BACKUP_BYTES = 512 * 1024;
1730
+ function createJournal(scope) {
1731
+ const memory = /* @__PURE__ */ new Map();
1732
+ const backupSizes = /* @__PURE__ */ new Map();
1733
+ const prefix = `sagepilot:web-journal:${scope}:`, purgeKey = prefix + "purged";
1734
+ let db = null, backup = null;
1735
+ let pending = Promise.resolve(), opened = null;
1736
+ let appends = null;
1737
+ let pendingBytes = 0, backupBytes = 0;
1738
+ let purge = null;
1739
+ let quotaEvicted = 0, expiredRecords = 0, storageFailures = 0;
1740
+ function valid2(record) {
1741
+ return !!record && typeof record.id === "string" && record.id.length > 0 && record.id.length <= 128 && Number.isSafeInteger(record.bytes) && record.bytes > 0 && record.bytes <= 32768 && Number.isFinite(record.expires) && record.value?.event_id === record.id && typeof record.value.owner?.session_id === "string" && (record.value.owner.customer_id === null || typeof record.value.owner.customer_id === "string");
1742
+ }
1743
+ function backupKeys(store) {
1744
+ const keys = [];
1745
+ for (let index = 0; index < store.length; index++) {
1746
+ const key = store.key(index);
1747
+ if (key?.startsWith(prefix) && key !== purgeKey) keys.push(key);
1748
+ }
1749
+ return keys;
1750
+ }
1751
+ function readBackup() {
1752
+ const records = [];
1753
+ try {
1754
+ backup = globalThis.localStorage ?? null;
1755
+ if (!backup) return records;
1756
+ purge = backup.getItem(purgeKey);
1757
+ for (const key of backupKeys(backup)) {
1758
+ const raw = backup.getItem(key), size = (key.length + (raw?.length ?? 0)) * 2;
1759
+ let record;
1760
+ if (raw && size <= BACKUP_BYTES) {
1761
+ try {
1762
+ record = JSON.parse(raw);
1763
+ } catch {
1764
+ }
1765
+ }
1766
+ if (!record || !valid2(record) || key !== prefix + record.id) {
1767
+ backup.removeItem(key);
1768
+ continue;
1769
+ }
1770
+ records.push(record);
1771
+ backupSizes.set(record.id, size);
1772
+ backupBytes += size;
1773
+ }
1774
+ } catch {
1775
+ backup = null;
1776
+ storageFailures++;
1777
+ }
1778
+ return records;
1779
+ }
1780
+ function saveBackup(records) {
1781
+ if (!backup) return;
1782
+ for (const record of records) {
1783
+ if (backupSizes.size >= BACKUP_RECORDS || backupBytes >= BACKUP_BYTES) break;
1784
+ if (backupSizes.has(record.id) || backupBytes + record.bytes > BACKUP_BYTES) continue;
1785
+ const key = prefix + record.id, raw = JSON.stringify(record), size = (key.length + raw.length) * 2;
1786
+ if (backupBytes + size > BACKUP_BYTES) continue;
1787
+ try {
1788
+ backup.setItem(key, raw);
1789
+ } catch {
1790
+ storageFailures++;
1791
+ break;
1792
+ }
1793
+ backupSizes.set(record.id, size);
1794
+ backupBytes += size;
1795
+ }
1796
+ }
1797
+ function removeBackup(ids) {
1798
+ if (!backup) return;
1799
+ for (const id2 of ids) {
1800
+ const size = backupSizes.get(id2);
1801
+ if (size === void 0) continue;
1802
+ try {
1803
+ backup.removeItem(prefix + id2);
1804
+ } catch {
1805
+ storageFailures++;
1806
+ continue;
1807
+ }
1808
+ backupSizes.delete(id2);
1809
+ backupBytes -= size;
1810
+ }
1811
+ }
1812
+ function fallback() {
1813
+ db?.close();
1814
+ db = null;
1815
+ storageFailures++;
1816
+ saveBackup(memory.values());
1817
+ }
1818
+ async function persist(puts, deletes, clear = false) {
1819
+ removeBackup(deletes);
1820
+ if (db) {
1821
+ try {
1822
+ await new Promise((resolve, reject) => {
1823
+ const tx = db.transaction("events", "readwrite"), store = tx.objectStore("events");
1824
+ if (clear) store.clear();
1825
+ for (const id2 of deletes) store.delete(id2);
1826
+ for (const record of puts) store.put(record);
1827
+ tx.oncomplete = () => resolve();
1828
+ tx.onabort = tx.onerror = () => reject(tx.error ?? new Error("Journal transaction failed"));
1829
+ });
1830
+ removeBackup(puts.map((record) => record.id));
1831
+ return;
1832
+ } catch {
1833
+ fallback();
1834
+ }
1835
+ }
1836
+ saveBackup(deletes.length ? memory.values() : puts);
1837
+ }
1838
+ function readIndexedDB() {
1839
+ return new Promise((resolve, reject) => {
1840
+ let settled = false, connection = null;
1841
+ let timer = setTimeout(fail, 1500);
1842
+ function fail() {
1843
+ if (settled) return;
1844
+ settled = true;
1845
+ clearTimeout(timer);
1846
+ connection?.close();
1847
+ reject(new Error("IndexedDB journal is unavailable"));
1848
+ }
1849
+ try {
1850
+ const request = indexedDB.open("sagepilot-web-events-" + scope, 1);
1851
+ request.onupgradeneeded = () => {
1852
+ if (!request.result.objectStoreNames.contains("events")) request.result.createObjectStore("events", { keyPath: "id" });
1853
+ };
1854
+ request.onerror = request.onblocked = fail;
1855
+ request.onsuccess = () => {
1856
+ if (settled) {
1857
+ request.result.close();
1858
+ return;
1859
+ }
1860
+ connection = request.result;
1861
+ clearTimeout(timer);
1862
+ timer = setTimeout(fail, 5e3);
1863
+ try {
1864
+ const tx = connection.transaction("events", "readonly");
1865
+ const read = tx.objectStore("events").getAll(void 0, MAX_RECORDS + 1);
1866
+ tx.onabort = tx.onerror = fail;
1867
+ tx.oncomplete = () => {
1868
+ if (settled) return;
1869
+ settled = true;
1870
+ clearTimeout(timer);
1871
+ db = connection;
1872
+ db.onversionchange = fallback;
1873
+ resolve(read.result);
1874
+ };
1875
+ } catch {
1876
+ fail();
1877
+ }
1878
+ };
1879
+ } catch {
1880
+ fail();
1881
+ }
1882
+ });
1883
+ }
1884
+ function remove(id2) {
1885
+ const record = memory.get(id2);
1886
+ if (record) {
1887
+ memory.delete(id2);
1888
+ pendingBytes -= record.bytes;
1889
+ }
1890
+ }
1891
+ function prune() {
1892
+ const deleted = [], now = Date.now();
1893
+ for (const [id2, record] of memory) {
1894
+ if (record.expires <= now) {
1895
+ remove(id2);
1896
+ deleted.push(id2);
1897
+ expiredRecords++;
1898
+ }
1899
+ }
1900
+ while (memory.size > MAX_RECORDS || pendingBytes > MAX_BYTES) {
1901
+ const id2 = memory.keys().next().value;
1902
+ remove(id2);
1903
+ deleted.push(id2);
1904
+ quotaEvicted++;
1905
+ }
1906
+ return deleted;
1907
+ }
1908
+ async function restore() {
1909
+ const saved = readBackup();
1910
+ const primary = await readIndexedDB().catch(() => {
1911
+ storageFailures++;
1912
+ return [];
1913
+ });
1914
+ const corrupt = [];
1915
+ for (const record of [...saved, ...primary].filter((record2) => {
1916
+ if (valid2(record2) && (!purge || record2.purge === purge)) return true;
1917
+ if (typeof record2?.id === "string") corrupt.push(record2.id);
1918
+ return false;
1919
+ }).sort((a, b) => a.expires - b.expires)) {
1920
+ remove(record.id);
1921
+ memory.set(record.id, record);
1922
+ pendingBytes += record.bytes;
1923
+ }
1924
+ const deleted = [...corrupt, ...prune()];
1925
+ const existing = new Set(primary.filter(valid2).map((record) => record.id));
1926
+ if (db) removeBackup(saved.filter((record) => existing.has(record.id)).map((record) => record.id));
1927
+ const migrate = db ? saved.filter((record) => memory.has(record.id) && !existing.has(record.id)) : [];
1928
+ if (deleted.length || migrate.length) await persist(migrate, deleted);
1929
+ }
1930
+ function serial(action) {
1931
+ appends = null;
1932
+ const next = pending.then(async () => {
1933
+ await (opened ?? (opened = restore()));
1934
+ return action();
1935
+ });
1936
+ pending = next.then(() => {
1937
+ }, () => {
1938
+ storageFailures++;
1939
+ });
1940
+ return next;
1941
+ }
1942
+ return {
1943
+ /** Coalesces queued captures into one disk transaction while preserving the capture order. */
1944
+ append(record) {
1945
+ if (appends) {
1946
+ appends.records.push(record);
1947
+ return appends.committed;
1948
+ }
1949
+ const records = [record];
1950
+ const committed = serial(async () => {
1951
+ if (appends?.records === records) appends = null;
1952
+ for (const item of records) {
1953
+ if (purge) item.purge = purge;
1954
+ remove(item.id);
1955
+ memory.set(item.id, item);
1956
+ pendingBytes += item.bytes;
1957
+ }
1958
+ const deleted = memory.size > MAX_RECORDS || pendingBytes > MAX_BYTES ? prune() : [];
1959
+ await persist(records.filter((item) => memory.has(item.id)), deleted);
1960
+ });
1961
+ appends = { records, committed };
1962
+ return committed;
1963
+ },
1964
+ /** Selects a bounded upload batch without removing unacknowledged records. */
1965
+ batch() {
1966
+ return serial(async () => {
1967
+ const deleted = prune();
1968
+ if (deleted.length) await persist([], deleted);
1969
+ const selected = [];
1970
+ let bytes = 0;
1971
+ for (const record of memory.values()) {
1972
+ if (selected.length >= 50 || bytes + record.bytes > 96 * 1024) break;
1973
+ selected.push(record.value);
1974
+ bytes += record.bytes;
1975
+ }
1976
+ return selected;
1977
+ });
1978
+ },
1979
+ /** Deletes only terminal receipt IDs from memory and any writable journal. */
1980
+ acknowledge(ids) {
1981
+ return serial(async () => {
1982
+ for (const id2 of ids) remove(id2);
1983
+ await persist([], [...ids]);
1984
+ });
1985
+ },
1986
+ /** Purges evidence and fences the old generation if an inaccessible IndexedDB returns later. */
1987
+ clear() {
1988
+ return serial(async () => {
1989
+ memory.clear();
1990
+ pendingBytes = 0;
1991
+ purge = crypto.randomUUID();
1992
+ if (backup) {
1993
+ try {
1994
+ for (const key of backupKeys(backup)) backup.removeItem(key);
1995
+ backup.setItem(purgeKey, purge);
1996
+ } catch {
1997
+ storageFailures++;
1998
+ }
1999
+ }
2000
+ backupSizes.clear();
2001
+ backupBytes = 0;
2002
+ await persist([], [], true);
2003
+ });
2004
+ },
2005
+ /** Reports known storage limits without exposing evidence or credentials. */
2006
+ diagnostics() {
2007
+ let volatile = 0;
2008
+ if (!db) {
2009
+ for (const id2 of memory.keys()) if (!backupSizes.has(id2)) volatile++;
2010
+ }
2011
+ return {
2012
+ storage: db ? "indexeddb" : backup ? "localStorage" : "memory",
2013
+ pending_records: memory.size,
2014
+ pending_bytes: pendingBytes,
2015
+ memory_only_records: volatile,
2016
+ quota_evicted: quotaEvicted,
2017
+ expired_records: expiredRecords,
2018
+ storage_failures: storageFailures
2019
+ };
2020
+ },
2021
+ /** Closes browser handles after earlier writes settle. */
2022
+ async close() {
2023
+ await pending;
2024
+ db?.close();
2025
+ db = null;
2026
+ }
2027
+ };
2028
+ }
2029
+
2030
+ // src/popup.ts
2031
+ function popupCompletionKey(workspaceId, installationId) {
2032
+ return `sagepilot:sdk-popup:${workspaceId}:${installationId}:completed`;
2033
+ }
2034
+ function valid(value) {
2035
+ if (!value || !Number.isSafeInteger(value.revision) || value.revision < 1 || typeof value.enabled !== "boolean" || !/^#[0-9a-f]{6}$/i.test(value.accent_color) || !["email", "none"].includes(value.verification) || ![value.title, value.body, value.button_text, value.success_text].every((item) => typeof item === "string" && item.length <= 2e3) || !value.fields || ![value.fields.name, value.fields.email, value.fields.phone].every((item) => typeof item === "boolean") || !Array.isArray(value.required_fields) || value.required_fields.some((field) => !["name", "email", "phone"].includes(field) || !value.fields[field]) || value.verification === "email" && !value.fields.email || !Number.isFinite(value.frequency_hours) || value.frequency_hours < 1 || value.frequency_hours > 8760 || !value.trigger || !Number.isFinite(value.trigger.delay_seconds) || value.trigger.delay_seconds < 0 || value.trigger.delay_seconds > 300 || !Number.isFinite(value.trigger.scroll_percent) || value.trigger.scroll_percent < 0 || value.trigger.scroll_percent > 100 || !Array.isArray(value.trigger.page_paths) || value.trigger.page_paths.length > 20 || value.trigger.page_paths.some((path) => typeof path !== "string" || !path.startsWith("/") || path.startsWith("//") || /[?#\\]/.test(path) || path.length > 300)) return false;
2036
+ if (value.logo_url !== null) {
2037
+ try {
2038
+ const url = new URL(value.logo_url);
2039
+ if (url.protocol !== "https:" || url.username || url.password) return false;
2040
+ } catch {
2041
+ return false;
2042
+ }
2043
+ }
2044
+ return true;
2045
+ }
2046
+ function createPopup(options) {
2047
+ let config = null, host = null;
2048
+ let timer = null, entered = Date.now(), path = location.pathname, generation = 0;
2049
+ let previousFocus = null, cleanup = null;
2050
+ const scope = `sagepilot:sdk-popup:${options.client.workspaceId}:${options.client.installationId}`;
2051
+ const shownKey = scope + ":shown";
2052
+ const completedKey = popupCompletionKey(options.client.workspaceId, options.client.installationId);
2053
+ function remove() {
2054
+ generation++;
2055
+ cleanup?.();
2056
+ cleanup = null;
2057
+ host?.remove();
2058
+ host = null;
2059
+ if (previousFocus instanceof HTMLElement && previousFocus.isConnected) previousFocus.focus();
2060
+ previousFocus = null;
2061
+ }
2062
+ function eligible() {
2063
+ const session = options.client.session();
2064
+ if (!config?.enabled || host || !session || session.customer_id || options.storage.getItem(completedKey) === session.anonymous_id) return false;
2065
+ if (path !== location.pathname) {
2066
+ path = location.pathname;
2067
+ entered = Date.now();
2068
+ }
2069
+ if (config.trigger.page_paths.length && !config.trigger.page_paths.includes(path)) return false;
2070
+ if (Date.now() - entered < config.trigger.delay_seconds * 1e3) return false;
2071
+ const height = Math.max(document.documentElement.scrollHeight - innerHeight, 1);
2072
+ if (config.trigger.scroll_percent > 0 && scrollY / height * 100 < config.trigger.scroll_percent) return false;
2073
+ const raw = options.storage.getItem(shownKey), last = Number(raw);
2074
+ return !raw || !Number.isFinite(last) || Date.now() - last >= config.frequency_hours * 36e5;
2075
+ }
2076
+ function element(tag, text2) {
2077
+ const node = document.createElement(tag);
2078
+ if (text2 !== void 0) node.textContent = text2;
2079
+ return node;
2080
+ }
2081
+ function show() {
2082
+ if (!eligible() || !config) return;
2083
+ const published = config, revision = ++generation;
2084
+ previousFocus = document.activeElement;
2085
+ host = element("div");
2086
+ host.setAttribute("data-sagepilot-popup", "");
2087
+ host.style.cssText = "position:fixed;inset:0;z-index:2147483000;display:grid;place-items:center;padding:20px;background:#10251d66";
2088
+ const shadow = host.attachShadow({ mode: "open" });
2089
+ const style = element("style");
2090
+ style.textContent = `:host{font-family:system-ui,sans-serif;color:#172c23;color-scheme:light}*{box-sizing:border-box}.panel{background:white;width:min(420px,calc(100vw - 40px));max-height:90dvh;overflow:auto;border-radius:18px;padding:28px;box-shadow:0 16px 80px #0003;position:relative}h2{margin:4px 28px 10px 0;font-size:25px}p{line-height:1.5;color:#52645a}label{display:block;margin-top:14px;font-size:14px}input{display:block;width:100%;padding:11px;border:1px solid #b6c7bd;border-radius:7px;margin-top:6px;font:inherit;background:white;color:#172c23}button{cursor:pointer;font:inherit;color:inherit}button[type=submit]{display:block;width:100%;border:0;border-radius:8px;background:${published.accent_color};color:white;padding:13px;margin-top:20px;font-weight:600}button:disabled{opacity:.6;cursor:wait}.close{position:absolute;right:15px;top:12px;border:0;background:transparent;font-size:25px}.status{min-height:1.3em;font-size:14px}.logo{max-width:160px;max-height:60px;object-fit:contain}input:focus-visible,button:focus-visible{outline:3px solid #729e82;outline-offset:3px}@media(max-width:480px){.panel{padding:22px}h2{font-size:22px}}`;
2091
+ shadow.append(style);
2092
+ const panel = element("section");
2093
+ panel.className = "panel";
2094
+ panel.setAttribute("role", "dialog");
2095
+ panel.setAttribute("aria-modal", "true");
2096
+ panel.setAttribute("aria-labelledby", "sage-popup-title");
2097
+ const close = element("button", "\xD7");
2098
+ close.type = "button";
2099
+ close.className = "close";
2100
+ close.setAttribute("aria-label", "Close form");
2101
+ function dismiss() {
2102
+ options.emit("popup_dismissed");
2103
+ remove();
2104
+ }
2105
+ close.addEventListener("click", dismiss);
2106
+ panel.append(close);
2107
+ if (published.logo_url) {
2108
+ const logo = element("img");
2109
+ logo.src = published.logo_url;
2110
+ logo.alt = "";
2111
+ logo.className = "logo";
2112
+ logo.referrerPolicy = "no-referrer";
2113
+ panel.append(logo);
2114
+ }
2115
+ const title = element("h2", published.title);
2116
+ title.id = "sage-popup-title";
2117
+ panel.append(title, element("p", published.body));
2118
+ const form = element("form"), status = element("p");
2119
+ status.className = "status";
2120
+ status.setAttribute("role", "status");
2121
+ status.setAttribute("aria-live", "polite");
2122
+ const inputs = /* @__PURE__ */ new Map();
2123
+ const contactLabels = [];
2124
+ for (const field of ["name", "email", "phone"]) {
2125
+ if (!published.fields[field]) continue;
2126
+ const label2 = element("label", field === "phone" ? "Phone" : field === "email" ? "Email" : "Name");
2127
+ const input = element("input");
2128
+ input.name = field;
2129
+ input.type = field === "email" ? "email" : field === "phone" ? "tel" : "text";
2130
+ input.autocomplete = field === "phone" ? "tel" : field;
2131
+ input.maxLength = field === "phone" ? 32 : field === "email" ? 254 : 100;
2132
+ input.required = published.required_fields.includes(field) || field === "email" && published.verification === "email";
2133
+ label2.append(input);
2134
+ form.append(label2);
2135
+ contactLabels.push(label2);
2136
+ inputs.set(field, input);
2137
+ }
2138
+ const submit = element("button", published.button_text);
2139
+ submit.type = "submit";
2140
+ form.append(submit, status);
2141
+ panel.append(form);
2142
+ shadow.append(panel);
2143
+ document.body.append(host);
2144
+ options.storage.setItem(shownKey, String(Date.now()));
2145
+ options.emit("popup_shown");
2146
+ const contactInputs = new Map(inputs);
2147
+ let submissionId = crypto.randomUUID(), challenge = null, busy = false;
2148
+ async function submitForm(event) {
2149
+ event.preventDefault();
2150
+ if (busy || revision !== generation) return;
2151
+ busy = true;
2152
+ submit.disabled = true;
2153
+ status.textContent = "";
2154
+ try {
2155
+ if (challenge) {
2156
+ await options.identify(challenge, inputs.get("code").value);
2157
+ if (revision !== generation) return;
2158
+ form.replaceChildren(element("p", published.success_text), close);
2159
+ close.className = "close";
2160
+ } else {
2161
+ const contact = { submission_id: submissionId, popup_revision: published.revision, marketing_consent: true };
2162
+ for (const field of ["name", "email", "phone"]) {
2163
+ const value = inputs.get(field)?.value.trim();
2164
+ if (value) contact[field] = value;
2165
+ }
2166
+ const result = await options.submit(contact);
2167
+ if (revision !== generation) return;
2168
+ options.emit("popup_submitted");
2169
+ for (const input of inputs.values()) input.value = "";
2170
+ if (result.status === "verification_required") {
2171
+ let restartForm2 = function() {
2172
+ if (busy) return;
2173
+ challenge = null;
2174
+ submissionId = crypto.randomUUID();
2175
+ inputs.clear();
2176
+ for (const [key, value] of contactInputs) inputs.set(key, value);
2177
+ submit.textContent = published.button_text;
2178
+ status.textContent = "";
2179
+ form.replaceChildren(...contactLabels, submit, status);
2180
+ inputs.values().next().value?.focus();
2181
+ };
2182
+ var restartForm = restartForm2;
2183
+ challenge = result.challenge_id;
2184
+ inputs.clear();
2185
+ const label2 = element("label", "Email verification code"), code = element("input");
2186
+ code.name = "code";
2187
+ code.inputMode = "numeric";
2188
+ code.autocomplete = "one-time-code";
2189
+ code.pattern = "[0-9]{6}";
2190
+ code.maxLength = 6;
2191
+ code.required = true;
2192
+ inputs.set("code", code);
2193
+ label2.append(code);
2194
+ submit.textContent = "Verify email";
2195
+ const restart = element("button", "Start again or use another email");
2196
+ restart.type = "button";
2197
+ restart.addEventListener("click", restartForm2);
2198
+ form.replaceChildren(label2, submit, restart, status);
2199
+ status.textContent = "Check your email for the six-digit code.";
2200
+ code.focus();
2201
+ } else form.replaceChildren(element("p", published.success_text));
2202
+ }
2203
+ } catch (error) {
2204
+ if (revision === generation) status.textContent = challenge ? "Verification failed. Check the code and try again." : "We couldn't save the form. Please try again.";
2205
+ options.report(error);
2206
+ } finally {
2207
+ busy = false;
2208
+ submit.disabled = false;
2209
+ }
2210
+ }
2211
+ form.addEventListener("submit", submitForm);
2212
+ function keydown(event) {
2213
+ if (event.key === "Escape") {
2214
+ event.preventDefault();
2215
+ dismiss();
2216
+ return;
2217
+ }
2218
+ if (event.key !== "Tab") return;
2219
+ const focusable = [...shadow.querySelectorAll("button:not(:disabled),input:not(:disabled)")];
2220
+ const first = focusable[0], last = focusable[focusable.length - 1];
2221
+ if (event.shiftKey && shadow.activeElement === first) {
2222
+ event.preventDefault();
2223
+ last?.focus();
2224
+ } else if (!event.shiftKey && shadow.activeElement === last) {
2225
+ event.preventDefault();
2226
+ first?.focus();
2227
+ }
2228
+ }
2229
+ shadow.addEventListener("keydown", keydown);
2230
+ cleanup = () => {
2231
+ form.removeEventListener("submit", submitForm);
2232
+ shadow.removeEventListener("keydown", keydown);
2233
+ };
2234
+ (inputs.values().next().value ?? close).focus();
2235
+ }
2236
+ return {
2237
+ /** Applies only a validated published revision; disabling immediately removes an open form. */
2238
+ configure(value) {
2239
+ if (value && !valid(value)) {
2240
+ options.report(new Error("Invalid published popup configuration."));
2241
+ value = null;
2242
+ }
2243
+ if (value?.revision === config?.revision && value?.enabled === config?.enabled) return;
2244
+ remove();
2245
+ config = value ?? null;
2246
+ entered = Date.now();
2247
+ path = location.pathname;
2248
+ if (timer) clearInterval(timer);
2249
+ timer = null;
2250
+ if (config?.enabled) {
2251
+ timer = setInterval(show, 1e3);
2252
+ show();
2253
+ }
2254
+ },
2255
+ /** Cancels rendering and listeners when the installation runtime is destroyed. */
2256
+ destroy() {
2257
+ if (timer) clearInterval(timer);
2258
+ timer = null;
2259
+ config = null;
2260
+ remove();
2261
+ }
2262
+ };
2263
+ }
2264
+
2265
+ // src/index.ts
2266
+ var instances = /* @__PURE__ */ new Map();
2267
+ var VISITOR_KEY = "sagepilot_anonymous_id";
2268
+ var VISITOR_ID2 = /^[A-Za-z0-9_-]{1,128}$/;
2269
+ function createSagepilotWebEvents(options) {
2270
+ const scope = options.key.toLowerCase();
2271
+ const existing = instances.get(scope);
2272
+ if (existing) return existing;
2273
+ let anonymousStorage = "memory";
2274
+ let browserId = null;
2275
+ let cookieRequests = Promise.resolve();
2276
+ let cookieSynced = false;
2277
+ const memory = /* @__PURE__ */ new Map();
2278
+ const storage = {
2279
+ getItem(key) {
2280
+ try {
2281
+ return localStorage.getItem(key) ?? memory.get(key) ?? null;
2282
+ } catch {
2283
+ return memory.get(key) ?? null;
2284
+ }
2285
+ },
2286
+ setItem(key, value) {
2287
+ memory.set(key, value);
2288
+ try {
2289
+ localStorage.setItem(key, value);
2290
+ } catch {
2291
+ }
2292
+ },
2293
+ removeItem(key) {
2294
+ memory.delete(key);
2295
+ try {
2296
+ localStorage.removeItem(key);
2297
+ } catch {
2298
+ }
2299
+ }
2300
+ };
2301
+ const client = createSagepilotInstallationClient({
2302
+ key: options.key,
2303
+ apiHost: options.apiHost,
2304
+ platform: "web",
2305
+ fetch: options.fetch,
2306
+ storage,
2307
+ sdk: { name, version }
2308
+ });
2309
+ const completedKey = popupCompletionKey(client.workspaceId, client.installationId);
2310
+ const cookieKey = `sagepilot:sdk-visitor-cookie:${scope}`;
2311
+ const journal = createJournal(scope.replace(/:/g, "_"));
2312
+ let localPermission = options.collectionEnabled === true, started = false, destroyed = false, policyKnown = false;
2313
+ let identityChanging = false;
2314
+ let starting = null, draining = null;
2315
+ let generation = 0, epoch = "", failures = 0, rejected = 0, dropped = 0, lastRefresh = 0, capturePending = 0, captureBytes = 0, networkMutations = 0;
2316
+ let retryTimer = null;
2317
+ let policyTimer = null;
2318
+ let stopPage = null, stopNetwork = null;
2319
+ let products = null;
2320
+ function report(error) {
2321
+ try {
2322
+ options.onError?.(error);
2323
+ } catch {
2324
+ }
2325
+ }
2326
+ function canUseVisitorCookie() {
2327
+ const session = client.session();
2328
+ return !destroyed && localPermission && session?.collection_enabled === true && session.visitor_cookie_enabled === true;
2329
+ }
2330
+ function syncVisitorCookie(anonymousId, allowed) {
2331
+ if (!allowed) cookieSynced = false;
2332
+ const operation = cookieRequests.then(async () => {
2333
+ if (allowed) {
2334
+ if (!canUseVisitorCookie()) return null;
2335
+ storage.setItem(cookieKey, "1");
2336
+ } else if (!storage.getItem(cookieKey)) return null;
2337
+ try {
2338
+ const visitor = await client.syncVisitorCookie(
2339
+ anonymousId,
2340
+ allowed,
2341
+ anonymousId !== null && storage.getItem(completedKey) === anonymousId
2342
+ );
2343
+ if (visitor.anonymous_id === null) {
2344
+ cookieSynced = false;
2345
+ storage.removeItem(cookieKey);
2346
+ }
2347
+ return visitor;
2348
+ } catch (error) {
2349
+ if (!destroyed) report(error);
2350
+ return null;
2351
+ }
2352
+ });
2353
+ cookieRequests = operation;
2354
+ return operation;
2355
+ }
2356
+ function rememberBrowser(anonymousId, restored) {
2357
+ memory.set(VISITOR_KEY, anonymousId);
2358
+ try {
2359
+ if (localStorage.getItem(VISITOR_KEY) !== anonymousId) localStorage.setItem(VISITOR_KEY, anonymousId);
2360
+ anonymousStorage = "localStorage";
2361
+ } catch {
2362
+ anonymousStorage = "memory";
2363
+ }
2364
+ if (restored) anonymousStorage = "cookie";
2365
+ browserId = anonymousId;
2366
+ }
2367
+ async function syncCookiePolicy(revision) {
2368
+ if (destroyed || revision !== generation) return;
2369
+ const session = client.session();
2370
+ if (!session?.collection_enabled || session.visitor_cookie_enabled !== true) {
2371
+ await syncVisitorCookie(null, false);
2372
+ if (browserId && anonymousStorage === "cookie") rememberBrowser(browserId, false);
2373
+ return;
2374
+ }
2375
+ if (cookieSynced || !localPermission) return;
2376
+ const currentId = session.anonymous_id;
2377
+ const cookie = await syncVisitorCookie(currentId, true);
2378
+ const restoredId = cookie?.anonymous_id;
2379
+ if (!restoredId || revision !== generation || !canUseVisitorCookie() || client.session()?.anonymous_id !== currentId) return;
2380
+ if (cookie.popup_completed) {
2381
+ storage.setItem(completedKey, restoredId);
2382
+ popup.configure(null);
2383
+ }
2384
+ rememberBrowser(restoredId, cookie.restored);
2385
+ if (restoredId !== currentId) {
2386
+ policyKnown = false;
2387
+ epoch = crypto.randomUUID();
2388
+ detach(true);
2389
+ popup.configure(null);
2390
+ client.destroy();
2391
+ await client.start(restoredId);
2392
+ }
2393
+ if (revision === generation && canUseVisitorCookie()) cookieSynced = true;
2394
+ }
2395
+ async function startVisitor(revision) {
2396
+ const saved = browserId ?? storage.getItem(VISITOR_KEY);
2397
+ const anonymousId = saved && VISITOR_ID2.test(saved) ? saved : crypto.randomUUID();
2398
+ rememberBrowser(anonymousId, false);
2399
+ await client.start(anonymousId);
2400
+ await syncCookiePolicy(revision);
2401
+ }
2402
+ async function completeVisitor(anonymousId) {
2403
+ if (!anonymousId || destroyed || client.session()?.anonymous_id !== anonymousId) return;
2404
+ storage.setItem(completedKey, anonymousId);
2405
+ if (canUseVisitorCookie()) await syncVisitorCookie(anonymousId, true);
2406
+ }
2407
+ async function clearCollection() {
2408
+ await Promise.all([journal.clear(), syncVisitorCookie(null, false)]);
2409
+ }
2410
+ function enabled() {
2411
+ if (destroyed || identityChanging || !started || !localPermission || !policyKnown) return false;
2412
+ const session = client.session();
2413
+ return session?.collection_enabled === true && Date.parse(session.expires_at) > Date.now();
2414
+ }
2415
+ function captureEpoch() {
2416
+ return enabled() ? epoch : null;
2417
+ }
2418
+ function excluded(input) {
2419
+ try {
2420
+ const url = new URL(input, location.href), session = client.session();
2421
+ return url.origin === client.apiHost && url.pathname.startsWith("/platform-api/v1/sdk/") || url.origin === location.origin && url.pathname === client.visitorCookiePath || !!session && url.origin + url.pathname === session.ingestion_url;
2422
+ } catch {
2423
+ return true;
2424
+ }
2425
+ }
2426
+ const capture = (kind, data, capturedEpoch, time) => {
2427
+ const session = client.session();
2428
+ if (!session || !enabled() || epoch !== capturedEpoch) {
2429
+ dropped++;
2430
+ return;
2431
+ }
2432
+ const observation = {
2433
+ event_id: crypto.randomUUID(),
2434
+ kind,
2435
+ occurred_at: new Date(time).toISOString(),
2436
+ owner: { session_id: session.session_id, customer_id: session.customer_id, identity_epoch: capturedEpoch },
2437
+ workspace_id: session.workspace_id,
2438
+ installation_id: session.installation_id,
2439
+ platform: "web",
2440
+ sdk_name: name,
2441
+ sdk_version: version,
2442
+ clock_quality: "unverified",
2443
+ clock_source: "device_wall",
2444
+ data
2445
+ };
2446
+ const bytes = new TextEncoder().encode(JSON.stringify(observation)).byteLength;
2447
+ if (bytes > 32768) {
2448
+ rejected++;
2449
+ return;
2450
+ }
2451
+ if (capturePending >= 128 || captureBytes + bytes > 512 * 1024) {
2452
+ dropped++;
2453
+ return;
2454
+ }
2455
+ capturePending++;
2456
+ captureBytes += bytes;
2457
+ void journal.append({ id: observation.event_id, value: observation, bytes, expires: Date.now() + JOURNAL_TTL_MS }).catch(report).finally(() => {
2458
+ capturePending--;
2459
+ captureBytes -= bytes;
2460
+ });
2461
+ };
2462
+ const popup = createPopup({
2463
+ client,
2464
+ storage,
2465
+ report,
2466
+ emit(action) {
2467
+ if (enabled()) capture("lifecycle", { action }, epoch, Date.now());
2468
+ },
2469
+ submit(input) {
2470
+ return addon.submitContact(input);
2471
+ },
2472
+ identify(challenge, code) {
2473
+ return addon.verifyContact(challenge, code);
2474
+ }
2475
+ });
2476
+ async function changeIdentity(operation) {
2477
+ if (!started || destroyed) throw new Error("Start the web-events installation first.");
2478
+ if (identityChanging) throw new Error("An SDK identity operation is already running.");
2479
+ identityChanging = true;
2480
+ generation++;
2481
+ epoch = crypto.randomUUID();
2482
+ detach(true);
2483
+ client.cancelPending();
2484
+ try {
2485
+ await operation();
2486
+ } finally {
2487
+ identityChanging = false;
2488
+ if (!destroyed) {
2489
+ epoch = crypto.randomUUID();
2490
+ if (enabled()) attach();
2491
+ else detach();
2492
+ schedule();
2493
+ }
2494
+ }
2495
+ }
2496
+ async function refreshConfig() {
2497
+ if (identityChanging) return;
2498
+ const revision = generation;
2499
+ const previous = client.session();
2500
+ try {
2501
+ await client.refresh(true);
2502
+ await syncCookiePolicy(revision);
2503
+ } catch (error) {
2504
+ if (revision === generation && !destroyed) {
2505
+ popup.configure(null);
2506
+ const status = error instanceof SagepilotInstallationError ? error.status ?? 0 : 0;
2507
+ const temporary = error instanceof SagepilotInstallationError && (error.code === "network_error" || status === 408 || status === 425 || status === 429 || status >= 500);
2508
+ if (!temporary) suspendCapture();
2509
+ }
2510
+ throw error;
2511
+ }
2512
+ if (revision !== generation || destroyed) return;
2513
+ if (previous?.session_id !== client.session()?.session_id || Date.parse(previous?.expires_at ?? "") <= Date.now()) {
2514
+ epoch = crypto.randomUUID();
2515
+ }
2516
+ lastRefresh = Date.now();
2517
+ policyKnown = true;
2518
+ popup.configure(client.session()?.popup);
2519
+ if (!client.session()?.collection_enabled) {
2520
+ detach();
2521
+ await journal.clear();
2522
+ } else {
2523
+ attach();
2524
+ schedule();
2525
+ }
2526
+ }
2527
+ function suspendCapture() {
2528
+ policyKnown = false;
2529
+ lastRefresh = 0;
2530
+ epoch = crypto.randomUUID();
2531
+ detach();
2532
+ schedule();
2533
+ }
2534
+ function detach(preserveProducts = false) {
2535
+ stopPage?.();
2536
+ stopPage = null;
2537
+ stopNetwork?.();
2538
+ stopNetwork = null;
2539
+ if (preserveProducts) products?.pause();
2540
+ else {
2541
+ products?.destroy();
2542
+ products = null;
2543
+ }
2544
+ if (retryTimer) clearTimeout(retryTimer);
2545
+ retryTimer = null;
2546
+ }
2547
+ function attach() {
2548
+ if (!enabled()) return;
2549
+ armNetwork();
2550
+ products?.activate();
2551
+ if (!stopPage) stopPage = observePage({
2552
+ epoch: captureEpoch,
2553
+ emit: capture,
2554
+ dropped: () => {
2555
+ dropped++;
2556
+ },
2557
+ networkVersion: () => networkMutations,
2558
+ productSnapshot: (root, price2, currency, label2) => products?.resolve(root, price2, currency, label2)
2559
+ });
2560
+ }
2561
+ function armNetwork() {
2562
+ if (destroyed || !localPermission || stopNetwork) return;
2563
+ products ?? (products = createProductIndex({ epoch: captureEpoch }));
2564
+ stopNetwork = observeNetwork({
2565
+ epoch: captureEpoch,
2566
+ excluded,
2567
+ emit: capture,
2568
+ dropped: () => {
2569
+ dropped++;
2570
+ },
2571
+ mutation: () => {
2572
+ networkMutations++;
2573
+ },
2574
+ products
2575
+ });
2576
+ }
2577
+ function schedule() {
2578
+ if (!started || destroyed || !localPermission || identityChanging || retryTimer) return;
2579
+ const delay = failures ? Math.min(6e4, 2e3 * 2 ** Math.min(failures, 5)) * (0.8 + Math.random() * 0.4) : 2e3;
2580
+ retryTimer = setTimeout(() => {
2581
+ retryTimer = null;
2582
+ void flush().catch(report).finally(schedule);
2583
+ }, delay);
2584
+ }
2585
+ async function flush() {
2586
+ if (draining) return draining;
2587
+ if (!started || destroyed || !localPermission || identityChanging) return;
2588
+ const revision = generation;
2589
+ draining = (async () => {
2590
+ try {
2591
+ if (!policyKnown || Date.now() - lastRefresh > 6e4) await refreshConfig();
2592
+ if (revision !== generation || !enabled()) return;
2593
+ for (let pass = 0; pass < 4; pass++) {
2594
+ const queued = await journal.batch();
2595
+ const first = queued[0]?.owner;
2596
+ const batch = queued.filter((item) => item.owner.session_id === first?.session_id && item.owner.customer_id === first?.customer_id);
2597
+ if (revision !== generation || !enabled()) return;
2598
+ if (!batch.length) {
2599
+ failures = 0;
2600
+ return;
2601
+ }
2602
+ if (!client.hasCaptureOwner(first)) {
2603
+ dropped += batch.length;
2604
+ await journal.acknowledge(new Set(batch.map((item) => item.event_id)));
2605
+ continue;
2606
+ }
2607
+ const receipt = await client.upload(batch, first);
2608
+ if (revision !== generation || !enabled()) return;
2609
+ const terminal = new Set(receipt.results.filter((item) => item.status !== "retryable").map((item) => item.event_id));
2610
+ dropped += receipt.results.filter((item) => item.status === "disabled" || item.status === "invalid").length;
2611
+ await journal.acknowledge(terminal);
2612
+ if (terminal.size !== batch.length) {
2613
+ failures = Math.min(failures + 1, 6);
2614
+ return;
2615
+ }
2616
+ failures = 0;
2617
+ }
2618
+ } catch (error) {
2619
+ if (revision === generation && !destroyed) {
2620
+ failures = Math.min(failures + 1, 6);
2621
+ if (client.session()?.collection_enabled === false) {
2622
+ detach();
2623
+ await journal.clear();
2624
+ } else if (policyKnown && error instanceof SagepilotInstallationError && [401, 403, 404, 410].includes(error.status ?? 0)) suspendCapture();
2625
+ }
2626
+ throw error;
2627
+ }
2628
+ })().finally(() => {
2629
+ draining = null;
2630
+ });
2631
+ return draining;
2632
+ }
2633
+ function online() {
2634
+ void flush().catch(report);
2635
+ }
2636
+ const addon = {
2637
+ /** Bootstraps this browser owner without loading chat, showing a popup or collecting before consent. */
2638
+ async start() {
2639
+ if (destroyed) throw new Error("This web-events collector was destroyed.");
2640
+ if (starting) return starting;
2641
+ if (started) return;
2642
+ if (typeof window === "undefined" || !globalThis.crypto?.randomUUID) throw new Error("Web events require a browser on HTTPS or loopback.");
2643
+ const revision = generation;
2644
+ starting = (async () => {
2645
+ armNetwork();
2646
+ await startVisitor(revision);
2647
+ if (destroyed || revision !== generation) return;
2648
+ started = true;
2649
+ policyKnown = true;
2650
+ epoch = crypto.randomUUID();
2651
+ lastRefresh = Date.now();
2652
+ popup.configure(client.session()?.popup);
2653
+ window.addEventListener("online", online);
2654
+ policyTimer = setInterval(() => {
2655
+ void refreshConfig().catch(report);
2656
+ }, 6e4);
2657
+ if (!localPermission || !client.session()?.collection_enabled) detach();
2658
+ if (!client.session()?.collection_enabled) await journal.clear();
2659
+ attach();
2660
+ schedule();
2661
+ })().catch((error) => {
2662
+ if (revision !== generation || destroyed) return;
2663
+ detach();
2664
+ throw error;
2665
+ }).finally(() => {
2666
+ starting = null;
2667
+ });
2668
+ return starting;
2669
+ },
2670
+ /** Fences immediately; disabling aborts SDK requests and purges unuploaded evidence. */
2671
+ async setCollectionEnabled(next) {
2672
+ if (destroyed) throw new Error("This web-events collector was destroyed.");
2673
+ if (typeof next !== "boolean") throw new Error("Collection permission must be boolean.");
2674
+ if (localPermission === next && started) {
2675
+ if (!next) await clearCollection();
2676
+ return;
2677
+ }
2678
+ localPermission = next;
2679
+ generation++;
2680
+ epoch = crypto.randomUUID();
2681
+ detach();
2682
+ client.cancelPending();
2683
+ if (!next) {
2684
+ await clearCollection();
2685
+ return;
2686
+ }
2687
+ if (starting) await starting;
2688
+ if (destroyed || !localPermission) return;
2689
+ if (!started) {
2690
+ await addon.start();
2691
+ return;
2692
+ }
2693
+ const revision = generation;
2694
+ policyKnown = false;
2695
+ try {
2696
+ await client.refresh(true);
2697
+ await syncCookiePolicy(revision);
2698
+ if (revision !== generation || destroyed || !localPermission) return;
2699
+ lastRefresh = Date.now();
2700
+ policyKnown = true;
2701
+ popup.configure(client.session()?.popup);
2702
+ if (!client.session()?.collection_enabled) await journal.clear();
2703
+ attach();
2704
+ schedule();
2705
+ } catch (error) {
2706
+ if (revision === generation && !destroyed) {
2707
+ lastRefresh = 0;
2708
+ schedule();
2709
+ }
2710
+ throw error;
2711
+ }
2712
+ },
2713
+ flush,
2714
+ /** Submits contact-only data through the explicit identity endpoint, never through autocapture. */
2715
+ async submitContact(input) {
2716
+ const anonymousId = client.session()?.anonymous_id;
2717
+ const result = await client.submitContact(input);
2718
+ if (result.status === "recorded") await completeVisitor(anonymousId);
2719
+ return result;
2720
+ },
2721
+ /** Verifies proof behind a capture fence; buffered anonymous observations remain anonymous. */
2722
+ async verifyContact(challengeId, code) {
2723
+ const anonymousId = client.session()?.anonymous_id;
2724
+ await changeIdentity(() => client.verifyContact(challengeId, code));
2725
+ await completeVisitor(anonymousId);
2726
+ },
2727
+ /** Applies an explicit logout boundary without relabeling earlier observations. */
2728
+ logout() {
2729
+ return changeIdentity(async () => {
2730
+ try {
2731
+ await client.logout();
2732
+ } catch (error) {
2733
+ policyKnown = false;
2734
+ lastRefresh = 0;
2735
+ throw error;
2736
+ }
2737
+ });
2738
+ },
2739
+ refreshConfig,
2740
+ /** Reports bounded operational counters, never contact values, credentials or queued payloads. */
2741
+ diagnostics() {
2742
+ return { ...journal.diagnostics(), enabled: enabled(), rejected_payloads: rejected, collector_dropped: dropped, anonymous_id_storage: anonymousStorage };
2743
+ },
2744
+ /** Stops the runtime and requests; consent revocation is the separate purge operation. */
2745
+ async destroy() {
2746
+ destroyed = true;
2747
+ generation++;
2748
+ detach();
2749
+ if (policyTimer) clearInterval(policyTimer);
2750
+ policyTimer = null;
2751
+ popup.destroy();
2752
+ client.destroy();
2753
+ if (typeof window !== "undefined") window.removeEventListener("online", online);
2754
+ await journal.close();
2755
+ if (instances.get(scope) === addon) instances.delete(scope);
2756
+ }
2757
+ };
2758
+ instances.set(scope, addon);
2759
+ return addon;
2760
+ }
2761
+ export {
2762
+ createSagepilotWebEvents
2763
+ };
2764
+ //# sourceMappingURL=index.js.map