@runbandit/sdk 3.1.2 → 3.3.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.
@@ -0,0 +1,319 @@
1
+ // src/session.ts
2
+ var SESSION_ID_KEY = "bandits_session_id";
3
+ var SESSION_CREATED_AT_KEY = "bandits_session_created_at";
4
+ var VISITOR_ID_KEY = "bandits_visitor_id";
5
+ var DEFAULT_SESSION_TTL_MS = 864e5;
6
+ function generateId() {
7
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
8
+ return crypto.randomUUID();
9
+ }
10
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
11
+ const r = Math.random() * 16 | 0;
12
+ return (c === "x" ? r : r & 3 | 8).toString(16);
13
+ });
14
+ }
15
+ function getOrCreateSessionId(options = {}) {
16
+ const { storage, now = () => Date.now(), ttlMs = DEFAULT_SESSION_TTL_MS } = options;
17
+ if (!storage) return generateId();
18
+ try {
19
+ const stored = storage.getItem(SESSION_ID_KEY);
20
+ const storedAt = storage.getItem(SESSION_CREATED_AT_KEY);
21
+ if (stored && storedAt) {
22
+ const age = now() - parseInt(storedAt, 10);
23
+ if (age < ttlMs) return stored;
24
+ }
25
+ const id = generateId();
26
+ storage.setItem(SESSION_ID_KEY, id);
27
+ storage.setItem(SESSION_CREATED_AT_KEY, String(now()));
28
+ return id;
29
+ } catch {
30
+ return generateId();
31
+ }
32
+ }
33
+ function getOrCreateVisitorId(storage) {
34
+ if (!storage) return generateId();
35
+ try {
36
+ const stored = storage.getItem(VISITOR_ID_KEY);
37
+ if (stored) return stored;
38
+ const id = generateId();
39
+ storage.setItem(VISITOR_ID_KEY, id);
40
+ return id;
41
+ } catch {
42
+ return generateId();
43
+ }
44
+ }
45
+ function defaultSessionStorage() {
46
+ return typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
47
+ }
48
+
49
+ // src/snapshot-cache.ts
50
+ var STORAGE_KEY = "bandits_snapshot";
51
+ var REPORT_QUEUE_KEY = "bandits_pending_reports";
52
+ var DECISIONS_KEY = "bandits_client_assignments";
53
+ function pickTreatment(treatments, draw) {
54
+ if (treatments.length === 0) return null;
55
+ let cumulative = 0;
56
+ for (const treatment of treatments) {
57
+ cumulative += treatment.pct;
58
+ if (draw < cumulative) return treatment;
59
+ }
60
+ return treatments[treatments.length - 1];
61
+ }
62
+ var DEFAULT_ASSIGNMENT_TTL_SECONDS = 86400;
63
+ function randomId() {
64
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
65
+ let id = "";
66
+ for (let i = 0; i < 24; i++) {
67
+ id += chars.charAt(Math.floor(Math.random() * chars.length));
68
+ }
69
+ return id;
70
+ }
71
+ var SnapshotClient = class {
72
+ config;
73
+ storage;
74
+ now;
75
+ fetchImpl;
76
+ snapshot = null;
77
+ queue = [];
78
+ sessionId;
79
+ inFlight = null;
80
+ constructor(config) {
81
+ this.config = config;
82
+ this.now = config.now ?? (() => Date.now());
83
+ this.fetchImpl = config.fetchImpl ?? ((...args) => fetch(...args));
84
+ this.storage = config.storage ?? (typeof window !== "undefined" && window.localStorage ? window.localStorage : null);
85
+ this.queue = this.readQueue();
86
+ this.sessionId = getOrCreateSessionId({
87
+ storage: this.storage ?? defaultSessionStorage(),
88
+ now: this.now,
89
+ ttlMs: config.sessionTtl
90
+ });
91
+ if (config.inlineSnapshot) {
92
+ this.snapshot = config.inlineSnapshot;
93
+ this.persistSnapshot(config.inlineSnapshot);
94
+ } else {
95
+ this.snapshot = this.readSnapshot();
96
+ }
97
+ }
98
+ getAssignmentSync(experimentId, userId) {
99
+ const snapshot = this.freshSnapshot();
100
+ if (!snapshot) return null;
101
+ const experiment = snapshot.experiments[experimentId];
102
+ if (!experiment || experiment.mode !== "snapshot") return null;
103
+ const key = `${experimentId}:${userId}`;
104
+ const decisions = this.readDecisions();
105
+ const stored = decisions[key];
106
+ if (stored && stored.expiresAt > this.now()) {
107
+ const held = experiment.treatments.find((t) => t.id === stored.treatmentId);
108
+ if (held) {
109
+ return {
110
+ clientAssignmentId: stored.clientAssignmentId,
111
+ experimentId,
112
+ treatmentId: held.id,
113
+ config: held.config
114
+ };
115
+ }
116
+ }
117
+ const treatment = pickTreatment(experiment.treatments, Math.random());
118
+ if (!treatment) return null;
119
+ const clientAssignmentId = randomId();
120
+ const ttlSeconds = experiment.assignmentTtl ?? DEFAULT_ASSIGNMENT_TTL_SECONDS;
121
+ decisions[key] = {
122
+ clientAssignmentId,
123
+ treatmentId: treatment.id,
124
+ expiresAt: this.now() + ttlSeconds * 1e3
125
+ };
126
+ this.writeDecisions(decisions);
127
+ this.queue.push({
128
+ clientAssignmentId,
129
+ experimentId,
130
+ treatmentId: treatment.id,
131
+ userId,
132
+ sessionId: this.sessionId
133
+ });
134
+ this.persistQueue();
135
+ return {
136
+ clientAssignmentId,
137
+ experimentId,
138
+ treatmentId: treatment.id,
139
+ config: treatment.config
140
+ };
141
+ }
142
+ async refresh() {
143
+ const response = await this.fetchImpl(`${this.config.apiUrl}/snapshot`, {
144
+ method: "GET",
145
+ headers: { "x-api-key": this.config.publishableKey }
146
+ });
147
+ if (!response.ok) return;
148
+ const snapshot = await response.json();
149
+ this.snapshot = snapshot;
150
+ this.persistSnapshot(snapshot);
151
+ }
152
+ async flush() {
153
+ if (this.inFlight) return this.inFlight;
154
+ this.inFlight = this.doFlush().finally(() => {
155
+ this.inFlight = null;
156
+ });
157
+ return this.inFlight;
158
+ }
159
+ async doFlush() {
160
+ if (this.queue.length === 0) return;
161
+ const reports = this.queue.slice(0, 1e3);
162
+ try {
163
+ const response = await this.fetchImpl(`${this.config.apiUrl}/assignments/report`, {
164
+ method: "POST",
165
+ headers: {
166
+ "content-type": "application/json",
167
+ "x-api-key": this.config.publishableKey
168
+ },
169
+ body: JSON.stringify({ reports }),
170
+ keepalive: true
171
+ });
172
+ if (!response.ok) return;
173
+ } catch {
174
+ return;
175
+ }
176
+ this.queue = this.queue.slice(reports.length);
177
+ this.persistQueue();
178
+ }
179
+ getSessionId() {
180
+ return this.sessionId;
181
+ }
182
+ pendingReportCount() {
183
+ return this.queue.length;
184
+ }
185
+ freshSnapshot() {
186
+ if (!this.snapshot) return null;
187
+ if (this.snapshot.ttl === 0) return null;
188
+ const ageMs = this.now() - this.snapshot.generatedAt;
189
+ if (ageMs > this.snapshot.ttl * 1e3) return null;
190
+ return this.snapshot;
191
+ }
192
+ readSnapshot() {
193
+ if (!this.storage) return null;
194
+ try {
195
+ const raw = this.storage.getItem(STORAGE_KEY);
196
+ return raw ? JSON.parse(raw) : null;
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+ persistSnapshot(snapshot) {
202
+ if (!this.storage) return;
203
+ try {
204
+ this.storage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
205
+ } catch {
206
+ }
207
+ }
208
+ readQueue() {
209
+ if (!this.storage) return [];
210
+ try {
211
+ const raw = this.storage.getItem(REPORT_QUEUE_KEY);
212
+ return raw ? JSON.parse(raw) : [];
213
+ } catch {
214
+ return [];
215
+ }
216
+ }
217
+ readDecisions() {
218
+ if (!this.storage) return {};
219
+ try {
220
+ const raw = this.storage.getItem(DECISIONS_KEY);
221
+ if (!raw) return {};
222
+ const parsed = JSON.parse(raw);
223
+ const now = this.now();
224
+ const live = {};
225
+ for (const [k, d] of Object.entries(parsed)) {
226
+ if (d && d.expiresAt > now) live[k] = d;
227
+ }
228
+ return live;
229
+ } catch {
230
+ return {};
231
+ }
232
+ }
233
+ writeDecisions(decisions) {
234
+ if (!this.storage) return;
235
+ try {
236
+ this.storage.setItem(DECISIONS_KEY, JSON.stringify(decisions));
237
+ } catch {
238
+ }
239
+ }
240
+ persistQueue() {
241
+ if (!this.storage) return;
242
+ try {
243
+ this.storage.setItem(REPORT_QUEUE_KEY, JSON.stringify(this.queue));
244
+ } catch {
245
+ }
246
+ }
247
+ };
248
+
249
+ // src/decision-cookie.ts
250
+ var VISITOR_COOKIE = "bandit_v";
251
+ var DECISIONS_COOKIE = "bandit_d";
252
+ var VISITOR_COOKIE_MAX_AGE_SECONDS = 400 * 86400;
253
+ var ENTRY_SEPARATOR = "~";
254
+ var FIELD_SEPARATOR = ".";
255
+ var SAFE_FIELD = /^[A-Za-z0-9_-]+$/;
256
+ function readCookie(header, name) {
257
+ if (!header) return null;
258
+ for (const part of header.split(";")) {
259
+ const index = part.indexOf("=");
260
+ if (index === -1) continue;
261
+ if (part.slice(0, index).trim() !== name) continue;
262
+ const value = part.slice(index + 1).trim();
263
+ try {
264
+ return decodeURIComponent(value);
265
+ } catch {
266
+ return value;
267
+ }
268
+ }
269
+ return null;
270
+ }
271
+ function parseDecisions(value, now) {
272
+ const decisions = {};
273
+ if (!value) return decisions;
274
+ for (const entry of value.split(ENTRY_SEPARATOR)) {
275
+ const [experimentId, treatmentId, clientAssignmentId, expiresAtSeconds] = entry.split(FIELD_SEPARATOR);
276
+ const expiresAt = Number(expiresAtSeconds) * 1e3;
277
+ if (!experimentId || !treatmentId || !clientAssignmentId || !Number.isFinite(expiresAt)) continue;
278
+ if (expiresAt <= now) continue;
279
+ decisions[experimentId] = { experimentId, treatmentId, clientAssignmentId, expiresAt };
280
+ }
281
+ return decisions;
282
+ }
283
+ function serializeDecisions(decisions) {
284
+ return Object.values(decisions).filter((d) => [d.experimentId, d.treatmentId, d.clientAssignmentId].every((f) => SAFE_FIELD.test(f))).map(
285
+ (d) => [d.experimentId, d.treatmentId, d.clientAssignmentId, Math.ceil(d.expiresAt / 1e3)].join(FIELD_SEPARATOR)
286
+ ).join(ENTRY_SEPARATOR);
287
+ }
288
+ function decisionsMaxAgeSeconds(decisions, now) {
289
+ const latest = Math.max(0, ...Object.values(decisions).map((d) => d.expiresAt));
290
+ return Math.max(0, Math.ceil((latest - now) / 1e3));
291
+ }
292
+ function serializeCookie(name, value, attributes) {
293
+ const parts = [`${name}=${encodeURIComponent(value)}`, "Path=/", `Max-Age=${attributes.maxAgeSeconds}`, "SameSite=Lax"];
294
+ if (attributes.domain) parts.push(`Domain=${attributes.domain}`);
295
+ if (attributes.secure) parts.push("Secure");
296
+ return parts.join("; ");
297
+ }
298
+
299
+ export {
300
+ SESSION_ID_KEY,
301
+ SESSION_CREATED_AT_KEY,
302
+ VISITOR_ID_KEY,
303
+ DEFAULT_SESSION_TTL_MS,
304
+ generateId,
305
+ getOrCreateSessionId,
306
+ getOrCreateVisitorId,
307
+ defaultSessionStorage,
308
+ pickTreatment,
309
+ SnapshotClient,
310
+ VISITOR_COOKIE,
311
+ DECISIONS_COOKIE,
312
+ VISITOR_COOKIE_MAX_AGE_SECONDS,
313
+ readCookie,
314
+ parseDecisions,
315
+ serializeDecisions,
316
+ decisionsMaxAgeSeconds,
317
+ serializeCookie
318
+ };
319
+ //# sourceMappingURL=chunk-BG5L4PBC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/session.ts","../src/snapshot-cache.ts","../src/decision-cookie.ts"],"sourcesContent":["\nexport const SESSION_ID_KEY = 'bandits_session_id';\nexport const SESSION_CREATED_AT_KEY = 'bandits_session_created_at';\n\nexport const VISITOR_ID_KEY = 'bandits_visitor_id';\n\nexport const DEFAULT_SESSION_TTL_MS = 86400000;\n\nexport function generateId(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n const r = (Math.random() * 16) | 0;\n return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);\n });\n}\n\nexport interface SessionOptions {\n storage?: Storage | null;\n now?: () => number;\n ttlMs?: number;\n}\n\nexport function getOrCreateSessionId(options: SessionOptions = {}): string {\n const { storage, now = () => Date.now(), ttlMs = DEFAULT_SESSION_TTL_MS } = options;\n if (!storage) return generateId();\n\n try {\n const stored = storage.getItem(SESSION_ID_KEY);\n const storedAt = storage.getItem(SESSION_CREATED_AT_KEY);\n if (stored && storedAt) {\n const age = now() - parseInt(storedAt, 10);\n if (age < ttlMs) return stored;\n }\n const id = generateId();\n storage.setItem(SESSION_ID_KEY, id);\n storage.setItem(SESSION_CREATED_AT_KEY, String(now()));\n return id;\n } catch {\n return generateId();\n }\n}\n\nexport function getOrCreateVisitorId(storage: Storage | null | undefined): string {\n if (!storage) return generateId();\n try {\n const stored = storage.getItem(VISITOR_ID_KEY);\n if (stored) return stored;\n const id = generateId();\n storage.setItem(VISITOR_ID_KEY, id);\n return id;\n } catch {\n return generateId();\n }\n}\n\nexport function defaultSessionStorage(): Storage | null {\n return typeof window !== 'undefined' && window.localStorage ? window.localStorage : null;\n}\n","export type {\n SnapshotTreatment,\n SnapshotExperiment,\n CompanySnapshot,\n} from '@runbandit/shared';\nimport type { CompanySnapshot, SnapshotTreatment } from '@runbandit/shared';\nimport { defaultSessionStorage, getOrCreateSessionId } from './session.js';\n\nexport interface SyncAssignment {\n clientAssignmentId: string;\n experimentId: string;\n treatmentId: string;\n config: unknown;\n}\n\ninterface PendingReport {\n clientAssignmentId: string;\n experimentId: string;\n treatmentId: string;\n userId: string;\n sessionId: string;\n}\n\nexport interface SnapshotClientConfig {\n apiUrl: string;\n publishableKey: string;\n storage?: Storage;\n now?: () => number;\n sessionTtl?: number;\n inlineSnapshot?: CompanySnapshot;\n fetchImpl?: typeof fetch;\n}\n\nconst STORAGE_KEY = 'bandits_snapshot';\nconst REPORT_QUEUE_KEY = 'bandits_pending_reports';\nconst DECISIONS_KEY = 'bandits_client_assignments';\n\ninterface StoredDecision {\n clientAssignmentId: string;\n treatmentId: string;\n expiresAt: number;\n}\n\nexport function pickTreatment(\n treatments: SnapshotTreatment[],\n draw: number\n): SnapshotTreatment | null {\n if (treatments.length === 0) return null;\n let cumulative = 0;\n for (const treatment of treatments) {\n cumulative += treatment.pct;\n if (draw < cumulative) return treatment;\n }\n return treatments[treatments.length - 1];\n}\n\nconst DEFAULT_ASSIGNMENT_TTL_SECONDS = 86400;\n\nfunction randomId(): string {\n const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';\n let id = '';\n for (let i = 0; i < 24; i++) {\n id += chars.charAt(Math.floor(Math.random() * chars.length));\n }\n return id;\n}\n\nexport class SnapshotClient {\n private config: SnapshotClientConfig;\n private storage: Storage | null;\n private now: () => number;\n private fetchImpl: typeof fetch;\n private snapshot: CompanySnapshot | null = null;\n private queue: PendingReport[] = [];\n private sessionId: string;\n private inFlight: Promise<void> | null = null;\n\n constructor(config: SnapshotClientConfig) {\n this.config = config;\n this.now = config.now ?? (() => Date.now());\n this.fetchImpl = config.fetchImpl ?? ((...args) => fetch(...args));\n this.storage =\n config.storage ??\n (typeof window !== 'undefined' && window.localStorage ? window.localStorage : null);\n\n this.queue = this.readQueue();\n this.sessionId = getOrCreateSessionId({\n storage: this.storage ?? defaultSessionStorage(),\n now: this.now,\n ttlMs: config.sessionTtl,\n });\n\n if (config.inlineSnapshot) {\n this.snapshot = config.inlineSnapshot;\n this.persistSnapshot(config.inlineSnapshot);\n } else {\n this.snapshot = this.readSnapshot();\n }\n }\n\n getAssignmentSync(experimentId: string, userId: string): SyncAssignment | null {\n const snapshot = this.freshSnapshot();\n if (!snapshot) return null;\n\n const experiment = snapshot.experiments[experimentId];\n if (!experiment || experiment.mode !== 'snapshot') return null;\n\n const key = `${experimentId}:${userId}`;\n const decisions = this.readDecisions();\n const stored = decisions[key];\n if (stored && stored.expiresAt > this.now()) {\n const held = experiment.treatments.find((t) => t.id === stored.treatmentId);\n if (held) {\n return {\n clientAssignmentId: stored.clientAssignmentId,\n experimentId,\n treatmentId: held.id,\n config: held.config,\n };\n }\n }\n\n const treatment = pickTreatment(experiment.treatments, Math.random());\n if (!treatment) return null;\n\n const clientAssignmentId = randomId();\n const ttlSeconds = experiment.assignmentTtl ?? DEFAULT_ASSIGNMENT_TTL_SECONDS;\n decisions[key] = {\n clientAssignmentId,\n treatmentId: treatment.id,\n expiresAt: this.now() + ttlSeconds * 1000,\n };\n this.writeDecisions(decisions);\n\n this.queue.push({\n clientAssignmentId,\n experimentId,\n treatmentId: treatment.id,\n userId,\n sessionId: this.sessionId,\n });\n this.persistQueue();\n\n return {\n clientAssignmentId,\n experimentId,\n treatmentId: treatment.id,\n config: treatment.config,\n };\n }\n\n async refresh(): Promise<void> {\n const response = await this.fetchImpl(`${this.config.apiUrl}/snapshot`, {\n method: 'GET',\n headers: { 'x-api-key': this.config.publishableKey },\n });\n if (!response.ok) return;\n const snapshot = (await response.json()) as CompanySnapshot;\n this.snapshot = snapshot;\n this.persistSnapshot(snapshot);\n }\n\n async flush(): Promise<void> {\n if (this.inFlight) return this.inFlight;\n this.inFlight = this.doFlush().finally(() => {\n this.inFlight = null;\n });\n return this.inFlight;\n }\n\n private async doFlush(): Promise<void> {\n if (this.queue.length === 0) return;\n const reports = this.queue.slice(0, 1000);\n\n try {\n const response = await this.fetchImpl(`${this.config.apiUrl}/assignments/report`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-api-key': this.config.publishableKey,\n },\n body: JSON.stringify({ reports }),\n keepalive: true,\n });\n if (!response.ok) return;\n } catch {\n return;\n }\n\n this.queue = this.queue.slice(reports.length);\n this.persistQueue();\n }\n\n getSessionId(): string {\n return this.sessionId;\n }\n\n pendingReportCount(): number {\n return this.queue.length;\n }\n\n private freshSnapshot(): CompanySnapshot | null {\n if (!this.snapshot) return null;\n if (this.snapshot.ttl === 0) return null;\n const ageMs = this.now() - this.snapshot.generatedAt;\n if (ageMs > this.snapshot.ttl * 1000) return null;\n return this.snapshot;\n }\n\n private readSnapshot(): CompanySnapshot | null {\n if (!this.storage) return null;\n try {\n const raw = this.storage.getItem(STORAGE_KEY);\n return raw ? (JSON.parse(raw) as CompanySnapshot) : null;\n } catch {\n return null;\n }\n }\n\n private persistSnapshot(snapshot: CompanySnapshot): void {\n if (!this.storage) return;\n try {\n this.storage.setItem(STORAGE_KEY, JSON.stringify(snapshot));\n } catch {}\n }\n\n private readQueue(): PendingReport[] {\n if (!this.storage) return [];\n try {\n const raw = this.storage.getItem(REPORT_QUEUE_KEY);\n return raw ? (JSON.parse(raw) as PendingReport[]) : [];\n } catch {\n return [];\n }\n }\n\n private readDecisions(): Record<string, StoredDecision> {\n if (!this.storage) return {};\n try {\n const raw = this.storage.getItem(DECISIONS_KEY);\n if (!raw) return {};\n const parsed = JSON.parse(raw) as Record<string, StoredDecision>;\n const now = this.now();\n const live: Record<string, StoredDecision> = {};\n for (const [k, d] of Object.entries(parsed)) {\n if (d && d.expiresAt > now) live[k] = d;\n }\n return live;\n } catch {\n return {};\n }\n }\n\n private writeDecisions(decisions: Record<string, StoredDecision>): void {\n if (!this.storage) return;\n try {\n this.storage.setItem(DECISIONS_KEY, JSON.stringify(decisions));\n } catch {}\n }\n\n private persistQueue(): void {\n if (!this.storage) return;\n try {\n this.storage.setItem(REPORT_QUEUE_KEY, JSON.stringify(this.queue));\n } catch {}\n }\n}\n","export const VISITOR_COOKIE = 'bandit_v';\nexport const DECISIONS_COOKIE = 'bandit_d';\nexport const VISITOR_COOKIE_MAX_AGE_SECONDS = 400 * 86400;\n\nexport interface CookieDecision {\n experimentId: string;\n treatmentId: string;\n clientAssignmentId: string;\n expiresAt: number;\n}\n\nexport type CookieDecisions = Record<string, CookieDecision>;\n\nconst ENTRY_SEPARATOR = '~';\nconst FIELD_SEPARATOR = '.';\nconst SAFE_FIELD = /^[A-Za-z0-9_-]+$/;\n\nexport function readCookie(header: string | null | undefined, name: string): string | null {\n if (!header) return null;\n for (const part of header.split(';')) {\n const index = part.indexOf('=');\n if (index === -1) continue;\n if (part.slice(0, index).trim() !== name) continue;\n const value = part.slice(index + 1).trim();\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n }\n return null;\n}\n\nexport function parseDecisions(value: string | null | undefined, now: number): CookieDecisions {\n const decisions: CookieDecisions = {};\n if (!value) return decisions;\n for (const entry of value.split(ENTRY_SEPARATOR)) {\n const [experimentId, treatmentId, clientAssignmentId, expiresAtSeconds] = entry.split(FIELD_SEPARATOR);\n const expiresAt = Number(expiresAtSeconds) * 1000;\n if (!experimentId || !treatmentId || !clientAssignmentId || !Number.isFinite(expiresAt)) continue;\n if (expiresAt <= now) continue;\n decisions[experimentId] = { experimentId, treatmentId, clientAssignmentId, expiresAt };\n }\n return decisions;\n}\n\nexport function serializeDecisions(decisions: CookieDecisions): string {\n return Object.values(decisions)\n .filter((d) => [d.experimentId, d.treatmentId, d.clientAssignmentId].every((f) => SAFE_FIELD.test(f)))\n .map((d) =>\n [d.experimentId, d.treatmentId, d.clientAssignmentId, Math.ceil(d.expiresAt / 1000)].join(FIELD_SEPARATOR)\n )\n .join(ENTRY_SEPARATOR);\n}\n\nexport function decisionsMaxAgeSeconds(decisions: CookieDecisions, now: number): number {\n const latest = Math.max(0, ...Object.values(decisions).map((d) => d.expiresAt));\n return Math.max(0, Math.ceil((latest - now) / 1000));\n}\n\nexport interface CookieAttributes {\n maxAgeSeconds: number;\n domain?: string;\n secure?: boolean;\n}\n\nexport function serializeCookie(name: string, value: string, attributes: CookieAttributes): string {\n const parts = [`${name}=${encodeURIComponent(value)}`, 'Path=/', `Max-Age=${attributes.maxAgeSeconds}`, 'SameSite=Lax'];\n if (attributes.domain) parts.push(`Domain=${attributes.domain}`);\n if (attributes.secure) parts.push('Secure');\n return parts.join('; ');\n}\n"],"mappings":"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","names":[]}