@runbandit/sdk 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,818 +1,886 @@
1
- // src/session.ts
2
- var SESSION_ID_KEY = "bandits_session_id";
3
- var SESSION_CREATED_AT_KEY = "bandits_session_created_at";
4
- var DEFAULT_SESSION_TTL_MS = 864e5;
5
- function generateId() {
6
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
7
- return crypto.randomUUID();
8
- }
9
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
10
- const r = Math.random() * 16 | 0;
11
- return (c === "x" ? r : r & 3 | 8).toString(16);
12
- });
1
+ // ../shared/dist/types.js
2
+ var ExperimentStatus;
3
+ (function(ExperimentStatus2) {
4
+ ExperimentStatus2["DRAFT"] = "draft";
5
+ ExperimentStatus2["ACTIVE"] = "active";
6
+ ExperimentStatus2["PAUSED"] = "paused";
7
+ ExperimentStatus2["COMPLETED"] = "completed";
8
+ ExperimentStatus2["ARCHIVED"] = "archived";
9
+ })(ExperimentStatus || (ExperimentStatus = {}));
10
+ var BanditAlgorithm;
11
+ (function(BanditAlgorithm2) {
12
+ BanditAlgorithm2["THOMPSON_SAMPLING"] = "thompson_sampling";
13
+ BanditAlgorithm2["CONTEXTUAL_LINEAR"] = "contextual_linear";
14
+ })(BanditAlgorithm || (BanditAlgorithm = {}));
15
+ var Posterior;
16
+ (function(Posterior2) {
17
+ Posterior2["BETA"] = "beta";
18
+ Posterior2["NORMAL_GAMMA"] = "normal_gamma";
19
+ })(Posterior || (Posterior = {}));
20
+ function normalizePosterior(posterior) {
21
+ return posterior === Posterior.NORMAL_GAMMA ? Posterior.NORMAL_GAMMA : Posterior.BETA;
13
22
  }
14
- function getOrCreateSessionId(options = {}) {
15
- const { storage, now = () => Date.now(), ttlMs = DEFAULT_SESSION_TTL_MS } = options;
16
- if (!storage) return generateId();
17
- try {
18
- const stored = storage.getItem(SESSION_ID_KEY);
19
- const storedAt = storage.getItem(SESSION_CREATED_AT_KEY);
20
- if (stored && storedAt) {
21
- const age = now() - parseInt(storedAt, 10);
22
- if (age < ttlMs) return stored;
23
- }
24
- const id = generateId();
25
- storage.setItem(SESSION_ID_KEY, id);
26
- storage.setItem(SESSION_CREATED_AT_KEY, String(now()));
27
- return id;
28
- } catch {
29
- return generateId();
23
+ var ForgettingMode;
24
+ (function(ForgettingMode2) {
25
+ ForgettingMode2["NONE"] = "none";
26
+ ForgettingMode2["DISCOUNTED"] = "discounted";
27
+ ForgettingMode2["SLIDING_WINDOW"] = "sliding_window";
28
+ })(ForgettingMode || (ForgettingMode = {}));
29
+ var DEFAULT_HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1e3;
30
+ var DEFAULT_WINDOW_MS = 14 * 24 * 60 * 60 * 1e3;
31
+ var STATIONARY = { mode: ForgettingMode.NONE };
32
+ function resolveForgetting(config) {
33
+ switch (config?.mode) {
34
+ case ForgettingMode.DISCOUNTED:
35
+ return {
36
+ mode: ForgettingMode.DISCOUNTED,
37
+ halfLifeMs: config.halfLifeMs && config.halfLifeMs > 0 ? config.halfLifeMs : DEFAULT_HALF_LIFE_MS
38
+ };
39
+ case ForgettingMode.SLIDING_WINDOW:
40
+ return {
41
+ mode: ForgettingMode.SLIDING_WINDOW,
42
+ windowMs: config.windowMs && config.windowMs > 0 ? config.windowMs : DEFAULT_WINDOW_MS
43
+ };
44
+ default:
45
+ return STATIONARY;
30
46
  }
31
47
  }
32
- function defaultSessionStorage() {
33
- return typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
48
+ var LEGACY_ALGORITHM_ALIASES = {
49
+ epsilon_greedy: BanditAlgorithm.THOMPSON_SAMPLING,
50
+ ucb1: BanditAlgorithm.THOMPSON_SAMPLING,
51
+ contextual_neural: BanditAlgorithm.CONTEXTUAL_LINEAR
52
+ };
53
+ function normalizeAlgorithm(algorithm) {
54
+ if (algorithm === BanditAlgorithm.THOMPSON_SAMPLING)
55
+ return BanditAlgorithm.THOMPSON_SAMPLING;
56
+ if (algorithm === BanditAlgorithm.CONTEXTUAL_LINEAR)
57
+ return BanditAlgorithm.CONTEXTUAL_LINEAR;
58
+ return LEGACY_ALGORITHM_ALIASES[algorithm ?? ""] ?? BanditAlgorithm.THOMPSON_SAMPLING;
34
59
  }
35
-
36
- // src/client.ts
37
- function currentPageUrl() {
38
- if (typeof location !== "undefined" && location.origin && location.pathname) {
39
- return location.origin + location.pathname;
60
+ var ContentType;
61
+ (function(ContentType2) {
62
+ ContentType2["PLAIN_TEXT"] = "plain_text";
63
+ ContentType2["MARKDOWN"] = "markdown";
64
+ ContentType2["HTML"] = "html";
65
+ ContentType2["CSS"] = "css";
66
+ ContentType2["JAVASCRIPT"] = "javascript";
67
+ ContentType2["JSON"] = "json";
68
+ ContentType2["IMAGE"] = "image";
69
+ ContentType2["VIDEO"] = "video";
70
+ ContentType2["AUDIO"] = "audio";
71
+ ContentType2["DOCUMENT"] = "document";
72
+ })(ContentType || (ContentType = {}));
73
+ function getContentTypeCategory(contentType) {
74
+ switch (contentType) {
75
+ case ContentType.PLAIN_TEXT:
76
+ case ContentType.MARKDOWN:
77
+ return "text";
78
+ case ContentType.HTML:
79
+ case ContentType.CSS:
80
+ case ContentType.JAVASCRIPT:
81
+ case ContentType.JSON:
82
+ return "code";
83
+ case ContentType.IMAGE:
84
+ case ContentType.VIDEO:
85
+ case ContentType.AUDIO:
86
+ case ContentType.DOCUMENT:
87
+ return "file";
40
88
  }
41
- return void 0;
42
89
  }
43
- var BanditsClient = class {
44
- config;
45
- namedEventQueue = [];
46
- sessionId;
47
- flushTimer;
48
- constructor(config) {
49
- this.config = {
50
- timeout: 5e3,
51
- batchSize: 100,
52
- flushInterval: 1e4,
53
- sessionTtl: 864e5,
54
- ...config
55
- };
56
- this.sessionId = this.getOrCreateSessionId();
57
- this.startBatchFlush();
58
- }
59
- async getAssignment(experimentId, userId, context) {
60
- const request = {
61
- experimentId,
62
- userId,
63
- context,
64
- sessionId: this.sessionId
65
- };
66
- const pageUrl = currentPageUrl();
67
- if (pageUrl) request.pageUrl = pageUrl;
68
- const response = await this.makeRequest(
69
- "/assignments",
70
- "POST",
71
- request
72
- );
73
- if (!response.success || !response.data) {
74
- throw new Error(response.error || "Failed to get assignment");
75
- }
76
- const assignment = response.data?.data ?? response.data;
77
- if (!assignment?.assignmentId) {
78
- throw new Error("Invalid assignment response");
79
- }
80
- return {
81
- assignmentId: assignment.assignmentId,
82
- treatmentId: assignment.treatmentId,
83
- experimentId,
84
- userId,
85
- config: assignment.treatment?.config || {},
86
- funnelId: assignment.funnelId
87
- };
88
- }
89
- trackNamedEvent(event, opts) {
90
- const request = {
91
- name: event.name,
92
- category: event.category || "custom",
93
- value: event.value,
94
- metadata: event.metadata,
95
- sessionId: this.sessionId
96
- };
97
- if (opts?.userId) request.userId = opts.userId;
98
- if (opts?.assignmentId) request.assignmentId = opts.assignmentId;
99
- if (opts?.immediate) {
100
- const url = `${this.normalizeApiUrl()}/events`;
101
- try {
102
- void fetch(url, {
103
- method: "POST",
104
- headers: {
105
- "Authorization": `Bearer ${this.config.apiKey}`,
106
- "Content-Type": "application/json"
107
- },
108
- body: JSON.stringify(request),
109
- keepalive: true
110
- });
111
- } catch {
112
- }
113
- return;
114
- }
115
- this.namedEventQueue.push(request);
116
- if (this.namedEventQueue.length >= this.config.batchSize) {
117
- this.flush();
118
- }
119
- }
120
- async flush() {
121
- const namedEvents = [...this.namedEventQueue];
122
- this.namedEventQueue = [];
123
- if (namedEvents.length === 0) return;
124
- try {
125
- await fetch(`${this.normalizeApiUrl()}/events/batch`, {
126
- method: "POST",
127
- headers: {
128
- "Authorization": `Bearer ${this.config.apiKey}`,
129
- "Content-Type": "application/json"
130
- },
131
- body: JSON.stringify({ events: namedEvents }),
132
- keepalive: true
133
- });
134
- } catch (error) {
135
- this.namedEventQueue.unshift(...namedEvents);
136
- throw error;
90
+ var FunnelStatus;
91
+ (function(FunnelStatus2) {
92
+ FunnelStatus2["DRAFT"] = "draft";
93
+ FunnelStatus2["ACTIVE"] = "active";
94
+ FunnelStatus2["PAUSED"] = "paused";
95
+ FunnelStatus2["COMPLETED"] = "completed";
96
+ })(FunnelStatus || (FunnelStatus = {}));
97
+
98
+ // ../shared/dist/utils.js
99
+ function validateTreatmentWeights(treatments) {
100
+ const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);
101
+ return totalWeight > 0;
102
+ }
103
+ function normalizeTreatmentWeights(treatments) {
104
+ const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);
105
+ return treatments.map((treatment) => ({
106
+ ...treatment,
107
+ weight: (treatment.weight || 1) / totalWeight
108
+ }));
109
+ }
110
+ function calculateConfidenceInterval(successes, trials, confidence = 0.95) {
111
+ if (trials === 0)
112
+ return { lower: 0, upper: 0 };
113
+ const z = confidence === 0.95 ? 1.96 : confidence === 0.99 ? 2.576 : 1.645;
114
+ const p = successes / trials;
115
+ const margin = z * Math.sqrt(p * (1 - p) / trials);
116
+ return {
117
+ lower: Math.max(0, p - margin),
118
+ upper: Math.min(1, p + margin)
119
+ };
120
+ }
121
+ function isValidEmail(email) {
122
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
123
+ return emailRegex.test(email);
124
+ }
125
+ function sanitizeMetadata(metadata) {
126
+ const sanitized = {};
127
+ for (const [key, value] of Object.entries(metadata)) {
128
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
129
+ sanitized[key] = value;
130
+ } else if (value !== null && value !== void 0) {
131
+ sanitized[key] = String(value);
137
132
  }
138
133
  }
139
- async identify(anonymousId, identifiedUserId, mergeReason) {
140
- const response = await this.makeRequest(
141
- "/identify",
142
- "POST",
143
- {
144
- anonymousId,
145
- identifiedUserId,
146
- mergeReason: mergeReason || "manual"
147
- }
148
- );
149
- if (!response.success) {
150
- throw new Error(response.error || "Failed to identify user");
151
- }
134
+ return sanitized;
135
+ }
136
+ function generateSubdomain(name) {
137
+ return name.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 63);
138
+ }
139
+ function isValidSubdomain(subdomain) {
140
+ if (!subdomain || subdomain.length === 0 || subdomain.length > 63) {
141
+ return false;
152
142
  }
153
- async label(userId, labels) {
154
- const response = await this.makeRequest("/labels", "POST", { userId, labels });
155
- if (!response.success) {
156
- throw new Error(response.error || "Failed to record labels");
157
- }
143
+ const subdomainRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
144
+ const reserved = [
145
+ "www",
146
+ "api",
147
+ "app",
148
+ "admin",
149
+ "dashboard",
150
+ "mail",
151
+ "ftp",
152
+ "localhost",
153
+ "staging",
154
+ "dev",
155
+ "test",
156
+ "demo",
157
+ "blog",
158
+ "docs",
159
+ "status",
160
+ "help",
161
+ "support",
162
+ "cdn",
163
+ "static",
164
+ "assets",
165
+ "secure",
166
+ "payment",
167
+ "pay"
168
+ ];
169
+ return subdomainRegex.test(subdomain) && !reserved.includes(subdomain);
170
+ }
171
+ function extractSubdomain(hostname, baseDomain) {
172
+ if (!hostname || !baseDomain) {
173
+ return null;
158
174
  }
159
- async trackFunnelReward(funnelId, userId, value) {
160
- const response = await this.makeRequest(
161
- `/funnels/${funnelId}/reward`,
162
- "POST",
163
- { userId, value }
164
- );
165
- if (!response.success) {
166
- throw new Error(response.error || "Failed to record funnel reward");
167
- }
175
+ const cleanHostname = hostname.split(":")[0];
176
+ if (!cleanHostname.endsWith(baseDomain)) {
177
+ return null;
168
178
  }
169
- async getBundle(bundleId, userId, context) {
170
- const request = {
171
- bundleId,
172
- userId,
173
- context,
174
- sessionId: this.sessionId
175
- };
176
- const pageUrl = currentPageUrl();
177
- if (pageUrl) request.pageUrl = pageUrl;
178
- const response = await this.makeRequest("/bundles/assignment", "POST", request);
179
- if (!response.success || !response.data) {
180
- throw new Error(response.error || "Failed to get bundle assignment");
181
- }
182
- const payload = response.data?.data ?? response.data;
183
- if (!payload?.bundleAssignmentId || !payload?.slots) {
184
- throw new Error("Invalid bundle assignment response");
185
- }
186
- return {
187
- bundleAssignmentId: payload.bundleAssignmentId,
188
- slots: payload.slots
189
- };
179
+ const subdomain = cleanHostname.slice(0, -(baseDomain.length + 1));
180
+ if (!subdomain || subdomain === "www") {
181
+ return null;
190
182
  }
191
- async trackBundleReward(bundleId, userId, value) {
192
- const response = await this.makeRequest(`/bundles/${bundleId}/reward`, "POST", {
193
- userId,
194
- value
195
- });
196
- if (!response.success) {
197
- throw new Error(response.error || "Failed to record bundle reward");
198
- }
183
+ return subdomain;
184
+ }
185
+ function buildSubdomainUrl(subdomain, baseDomain, path = "", protocol = "https") {
186
+ const url = `${protocol}://${subdomain}.${baseDomain}`;
187
+ return path ? `${url}${path.startsWith("/") ? path : "/" + path}` : url;
188
+ }
189
+
190
+ // ../shared/dist/email-token.js
191
+ var EMAIL_TOKEN_PARAM = "bt";
192
+ var encoder = new TextEncoder();
193
+ function base64UrlEncode(bytes) {
194
+ let binary = "";
195
+ for (const byte of bytes)
196
+ binary += String.fromCharCode(byte);
197
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
198
+ }
199
+ function base64UrlDecode(value) {
200
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/");
201
+ const binary = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
202
+ const bytes = new Uint8Array(binary.length);
203
+ for (let i = 0; i < binary.length; i++)
204
+ bytes[i] = binary.charCodeAt(i);
205
+ return bytes;
206
+ }
207
+ async function hmac(secret, message) {
208
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
209
+ return new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(message)));
210
+ }
211
+ function timingSafeEqual(a, b) {
212
+ if (a.length !== b.length)
213
+ return false;
214
+ let diff = 0;
215
+ for (let i = 0; i < a.length; i++)
216
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
217
+ return diff === 0;
218
+ }
219
+ async function hashRecipient(recipientId, companySecret) {
220
+ const digest = await hmac(companySecret, `recipient:${recipientId}`);
221
+ return base64UrlEncode(digest.slice(0, 16));
222
+ }
223
+ async function mintEmailToken(payload, key) {
224
+ const body = base64UrlEncode(encoder.encode(JSON.stringify({
225
+ c: payload.c,
226
+ r: payload.r,
227
+ k: payload.k,
228
+ l: payload.l,
229
+ v: key.version
230
+ })));
231
+ const signature = base64UrlEncode(await hmac(key.secret, body));
232
+ return `${body}.${signature}`;
233
+ }
234
+ async function verifyEmailToken(token, keys) {
235
+ const parts = token.split(".");
236
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
237
+ return { valid: false, reason: "malformed" };
199
238
  }
200
- generateId() {
201
- return generateId();
239
+ const [body, signature] = parts;
240
+ let payload;
241
+ try {
242
+ payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(body)));
243
+ } catch {
244
+ return { valid: false, reason: "malformed" };
202
245
  }
203
- normalizeApiUrl() {
204
- let baseUrl = this.config.apiUrl.replace(/\/$/, "");
205
- if (!baseUrl.endsWith("/api")) {
206
- baseUrl = `${baseUrl}/api`;
207
- }
208
- return baseUrl;
246
+ if (!payload || typeof payload.c !== "string" || typeof payload.r !== "string" || !Array.isArray(payload.k) || !payload.k.every((arm) => Number.isInteger(arm) && arm >= 0) || !Number.isInteger(payload.l) || payload.l < 0 || !Number.isInteger(payload.v)) {
247
+ return { valid: false, reason: "malformed" };
209
248
  }
210
- getOrCreateSessionId() {
211
- return getOrCreateSessionId({
212
- storage: defaultSessionStorage(),
213
- ttlMs: this.config.sessionTtl
214
- });
249
+ const key = keys.find((candidate) => candidate.version === payload.v);
250
+ if (!key)
251
+ return { valid: false, reason: "unknown_key" };
252
+ const expected = base64UrlEncode(await hmac(key.secret, body));
253
+ if (!timingSafeEqual(expected, signature)) {
254
+ return { valid: false, reason: "bad_signature" };
215
255
  }
216
- async makeRequest(endpoint, method, body, extraHeaders) {
217
- const url = `${this.normalizeApiUrl()}${endpoint}`;
218
- const headers = {
219
- "Authorization": `Bearer ${this.config.apiKey}`,
220
- "Content-Type": "application/json",
221
- ...extraHeaders
222
- };
223
- const requestConfig = {
224
- method,
225
- headers,
226
- signal: AbortSignal.timeout(this.config.timeout)
227
- };
228
- if (body && method !== "GET") {
229
- requestConfig.body = JSON.stringify(body);
230
- }
231
- try {
232
- const response = await fetch(url, requestConfig);
233
- const data = await response.json();
234
- return {
235
- success: response.ok,
236
- data: response.ok ? data : void 0,
237
- error: response.ok ? void 0 : data?.message || "Request failed",
238
- timestamp: /* @__PURE__ */ new Date()
239
- };
240
- } catch (error) {
241
- return {
242
- success: false,
243
- error: error instanceof Error ? error.message : "Network error",
244
- timestamp: /* @__PURE__ */ new Date()
245
- };
246
- }
256
+ return { valid: true, payload };
257
+ }
258
+ function appendEmailToken(url, token) {
259
+ const hashIndex = url.indexOf("#");
260
+ const fragment = hashIndex === -1 ? "" : url.slice(hashIndex);
261
+ const base = hashIndex === -1 ? url : url.slice(0, hashIndex);
262
+ const separator = base.includes("?") ? "&" : "?";
263
+ return `${base}${separator}${EMAIL_TOKEN_PARAM}=${encodeURIComponent(token)}${fragment}`;
264
+ }
265
+
266
+ // ../shared/dist/email-plan.js
267
+ async function uniformFor(salt, recipientId, slotIndex) {
268
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(salt), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
269
+ const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${recipientId}:${slotIndex}`)));
270
+ let value = 0;
271
+ for (let i = 0; i < 6; i++)
272
+ value = value * 256 + mac[i];
273
+ return value / 2 ** 48;
274
+ }
275
+ function pickArm(probabilities, uniform) {
276
+ if (probabilities.length === 0)
277
+ return -1;
278
+ const total = probabilities.reduce((sum, p) => sum + Math.max(p, 0), 0);
279
+ if (total <= 0) {
280
+ return Math.min(probabilities.length - 1, Math.floor(uniform * probabilities.length));
247
281
  }
248
- startBatchFlush() {
249
- this.flushTimer = setInterval(() => {
250
- this.flush().catch(console.error);
251
- }, this.config.flushInterval);
282
+ const target = uniform * total;
283
+ let cumulative = 0;
284
+ for (let i = 0; i < probabilities.length; i++) {
285
+ cumulative += Math.max(probabilities[i], 0);
286
+ if (target < cumulative)
287
+ return i;
252
288
  }
253
- destroy() {
254
- if (this.flushTimer) {
255
- clearInterval(this.flushTimer);
256
- }
257
- this.flush().catch(console.error);
258
- if (typeof window !== "undefined" && window.localStorage) {
259
- localStorage.removeItem("bandits_session_id");
260
- localStorage.removeItem("bandits_session_created_at");
261
- }
289
+ return probabilities.length - 1;
290
+ }
291
+ async function sampleCombination(plan, recipientId) {
292
+ const combination = [];
293
+ for (let i = 0; i < plan.slots.length; i++) {
294
+ const slot = plan.slots[i];
295
+ const uniform = await uniformFor(plan.salt, recipientId, i);
296
+ const chosen = pickArm(slot.probabilities, uniform);
297
+ combination.push(chosen === -1 ? 0 : slot.arms[chosen]?.armIndex ?? 0);
262
298
  }
263
- };
264
-
265
- // ../shared/dist/types.js
266
- var ExperimentStatus;
267
- (function(ExperimentStatus2) {
268
- ExperimentStatus2["DRAFT"] = "draft";
269
- ExperimentStatus2["ACTIVE"] = "active";
270
- ExperimentStatus2["PAUSED"] = "paused";
271
- ExperimentStatus2["COMPLETED"] = "completed";
272
- ExperimentStatus2["ARCHIVED"] = "archived";
273
- })(ExperimentStatus || (ExperimentStatus = {}));
274
- var BanditAlgorithm;
275
- (function(BanditAlgorithm2) {
276
- BanditAlgorithm2["THOMPSON_SAMPLING"] = "thompson_sampling";
277
- BanditAlgorithm2["CONTEXTUAL_LINEAR"] = "contextual_linear";
278
- })(BanditAlgorithm || (BanditAlgorithm = {}));
279
- var Posterior;
280
- (function(Posterior2) {
281
- Posterior2["BETA"] = "beta";
282
- Posterior2["NORMAL_GAMMA"] = "normal_gamma";
283
- })(Posterior || (Posterior = {}));
284
- function normalizePosterior(posterior) {
285
- return posterior === Posterior.NORMAL_GAMMA ? Posterior.NORMAL_GAMMA : Posterior.BETA;
299
+ return combination;
286
300
  }
287
- var ForgettingMode;
288
- (function(ForgettingMode2) {
289
- ForgettingMode2["NONE"] = "none";
290
- ForgettingMode2["DISCOUNTED"] = "discounted";
291
- ForgettingMode2["SLIDING_WINDOW"] = "sliding_window";
292
- })(ForgettingMode || (ForgettingMode = {}));
293
- var DEFAULT_HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1e3;
294
- var DEFAULT_WINDOW_MS = 14 * 24 * 60 * 60 * 1e3;
295
- var STATIONARY = { mode: ForgettingMode.NONE };
296
- function resolveForgetting(config) {
297
- switch (config?.mode) {
298
- case ForgettingMode.DISCOUNTED:
299
- return {
300
- mode: ForgettingMode.DISCOUNTED,
301
- halfLifeMs: config.halfLifeMs && config.halfLifeMs > 0 ? config.halfLifeMs : DEFAULT_HALF_LIFE_MS
302
- };
303
- case ForgettingMode.SLIDING_WINDOW:
304
- return {
305
- mode: ForgettingMode.SLIDING_WINDOW,
306
- windowMs: config.windowMs && config.windowMs > 0 ? config.windowMs : DEFAULT_WINDOW_MS
307
- };
308
- default:
309
- return STATIONARY;
301
+ async function impressionManifest(plan, recipientIds) {
302
+ const counts = /* @__PURE__ */ new Map();
303
+ for (const recipientId of recipientIds) {
304
+ for (let i = 0; i < plan.slots.length; i++) {
305
+ const slot = plan.slots[i];
306
+ const uniform = await uniformFor(plan.salt, recipientId, i);
307
+ const chosen = pickArm(slot.probabilities, uniform);
308
+ const arm = slot.arms[chosen === -1 ? 0 : chosen];
309
+ if (!arm)
310
+ continue;
311
+ const key = `${slot.experimentId}:${arm.treatmentId}`;
312
+ const entry = counts.get(key);
313
+ if (entry)
314
+ entry.count++;
315
+ else
316
+ counts.set(key, {
317
+ experimentId: slot.experimentId,
318
+ treatmentId: arm.treatmentId,
319
+ count: 1
320
+ });
321
+ }
310
322
  }
323
+ return Array.from(counts.values());
311
324
  }
312
- var LEGACY_ALGORITHM_ALIASES = {
313
- epsilon_greedy: BanditAlgorithm.THOMPSON_SAMPLING,
314
- ucb1: BanditAlgorithm.THOMPSON_SAMPLING,
315
- contextual_neural: BanditAlgorithm.CONTEXTUAL_LINEAR
316
- };
317
- function normalizeAlgorithm(algorithm) {
318
- if (algorithm === BanditAlgorithm.THOMPSON_SAMPLING)
319
- return BanditAlgorithm.THOMPSON_SAMPLING;
320
- if (algorithm === BanditAlgorithm.CONTEXTUAL_LINEAR)
321
- return BanditAlgorithm.CONTEXTUAL_LINEAR;
322
- return LEGACY_ALGORITHM_ALIASES[algorithm ?? ""] ?? BanditAlgorithm.THOMPSON_SAMPLING;
325
+
326
+ // ../shared/dist/email-blocks.js
327
+ function isSlotBinding(value) {
328
+ return typeof value === "object" && value !== null && typeof value.$slot === "string";
323
329
  }
324
- var ContentType;
325
- (function(ContentType2) {
326
- ContentType2["PLAIN_TEXT"] = "plain_text";
327
- ContentType2["MARKDOWN"] = "markdown";
328
- ContentType2["HTML"] = "html";
329
- ContentType2["CSS"] = "css";
330
- ContentType2["JAVASCRIPT"] = "javascript";
331
- ContentType2["JSON"] = "json";
332
- ContentType2["IMAGE"] = "image";
333
- ContentType2["VIDEO"] = "video";
334
- ContentType2["AUDIO"] = "audio";
335
- ContentType2["DOCUMENT"] = "document";
336
- })(ContentType || (ContentType = {}));
337
- function getContentTypeCategory(contentType) {
338
- switch (contentType) {
339
- case ContentType.PLAIN_TEXT:
340
- case ContentType.MARKDOWN:
341
- return "text";
342
- case ContentType.HTML:
343
- case ContentType.CSS:
344
- case ContentType.JAVASCRIPT:
345
- case ContentType.JSON:
346
- return "code";
347
- case ContentType.IMAGE:
348
- case ContentType.VIDEO:
349
- case ContentType.AUDIO:
350
- case ContentType.DOCUMENT:
351
- return "file";
330
+ var CONTENT_WIDTH = 600;
331
+ function escapeHtml(value) {
332
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
333
+ }
334
+ function resolve(value, slots, missing) {
335
+ if (value === void 0)
336
+ return "";
337
+ if (!isSlotBinding(value))
338
+ return value;
339
+ const resolved = slots[value.$slot];
340
+ if (resolved === void 0) {
341
+ missing.push(value.$slot);
342
+ return "";
352
343
  }
344
+ return resolved;
353
345
  }
354
- var FunnelStatus;
355
- (function(FunnelStatus2) {
356
- FunnelStatus2["DRAFT"] = "draft";
357
- FunnelStatus2["ACTIVE"] = "active";
358
- FunnelStatus2["PAUSED"] = "paused";
359
- FunnelStatus2["COMPLETED"] = "completed";
360
- })(FunnelStatus || (FunnelStatus = {}));
361
-
362
- // ../shared/dist/utils.js
363
- function generateId2() {
364
- return Math.random().toString(36).substring(2) + Date.now().toString(36);
346
+ function extractDocumentSlots(document) {
347
+ const names = [];
348
+ const seen = /* @__PURE__ */ new Set();
349
+ const visit = (value) => {
350
+ if (isSlotBinding(value)) {
351
+ if (!seen.has(value.$slot)) {
352
+ seen.add(value.$slot);
353
+ names.push(value.$slot);
354
+ }
355
+ return;
356
+ }
357
+ if (Array.isArray(value))
358
+ value.forEach(visit);
359
+ else if (typeof value === "object" && value !== null)
360
+ Object.values(value).forEach(visit);
361
+ };
362
+ visit(document.subject);
363
+ visit(document.preheader);
364
+ document.blocks.forEach(visit);
365
+ return names;
365
366
  }
366
- function validateTreatmentWeights(treatments) {
367
- const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);
368
- return totalWeight > 0;
367
+ function renderBlock(block, slots, missing) {
368
+ const align = block.align ?? "left";
369
+ switch (block.type) {
370
+ case "heading": {
371
+ const size = block.level === 3 ? 18 : block.level === 2 ? 22 : 28;
372
+ const color = escapeHtml(resolve(block.color, slots, missing) || "#111111");
373
+ return `<tr><td style="padding:12px 24px;text-align:${align};font-family:Arial,Helvetica,sans-serif;font-size:${size}px;line-height:1.3;font-weight:bold;color:${color}">${escapeHtml(resolve(block.text, slots, missing))}</td></tr>`;
374
+ }
375
+ case "text": {
376
+ const color = escapeHtml(resolve(block.color, slots, missing) || "#333333");
377
+ return `<tr><td style="padding:8px 24px;text-align:${align};font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:${color}">${escapeHtml(resolve(block.text, slots, missing))}</td></tr>`;
378
+ }
379
+ case "button": {
380
+ const href = escapeHtml(resolve(block.href, slots, missing));
381
+ const label = escapeHtml(resolve(block.text, slots, missing));
382
+ const bg = escapeHtml(resolve(block.backgroundColor, slots, missing) || "#1a73e8");
383
+ const fg = escapeHtml(resolve(block.textColor, slots, missing) || "#ffffff");
384
+ return `<tr><td style="padding:16px 24px;text-align:${align}">
385
+ <!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${href}" style="height:44px;v-text-anchor:middle;width:220px;" arcsize="12%" strokecolor="${bg}" fillcolor="${bg}"><w:anchorlock/><center style="color:${fg};font-family:Arial,sans-serif;font-size:16px;font-weight:bold;">${label}</center></v:roundrect><![endif]-->
386
+ <!--[if !mso]><!--><a href="${href}" style="display:inline-block;padding:13px 28px;background-color:${bg};color:${fg};font-family:Arial,Helvetica,sans-serif;font-size:16px;font-weight:bold;text-decoration:none;border-radius:6px">${label}</a><!--<![endif]-->
387
+ </td></tr>`;
388
+ }
389
+ case "image": {
390
+ const src = escapeHtml(resolve(block.src, slots, missing));
391
+ const alt = escapeHtml(resolve(block.alt, slots, missing));
392
+ const width = block.width ?? CONTENT_WIDTH;
393
+ const img = `<img src="${src}" alt="${alt}" width="${width}" style="display:block;border:0;outline:none;text-decoration:none;max-width:100%;height:auto" />`;
394
+ const href = block.href ? escapeHtml(resolve(block.href, slots, missing)) : "";
395
+ return `<tr><td style="padding:0;text-align:center">${href ? `<a href="${href}">${img}</a>` : img}</td></tr>`;
396
+ }
397
+ case "divider":
398
+ return `<tr><td style="padding:12px 24px"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="border-top:1px solid ${escapeHtml(block.color ?? "#e5e5e5")};font-size:0;line-height:0">&nbsp;</td></tr></table></td></tr>`;
399
+ case "spacer":
400
+ return `<tr><td style="height:${block.height ?? 24}px;font-size:0;line-height:0">&nbsp;</td></tr>`;
401
+ default:
402
+ return "";
403
+ }
369
404
  }
370
- function normalizeTreatmentWeights(treatments) {
371
- const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);
372
- return treatments.map((treatment) => ({
373
- ...treatment,
374
- weight: (treatment.weight || 1) / totalWeight
375
- }));
405
+ function renderEmailDocument(document, slots) {
406
+ const missingSlots = [];
407
+ const width = document.contentWidth ?? CONTENT_WIDTH;
408
+ const background = escapeHtml(document.backgroundColor ?? "#f4f4f5");
409
+ const subject = resolve(document.subject, slots, missingSlots);
410
+ const preheader = resolve(document.preheader, slots, missingSlots);
411
+ const body = document.blocks.map((block) => renderBlock(block, slots, missingSlots)).join("\n");
412
+ const html = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
413
+ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
414
+ <head>
415
+ <meta charset="utf-8" />
416
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
417
+ <meta name="x-apple-disable-message-reformatting" />
418
+ <title>${escapeHtml(subject)}</title>
419
+ <!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
420
+ </head>
421
+ <body style="margin:0;padding:0;background-color:${background}">
422
+ <div style="display:none;font-size:0;line-height:0;max-height:0;overflow:hidden;mso-hide:all">${escapeHtml(preheader)}</div>
423
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:${background}">
424
+ <tr><td align="center" style="padding:24px 12px">
425
+ <table role="presentation" width="${width}" cellpadding="0" cellspacing="0" border="0" style="width:${width}px;max-width:100%;background-color:#ffffff;border-radius:8px">
426
+ ${body}
427
+ </table>
428
+ </td></tr>
429
+ </table>
430
+ </body>
431
+ </html>`;
432
+ return { subject, html, missingSlots };
376
433
  }
377
- function calculateConfidenceInterval(successes, trials, confidence = 0.95) {
378
- if (trials === 0)
379
- return { lower: 0, upper: 0 };
380
- const z = confidence === 0.95 ? 1.96 : confidence === 0.99 ? 2.576 : 1.645;
381
- const p = successes / trials;
382
- const margin = z * Math.sqrt(p * (1 - p) / trials);
383
- return {
384
- lower: Math.max(0, p - margin),
385
- upper: Math.min(1, p + margin)
386
- };
434
+
435
+ // ../shared/dist/storefront-content.js
436
+ var STOREFRONT_KINDS = ["text", "image", "ordering", "offer"];
437
+ var PLACEMENTS_BY_KIND = {
438
+ text: ["hero_headline", "hero_subhead", "cta_primary", "banner_text", "badge_text"],
439
+ image: ["hero_image", "banner_image", "feature_image"],
440
+ ordering: ["product_list_primary", "product_list_secondary"],
441
+ offer: ["offer_banner"]
442
+ };
443
+ var PLACEMENTS = STOREFRONT_KINDS.flatMap((kind) => PLACEMENTS_BY_KIND[kind]);
444
+ function isPlacement(value) {
445
+ return typeof value === "string" && PLACEMENTS.includes(value);
387
446
  }
388
- function isValidEmail(email) {
389
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
390
- return emailRegex.test(email);
447
+ function kindOfPlacement(placement) {
448
+ const found = STOREFRONT_KINDS.find((kind) => PLACEMENTS_BY_KIND[kind].includes(placement));
449
+ if (!found)
450
+ throw new Error(`Unknown placement: ${placement}`);
451
+ return found;
391
452
  }
392
- function sanitizeMetadata(metadata) {
393
- const sanitized = {};
394
- for (const [key, value] of Object.entries(metadata)) {
395
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
396
- sanitized[key] = value;
397
- } else if (value !== null && value !== void 0) {
398
- sanitized[key] = String(value);
399
- }
453
+ var CONTENT_TYPE_BY_KIND = {
454
+ text: { contentType: "TEXT", contentSubtype: "plain_text" },
455
+ image: { contentType: "FILE", contentSubtype: "image" },
456
+ ordering: { contentType: "CODE", contentSubtype: "json" },
457
+ offer: { contentType: "CODE", contentSubtype: "json" }
458
+ };
459
+ var MIN_ORDERING_ITEMS = 2;
460
+ var MAX_ORDERING_ITEMS = 12;
461
+ function isShopifyCdnUrl(value) {
462
+ let url;
463
+ try {
464
+ url = new URL(value);
465
+ } catch {
466
+ return false;
400
467
  }
401
- return sanitized;
468
+ return url.protocol === "https:" && url.hostname === "cdn.shopify.com";
402
469
  }
403
- function generateSubdomain(name) {
404
- return name.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 63);
470
+ function isRecord(value) {
471
+ return typeof value === "object" && value !== null && !Array.isArray(value);
405
472
  }
406
- function isValidSubdomain(subdomain) {
407
- if (!subdomain || subdomain.length === 0 || subdomain.length > 63) {
408
- return false;
409
- }
410
- const subdomainRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
411
- const reserved = [
412
- "www",
413
- "api",
414
- "app",
415
- "admin",
416
- "dashboard",
417
- "mail",
418
- "ftp",
419
- "localhost",
420
- "staging",
421
- "dev",
422
- "test",
423
- "demo",
424
- "blog",
425
- "docs",
426
- "status",
427
- "help",
428
- "support",
429
- "cdn",
430
- "static",
431
- "assets",
432
- "secure",
433
- "payment",
434
- "pay"
435
- ];
436
- return subdomainRegex.test(subdomain) && !reserved.includes(subdomain);
473
+ function isNonEmptyString(value) {
474
+ return typeof value === "string" && value.trim().length > 0;
437
475
  }
438
- function extractSubdomain(hostname, baseDomain) {
439
- if (!hostname || !baseDomain) {
476
+ function isPositiveInt(value) {
477
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
478
+ }
479
+ function parseImage(raw) {
480
+ if (!isRecord(raw))
440
481
  return null;
441
- }
442
- const cleanHostname = hostname.split(":")[0];
443
- if (!cleanHostname.endsWith(baseDomain)) {
482
+ const { url, alt, width, height, sourceId } = raw;
483
+ if (typeof url !== "string" || !isShopifyCdnUrl(url))
444
484
  return null;
445
- }
446
- const subdomain = cleanHostname.slice(0, -(baseDomain.length + 1));
447
- if (!subdomain || subdomain === "www") {
485
+ if (typeof alt !== "string")
448
486
  return null;
449
- }
450
- return subdomain;
451
- }
452
- function buildSubdomainUrl(subdomain, baseDomain, path = "", protocol = "https") {
453
- const url = `${protocol}://${subdomain}.${baseDomain}`;
454
- return path ? `${url}${path.startsWith("/") ? path : "/" + path}` : url;
455
- }
456
-
457
- // ../shared/dist/email-token.js
458
- var EMAIL_TOKEN_PARAM = "bt";
459
- var encoder = new TextEncoder();
460
- function base64UrlEncode(bytes) {
461
- let binary = "";
462
- for (const byte of bytes)
463
- binary += String.fromCharCode(byte);
464
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
465
- }
466
- function base64UrlDecode(value) {
467
- const padded = value.replace(/-/g, "+").replace(/_/g, "/");
468
- const binary = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
469
- const bytes = new Uint8Array(binary.length);
470
- for (let i = 0; i < binary.length; i++)
471
- bytes[i] = binary.charCodeAt(i);
472
- return bytes;
473
- }
474
- async function hmac(secret, message) {
475
- const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
476
- return new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(message)));
477
- }
478
- function timingSafeEqual(a, b) {
479
- if (a.length !== b.length)
480
- return false;
481
- let diff = 0;
482
- for (let i = 0; i < a.length; i++)
483
- diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
484
- return diff === 0;
487
+ if (!isPositiveInt(width) || !isPositiveInt(height))
488
+ return null;
489
+ if (!isNonEmptyString(sourceId))
490
+ return null;
491
+ return { url, alt, width, height, sourceId };
485
492
  }
486
- async function hashRecipient(recipientId, companySecret) {
487
- const digest = await hmac(companySecret, `recipient:${recipientId}`);
488
- return base64UrlEncode(digest.slice(0, 16));
493
+ function parseOrdering(raw) {
494
+ if (!Array.isArray(raw))
495
+ return null;
496
+ if (raw.length < MIN_ORDERING_ITEMS || raw.length > MAX_ORDERING_ITEMS)
497
+ return null;
498
+ if (!raw.every((id) => typeof id === "string" && /^\d+$/.test(id)))
499
+ return null;
500
+ if (new Set(raw).size !== raw.length)
501
+ return null;
502
+ return raw;
489
503
  }
490
- async function mintEmailToken(payload, key) {
491
- const body = base64UrlEncode(encoder.encode(JSON.stringify({
492
- c: payload.c,
493
- r: payload.r,
494
- k: payload.k,
495
- l: payload.l,
496
- v: key.version
497
- })));
498
- const signature = base64UrlEncode(await hmac(key.secret, body));
499
- return `${body}.${signature}`;
504
+ function parseOffer(raw) {
505
+ if (raw === null)
506
+ return null;
507
+ if (!isRecord(raw))
508
+ return void 0;
509
+ const { discountId, code, headline, subtext } = raw;
510
+ if (!isNonEmptyString(discountId) || !isNonEmptyString(code))
511
+ return void 0;
512
+ if (typeof headline !== "string" || typeof subtext !== "string")
513
+ return void 0;
514
+ return { discountId, code, headline, subtext };
500
515
  }
501
- async function verifyEmailToken(token, keys) {
502
- const parts = token.split(".");
503
- if (parts.length !== 2 || !parts[0] || !parts[1]) {
504
- return { valid: false, reason: "malformed" };
505
- }
506
- const [body, signature] = parts;
507
- let payload;
508
- try {
509
- payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(body)));
510
- } catch {
511
- return { valid: false, reason: "malformed" };
512
- }
513
- if (!payload || typeof payload.c !== "string" || typeof payload.r !== "string" || !Array.isArray(payload.k) || !payload.k.every((arm) => Number.isInteger(arm) && arm >= 0) || !Number.isInteger(payload.l) || payload.l < 0 || !Number.isInteger(payload.v)) {
514
- return { valid: false, reason: "malformed" };
515
- }
516
- const key = keys.find((candidate) => candidate.version === payload.v);
517
- if (!key)
518
- return { valid: false, reason: "unknown_key" };
519
- const expected = base64UrlEncode(await hmac(key.secret, body));
520
- if (!timingSafeEqual(expected, signature)) {
521
- return { valid: false, reason: "bad_signature" };
516
+ function parseTreatmentContent(kind, config) {
517
+ if (!isRecord(config))
518
+ return null;
519
+ const content = config.content;
520
+ switch (kind) {
521
+ case "text":
522
+ return isNonEmptyString(content) ? { kind, text: content } : null;
523
+ case "image": {
524
+ const image = parseImage(content);
525
+ return image ? { kind, image } : null;
526
+ }
527
+ case "ordering": {
528
+ const productIds = parseOrdering(content);
529
+ return productIds ? { kind, productIds } : null;
530
+ }
531
+ case "offer": {
532
+ const offer = parseOffer(content);
533
+ return offer === void 0 ? null : { kind, offer };
534
+ }
522
535
  }
523
- return { valid: true, payload };
524
- }
525
- function appendEmailToken(url, token) {
526
- const hashIndex = url.indexOf("#");
527
- const fragment = hashIndex === -1 ? "" : url.slice(hashIndex);
528
- const base = hashIndex === -1 ? url : url.slice(0, hashIndex);
529
- const separator = base.includes("?") ? "&" : "?";
530
- return `${base}${separator}${EMAIL_TOKEN_PARAM}=${encodeURIComponent(token)}${fragment}`;
531
536
  }
532
-
533
- // ../shared/dist/email-plan.js
534
- async function uniformFor(salt, recipientId, slotIndex) {
535
- const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(salt), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
536
- const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${recipientId}:${slotIndex}`)));
537
- let value = 0;
538
- for (let i = 0; i < 6; i++)
539
- value = value * 256 + mac[i];
540
- return value / 2 ** 48;
537
+ function serializeTreatmentConfig(content) {
538
+ switch (content.kind) {
539
+ case "text":
540
+ return { content: content.text };
541
+ case "image":
542
+ return { content: content.image };
543
+ case "ordering":
544
+ return { content: content.productIds };
545
+ case "offer":
546
+ return { content: content.offer };
547
+ }
541
548
  }
542
- function pickArm(probabilities, uniform) {
543
- if (probabilities.length === 0)
544
- return -1;
545
- const total = probabilities.reduce((sum, p) => sum + Math.max(p, 0), 0);
546
- if (total <= 0) {
547
- return Math.min(probabilities.length - 1, Math.floor(uniform * probabilities.length));
549
+
550
+ // ../shared/dist/conversion-path.js
551
+ var SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
552
+ var isWildcard = (segment) => segment === "*" || segment === "**";
553
+ function segmentsOf(path) {
554
+ return path.split("/").filter(Boolean);
555
+ }
556
+ function normalizeConversionPath(input) {
557
+ const trimmed = input?.trim();
558
+ if (!trimmed)
559
+ return void 0;
560
+ if (/[?#]/.test(trimmed)) {
561
+ throw new Error("Leave out the query string: enter a path like /thank-you or /orders/*/thanks");
548
562
  }
549
- const target = uniform * total;
550
- let cumulative = 0;
551
- for (let i = 0; i < probabilities.length; i++) {
552
- cumulative += Math.max(probabilities[i], 0);
553
- if (target < cumulative)
554
- return i;
563
+ const withScheme = SCHEME.test(trimmed) ? trimmed : `https://placeholder.invalid/${trimmed.replace(/^\/+/, "")}`;
564
+ let pathname;
565
+ try {
566
+ pathname = new URL(withScheme).pathname;
567
+ } catch {
568
+ throw new Error("Enter the conversion page as a path like /thank-you");
555
569
  }
556
- return probabilities.length - 1;
557
- }
558
- async function sampleCombination(plan, recipientId) {
559
- const combination = [];
560
- for (let i = 0; i < plan.slots.length; i++) {
561
- const slot = plan.slots[i];
562
- const uniform = await uniformFor(plan.salt, recipientId, i);
563
- const chosen = pickArm(slot.probabilities, uniform);
564
- combination.push(chosen === -1 ? 0 : slot.arms[chosen]?.armIndex ?? 0);
570
+ const segments = segmentsOf(pathname);
571
+ if (segments.some((segment) => segment.includes("*") && !isWildcard(segment))) {
572
+ throw new Error("Use * for a whole path segment, like /orders/*/thanks");
565
573
  }
566
- return combination;
574
+ if (segments.length > 0 && segments.every(isWildcard)) {
575
+ throw new Error("That pattern matches every page. Add a fixed part, like /orders/*/thanks");
576
+ }
577
+ return pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
567
578
  }
568
- async function impressionManifest(plan, recipientIds) {
569
- const counts = /* @__PURE__ */ new Map();
570
- for (const recipientId of recipientIds) {
571
- for (let i = 0; i < plan.slots.length; i++) {
572
- const slot = plan.slots[i];
573
- const uniform = await uniformFor(plan.salt, recipientId, i);
574
- const chosen = pickArm(slot.probabilities, uniform);
575
- const arm = slot.arms[chosen === -1 ? 0 : chosen];
576
- if (!arm)
577
- continue;
578
- const key = `${slot.experimentId}:${arm.treatmentId}`;
579
- const entry = counts.get(key);
580
- if (entry)
581
- entry.count++;
582
- else
583
- counts.set(key, {
584
- experimentId: slot.experimentId,
585
- treatmentId: arm.treatmentId,
586
- count: 1
587
- });
579
+ function matchSegments(pattern, path) {
580
+ if (pattern.length === 0)
581
+ return path.length === 0;
582
+ const [head, ...rest] = pattern;
583
+ if (head === "**") {
584
+ for (let skip = 0; skip <= path.length; skip++) {
585
+ if (matchSegments(rest, path.slice(skip)))
586
+ return true;
588
587
  }
588
+ return false;
589
589
  }
590
- return Array.from(counts.values());
590
+ if (path.length === 0)
591
+ return false;
592
+ return (head === "*" || head === path[0]) && matchSegments(rest, path.slice(1));
593
+ }
594
+ function matchesConversionPath(pattern, pathname) {
595
+ return matchSegments(segmentsOf(pattern), segmentsOf(pathname));
591
596
  }
592
597
 
593
- // ../shared/dist/email-blocks.js
594
- function isSlotBinding(value) {
595
- return typeof value === "object" && value !== null && typeof value.$slot === "string";
598
+ // src/session.ts
599
+ var SESSION_ID_KEY = "bandits_session_id";
600
+ var SESSION_CREATED_AT_KEY = "bandits_session_created_at";
601
+ var VISITOR_ID_KEY = "bandits_visitor_id";
602
+ var DEFAULT_SESSION_TTL_MS = 864e5;
603
+ function generateId() {
604
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
605
+ return crypto.randomUUID();
606
+ }
607
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
608
+ const r = Math.random() * 16 | 0;
609
+ return (c === "x" ? r : r & 3 | 8).toString(16);
610
+ });
596
611
  }
597
- var CONTENT_WIDTH = 600;
598
- function escapeHtml(value) {
599
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
612
+ function getOrCreateSessionId(options = {}) {
613
+ const { storage, now = () => Date.now(), ttlMs = DEFAULT_SESSION_TTL_MS } = options;
614
+ if (!storage) return generateId();
615
+ try {
616
+ const stored = storage.getItem(SESSION_ID_KEY);
617
+ const storedAt = storage.getItem(SESSION_CREATED_AT_KEY);
618
+ if (stored && storedAt) {
619
+ const age = now() - parseInt(storedAt, 10);
620
+ if (age < ttlMs) return stored;
621
+ }
622
+ const id = generateId();
623
+ storage.setItem(SESSION_ID_KEY, id);
624
+ storage.setItem(SESSION_CREATED_AT_KEY, String(now()));
625
+ return id;
626
+ } catch {
627
+ return generateId();
628
+ }
600
629
  }
601
- function resolve(value, slots, missing) {
602
- if (value === void 0)
603
- return "";
604
- if (!isSlotBinding(value))
605
- return value;
606
- const resolved = slots[value.$slot];
607
- if (resolved === void 0) {
608
- missing.push(value.$slot);
609
- return "";
630
+ function getOrCreateVisitorId(storage) {
631
+ if (!storage) return generateId();
632
+ try {
633
+ const stored = storage.getItem(VISITOR_ID_KEY);
634
+ if (stored) return stored;
635
+ const id = generateId();
636
+ storage.setItem(VISITOR_ID_KEY, id);
637
+ return id;
638
+ } catch {
639
+ return generateId();
610
640
  }
611
- return resolved;
612
641
  }
613
- function extractDocumentSlots(document) {
614
- const names = [];
615
- const seen = /* @__PURE__ */ new Set();
616
- const visit = (value) => {
617
- if (isSlotBinding(value)) {
618
- if (!seen.has(value.$slot)) {
619
- seen.add(value.$slot);
620
- names.push(value.$slot);
642
+ function defaultSessionStorage() {
643
+ return typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
644
+ }
645
+
646
+ // src/client.ts
647
+ function currentPageUrl() {
648
+ if (typeof location !== "undefined" && location.origin && location.pathname) {
649
+ return location.origin + location.pathname;
650
+ }
651
+ return void 0;
652
+ }
653
+ var BanditsClient = class {
654
+ config;
655
+ namedEventQueue = [];
656
+ sessionId;
657
+ visitorId;
658
+ flushTimer;
659
+ constructor(config) {
660
+ this.config = {
661
+ timeout: 5e3,
662
+ batchSize: 100,
663
+ flushInterval: 1e4,
664
+ sessionTtl: 864e5,
665
+ ...config
666
+ };
667
+ this.sessionId = this.getOrCreateSessionId();
668
+ this.visitorId = getOrCreateVisitorId(defaultSessionStorage());
669
+ this.startBatchFlush();
670
+ }
671
+ async getAssignment(experimentId, userId = this.visitorId, context) {
672
+ const request = {
673
+ experimentId,
674
+ userId,
675
+ context,
676
+ sessionId: this.sessionId
677
+ };
678
+ const pageUrl = currentPageUrl();
679
+ if (pageUrl) request.pageUrl = pageUrl;
680
+ const response = await this.makeRequest(
681
+ "/assignments",
682
+ "POST",
683
+ request
684
+ );
685
+ if (!response.success || !response.data) {
686
+ throw new Error(response.error || "Failed to get assignment");
687
+ }
688
+ const assignment = response.data?.data ?? response.data;
689
+ if (!assignment?.assignmentId) {
690
+ throw new Error("Invalid assignment response");
691
+ }
692
+ return {
693
+ assignmentId: assignment.assignmentId,
694
+ treatmentId: assignment.treatmentId,
695
+ experimentId,
696
+ userId,
697
+ config: assignment.treatment?.config || {},
698
+ funnelId: assignment.funnelId
699
+ };
700
+ }
701
+ trackNamedEvent(event, opts) {
702
+ const request = {
703
+ name: event.name,
704
+ category: event.category || "custom",
705
+ value: event.value,
706
+ metadata: event.metadata,
707
+ sessionId: this.sessionId
708
+ };
709
+ if (opts?.userId) request.userId = opts.userId;
710
+ if (opts?.assignmentId) request.assignmentId = opts.assignmentId;
711
+ if (opts?.immediate) {
712
+ const url = `${this.normalizeApiUrl()}/events`;
713
+ try {
714
+ void fetch(url, {
715
+ method: "POST",
716
+ headers: {
717
+ "Authorization": `Bearer ${this.config.apiKey}`,
718
+ "Content-Type": "application/json"
719
+ },
720
+ body: JSON.stringify(request),
721
+ keepalive: true
722
+ });
723
+ } catch {
621
724
  }
622
725
  return;
623
726
  }
624
- if (Array.isArray(value))
625
- value.forEach(visit);
626
- else if (typeof value === "object" && value !== null)
627
- Object.values(value).forEach(visit);
628
- };
629
- visit(document.subject);
630
- visit(document.preheader);
631
- document.blocks.forEach(visit);
632
- return names;
633
- }
634
- function renderBlock(block, slots, missing) {
635
- const align = block.align ?? "left";
636
- switch (block.type) {
637
- case "heading": {
638
- const size = block.level === 3 ? 18 : block.level === 2 ? 22 : 28;
639
- const color = escapeHtml(resolve(block.color, slots, missing) || "#111111");
640
- return `<tr><td style="padding:12px 24px;text-align:${align};font-family:Arial,Helvetica,sans-serif;font-size:${size}px;line-height:1.3;font-weight:bold;color:${color}">${escapeHtml(resolve(block.text, slots, missing))}</td></tr>`;
727
+ this.namedEventQueue.push(request);
728
+ if (this.namedEventQueue.length >= this.config.batchSize) {
729
+ this.flush();
730
+ }
731
+ }
732
+ async flush() {
733
+ const namedEvents = [...this.namedEventQueue];
734
+ this.namedEventQueue = [];
735
+ if (namedEvents.length === 0) return;
736
+ try {
737
+ await fetch(`${this.normalizeApiUrl()}/events/batch`, {
738
+ method: "POST",
739
+ headers: {
740
+ "Authorization": `Bearer ${this.config.apiKey}`,
741
+ "Content-Type": "application/json"
742
+ },
743
+ body: JSON.stringify({ events: namedEvents }),
744
+ keepalive: true
745
+ });
746
+ } catch (error) {
747
+ this.namedEventQueue.unshift(...namedEvents);
748
+ throw error;
749
+ }
750
+ }
751
+ async identify(anonymousId, identifiedUserId, mergeReason) {
752
+ const response = await this.makeRequest(
753
+ "/identify",
754
+ "POST",
755
+ {
756
+ anonymousId,
757
+ identifiedUserId,
758
+ mergeReason: mergeReason || "manual"
759
+ }
760
+ );
761
+ if (!response.success) {
762
+ throw new Error(response.error || "Failed to identify user");
763
+ }
764
+ }
765
+ async label(userId, labels) {
766
+ const response = await this.makeRequest("/labels", "POST", { userId, labels });
767
+ if (!response.success) {
768
+ throw new Error(response.error || "Failed to record labels");
769
+ }
770
+ }
771
+ async trackFunnelReward(funnelId, userId, value) {
772
+ const response = await this.makeRequest(
773
+ `/funnels/${funnelId}/reward`,
774
+ "POST",
775
+ { userId, value }
776
+ );
777
+ if (!response.success) {
778
+ throw new Error(response.error || "Failed to record funnel reward");
641
779
  }
642
- case "text": {
643
- const color = escapeHtml(resolve(block.color, slots, missing) || "#333333");
644
- return `<tr><td style="padding:8px 24px;text-align:${align};font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:${color}">${escapeHtml(resolve(block.text, slots, missing))}</td></tr>`;
780
+ }
781
+ convert(funnelId, value = 1) {
782
+ return this.trackFunnelReward(funnelId, this.visitorId, value);
783
+ }
784
+ async convertOn(funnelId, pathPattern, value = 1) {
785
+ if (typeof location === "undefined" || !matchesConversionPath(pathPattern, location.pathname)) return false;
786
+ await this.convert(funnelId, value);
787
+ return true;
788
+ }
789
+ async getBundle(bundleId, userId, context) {
790
+ const request = {
791
+ bundleId,
792
+ userId,
793
+ context,
794
+ sessionId: this.sessionId
795
+ };
796
+ const pageUrl = currentPageUrl();
797
+ if (pageUrl) request.pageUrl = pageUrl;
798
+ const response = await this.makeRequest("/bundles/assignment", "POST", request);
799
+ if (!response.success || !response.data) {
800
+ throw new Error(response.error || "Failed to get bundle assignment");
645
801
  }
646
- case "button": {
647
- const href = escapeHtml(resolve(block.href, slots, missing));
648
- const label = escapeHtml(resolve(block.text, slots, missing));
649
- const bg = escapeHtml(resolve(block.backgroundColor, slots, missing) || "#1a73e8");
650
- const fg = escapeHtml(resolve(block.textColor, slots, missing) || "#ffffff");
651
- return `<tr><td style="padding:16px 24px;text-align:${align}">
652
- <!--[if mso]><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="${href}" style="height:44px;v-text-anchor:middle;width:220px;" arcsize="12%" strokecolor="${bg}" fillcolor="${bg}"><w:anchorlock/><center style="color:${fg};font-family:Arial,sans-serif;font-size:16px;font-weight:bold;">${label}</center></v:roundrect><![endif]-->
653
- <!--[if !mso]><!--><a href="${href}" style="display:inline-block;padding:13px 28px;background-color:${bg};color:${fg};font-family:Arial,Helvetica,sans-serif;font-size:16px;font-weight:bold;text-decoration:none;border-radius:6px">${label}</a><!--<![endif]-->
654
- </td></tr>`;
802
+ const payload = response.data?.data ?? response.data;
803
+ if (!payload?.bundleAssignmentId || !payload?.slots) {
804
+ throw new Error("Invalid bundle assignment response");
655
805
  }
656
- case "image": {
657
- const src = escapeHtml(resolve(block.src, slots, missing));
658
- const alt = escapeHtml(resolve(block.alt, slots, missing));
659
- const width = block.width ?? CONTENT_WIDTH;
660
- const img = `<img src="${src}" alt="${alt}" width="${width}" style="display:block;border:0;outline:none;text-decoration:none;max-width:100%;height:auto" />`;
661
- const href = block.href ? escapeHtml(resolve(block.href, slots, missing)) : "";
662
- return `<tr><td style="padding:0;text-align:center">${href ? `<a href="${href}">${img}</a>` : img}</td></tr>`;
806
+ return {
807
+ bundleAssignmentId: payload.bundleAssignmentId,
808
+ slots: payload.slots
809
+ };
810
+ }
811
+ async trackBundleReward(bundleId, userId, value) {
812
+ const response = await this.makeRequest(`/bundles/${bundleId}/reward`, "POST", {
813
+ userId,
814
+ value
815
+ });
816
+ if (!response.success) {
817
+ throw new Error(response.error || "Failed to record bundle reward");
663
818
  }
664
- case "divider":
665
- return `<tr><td style="padding:12px 24px"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr><td style="border-top:1px solid ${escapeHtml(block.color ?? "#e5e5e5")};font-size:0;line-height:0">&nbsp;</td></tr></table></td></tr>`;
666
- case "spacer":
667
- return `<tr><td style="height:${block.height ?? 24}px;font-size:0;line-height:0">&nbsp;</td></tr>`;
668
- default:
669
- return "";
670
819
  }
671
- }
672
- function renderEmailDocument(document, slots) {
673
- const missingSlots = [];
674
- const width = document.contentWidth ?? CONTENT_WIDTH;
675
- const background = escapeHtml(document.backgroundColor ?? "#f4f4f5");
676
- const subject = resolve(document.subject, slots, missingSlots);
677
- const preheader = resolve(document.preheader, slots, missingSlots);
678
- const body = document.blocks.map((block) => renderBlock(block, slots, missingSlots)).join("\n");
679
- const html = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
680
- <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
681
- <head>
682
- <meta charset="utf-8" />
683
- <meta name="viewport" content="width=device-width,initial-scale=1" />
684
- <meta name="x-apple-disable-message-reformatting" />
685
- <title>${escapeHtml(subject)}</title>
686
- <!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->
687
- </head>
688
- <body style="margin:0;padding:0;background-color:${background}">
689
- <div style="display:none;font-size:0;line-height:0;max-height:0;overflow:hidden;mso-hide:all">${escapeHtml(preheader)}</div>
690
- <table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:${background}">
691
- <tr><td align="center" style="padding:24px 12px">
692
- <table role="presentation" width="${width}" cellpadding="0" cellspacing="0" border="0" style="width:${width}px;max-width:100%;background-color:#ffffff;border-radius:8px">
693
- ${body}
694
- </table>
695
- </td></tr>
696
- </table>
697
- </body>
698
- </html>`;
699
- return { subject, html, missingSlots };
700
- }
701
-
702
- // ../shared/dist/storefront-content.js
703
- var STOREFRONT_KINDS = ["text", "image", "ordering", "offer"];
704
- var PLACEMENTS_BY_KIND = {
705
- text: ["hero_headline", "hero_subhead", "cta_primary", "banner_text", "badge_text"],
706
- image: ["hero_image", "banner_image", "feature_image"],
707
- ordering: ["product_list_primary", "product_list_secondary"],
708
- offer: ["offer_banner"]
709
- };
710
- var PLACEMENTS = STOREFRONT_KINDS.flatMap((kind) => PLACEMENTS_BY_KIND[kind]);
711
- function isPlacement(value) {
712
- return typeof value === "string" && PLACEMENTS.includes(value);
713
- }
714
- function kindOfPlacement(placement) {
715
- const found = STOREFRONT_KINDS.find((kind) => PLACEMENTS_BY_KIND[kind].includes(placement));
716
- if (!found)
717
- throw new Error(`Unknown placement: ${placement}`);
718
- return found;
719
- }
720
- var CONTENT_TYPE_BY_KIND = {
721
- text: { contentType: "TEXT", contentSubtype: "plain_text" },
722
- image: { contentType: "FILE", contentSubtype: "image" },
723
- ordering: { contentType: "CODE", contentSubtype: "json" },
724
- offer: { contentType: "CODE", contentSubtype: "json" }
725
- };
726
- var MIN_ORDERING_ITEMS = 2;
727
- var MAX_ORDERING_ITEMS = 12;
728
- function isShopifyCdnUrl(value) {
729
- let url;
730
- try {
731
- url = new URL(value);
732
- } catch {
733
- return false;
820
+ generateId() {
821
+ return generateId();
734
822
  }
735
- return url.protocol === "https:" && url.hostname === "cdn.shopify.com";
736
- }
737
- function isRecord(value) {
738
- return typeof value === "object" && value !== null && !Array.isArray(value);
739
- }
740
- function isNonEmptyString(value) {
741
- return typeof value === "string" && value.trim().length > 0;
742
- }
743
- function isPositiveInt(value) {
744
- return typeof value === "number" && Number.isInteger(value) && value > 0;
745
- }
746
- function parseImage(raw) {
747
- if (!isRecord(raw))
748
- return null;
749
- const { url, alt, width, height, sourceId } = raw;
750
- if (typeof url !== "string" || !isShopifyCdnUrl(url))
751
- return null;
752
- if (typeof alt !== "string")
753
- return null;
754
- if (!isPositiveInt(width) || !isPositiveInt(height))
755
- return null;
756
- if (!isNonEmptyString(sourceId))
757
- return null;
758
- return { url, alt, width, height, sourceId };
759
- }
760
- function parseOrdering(raw) {
761
- if (!Array.isArray(raw))
762
- return null;
763
- if (raw.length < MIN_ORDERING_ITEMS || raw.length > MAX_ORDERING_ITEMS)
764
- return null;
765
- if (!raw.every((id) => typeof id === "string" && /^\d+$/.test(id)))
766
- return null;
767
- if (new Set(raw).size !== raw.length)
768
- return null;
769
- return raw;
770
- }
771
- function parseOffer(raw) {
772
- if (raw === null)
773
- return null;
774
- if (!isRecord(raw))
775
- return void 0;
776
- const { discountId, code, headline, subtext } = raw;
777
- if (!isNonEmptyString(discountId) || !isNonEmptyString(code))
778
- return void 0;
779
- if (typeof headline !== "string" || typeof subtext !== "string")
780
- return void 0;
781
- return { discountId, code, headline, subtext };
782
- }
783
- function parseTreatmentContent(kind, config) {
784
- if (!isRecord(config))
785
- return null;
786
- const content = config.content;
787
- switch (kind) {
788
- case "text":
789
- return isNonEmptyString(content) ? { kind, text: content } : null;
790
- case "image": {
791
- const image = parseImage(content);
792
- return image ? { kind, image } : null;
823
+ normalizeApiUrl() {
824
+ let baseUrl = this.config.apiUrl.replace(/\/$/, "");
825
+ if (!baseUrl.endsWith("/api")) {
826
+ baseUrl = `${baseUrl}/api`;
793
827
  }
794
- case "ordering": {
795
- const productIds = parseOrdering(content);
796
- return productIds ? { kind, productIds } : null;
828
+ return baseUrl;
829
+ }
830
+ getOrCreateSessionId() {
831
+ return getOrCreateSessionId({
832
+ storage: defaultSessionStorage(),
833
+ ttlMs: this.config.sessionTtl
834
+ });
835
+ }
836
+ async makeRequest(endpoint, method, body, extraHeaders) {
837
+ const url = `${this.normalizeApiUrl()}${endpoint}`;
838
+ const headers = {
839
+ "Authorization": `Bearer ${this.config.apiKey}`,
840
+ "Content-Type": "application/json",
841
+ ...extraHeaders
842
+ };
843
+ const requestConfig = {
844
+ method,
845
+ headers,
846
+ signal: AbortSignal.timeout(this.config.timeout)
847
+ };
848
+ if (body && method !== "GET") {
849
+ requestConfig.body = JSON.stringify(body);
797
850
  }
798
- case "offer": {
799
- const offer = parseOffer(content);
800
- return offer === void 0 ? null : { kind, offer };
851
+ try {
852
+ const response = await fetch(url, requestConfig);
853
+ const data = await response.json();
854
+ return {
855
+ success: response.ok,
856
+ data: response.ok ? data : void 0,
857
+ error: response.ok ? void 0 : data?.message || "Request failed",
858
+ timestamp: /* @__PURE__ */ new Date()
859
+ };
860
+ } catch (error) {
861
+ return {
862
+ success: false,
863
+ error: error instanceof Error ? error.message : "Network error",
864
+ timestamp: /* @__PURE__ */ new Date()
865
+ };
801
866
  }
802
867
  }
803
- }
804
- function serializeTreatmentConfig(content) {
805
- switch (content.kind) {
806
- case "text":
807
- return { content: content.text };
808
- case "image":
809
- return { content: content.image };
810
- case "ordering":
811
- return { content: content.productIds };
812
- case "offer":
813
- return { content: content.offer };
868
+ startBatchFlush() {
869
+ this.flushTimer = setInterval(() => {
870
+ this.flush().catch(console.error);
871
+ }, this.config.flushInterval);
814
872
  }
815
- }
873
+ destroy() {
874
+ if (this.flushTimer) {
875
+ clearInterval(this.flushTimer);
876
+ }
877
+ this.flush().catch(console.error);
878
+ if (typeof window !== "undefined" && window.localStorage) {
879
+ localStorage.removeItem("bandits_session_id");
880
+ localStorage.removeItem("bandits_session_created_at");
881
+ }
882
+ }
883
+ };
816
884
 
817
885
  // src/snapshot-cache.ts
818
886
  var STORAGE_KEY = "bandits_snapshot";
@@ -1228,6 +1296,7 @@ export {
1228
1296
  STATIONARY,
1229
1297
  STOREFRONT_KINDS,
1230
1298
  SnapshotClient,
1299
+ VISITOR_ID_KEY,
1231
1300
  appendEmailToken,
1232
1301
  buildSubdomainUrl,
1233
1302
  calculateConfidenceInterval,
@@ -1235,10 +1304,11 @@ export {
1235
1304
  extractEmailLinks,
1236
1305
  extractSlotNames,
1237
1306
  extractSubdomain,
1238
- generateId2 as generateId,
1307
+ generateId,
1239
1308
  generateSubdomain,
1240
1309
  getContentTypeCategory,
1241
1310
  getOrCreateSessionId,
1311
+ getOrCreateVisitorId,
1242
1312
  hashRecipient,
1243
1313
  impressionManifest,
1244
1314
  isPlacement,
@@ -1247,8 +1317,10 @@ export {
1247
1317
  isValidEmail,
1248
1318
  isValidSubdomain,
1249
1319
  kindOfPlacement,
1320
+ matchesConversionPath,
1250
1321
  mintEmailToken,
1251
1322
  normalizeAlgorithm,
1323
+ normalizeConversionPath,
1252
1324
  normalizePosterior,
1253
1325
  normalizeTreatmentWeights,
1254
1326
  parseTreatmentContent,