@runbandit/sdk 2.0.1 → 2.1.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.d.ts +1 -581
- package/dist/index.js +6 -59
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/session.ts","../src/client.ts","../../shared/src/types.ts","../../shared/src/utils.ts","../../shared/src/email-token.ts","../../shared/src/email-plan.ts","../../shared/src/email-blocks.ts","../src/snapshot-cache.ts","../src/email-links.ts","../src/email.ts","../src/email-plan.ts"],"sourcesContent":["/**\n * Session identity, shared by every client in this package.\n *\n * Reward attribution joins `assignments` by `sessionId`, so a decision made by\n * `SnapshotClient` and an event tracked by `BanditsClient` must agree on the\n * id or the conversion never lands on the impression. One implementation,\n * used by both, is the only way that rule stays true.\n */\n\nexport const SESSION_ID_KEY = 'bandits_session_id';\nexport const SESSION_CREATED_AT_KEY = 'bandits_session_created_at';\n\n/** 24 hours, in milliseconds. */\nexport const DEFAULT_SESSION_TTL_MS = 86400000;\n\n/** A UUID v4, falling back to a Math.random shape where crypto is absent. */\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 /** Where the session lives. Null (no browser storage) means ephemeral. */\n storage?: Storage | null;\n /** Injectable clock, in milliseconds. */\n now?: () => number;\n ttlMs?: number;\n}\n\n/**\n * Return the current session id, minting and persisting a new one when there\n * is none or the stored one has aged past `ttlMs`.\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\n/** The browser's localStorage, or null outside a browser. */\nexport function defaultSessionStorage(): Storage | null {\n return typeof window !== 'undefined' && window.localStorage ? window.localStorage : null;\n}\n","import {\n AssignmentRequest,\n TrackNamedEventRequest,\n BanditResult,\n APIResponse,\n ContextualFeatures,\n BundleAssignmentRequest,\n BundleAssignmentResponse,\n} from '@runbandit/shared';\nimport { defaultSessionStorage, generateId, getOrCreateSessionId } from './session.js';\n\nexport interface BanditsClientConfig {\n apiUrl: string; // Base URL - /api prefix is added automatically (e.g., 'https://runbandit.com')\n apiKey: string;\n timeout?: number;\n batchSize?: number;\n flushInterval?: number;\n sessionTtl?: number; // default 24 hours in ms\n}\n\nexport interface Assignment {\n assignmentId: string;\n treatmentId: string;\n experimentId: string;\n userId: string;\n config: any;\n}\n\n/** Browsers only; never the query string or fragment (they can carry tokens). */\nfunction currentPageUrl(): string | undefined {\n if (typeof location !== 'undefined' && location.origin && location.pathname) {\n return location.origin + location.pathname;\n }\n return undefined;\n}\n\nexport class BanditsClient {\n private config: BanditsClientConfig;\n private namedEventQueue: TrackNamedEventRequest[] = [];\n private sessionId: string;\n private flushTimer?: NodeJS.Timeout;\n\n constructor(config: BanditsClientConfig) {\n this.config = {\n timeout: 5000,\n batchSize: 100,\n flushInterval: 10000,\n sessionTtl: 86400000, // 24 hours in ms\n ...config\n };\n this.sessionId = this.getOrCreateSessionId();\n this.startBatchFlush();\n }\n\n async getAssignment(\n experimentId: string,\n userId: string,\n context?: ContextualFeatures\n ): Promise<Assignment> {\n const request: AssignmentRequest = {\n experimentId,\n userId,\n context,\n sessionId: this.sessionId\n };\n const pageUrl = currentPageUrl();\n if (pageUrl) request.pageUrl = pageUrl;\n\n const response = await this.makeRequest<any>(\n '/assignments',\n 'POST',\n request\n );\n\n if (!response.success || !response.data) {\n throw new Error(response.error || 'Failed to get assignment');\n }\n\n // API returns { success, data: { assignmentId, treatmentId, treatment, confidence, probabilityBest } }.\n // `confidence` is deprecated (a single volatile posterior sample); `probabilityBest` is the\n // Monte Carlo P(this treatment is best) and may be absent for algorithms that don't compute\n // it. Neither is currently surfaced on the returned Assignment - both stay server-side.\n // makeRequest puts the raw JSON body in response.data, so unwrap one level.\n const assignment = response.data?.data ?? response.data;\n\n if (!assignment?.assignmentId) {\n throw new Error('Invalid assignment response');\n }\n\n return {\n assignmentId: assignment.assignmentId,\n treatmentId: assignment.treatmentId,\n experimentId,\n userId,\n config: assignment.treatment?.config || {}\n };\n }\n\n /**\n * Track a named event for session-based attribution (recommended)\n * Events are matched to bandits by sessionId — no assignmentId needed\n * Set immediate: true to fire synchronously (survives navigation)\n */\n trackNamedEvent(\n event: { name: string; category?: string; value?: number; metadata?: Record<string, any> },\n opts?: { immediate?: boolean }\n ): void {\n const request: TrackNamedEventRequest = {\n name: event.name,\n category: event.category || 'custom',\n value: event.value,\n metadata: event.metadata,\n sessionId: this.sessionId,\n };\n\n if (opts?.immediate) {\n // Fire now and survive navigation (e.g. CTA click -> route change)\n const url = `${this.normalizeApiUrl()}/events`;\n try {\n void fetch(url, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.config.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n keepalive: true,\n });\n } catch {\n // best-effort\n }\n return;\n }\n\n this.namedEventQueue.push(request);\n if (this.namedEventQueue.length >= this.config.batchSize!) {\n this.flush();\n }\n }\n\n async flush(): Promise<void> {\n const namedEvents = [...this.namedEventQueue];\n this.namedEventQueue = [];\n if (namedEvents.length === 0) return;\n\n try {\n await fetch(`${this.normalizeApiUrl()}/events/batch`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.config.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events: namedEvents }),\n keepalive: true,\n });\n } catch (error) {\n this.namedEventQueue.unshift(...namedEvents);\n throw error;\n }\n }\n\n /**\n * Link an anonymous user ID to a durable identity (e.g. email, account ID).\n * All historical assignments and events for the anonymous ID are re-attributed\n * to the identified user. Future calls to getAssignment() with either ID will\n * resolve to the same assignment.\n *\n * Requires admin authentication — the client must be configured with a\n * personal access token (PAT) instead of an API key, and companyId must\n * be provided. This is a management operation, not an SDK client operation.\n *\n * @param anonymousId - The temporary/anonymous user ID (e.g. crypto.randomUUID())\n * @param identifiedUserId - The permanent user ID (e.g. email, database ID)\n * @param companyId - The company ID (required for PAT auth)\n * @param mergeReason - Why the identification is happening\n */\n async identify(\n anonymousId: string,\n identifiedUserId: string,\n companyId: string,\n mergeReason?: 'user_signup' | 'email_verification' | 'social_login' | 'account_recovery' | 'manual'\n ): Promise<void> {\n const response = await this.makeRequest(\n '/identify',\n 'POST',\n {\n anonymousId,\n identifiedUserId,\n mergeReason: mergeReason || 'manual',\n },\n { 'x-company-id': companyId }\n );\n\n if (!response.success) {\n throw new Error(response.error || 'Failed to identify user');\n }\n }\n\n /**\n * Record a funnel payout event.\n * Call this when the terminal conversion happens (e.g., purchase).\n *\n * @param funnelId - The funnel ID\n * @param userId - The user who converted\n * @param value - The payout value (e.g., purchase amount in dollars)\n */\n async trackFunnelReward(\n funnelId: string,\n userId: string,\n value: number\n ): Promise<void> {\n const response = await this.makeRequest(\n `/funnels/${funnelId}/reward`,\n 'POST',\n { userId, value }\n );\n\n if (!response.success) {\n throw new Error(response.error || 'Failed to record funnel reward');\n }\n }\n\n async getBundle(\n bundleId: string,\n userId: string,\n context?: ContextualFeatures\n ): Promise<BundleAssignmentResponse> {\n const request: BundleAssignmentRequest = {\n bundleId,\n userId,\n context,\n sessionId: this.sessionId,\n };\n const pageUrl = currentPageUrl();\n if (pageUrl) request.pageUrl = pageUrl;\n\n const response = await this.makeRequest<any>('/bundles/assignment', 'POST', request);\n\n if (!response.success || !response.data) {\n throw new Error(response.error || 'Failed to get bundle assignment');\n }\n\n // makeRequest wraps the raw JSON body in response.data; unwrap one level.\n const payload = response.data?.data ?? response.data;\n if (!payload?.bundleAssignmentId || !payload?.slots) {\n throw new Error('Invalid bundle assignment response');\n }\n\n return {\n bundleAssignmentId: payload.bundleAssignmentId,\n slots: payload.slots,\n };\n }\n\n async trackBundleReward(bundleId: string, userId: string, value: number): Promise<void> {\n const response = await this.makeRequest(`/bundles/${bundleId}/reward`, 'POST', {\n userId,\n value,\n });\n if (!response.success) {\n throw new Error(response.error || 'Failed to record bundle reward');\n }\n }\n\n private generateId(): string {\n return generateId();\n }\n\n /**\n * Normalize the apiUrl by adding /api prefix if not present.\n * Supports both 'https://runbandit.com' and 'https://runbandit.com/api'.\n */\n private normalizeApiUrl(): string {\n let baseUrl = this.config.apiUrl.replace(/\\/$/, '');\n if (!baseUrl.endsWith('/api')) {\n baseUrl = `${baseUrl}/api`;\n }\n return baseUrl;\n }\n\n private getOrCreateSessionId(): string {\n return getOrCreateSessionId({\n storage: defaultSessionStorage(),\n ttlMs: this.config.sessionTtl,\n });\n }\n\n private async makeRequest<T = any>(\n endpoint: string,\n method: 'GET' | 'POST' | 'PUT' | 'DELETE',\n body?: any,\n extraHeaders?: Record<string, string>\n ): Promise<APIResponse<T>> {\n const url = `${this.normalizeApiUrl()}${endpoint}`;\n\n const headers: Record<string, string> = {\n 'Authorization': `Bearer ${this.config.apiKey}`,\n 'Content-Type': 'application/json',\n ...extraHeaders,\n };\n\n const requestConfig: RequestInit = {\n method,\n headers,\n signal: AbortSignal.timeout(this.config.timeout!)\n };\n\n if (body && method !== 'GET') {\n requestConfig.body = JSON.stringify(body);\n }\n\n try {\n const response = await fetch(url, requestConfig);\n const data = await response.json();\n \n return {\n success: response.ok,\n data: response.ok ? data as T : undefined,\n error: response.ok ? undefined : (data as any)?.message || 'Request failed',\n timestamp: new Date()\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Network error',\n timestamp: new Date()\n };\n }\n }\n\n private startBatchFlush(): void {\n this.flushTimer = setInterval(() => {\n this.flush().catch(console.error);\n }, this.config.flushInterval);\n }\n\n destroy(): void {\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n }\n this.flush().catch(console.error);\n if (typeof window !== 'undefined' && window.localStorage) {\n localStorage.removeItem('bandits_session_id');\n localStorage.removeItem('bandits_session_created_at');\n }\n }\n}","export interface Company {\n id: string;\n name: string;\n subdomain?: string; // nullable for backwards compatibility with existing data\n domain?: string;\n plan: string;\n apiKey: string;\n status: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface User {\n id: string;\n email: string;\n name: string;\n image?: string;\n role: string;\n companyId: string | null;\n emailVerified?: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface Session {\n id: string;\n userId: string;\n token: string;\n expiresAt: Date;\n ipAddress?: string | null;\n userAgent?: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface Account {\n id: string;\n userId: string;\n provider: string;\n providerAccountId: string;\n accessToken?: string;\n refreshToken?: string;\n accessTokenExpiresAt?: Date;\n refreshTokenExpiresAt?: Date;\n scope?: string;\n idToken?: string;\n sessionState?: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface Experiment {\n id: string;\n name: string;\n description?: string;\n algorithm: BanditAlgorithm;\n forgetting?: ForgettingConfig;\n status: ExperimentStatus;\n contentType: ContentType;\n treatments: Treatment[];\n context?: Record<string, any>;\n bundleId?: string; // if set, experiment is a slot in a bundle (exclusive with funnelId)\n image?: string; // URL for uploaded image\n emoji?: string; // Emoji character as alternative to image\n assignmentTtl: number; // TTL in seconds for assignment expiration\n companyId: string;\n createdAt: Date;\n updatedAt: Date;\n endAt?: Date;\n placement?: Placement; // storefront slot a theme app extension block binds to\n}\n\n/**\n * Storefront slots a theme app extension block can occupy.\n *\n * Closed set: theme block settings are a static schema compiled into the\n * extension, so a placement cannot be added without shipping a new extension\n * version. Keep in sync with the `placement` select in\n * extensions/bandit-storefront/blocks/bandit_text.liquid.\n */\nexport const PLACEMENTS = [\n 'hero_headline',\n 'hero_subhead',\n 'cta_primary',\n 'banner_text',\n 'badge_text',\n] as const;\n\nexport type Placement = (typeof PLACEMENTS)[number];\n\nexport interface Treatment {\n id: string;\n experimentId: string;\n name: string;\n description?: string;\n config: VariantConfig;\n weight?: number;\n companyId: string;\n createdAt: Date;\n}\n\nexport interface AlgorithmResult {\n treatmentId: string;\n /**\n * @deprecated Per-algorithm, not comparable across algorithms, and about to\n * be redefined in a future major version. Historically a single raw\n * posterior draw for the selected arm (e.g. one Beta or LinUCB-squash\n * sample) - in [0,1], but a *volatile* one: it is a single sample, not an\n * estimate, so it swings arm-to-arm and request-to-request even when the\n * underlying posteriors haven't moved. Kept for backward compatibility with\n * documented example values (`api/assignments/route.doc.ts`,\n * `src/lib/api-reference.ts`, `specs/assignments.md`). New consumers should\n * use `probabilityBest` instead. See\n * `docs/plans/2026-09-14-gaussian-thompson-migration-plan.md` task 4 for\n * why this is an additive field rather than an in-place redefinition.\n */\n confidence: number;\n /**\n * P(this arm is the best arm), estimated by Monte Carlo sampling of the\n * arms' posteriors many times and tallying wins (see\n * `MultiArmBandit.getSelectionProbabilities()`). In [0,1], sums to ~1\n * across an experiment's arms, and is far less volatile than `confidence`\n * since it averages over many draws instead of reporting one.\n *\n * Not every algorithm can produce this cheaply on the assignment hot path:\n * - `ThompsonSampling` populates it (reduced sample count - see comment at\n * the call site for the cost tradeoff).\n * - `ContinuousThompson` and `FunnelBandit` support Monte Carlo estimation\n * via their own `getSelectionProbabilities()` / repeated `selectArm()`\n * calls, but neither is wired into `probabilityBest` on the live\n * assignment path (todo - see migration plan task 7 rollout).\n * - `LinearContextualBandit` does not populate this field at all: LinUCB is\n * not a posterior sampler, so there is nothing to Monte Carlo over. The\n * field is simply absent (`undefined`), not a stand-in zero.\n */\n probabilityBest?: number;\n}\n\nexport interface BanditResult {\n assignmentId: string;\n treatmentId: string;\n /** @deprecated see `AlgorithmResult.confidence`. */\n confidence: number;\n /** See `AlgorithmResult.probabilityBest`. */\n probabilityBest?: number;\n metadata?: Record<string, any>;\n}\n\nexport interface ContextualFeatures {\n [key: string]: string | number | boolean;\n}\n\nexport enum ExperimentStatus {\n DRAFT = 'draft',\n ACTIVE = 'active',\n PAUSED = 'paused',\n COMPLETED = 'completed',\n ARCHIVED = 'archived'\n}\n\n\nexport interface TreatmentMetadata {\n strategy?: string; // e.g. \"social proof\", \"urgency\"\n tone?: string; // e.g. \"warm\", \"snarky\", \"deadpan corporate\"\n ctaStyle?: string; // e.g. \"direct\", \"soft question\", \"implied next step\"\n emotionalHook?: string; // e.g. \"fomo\", \"belonging\", \"righteous indignation\"\n reasoning?: string; // LLM's explanation of why it chose this approach\n tags?: string[];\n generated?: boolean;\n generatedAt?: string;\n}\n\nexport interface VariantConfig {\n content: string;\n metadata?: TreatmentMetadata & Record<string, any>;\n}\n\n/**\n * Bandit algorithm families.\n *\n * Every family is Thompson-style: a Bayesian posterior per arm, sampled at\n * selection time. How much of the past that posterior remembers is controlled\n * separately, by `ForgettingMode`.\n */\nexport enum BanditAlgorithm {\n THOMPSON_SAMPLING = 'thompson_sampling',\n CONTEXTUAL_LINEAR = 'contextual_linear'\n}\n\n/**\n * Which posterior a Thompson-family experiment samples from.\n *\n * See the migration plan (docs/plans/2026-09-14-gaussian-thompson-migration-plan.md,\n * task 7): this is a per-experiment rollout flag, not a replacement of\n * `BanditAlgorithm`. `BETA` is the existing Beta-Binomial posterior\n * (`ThompsonSampling`); `NORMAL_GAMMA` is the extracted Normal-Gamma one\n * (`ContinuousThompson`). Missing/undefined means `BETA` - every experiment\n * written before this field existed must keep selecting exactly as it does\n * today.\n */\nexport enum Posterior {\n BETA = 'beta',\n NORMAL_GAMMA = 'normal_gamma'\n}\n\n/**\n * Map a stored posterior string onto a supported one, defaulting missing or\n * unrecognised values to `BETA` - the behaviour every existing experiment\n * already has. Only `create` (new experiments) or an explicit switch should\n * ever produce `NORMAL_GAMMA`.\n */\nexport function normalizePosterior(posterior: string | null | undefined): Posterior {\n return posterior === Posterior.NORMAL_GAMMA ? Posterior.NORMAL_GAMMA : Posterior.BETA;\n}\n\n/**\n * How an algorithm forgets old observations.\n *\n * Standard bandits assume the arms are stationary: a conversion from six months\n * ago counts exactly as much as one from this morning. Real treatments are\n * restless - creative fatigues, seasons turn, traffic mix shifts - so the two\n * non-stationary modes below down-weight or drop stale evidence.\n *\n * - NONE stationary; every observation counts forever\n * - DISCOUNTED observation weight decays as 0.5^(age / halfLifeMs)\n * - SLIDING_WINDOW observations outside the last `windowMs` are dropped entirely\n *\n * Both non-stationary modes measure age in wall-clock time rather than in\n * observation count, so a quiet experiment forgets at the same rate as a busy\n * one.\n */\nexport enum ForgettingMode {\n NONE = 'none',\n DISCOUNTED = 'discounted',\n SLIDING_WINDOW = 'sliding_window'\n}\n\nexport interface ForgettingConfig {\n mode: ForgettingMode;\n /** Half-life in milliseconds. DISCOUNTED only. */\n halfLifeMs?: number;\n /** Window length in milliseconds. SLIDING_WINDOW only. */\n windowMs?: number;\n}\n\n/** Default half-life for DISCOUNTED: one week. */\nexport const DEFAULT_HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000;\n\n/** Default window for SLIDING_WINDOW: two weeks. */\nexport const DEFAULT_WINDOW_MS = 14 * 24 * 60 * 60 * 1000;\n\n/** The stationary default applied when an experiment has no forgetting config. */\nexport const STATIONARY: ForgettingConfig = { mode: ForgettingMode.NONE };\n\n/**\n * Fill in the defaults for a partially-specified forgetting config.\n * An unrecognised or missing mode degrades to NONE rather than throwing, so a\n * stale row written by an older deploy still selects treatments.\n */\nexport function resolveForgetting(config?: Partial<ForgettingConfig> | null): ForgettingConfig {\n switch (config?.mode) {\n case ForgettingMode.DISCOUNTED:\n return {\n mode: ForgettingMode.DISCOUNTED,\n halfLifeMs: config.halfLifeMs && config.halfLifeMs > 0\n ? config.halfLifeMs\n : DEFAULT_HALF_LIFE_MS\n };\n case ForgettingMode.SLIDING_WINDOW:\n return {\n mode: ForgettingMode.SLIDING_WINDOW,\n windowMs: config.windowMs && config.windowMs > 0\n ? config.windowMs\n : DEFAULT_WINDOW_MS\n };\n default:\n return STATIONARY;\n }\n}\n\n/**\n * Algorithms that existed before the Thompson-only refactor, kept only so that\n * experiment rows written by older deploys still resolve on read.\n */\nconst LEGACY_ALGORITHM_ALIASES: Record<string, BanditAlgorithm> = {\n epsilon_greedy: BanditAlgorithm.THOMPSON_SAMPLING,\n ucb1: BanditAlgorithm.THOMPSON_SAMPLING,\n contextual_neural: BanditAlgorithm.CONTEXTUAL_LINEAR\n};\n\n/**\n * Map a stored algorithm string onto a supported algorithm.\n *\n * Retired names (epsilon_greedy, ucb1) map onto standard Thompson Sampling,\n * which reads the same successes/trials counters they did, so a running\n * experiment keeps its learned history. contextual_neural maps onto\n * contextual_linear, which is what it already fell back to at runtime.\n */\nexport function normalizeAlgorithm(algorithm: string | null | undefined): BanditAlgorithm {\n if (algorithm === BanditAlgorithm.THOMPSON_SAMPLING) return BanditAlgorithm.THOMPSON_SAMPLING;\n if (algorithm === BanditAlgorithm.CONTEXTUAL_LINEAR) return BanditAlgorithm.CONTEXTUAL_LINEAR;\n return LEGACY_ALGORITHM_ALIASES[algorithm ?? ''] ?? BanditAlgorithm.THOMPSON_SAMPLING;\n}\n\nexport enum ContentType {\n PLAIN_TEXT = 'plain_text',\n MARKDOWN = 'markdown',\n HTML = 'html',\n CSS = 'css',\n JAVASCRIPT = 'javascript',\n JSON = 'json',\n IMAGE = 'image',\n VIDEO = 'video',\n AUDIO = 'audio',\n DOCUMENT = 'document',\n}\n\nexport function getContentTypeCategory(contentType: ContentType): 'text' | 'code' | 'file' {\n switch (contentType) {\n case ContentType.PLAIN_TEXT:\n case ContentType.MARKDOWN:\n return 'text';\n case ContentType.HTML:\n case ContentType.CSS:\n case ContentType.JAVASCRIPT:\n case ContentType.JSON:\n return 'code';\n case ContentType.IMAGE:\n case ContentType.VIDEO:\n case ContentType.AUDIO:\n case ContentType.DOCUMENT:\n return 'file';\n }\n}\n\nexport interface BanditConfig {\n algorithm: BanditAlgorithm;\n parameters: Record<string, any>;\n}\n\nexport interface ExperimentStats {\n totalAssignments: number;\n totalConversions: number;\n conversionRate: number;\n treatmentStats: Record<string, TreatmentStats>;\n}\n\nexport interface TreatmentStats {\n assignments: number;\n conversions: number;\n conversionRate: number;\n confidence: number;\n lift?: number;\n}\n\nexport interface APIResponse<T = any> {\n success: boolean;\n data?: T;\n error?: string;\n timestamp: Date;\n}\n\nexport interface CreateExperimentRequest {\n name: string;\n description?: string;\n algorithm: BanditAlgorithm;\n contentType: ContentType;\n algorithmConfig?: Record<string, any>;\n treatments?: Omit<Treatment, 'id' | 'experimentId' | 'createdAt'>[];\n}\n\nexport interface AssignmentRequest {\n experimentId: string;\n userId: string;\n context?: ContextualFeatures;\n sessionId?: string;\n /** Browser page the assignment was requested from (origin + pathname). */\n pageUrl?: string;\n}\n\n\nexport interface TrackNamedEventRequest {\n name: string;\n category?: string;\n value?: number;\n metadata?: Record<string, any>;\n sessionId: string;\n}\n\n// --- Funnel RL Types ---\n\nexport enum FunnelStatus {\n DRAFT = 'draft',\n ACTIVE = 'active',\n PAUSED = 'paused',\n COMPLETED = 'completed',\n}\n\nexport interface Funnel {\n id: string;\n name: string;\n description?: string;\n status: FunnelStatus;\n conversionWindowMs: number;\n companyId: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface FunnelRewardRequest {\n funnelId: string;\n userId: string;\n value: number;\n}\n\n// --- API Key Management Types ---\n\nexport interface ApiKey {\n id: string;\n name: string;\n keyPrefix: string;\n status: 'active' | 'revoked';\n createdBy: string;\n createdAt: Date;\n lastUsedAt?: Date;\n revokedAt?: Date;\n revokedBy?: string;\n}\n\nexport interface CreateApiKeyRequest {\n name: string;\n}\n\nexport interface CreateApiKeyResponse {\n id: string;\n key: string;\n name: string;\n}\n\n// --- Identity Resolution Types ---\n\nexport type IdentifyMergeReason =\n | 'user_signup'\n | 'email_verification'\n | 'social_login'\n | 'account_recovery'\n | 'manual';\n\nexport interface IdentifyUserRequest {\n anonymousId: string;\n identifiedUserId: string;\n mergeReason?: IdentifyMergeReason;\n}\n\nexport interface IdentifyUserResponse {\n anonymousId: string;\n identifiedUserId: string;\n alreadyMapped: boolean;\n}\n\nexport interface ResolvedIdentity {\n canonicalId: string;\n anonymousIds: string[];\n isIdentified: boolean;\n}\n\n// --- Bundle Types ---\n\nexport type BundleMode = 'factored' | 'joint';\n\nexport interface Bundle {\n id: string;\n name: string;\n description?: string;\n status: ExperimentStatus; // reuse draft/active/paused/completed\n mode: BundleMode; // 'factored' default; 'joint' out of v1 scope\n funnelId?: string; // if set, bundle is a funnel member (learning owned by the funnel)\n assignmentTtl: number; // seconds; shared TTL for the whole bundle's page\n conversionWindowMs: number; // standalone reward window; default 604800000 (7 days)\n companyId: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface BundleAssignmentRequest {\n bundleId: string;\n userId: string;\n context?: ContextualFeatures;\n sessionId?: string;\n pageUrl?: string;\n}\n\nexport interface BundleSlotAssignment {\n assignmentId: string;\n experimentId: string;\n treatmentId: string;\n config: VariantConfig | Record<string, unknown>;\n}\n\nexport interface BundleAssignmentResponse {\n bundleAssignmentId: string;\n slots: Record<string, BundleSlotAssignment>;\n}\n\n/**\n * One treatment's published allocation within a snapshot experiment.\n *\n * This is the wire contract between the snapshot publisher (dashboard API)\n * and its only consumer (the SDK). Declared once here so both sides share\n * the same shape instead of drifting apart.\n */\nexport interface SnapshotTreatment {\n id: string;\n name?: string;\n config: unknown;\n /** Probability the next visitor is assigned this arm. Sums to ~1 per experiment. */\n pct: number;\n}\n\nexport interface SnapshotExperiment {\n /**\n * 'snapshot' means the client may decide locally from `pct`. 'network' means\n * it must call /api/assignments, because the experiment's state cannot be\n * reduced to a static distribution.\n */\n mode: 'snapshot' | 'network';\n /**\n * Assignment lifetime in SECONDS (the unit `assignments.expiresAt` uses).\n * The client holds a decision this long so that repeated pageviews in one\n * session report one impression, matching the server path's one assignment\n * per (experiment, user).\n */\n assignmentTtl: number;\n treatments: SnapshotTreatment[];\n}\n\nexport interface CompanySnapshot {\n ttl: number;\n generatedAt: number;\n experiments: Record<string, SnapshotExperiment>;\n}\n","import { Treatment } from './types.js';\n\nexport function generateId(): string {\n return Math.random().toString(36).substring(2) + Date.now().toString(36);\n}\n\nexport function validateTreatmentWeights(treatments: Treatment[]): boolean {\n const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);\n return totalWeight > 0;\n}\n\nexport function normalizeTreatmentWeights(treatments: Treatment[]): Treatment[] {\n const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);\n \n return treatments.map(treatment => ({\n ...treatment,\n weight: (treatment.weight || 1) / totalWeight\n }));\n}\n\nexport function calculateConfidenceInterval(\n successes: number,\n trials: number,\n confidence: number = 0.95\n): { lower: number; upper: number } {\n if (trials === 0) return { lower: 0, upper: 0 };\n \n const z = confidence === 0.95 ? 1.96 : confidence === 0.99 ? 2.576 : 1.645;\n const p = successes / trials;\n const margin = z * Math.sqrt((p * (1 - p)) / trials);\n \n return {\n lower: Math.max(0, p - margin),\n upper: Math.min(1, p + margin)\n };\n}\n\nexport function isValidEmail(email: string): boolean {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return emailRegex.test(email);\n}\n\nexport function sanitizeMetadata(metadata: Record<string, any>): Record<string, any> {\n const sanitized: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n sanitized[key] = value;\n } else if (value !== null && value !== undefined) {\n sanitized[key] = String(value);\n }\n }\n\n return sanitized;\n}\n\n/**\n * Generates a URL-safe subdomain from a company name\n * @param name Company name\n * @returns URL-safe subdomain\n */\nexport function generateSubdomain(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '') // Remove special characters\n .replace(/\\s+/g, '-') // Replace spaces with hyphens\n .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen\n .replace(/^-|-$/g, '') // Remove leading/trailing hyphens\n .substring(0, 63); // DNS label max length\n}\n\n/**\n * Validates if a subdomain is properly formatted\n * @param subdomain Subdomain to validate\n * @returns true if valid, false otherwise\n */\nexport function isValidSubdomain(subdomain: string): boolean {\n if (!subdomain || subdomain.length === 0 || subdomain.length > 63) {\n return false;\n }\n\n // Subdomain rules: lowercase letters, numbers, hyphens\n // Cannot start or end with hyphen\n // Cannot contain consecutive hyphens\n const subdomainRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;\n\n // Reserved subdomains\n const reserved = [\n 'www', 'api', 'app', 'admin', 'dashboard', 'mail', 'ftp', 'localhost',\n 'staging', 'dev', 'test', 'demo', 'blog', 'docs', 'status', 'help',\n 'support', 'cdn', 'static', 'assets', 'secure', 'payment', 'pay'\n ];\n\n return subdomainRegex.test(subdomain) && !reserved.includes(subdomain);\n}\n\n/**\n * Extracts subdomain from hostname\n * @param hostname Full hostname (e.g., 'acme.example.com')\n * @param baseDomain Base domain (e.g., 'example.com')\n * @returns Subdomain or null if not found\n */\nexport function extractSubdomain(hostname: string, baseDomain: string): string | null {\n if (!hostname || !baseDomain) {\n return null;\n }\n\n // Remove port if present\n const cleanHostname = hostname.split(':')[0];\n\n // Check if hostname ends with base domain\n if (!cleanHostname.endsWith(baseDomain)) {\n return null;\n }\n\n // Extract subdomain part\n const subdomain = cleanHostname.slice(0, -(baseDomain.length + 1));\n\n // Return null if no subdomain or if it's www\n if (!subdomain || subdomain === 'www') {\n return null;\n }\n\n return subdomain;\n}\n\n/**\n * Builds full URL with subdomain\n * @param subdomain Subdomain\n * @param baseDomain Base domain\n * @param path Optional path\n * @param protocol Protocol (default: https)\n * @returns Full URL\n */\nexport function buildSubdomainUrl(\n subdomain: string,\n baseDomain: string,\n path: string = '',\n protocol: string = 'https'\n): string {\n const url = `${protocol}://${subdomain}.${baseDomain}`;\n return path ? `${url}${path.startsWith('/') ? path : '/' + path}` : url;\n}","/**\n * The email tracking token.\n *\n * Every tracked link in an email carries one of these as a query parameter.\n * It is the impression record: which campaign, which recipient, which arm was\n * chosen in every slot, and which link was clicked - all of it self-contained\n * and signed, so no row has to be written at send time.\n *\n * Three constraints shape the format, and all three are unusual:\n *\n * 1. **It must be mintable offline.** In `plan` mode the customer's own SDK\n * mints tokens for millions of recipients with no network calls, so the\n * token cannot be a database id we hand out. Minting is a pure function of\n * (payload, key).\n *\n * 2. **It must outlive everything else in the system.** A link minted today\n * may be clicked in three years. That makes this format a public contract\n * with no deprecation path: campaign link tables are never deleted, slot\n * order is never changed, and several signing keys stay valid at once so a\n * rotation cannot break links already sitting in inboxes.\n *\n * 3. **It travels somewhere hostile.** URLs get forwarded, logged by proxies\n * and leaked through `Referer`. Nothing in here may be a secret, and\n * nothing in here may be trusted before the signature is checked.\n *\n * See specs/email.md, \"Tokens\".\n */\n\n/**\n * The query parameter tracked links are tagged with.\n *\n * Kept distinct from the well-known ad-tech parameters (`fbclid`, `gclid`,\n * `utm_*`): Safari's Link Tracking Protection and Firefox's strict ETP strip\n * params from a curated list of known trackers, and sharing a name with one of\n * them would mean being stripped by association.\n */\nexport const EMAIL_TOKEN_PARAM = 'bt';\n\n/**\n * One signing key. Several are valid concurrently - see `verifyEmailToken`.\n *\n * `version` is carried in the token so verification can select the right key\n * directly instead of trying each in turn, which would leak timing information\n * and get slower with every rotation.\n */\nexport interface SigningKey {\n version: number;\n secret: string;\n}\n\n/**\n * The signed body of a token. Field names are single characters because this\n * is base64'd into a URL that also has to survive an email client's line\n * wrapping - the verbosity is not worth the bytes.\n */\nexport interface EmailTokenPayload {\n /** Campaign id. */\n c: string;\n /** Pseudonymous recipient - see `hashRecipient`. Never a raw identifier. */\n r: string;\n /**\n * The arm chosen in each slot, POSITIONALLY: `k[i]` is the arm index chosen\n * for the campaign's `slots[i]`.\n *\n * Positional rather than a `{ slotId: armId }` map because ids are ~32\n * characters each and a six-slot email would spend over 200 characters\n * restating what the campaign already knows. The cost is that a campaign's\n * slot order becomes load-bearing forever, which is why reordering slots\n * after a send is rejected at the campaign level.\n *\n * The whole combination is here, not just the clicked slot, because a click\n * has to credit every slot the recipient saw - and because in `plan` mode\n * this token IS the impression record, reconstructable from a first click\n * even if the send-time manifest never arrived.\n */\n k: number[];\n /**\n * Index into the campaign's link table - never a URL.\n *\n * A token carrying its own destination would be an open redirect the moment\n * the signature check had a bug, and would make every token long enough to\n * matter. An index can only ever address a link the campaign already\n * declared.\n */\n l: number;\n /** Signing key version. Set by `mintEmailToken`; do not pass it in. */\n v?: number;\n}\n\nexport type VerifiedToken =\n | { valid: true; payload: Required<EmailTokenPayload> }\n | { valid: false; reason: 'malformed' | 'unknown_key' | 'bad_signature' };\n\n// ---------------------------------------------------------------------------\n// Encoding helpers\n//\n// Hand-rolled rather than pulled from a library because @runbandit/shared is\n// bundled into the SDK, which runs in customers' browsers, Node servers and\n// Workers alike. WebCrypto is the one primitive available in all three.\n// ---------------------------------------------------------------------------\n\nconst encoder = new TextEncoder();\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction base64UrlDecode(value: string): Uint8Array {\n const padded = value.replace(/-/g, '+').replace(/_/g, '/');\n const binary = atob(padded + '='.repeat((4 - (padded.length % 4)) % 4));\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\nasync function hmac(secret: string, message: string): Promise<Uint8Array> {\n const key = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n return new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(message)));\n}\n\n/**\n * Compare two signatures without leaking where they diverge.\n *\n * A plain `===` returns as soon as it finds a differing byte, which lets an\n * attacker with enough samples recover a valid signature one byte at a time.\n * The length check is deliberately not short-circuited on content.\n */\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Pseudonymise a recipient identifier.\n *\n * Deliberately a one-way HMAC rather than something we can reverse. We do not\n * need to: the destination page is the customer's own, and their SDK resolves\n * the real identity through `identify()` on arrival. What Bandit needs is a\n * stable key to attribute a click and to dedupe first-touch, and a hash serves\n * that without ever putting a customer's user ids in a URL that gets\n * forwarded, proxy-logged and leaked through `Referer`.\n *\n * Keyed on a company-level secret rather than a campaign one, so the same\n * recipient hashes identically across every campaign that company sends. That\n * is what makes cross-campaign frequency and fatigue analysis possible later -\n * an unsubscribe is usually the result of the fifth email, not the first.\n *\n * Truncated to 16 bytes: 128 bits is far beyond collision range for any real\n * list, and the 22 characters saved matter in a URL.\n */\nexport async function hashRecipient(recipientId: string, companySecret: string): Promise<string> {\n const digest = await hmac(companySecret, `recipient:${recipientId}`);\n return base64UrlEncode(digest.slice(0, 16));\n}\n\n/**\n * Mint a token. Pure function of (payload, key) - the same inputs always\n * produce the same token, which is what lets the SDK mint offline in `plan`\n * mode and the server arrive independently at the identical value.\n */\nexport async function mintEmailToken(\n payload: EmailTokenPayload,\n key: SigningKey\n): Promise<string> {\n // Key order is fixed so the encoding is stable: two callers serialising the\n // same payload must produce byte-identical output, or offline minting and\n // server-side minting would disagree.\n const body = base64UrlEncode(\n encoder.encode(\n JSON.stringify({\n c: payload.c,\n r: payload.r,\n k: payload.k,\n l: payload.l,\n v: key.version,\n })\n )\n );\n\n const signature = base64UrlEncode(await hmac(key.secret, body));\n return `${body}.${signature}`;\n}\n\n/**\n * Verify a token against every currently-held key.\n *\n * Never throws. This is called from a redirect endpoint and a click ingest\n * route, both of which take their input straight off a URL - a crash there is\n * a broken link in a real person's inbox, and the input is attacker-controlled\n * by definition.\n *\n * Pass every key still in rotation, oldest included. Dropping a key\n * permanently breaks every link ever minted under it, and those links live in\n * inboxes we cannot reach.\n */\nexport async function verifyEmailToken(\n token: string,\n keys: SigningKey[]\n): Promise<VerifiedToken> {\n const parts = token.split('.');\n if (parts.length !== 2 || !parts[0] || !parts[1]) {\n return { valid: false, reason: 'malformed' };\n }\n const [body, signature] = parts;\n\n let payload: EmailTokenPayload;\n try {\n payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(body)));\n } catch {\n return { valid: false, reason: 'malformed' };\n }\n\n if (\n !payload ||\n typeof payload.c !== 'string' ||\n typeof payload.r !== 'string' ||\n !Array.isArray(payload.k) ||\n !payload.k.every((arm) => Number.isInteger(arm) && arm >= 0) ||\n !Number.isInteger(payload.l) ||\n (payload.l as number) < 0 ||\n !Number.isInteger(payload.v)\n ) {\n return { valid: false, reason: 'malformed' };\n }\n\n const key = keys.find((candidate) => candidate.version === payload.v);\n if (!key) return { valid: false, reason: 'unknown_key' };\n\n const expected = base64UrlEncode(await hmac(key.secret, body));\n if (!timingSafeEqual(expected, signature)) {\n return { valid: false, reason: 'bad_signature' };\n }\n\n return { valid: true, payload: payload as Required<EmailTokenPayload> };\n}\n\n/**\n * Append a token to a destination URL.\n *\n * Appends rather than replaces, which is the whole reason the default tracking\n * mode needs no redirect service, no DNS setup and no custom certificate: the\n * link still points at the customer's own domain, and their existing Bandit\n * SDK reads the parameter on page load. Established practice - every ESP and\n * marketer already decorates outbound links this way.\n *\n * Existing query parameters and fragments are preserved; a fragment must stay\n * last or the browser will treat the token as part of it.\n */\nexport function appendEmailToken(url: string, token: string): string {\n const hashIndex = url.indexOf('#');\n const fragment = hashIndex === -1 ? '' : url.slice(hashIndex);\n const base = hashIndex === -1 ? url : url.slice(0, hashIndex);\n const separator = base.includes('?') ? '&' : '?';\n\n return `${base}${separator}${EMAIL_TOKEN_PARAM}=${encodeURIComponent(token)}${fragment}`;\n}\n","/**\n * `plan` mode - assignment for bulk sends, with no per-recipient round trip.\n *\n * `resolve` and `batch` both make an HTTP call carrying recipients. That is\n * fine to ten thousand and untenable at two million: the customer's send\n * pipeline would spend its entire budget waiting on us.\n *\n * So instead of answering per recipient, we hand over the *policy*. The plan\n * carries each slot's arm set and the selection probabilities the bandit's\n * posterior currently implies, plus a salt and the campaign's signing key. The\n * SDK then derives each recipient's arm locally and mints their token offline -\n * no network, perfectly parallel, as fast as the customer's own loop.\n *\n * Two properties make this work, and both are load-bearing:\n *\n * **Determinism.** The sampler here is the single implementation. The server\n * and the customer's SDK must derive the identical arm for the identical\n * recipient, or the impression manifest reports one thing and the tokens say\n * another. It is pure, keyed only on (salt, recipientId, slot), and pinned by\n * test.\n *\n * **Staleness is bounded by chunking.** A plan is one snapshot of the\n * posterior, so a whole blast served from a single plan is one learning epoch.\n * `refreshEvery` lets the caller re-request every N recipients, turning a 2M\n * send into a few dozen epochs instead of one - the customer's own dial\n * between throughput and adaptivity.\n */\n\nexport interface PlanArm {\n treatmentId: string;\n /**\n * Position of this arm in its slot's treatment list.\n *\n * This is what a token carries, so it must be the same index the render and\n * click paths resolve against - not this array's position, which is only\n * incidentally the same.\n */\n armIndex: number;\n}\n\nexport interface PlanSlot {\n experimentId: string;\n /** Slot name, for substituting into the template. */\n name: string;\n arms: PlanArm[];\n /**\n * P(each arm is best), positionally aligned with `arms`, summing to ~1.\n *\n * Thompson sampling's allocation, computed once from the posterior rather\n * than re-sampled per recipient. Sampling locally from these probabilities\n * is distributionally identical to sampling the posterior directly, which is\n * what makes the offline derivation legitimate rather than an approximation.\n */\n probabilities: number[];\n /** Arm content, positionally aligned with `arms`. */\n configs: unknown[];\n}\n\nexport interface EmailPlan {\n campaignId: string;\n slots: PlanSlot[];\n linkTable: string[];\n /**\n * Per-plan randomisation. Two campaigns must not assign the same recipient\n * the same way merely because the probabilities happen to match, or the\n * population splits into permanent cohorts and cross-campaign comparisons\n * quietly stop being independent.\n */\n salt: string;\n /**\n * The campaign's current signing key, so the SDK can mint offline.\n *\n * This is the customer's own campaign secret, sent to their server - not to\n * a browser. It lets them mint tokens for their own campaign, which they\n * could already do by calling the click endpoint directly; it does not let\n * them touch anyone else's.\n */\n signingKey: { version: number; secret: string };\n /** Re-request after this many recipients to pick up what the send has learned. */\n refreshEvery: number;\n /** When this plan stops being usable. */\n expiresAt: number;\n}\n\n/**\n * A uniform draw in [0,1) for one recipient and slot.\n *\n * HMAC rather than a plain hash so the mapping cannot be reproduced by anyone\n * without the salt - otherwise a recipient could work out which arm they were\n * in, and more importantly a third party could reconstruct the whole\n * assignment for a campaign they observed one token from.\n *\n * Six bytes gives 2^48 distinct values, far finer than any arm allocation\n * needs, and avoids the precision loss of stuffing 64 bits into a double.\n */\nexport async function uniformFor(\n salt: string,\n recipientId: string,\n slotIndex: number\n): Promise<number> {\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(salt),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n const mac = new Uint8Array(\n await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${recipientId}:${slotIndex}`))\n );\n\n let value = 0;\n for (let i = 0; i < 6; i++) value = value * 256 + mac[i];\n return value / 2 ** 48;\n}\n\n/**\n * Pick an arm index from a probability vector given a uniform draw.\n *\n * Separated from the draw so the selection itself is synchronous and trivially\n * testable against hand-written vectors.\n *\n * Degenerate vectors are handled rather than trusted: probabilities that do\n * not quite sum to 1 (Monte Carlo estimates never do) must not leave a\n * recipient unassigned, so the last arm absorbs the remainder.\n */\nexport function pickArm(probabilities: number[], uniform: number): number {\n if (probabilities.length === 0) return -1;\n\n const total = probabilities.reduce((sum, p) => sum + Math.max(p, 0), 0);\n if (total <= 0) {\n // No arm has any mass - fall back to uniform rather than always picking\n // the first, which would silently starve every other arm.\n return Math.min(probabilities.length - 1, Math.floor(uniform * probabilities.length));\n }\n\n const target = uniform * total;\n let cumulative = 0;\n for (let i = 0; i < probabilities.length; i++) {\n cumulative += Math.max(probabilities[i], 0);\n if (target < cumulative) return i;\n }\n\n return probabilities.length - 1;\n}\n\n/** The arm each slot assigns this recipient, positionally by slot. */\nexport async function sampleCombination(\n plan: EmailPlan,\n recipientId: string\n): Promise<number[]> {\n const combination: number[] = [];\n\n for (let i = 0; i < plan.slots.length; i++) {\n const slot = plan.slots[i];\n const uniform = await uniformFor(plan.salt, recipientId, i);\n const chosen = pickArm(slot.probabilities, uniform);\n // The token carries the arm's index in the experiment's treatment list,\n // which is not necessarily its position in this plan's array.\n combination.push(chosen === -1 ? 0 : slot.arms[chosen]?.armIndex ?? 0);\n }\n\n return combination;\n}\n\n/**\n * Tally a plan-mode send into per-arm impression counts.\n *\n * Aggregated because a send is a burst: the bandit needs a denominator, not a\n * row per recipient, and the per-recipient detail already lives in each token.\n */\nexport async function impressionManifest(\n plan: EmailPlan,\n recipientIds: string[]\n): Promise<Array<{ experimentId: string; treatmentId: string; count: number }>> {\n const counts = new Map<string, { experimentId: string; treatmentId: string; count: number }>();\n\n for (const recipientId of recipientIds) {\n for (let i = 0; i < plan.slots.length; i++) {\n const slot = plan.slots[i];\n const uniform = await uniformFor(plan.salt, recipientId, i);\n const chosen = pickArm(slot.probabilities, uniform);\n const arm = slot.arms[chosen === -1 ? 0 : chosen];\n if (!arm) continue;\n\n const key = `${slot.experimentId}:${arm.treatmentId}`;\n const entry = counts.get(key);\n if (entry) entry.count++;\n else\n counts.set(key, {\n experimentId: slot.experimentId,\n treatmentId: arm.treatmentId,\n count: 1,\n });\n }\n }\n\n return Array.from(counts.values());\n}\n","/**\n * Email as a structured document.\n *\n * The other authoring mode takes the customer's own HTML template and\n * substitutes slots into it. This one inverts that: the email is authored in\n * Bandit as a block tree, and every block property is a candidate slot. Four\n * slots with four options each is sixteen authored pieces and 256 possible\n * emails, none of which anyone hand-writes.\n *\n * This is about AUTHORING, not learning - cross-campaign learning works\n * identically either way. What it buys is that a marketer can run a\n * multi-armed email without owning an HTML template at all.\n *\n * ## Why this renderer is hand-written\n *\n * MJML is the obvious choice and was the original plan. Two things rule it\n * out. The dashboard runs on Cloudflare Workers, where MJML's several\n * megabytes sit badly against the bundle limit; and the same rendering has to\n * be available in `@runbandit/sdk`, which ships with no runtime dependencies\n * because customers did not agree to a transitive tree in order to send an\n * email. One small renderer serving both beats two that can disagree.\n *\n * ## Why the output looks like 1999\n *\n * Because email clients do. Outlook renders through Word, which ignores\n * `div`-based layout, flexbox and grid entirely; Gmail strips `<style>` blocks\n * in several contexts. Tables with inline styles are not nostalgia, they are\n * the only layout primitive that survives everywhere - and a button that needs\n * a background colour in Outlook needs a VML rectangle, which is why the\n * button block emits a conditional comment.\n */\n\n/** A value that comes from a bandit slot rather than being authored directly. */\nexport interface SlotBinding {\n $slot: string;\n}\n\nexport type Bound<T> = T | SlotBinding;\n\nexport function isSlotBinding(value: unknown): value is SlotBinding {\n return typeof value === 'object' && value !== null && typeof (value as SlotBinding).$slot === 'string';\n}\n\nexport interface HeadingBlock {\n type: 'heading';\n text: Bound<string>;\n level?: 1 | 2 | 3;\n align?: 'left' | 'center' | 'right';\n color?: Bound<string>;\n}\n\nexport interface TextBlock {\n type: 'text';\n text: Bound<string>;\n align?: 'left' | 'center' | 'right';\n color?: Bound<string>;\n}\n\nexport interface ButtonBlock {\n type: 'button';\n text: Bound<string>;\n href: Bound<string>;\n backgroundColor?: Bound<string>;\n textColor?: Bound<string>;\n align?: 'left' | 'center' | 'right';\n}\n\nexport interface ImageBlock {\n type: 'image';\n src: Bound<string>;\n alt: Bound<string>;\n href?: Bound<string>;\n width?: number;\n}\n\nexport interface DividerBlock {\n type: 'divider';\n color?: string;\n}\n\nexport interface SpacerBlock {\n type: 'spacer';\n height?: number;\n}\n\nexport type EmailBlock =\n | HeadingBlock\n | TextBlock\n | ButtonBlock\n | ImageBlock\n | DividerBlock\n | SpacerBlock;\n\nexport interface EmailDocument {\n /** Subject line - a slot like any other, and usually the highest-leverage one. */\n subject: Bound<string>;\n preheader?: Bound<string>;\n blocks: EmailBlock[];\n backgroundColor?: string;\n contentWidth?: number;\n}\n\n/** Default content width. 600px is what every email client has agreed on. */\nconst CONTENT_WIDTH = 600;\n\n/**\n * Escape text for HTML.\n *\n * Applied to every authored value on its way out, because slot content is\n * arbitrary marketing copy - an unescaped ampersand or angle bracket in a\n * subject line is a broken email, and in a link attribute it is worse.\n */\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\n/**\n * Resolve a possibly-bound value against the chosen slot content.\n *\n * A binding with no supplied value renders empty rather than leaving\n * `[object Object]` or a literal placeholder in someone's inbox. The caller\n * learns about it through `missingSlots`, which is where it can still be\n * acted on.\n */\nfunction resolve(value: Bound<string> | undefined, slots: Record<string, string>, missing: string[]): string {\n if (value === undefined) return '';\n if (!isSlotBinding(value)) return value;\n\n const resolved = slots[value.$slot];\n if (resolved === undefined) {\n missing.push(value.$slot);\n return '';\n }\n return resolved;\n}\n\n/** Every slot name the document refers to, in document order, de-duplicated. */\nexport function extractDocumentSlots(document: EmailDocument): string[] {\n const names: string[] = [];\n const seen = new Set<string>();\n\n const visit = (value: unknown) => {\n if (isSlotBinding(value)) {\n if (!seen.has(value.$slot)) {\n seen.add(value.$slot);\n names.push(value.$slot);\n }\n return;\n }\n if (Array.isArray(value)) value.forEach(visit);\n else if (typeof value === 'object' && value !== null) Object.values(value).forEach(visit);\n };\n\n visit(document.subject);\n visit(document.preheader);\n document.blocks.forEach(visit);\n\n return names;\n}\n\nfunction renderBlock(block: EmailBlock, slots: Record<string, string>, missing: string[]): string {\n const align = (block as { align?: string }).align ?? 'left';\n\n switch (block.type) {\n case 'heading': {\n const size = block.level === 3 ? 18 : block.level === 2 ? 22 : 28;\n const color = escapeHtml(resolve(block.color, slots, missing) || '#111111');\n 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(\n resolve(block.text, slots, missing)\n )}</td></tr>`;\n }\n\n case 'text': {\n const color = escapeHtml(resolve(block.color, slots, missing) || '#333333');\n 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(\n resolve(block.text, slots, missing)\n )}</td></tr>`;\n }\n\n case 'button': {\n const href = escapeHtml(resolve(block.href, slots, missing));\n const label = escapeHtml(resolve(block.text, slots, missing));\n const bg = escapeHtml(resolve(block.backgroundColor, slots, missing) || '#1a73e8');\n const fg = escapeHtml(resolve(block.textColor, slots, missing) || '#ffffff');\n\n // The VML rectangle is what gives Outlook a background colour at all -\n // it ignores padding and background on an anchor. Both copies point at\n // the same href so the link rewriter tokenises them identically and it\n // does not matter which client the recipient opens it in.\n return `<tr><td style=\"padding:16px 24px;text-align:${align}\">\n<!--[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]-->\n<!--[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]-->\n</td></tr>`;\n }\n\n case 'image': {\n const src = escapeHtml(resolve(block.src, slots, missing));\n // Alt text is required, not optional: images are blocked by default in\n // most clients, so for many recipients the alt text IS the email.\n const alt = escapeHtml(resolve(block.alt, slots, missing));\n const width = block.width ?? CONTENT_WIDTH;\n const img = `<img src=\"${src}\" alt=\"${alt}\" width=\"${width}\" style=\"display:block;border:0;outline:none;text-decoration:none;max-width:100%;height:auto\" />`;\n const href = block.href ? escapeHtml(resolve(block.href, slots, missing)) : '';\n\n return `<tr><td style=\"padding:0;text-align:center\">${\n href ? `<a href=\"${href}\">${img}</a>` : img\n }</td></tr>`;\n }\n\n case 'divider':\n 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(\n block.color ?? '#e5e5e5'\n )};font-size:0;line-height:0\"> </td></tr></table></td></tr>`;\n\n case 'spacer':\n return `<tr><td style=\"height:${block.height ?? 24}px;font-size:0;line-height:0\"> </td></tr>`;\n\n default:\n // Unknown block types are skipped rather than thrown on: a document\n // authored by a newer dashboard must still render on an older worker,\n // and dropping one block beats failing the whole send.\n return '';\n }\n}\n\nexport interface RenderedDocument {\n subject: string;\n html: string;\n /** Slots the document referenced that no value was supplied for. */\n missingSlots: string[];\n}\n\n/**\n * Render a document to email-safe HTML.\n *\n * The output is deliberately a complete document with the table scaffolding\n * every client needs, not a fragment: there is no \"outer template\" the\n * customer supplies in this mode, which is the entire point of it.\n */\nexport function renderEmailDocument(\n document: EmailDocument,\n slots: Record<string, string>\n): RenderedDocument {\n const missingSlots: string[] = [];\n const width = document.contentWidth ?? CONTENT_WIDTH;\n const background = escapeHtml(document.backgroundColor ?? '#f4f4f5');\n\n const subject = resolve(document.subject, slots, missingSlots);\n const preheader = resolve(document.preheader, slots, missingSlots);\n\n const body = document.blocks.map((block) => renderBlock(block, slots, missingSlots)).join('\\n');\n\n const html = `<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n<meta name=\"x-apple-disable-message-reformatting\" />\n<title>${escapeHtml(subject)}</title>\n<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->\n</head>\n<body style=\"margin:0;padding:0;background-color:${background}\">\n<div style=\"display:none;font-size:0;line-height:0;max-height:0;overflow:hidden;mso-hide:all\">${escapeHtml(\n preheader\n )}</div>\n<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background-color:${background}\">\n<tr><td align=\"center\" style=\"padding:24px 12px\">\n<table role=\"presentation\" width=\"${width}\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width:${width}px;max-width:100%;background-color:#ffffff;border-radius:8px\">\n${body}\n</table>\n</td></tr>\n</table>\n</body>\n</html>`;\n\n return { subject, html, missingSlots };\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 /**\n * Reward attribution joins assignments by sessionId. Omitting it means a\n * snapshot-served impression can never convert: armTrials grows while\n * armSuccesses stays at zero and the next published allocation degenerates.\n */\n sessionId: string;\n}\n\nexport interface SnapshotClientConfig {\n apiUrl: string;\n publishableKey: string;\n /** Defaults to window.localStorage. Injectable for tests and SSR. */\n storage?: Storage;\n /** Injectable clock. Defaults to Date.now. */\n now?: () => number;\n /** Session lifetime in ms. Must match the BanditsClient using the same storage. */\n sessionTtl?: number;\n /**\n * A snapshot embedded in the page at render time. When present there is no\n * cold start at all - the decision happens with zero network calls, which is\n * the whole point of the design.\n */\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\n/** A decision already made and already reported. `expiresAt` is in ms. */\ninterface StoredDecision {\n clientAssignmentId: string;\n treatmentId: string;\n expiresAt: number;\n}\n\n/**\n * Pick an arm from a published allocation.\n *\n * One uniform draw against the cumulative distribution. This is equivalent to\n * Thompson sampling because `pct` *is* the distribution Thompson sampling\n * draws from - the Monte Carlo ran server-side, once, instead of here on every\n * pageview.\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 // Falls through when the probabilities sum marginally below 1 (they are\n // Monte Carlo estimates). The last arm absorbs the remainder.\n return treatments[treatments.length - 1];\n}\n\n/** Fallback assignment lifetime in seconds, matching the server's. */\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 // Deliberately the same storage keys BanditsClient uses, so a page running\n // both clients reports one session and conversions attribute.\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 /**\n * Decide an arm with no network call, or return null.\n *\n * Null means \"this caller must fall back\" - a cold cache, an expired one, an\n * unknown experiment, or one whose state cannot be served from a static\n * document. Returning null rather than the control is deliberate: the caller\n * renders control itself, and no impression is reported for it. Reporting a\n * forced-control impression would inflate the control arm's trials with\n * decisions the bandit never made.\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 // A decision already made for this visitor is the same impression, not a\n // new one. Returning it unchanged - and queueing nothing - keeps the\n // client path counting one trial per visitor per TTL, the way the server\n // path's (experimentId, userId) dedup does. Without it, five pageviews\n // would be five trials against at most one success, biasing every arm's\n // posterior downward and unevenly.\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 the held arm is gone from the snapshot we have nothing to render,\n // so fall through and decide again.\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 is milliseconds here; assignmentTtl arrives in seconds.\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 /** Fetch a fresh snapshot and cache it. Safe to call on every pageview. */\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 /**\n * Post queued impressions.\n *\n * The queue is only cleared on success. A failed flush leaves the reports in\n * storage, so a pageview that happened offline is still counted when the\n * next one succeeds - the trial really did occur, and dropping it would bias\n * the arm's denominator downward.\n */\n async flush(): Promise<void> {\n // Re-entrancy guard: overlapping callers (e.g. a `visibilitychange` flush\n // racing a `pagehide` flush) must not both snapshot the queue - the\n // second snapshot would slice off reports the first flush never sent,\n // losing them. Callers already in flight get the same in-progress promise.\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 // The server rejects batches over 1000 with a 400. Cap what we post per\n // call so a queue that grew past the limit (bad connection, sustained\n // 503s) doesn't deadlock itself: a 400 is `!response.ok`, so an uncapped\n // post would never clear and every future flush would 400 forever.\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 /** The session id these reports are attributed under. */\n getSessionId(): string {\n return this.sessionId;\n }\n\n pendingReportCount(): number {\n return this.queue.length;\n }\n\n /** The cached snapshot, or null when absent or past its ttl. */\n private freshSnapshot(): CompanySnapshot | null {\n if (!this.snapshot) return null;\n // ttl 0 is demo mode: never serve from cache, always go back for a fresh\n // allocation.\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 /** Stored decisions, pruned of expired entries. */\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","import { EMAIL_TOKEN_PARAM } from '@runbandit/shared';\n\n/**\n * Link rewriting for email HTML, done entirely in the caller's process.\n *\n * This runs in the customer's own backend, not on ours. Their email HTML and\n * whatever merge data is already substituted into it never leave their\n * infrastructure, and we still get complete link coverage - including the\n * static links the bandit is not testing, which a slot-values-only API could\n * never reach. It also composes with `plan` mode: millions of recipients,\n * fully rewritten, with no per-recipient round trip.\n *\n * Written as a small scanner rather than with regular expressions. Email HTML\n * is the worst markup in common circulation - Outlook conditionals, deeply\n * nested tables, unquoted attributes, and whatever a drag-and-drop builder\n * emitted - and a regex that looks right will quietly corrupt a document that\n * has already been sent to a million people. Nothing outside the hrefs it\n * rewrites is ever modified.\n *\n * A parser dependency would be the obvious alternative, but @runbandit/sdk\n * ships with no runtime dependencies: it runs in browsers, Node and Workers,\n * and it is installed by customers who did not ask to take on a transitive\n * tree to send an email.\n */\n\n/** Schemes that are never a trackable destination. */\nconst UNTRACKABLE_SCHEME = /^(mailto:|tel:|sms:|javascript:|data:|#)/i;\n\n/** Opt-out, for a link the author does not want touched. */\nconst NOTRACK_ATTRIBUTE = 'data-bandit-notrack';\n\nexport interface RewriteOptions {\n /**\n * The campaign's ordered destination URLs. A token carries an index into\n * THIS array, not into the order links happen to appear in the document -\n * otherwise moving a paragraph would repoint every token already sent.\n */\n linkTable: string[];\n /** One token per link table entry, positionally aligned. */\n tokens: string[];\n}\n\nexport interface RewriteResult {\n html: string;\n /** How many hrefs were tokenised. */\n rewritten: number;\n /**\n * Trackable links found in the document but absent from the link table.\n *\n * Reported rather than guessed at: a link we cannot attribute is left\n * exactly as authored, because a dead link is far worse than an untracked\n * one. Surfacing them is what lets the campaign append the missing entries\n * (appending is always allowed; reordering is not).\n */\n untracked: string[];\n}\n\ninterface Anchor {\n /** Offset of the href value within the source. */\n valueStart: number;\n valueEnd: number;\n url: string;\n}\n\n/**\n * Whether an href is a destination worth tracking.\n *\n * Anchors and `javascript:` are not navigations we can attribute; `mailto:`\n * and `tel:` hand off to another application entirely.\n */\nfunction isTrackable(url: string): boolean {\n const trimmed = url.trim();\n if (!trimmed || UNTRACKABLE_SCHEME.test(trimmed)) return false;\n return /^https?:\\/\\//i.test(trimmed);\n}\n\n/**\n * Append the token to a URL as written in an HTML attribute.\n *\n * Two details that matter more than they look:\n *\n * The fragment must stay last. A token placed after `#` becomes part of the\n * fragment, which browsers never send to the server, so the click would\n * simply never be reported.\n *\n * The separator matches the document's own entity encoding. An href written\n * with `&` is correctly encoded HTML; appending a bare `&` beside it\n * produces a document that is internally inconsistent, and strictly wrong.\n */\nfunction appendToken(url: string, token: string): string {\n const hashIndex = url.indexOf('#');\n const fragment = hashIndex === -1 ? '' : url.slice(hashIndex);\n const base = hashIndex === -1 ? url : url.slice(0, hashIndex);\n\n const separator = base.includes('?') ? (base.includes('&') ? '&' : '&') : '?';\n\n return `${base}${separator}${EMAIL_TOKEN_PARAM}=${token}${fragment}`;\n}\n\n/**\n * Scan the document and return every trackable anchor href with its position.\n *\n * The scanner's whole job is knowing what to skip:\n *\n * - `<script>` and `<style>` bodies are not markup. A URL in a stylesheet is\n * not a link.\n * - Ordinary comments are dead content.\n * - **Outlook conditional comments are not.** `<!--[if mso]> ... <![endif]-->`\n * is rendered by Outlook, so the markup inside it is live. Treating it like\n * an ordinary comment would leave every Outlook recipient's links untracked\n * while looking perfectly correct everywhere else - and Outlook is a large\n * share of corporate mail. This is the single easiest way to get email\n * rewriting quietly wrong.\n */\nfunction scanAnchors(html: string): Anchor[] {\n const anchors: Anchor[] = [];\n let i = 0;\n\n while (i < html.length) {\n const lt = html.indexOf('<', i);\n if (lt === -1) break;\n\n // Comments, including Outlook's conditionals.\n if (html.startsWith('<!--', lt)) {\n // A conditional opener (`<!--[if ...]>`) introduces live markup, so skip\n // only the opener itself and keep scanning. The same applies to the\n // downlevel-revealed form, whose `<!-->` and `<!--<![endif]-->` wrappers\n // are likewise just delimiters around real content.\n if (html.startsWith('<!--[if', lt) || html.startsWith('<!--<![endif]', lt)) {\n const close = html.indexOf('>', lt);\n i = close === -1 ? html.length : close + 1;\n continue;\n }\n\n const end = html.indexOf('-->', lt);\n // An unterminated comment swallows the rest of the document, which is\n // what a browser does too.\n i = end === -1 ? html.length : end + 3;\n continue;\n }\n\n // Raw-text elements: everything up to the closing tag is text, not markup.\n const rawText = /^<(script|style)\\b/i.exec(html.slice(lt, lt + 8));\n if (rawText) {\n const closing = new RegExp(`</${rawText[1]}\\\\s*>`, 'i');\n const rest = html.slice(lt);\n const match = closing.exec(rest);\n i = match ? lt + match.index + match[0].length : html.length;\n continue;\n }\n\n // Anchors, plus Outlook's VML button. `<v:roundrect href=\"...\">` is how\n // essentially every template builder gives a button a background colour\n // in Outlook, and it carries a real destination - but it is not an anchor,\n // so an anchors-only scanner leaves every Outlook button untracked while\n // looking entirely correct in a browser. Found by rendering one of our own\n // documents through this rewriter and counting.\n const isLinkElement =\n /^<a[\\s/>]/i.test(html.slice(lt, lt + 3)) ||\n /^<v:roundrect[\\s/>]/i.test(html.slice(lt, lt + 13));\n if (!isLinkElement) {\n i = lt + 1;\n continue;\n }\n\n const tagEnd = findTagEnd(html, lt);\n if (tagEnd === -1) break;\n\n const tag = html.slice(lt, tagEnd);\n if (!new RegExp(NOTRACK_ATTRIBUTE, 'i').test(tag)) {\n const href = findHref(html, lt, tagEnd);\n if (href && isTrackable(href.url)) anchors.push(href);\n }\n\n i = tagEnd;\n }\n\n return anchors;\n}\n\n/**\n * Find the offset just past a tag's closing `>`.\n *\n * Quote-aware, because `<a title=\"a > b\" href=\"...\">` is legal and a naive\n * search for `>` would cut the tag in half and mangle the attribute.\n */\nfunction findTagEnd(html: string, start: number): number {\n let quote: string | null = null;\n\n for (let i = start + 1; i < html.length; i++) {\n const ch = html[i];\n if (quote) {\n if (ch === quote) quote = null;\n continue;\n }\n if (ch === '\"' || ch === \"'\") {\n quote = ch;\n continue;\n }\n if (ch === '>') return i + 1;\n }\n\n return -1;\n}\n\n/** Locate the href attribute value inside an already-delimited tag. */\nfunction findHref(html: string, tagStart: number, tagEnd: number): Anchor | null {\n const tag = html.slice(tagStart, tagEnd);\n // Quoted, or unquoted and terminated by whitespace or the tag's own close.\n const match = /\\shref\\s*=\\s*(\"([^\"]*)\"|'([^']*)'|([^\\s>]+))/i.exec(tag);\n if (!match) return null;\n\n const quoted = match[2] !== undefined || match[3] !== undefined;\n const value = match[2] ?? match[3] ?? match[4] ?? '';\n\n // Offset of the value itself, skipping the opening quote when present.\n const valueStart = tagStart + match.index + match[0].indexOf(match[1]) + (quoted ? 1 : 0);\n\n return { valueStart, valueEnd: valueStart + value.length, url: value };\n}\n\n/**\n * Every trackable destination in the document, in order, de-duplicated.\n *\n * Used at campaign creation to build the link table that tokens index into.\n */\nexport function extractEmailLinks(html: string): string[] {\n const seen = new Set<string>();\n const urls: string[] = [];\n\n for (const anchor of scanAnchors(html)) {\n if (seen.has(anchor.url)) continue;\n seen.add(anchor.url);\n urls.push(anchor.url);\n }\n\n return urls;\n}\n\n/**\n * Tokenise every trackable link in the document.\n *\n * Tokens are appended, not substituted, so the link still points at the\n * customer's own domain: no redirect service, no DNS record, no certificate,\n * and no shared link-domain reputation to poison. Their landing page's\n * existing Bandit SDK reads the parameter on load.\n *\n * Synchronous and pure: minting is asynchronous (WebCrypto), so the caller\n * mints one token per link table entry first and passes them in. That keeps\n * this function trivially testable and lets a batch send mint once per\n * recipient rather than once per link per parse.\n */\nexport function rewriteEmailLinks(html: string, options: RewriteOptions): RewriteResult {\n const { linkTable, tokens } = options;\n\n const indexOfUrl = new Map<string, number>();\n linkTable.forEach((url, index) => {\n if (!indexOfUrl.has(url)) indexOfUrl.set(url, index);\n });\n\n const anchors = scanAnchors(html);\n const untracked: string[] = [];\n let rewritten = 0;\n\n // Rebuilt back to front so each splice leaves earlier offsets valid.\n let output = html;\n for (let i = anchors.length - 1; i >= 0; i--) {\n const anchor = anchors[i];\n const index = indexOfUrl.get(anchor.url);\n\n if (index === undefined || tokens[index] === undefined) {\n untracked.push(anchor.url);\n continue;\n }\n\n output =\n output.slice(0, anchor.valueStart) +\n appendToken(anchor.url, tokens[index]) +\n output.slice(anchor.valueEnd);\n rewritten++;\n }\n\n return { html: output, rewritten, untracked: untracked.reverse() };\n}\n","import { rewriteEmailLinks, extractEmailLinks } from './email-links.js';\n\n/**\n * Email rendering, performed entirely in the caller's process.\n *\n * Bandit is never in the send path. The customer asks what to say, renders it\n * here, and hands the result to their own ESP - so deliverability, suppression\n * lists, bounce handling and DMARC alignment stay where they already work, and\n * we do the one thing nobody else does.\n *\n * Their template and whatever merge data is already in it never reach our\n * servers. That is a consequence of rendering locally rather than a policy we\n * enforce, which is what makes it worth anything.\n */\n\n/**\n * Slot placeholder syntax: `{{bandit.subject}}`.\n *\n * Namespaced under `bandit.` so it cannot collide with the ESP's own merge\n * tags, which are substituted later and in a different process - `{{firstName}}`\n * has to survive this pass untouched and reach Mailchimp or Resend intact.\n */\nconst SLOT_PATTERN = /\\{\\{\\s*bandit\\.([a-zA-Z0-9_-]+)\\s*\\}\\}/g;\n\nexport interface RenderEmailOptions {\n /** Chosen content per slot, keyed by slot name, from /api/email/render. */\n slots: Record<string, string>;\n /** The campaign's ordered destination URLs. */\n linkTable: string[];\n /** One token per link table entry, minted for this recipient. */\n tokens: string[];\n}\n\nexport interface RenderedEmail {\n html: string;\n /** How many links were tokenised. */\n rewritten: number;\n /** Trackable links not present in the campaign's link table. */\n untracked: string[];\n /**\n * Placeholders the template asked for that no slot supplied.\n *\n * Reported rather than left in place as literal `{{bandit.x}}`, which is\n * what a recipient would otherwise read in their inbox.\n */\n missingSlots: string[];\n}\n\n/**\n * Substitute slot values, then tokenise every trackable link.\n *\n * Order matters: substitution happens first, so a slot whose value contains a\n * link gets that link tokenised too. Doing it the other way round would leave\n * links from bandit-selected content untracked - exactly the content whose\n * performance we are trying to measure.\n */\nexport function renderEmail(templateHtml: string, options: RenderEmailOptions): RenderedEmail {\n const missingSlots: string[] = [];\n\n const substituted = templateHtml.replace(SLOT_PATTERN, (match, name: string) => {\n const value = options.slots[name];\n if (value === undefined) {\n missingSlots.push(name);\n // Left as authored: a visible placeholder in a preview is a far better\n // failure than silently shipping an empty subject line.\n return match;\n }\n return value;\n });\n\n const { html, rewritten, untracked } = rewriteEmailLinks(substituted, {\n linkTable: options.linkTable,\n tokens: options.tokens,\n });\n\n return { html, rewritten, untracked, missingSlots };\n}\n\n/**\n * The slot names a template refers to.\n *\n * Used at authoring time to check a template against a campaign's slots before\n * anything is sent, rather than discovering the mismatch in an inbox.\n */\nexport function extractSlotNames(templateHtml: string): string[] {\n const names = new Set<string>();\n for (const match of templateHtml.matchAll(SLOT_PATTERN)) names.add(match[1]);\n return [...names];\n}\n\nexport { extractEmailLinks, rewriteEmailLinks };\nexport type { RewriteOptions, RewriteResult } from './email-links.js';\n","import {\n mintEmailToken,\n hashRecipient,\n impressionManifest,\n pickArm,\n uniformFor,\n type EmailPlan,\n} from '@runbandit/shared';\nimport { renderEmail, type RenderedEmail } from './email.js';\n\n/**\n * Offline rendering from a campaign plan.\n *\n * This is what makes a two-million-recipient send possible. The plan is\n * fetched once; every recipient after that is derived locally - arm selection,\n * token minting, slot substitution and link rewriting - with no network call\n * at all. The customer's send loop runs at whatever speed their own ESP client\n * manages, and we are not in it.\n *\n * Nothing here needs to reach us, which is also why the customer's template\n * and merge data never leave their infrastructure.\n */\n\nexport interface PlanRenderResult extends RenderedEmail {\n /** Arm chosen per slot, positionally against the plan's slots. */\n combination: number[];\n /** The token minted for each link, positionally against the link table. */\n tokens: string[];\n}\n\n/**\n * Render one recipient's email from a plan.\n *\n * Derivation is deterministic in (salt, recipientId, slot), so re-rendering the\n * same recipient - a retry, a resumed batch, a preview - produces byte-identical\n * output and the same tokens. A send that crashes halfway can simply be run\n * again without double-counting anyone or issuing a second set of links.\n */\nexport async function renderFromPlan(\n plan: EmailPlan,\n recipientId: string,\n templateHtml: string,\n companySecret: string\n): Promise<PlanRenderResult> {\n const slots: Record<string, string> = {};\n const combination: number[] = [];\n\n for (let i = 0; i < plan.slots.length; i++) {\n const slot = plan.slots[i];\n const chosen = pickArm(slot.probabilities, await uniformFor(plan.salt, recipientId, i));\n const index = chosen === -1 ? 0 : chosen;\n\n combination.push(slot.arms[index]?.armIndex ?? 0);\n\n const config = slot.configs[index];\n slots[slot.name] =\n typeof config === 'string'\n ? config\n : ((config as { content?: string; text?: string } | undefined)?.content ??\n (config as { content?: string; text?: string } | undefined)?.text ??\n '');\n }\n\n const recipientHash = await hashRecipient(recipientId, companySecret);\n\n const tokens = await Promise.all(\n plan.linkTable.map((_, linkIndex) =>\n mintEmailToken(\n { c: plan.campaignId, r: recipientHash, k: combination, l: linkIndex },\n plan.signingKey\n )\n )\n );\n\n const rendered = renderEmail(templateHtml, {\n slots,\n linkTable: plan.linkTable,\n tokens,\n });\n\n return { ...rendered, combination, tokens };\n}\n\n/**\n * Whether a plan should be refreshed before continuing a send.\n *\n * A plan is one snapshot of the posterior, so serving a whole blast from it\n * makes the entire send a single learning epoch - the bandit cannot react to\n * anything it learns partway through. Refreshing every `refreshEvery`\n * recipients turns a 2M send into a few dozen epochs for a few dozen API\n * calls, which is the trade the customer gets to make rather than us.\n */\nexport function shouldRefreshPlan(\n plan: EmailPlan,\n recipientsRenderedSincePlan: number,\n now: number = Date.now()\n): boolean {\n return recipientsRenderedSincePlan >= plan.refreshEvery || now >= plan.expiresAt;\n}\n\n/**\n * Post a plan-mode send's impressions so the bandit has a denominator.\n *\n * This is not optional bookkeeping. `plan` mode makes no per-recipient call,\n * so without this the only observations that ever reach the posterior are\n * clicks - every one of them a success - and every arm converges on\n * \"everything works\" while the dashboard shows excellent numbers measuring\n * nothing. The same omission was caught once already in the `batch` render\n * path; it is easier to make here precisely because nothing else round-trips.\n *\n * Aggregated per arm, so ten thousand recipients across a handful of arms is\n * one small request. `batchKey` makes it idempotent: a retried post must not\n * double the denominator and quietly halve every arm's apparent rate.\n */\nexport async function reportPlanImpressions(\n plan: EmailPlan,\n recipientIds: string[],\n options: { apiUrl: string; apiKey: string; batchKey?: string }\n): Promise<{ recorded: boolean; alreadyRecorded: boolean }> {\n const counts = await impressionManifest(plan, recipientIds);\n if (counts.length === 0) return { recorded: false, alreadyRecorded: false };\n\n const response = await fetch(`${options.apiUrl}/api/email/impressions`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': options.apiKey },\n body: JSON.stringify({\n campaignId: plan.campaignId,\n // Derived from the recipients this call covers, so a retry of the same\n // chunk collapses onto the same key without the caller needing to\n // invent and persist an idempotency token of its own.\n batchKey: options.batchKey ?? (await batchKeyFor(plan.campaignId, recipientIds)),\n counts,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to report impressions: ${response.status}`);\n }\n\n const body = (await response.json()) as { data?: { recorded: boolean; alreadyRecorded: boolean } };\n return body.data ?? { recorded: false, alreadyRecorded: false };\n}\n\n/** Stable fingerprint of (campaign, recipient set) - see `reportPlanImpressions`. */\nasync function batchKeyFor(campaignId: string, recipientIds: string[]): Promise<string> {\n const material = `${campaignId}|${[...recipientIds].sort().join(',')}`;\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(material));\n return Array.from(new Uint8Array(digest).slice(0, 16))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n"],"mappings":"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","names":["ExperimentStatus","BanditAlgorithm","Posterior","ForgettingMode","ContentType","FunnelStatus","generateId"]}
|
|
1
|
+
{"version":3,"sources":["../src/session.ts","../src/client.ts","../../shared/src/types.ts","../../shared/src/utils.ts","../../shared/src/email-token.ts","../../shared/src/email-plan.ts","../../shared/src/email-blocks.ts","../src/snapshot-cache.ts","../src/email-links.ts","../src/email.ts","../src/email-plan.ts"],"sourcesContent":["\nexport const SESSION_ID_KEY = 'bandits_session_id';\nexport const SESSION_CREATED_AT_KEY = 'bandits_session_created_at';\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 defaultSessionStorage(): Storage | null {\n return typeof window !== 'undefined' && window.localStorage ? window.localStorage : null;\n}\n","import {\n AssignmentRequest,\n TrackNamedEventRequest,\n BanditResult,\n APIResponse,\n ContextualFeatures,\n BundleAssignmentRequest,\n BundleAssignmentResponse,\n} from '@runbandit/shared';\nimport { defaultSessionStorage, generateId, getOrCreateSessionId } from './session.js';\n\nexport interface BanditsClientConfig {\n apiUrl: string;\n apiKey: string;\n timeout?: number;\n batchSize?: number;\n flushInterval?: number;\n sessionTtl?: number;\n}\n\nexport interface Assignment {\n assignmentId: string;\n treatmentId: string;\n experimentId: string;\n userId: string;\n config: any;\n}\n\nfunction currentPageUrl(): string | undefined {\n if (typeof location !== 'undefined' && location.origin && location.pathname) {\n return location.origin + location.pathname;\n }\n return undefined;\n}\n\nexport class BanditsClient {\n private config: BanditsClientConfig;\n private namedEventQueue: TrackNamedEventRequest[] = [];\n private sessionId: string;\n private flushTimer?: NodeJS.Timeout;\n\n constructor(config: BanditsClientConfig) {\n this.config = {\n timeout: 5000,\n batchSize: 100,\n flushInterval: 10000,\n sessionTtl: 86400000,\n ...config\n };\n this.sessionId = this.getOrCreateSessionId();\n this.startBatchFlush();\n }\n\n async getAssignment(\n experimentId: string,\n userId: string,\n context?: ContextualFeatures\n ): Promise<Assignment> {\n const request: AssignmentRequest = {\n experimentId,\n userId,\n context,\n sessionId: this.sessionId\n };\n const pageUrl = currentPageUrl();\n if (pageUrl) request.pageUrl = pageUrl;\n\n const response = await this.makeRequest<any>(\n '/assignments',\n 'POST',\n request\n );\n\n if (!response.success || !response.data) {\n throw new Error(response.error || 'Failed to get assignment');\n }\n\n const assignment = response.data?.data ?? response.data;\n\n if (!assignment?.assignmentId) {\n throw new Error('Invalid assignment response');\n }\n\n return {\n assignmentId: assignment.assignmentId,\n treatmentId: assignment.treatmentId,\n experimentId,\n userId,\n config: assignment.treatment?.config || {}\n };\n }\n\n trackNamedEvent(\n event: { name: string; category?: string; value?: number; metadata?: Record<string, any> },\n opts?: { immediate?: boolean }\n ): void {\n const request: TrackNamedEventRequest = {\n name: event.name,\n category: event.category || 'custom',\n value: event.value,\n metadata: event.metadata,\n sessionId: this.sessionId,\n };\n\n if (opts?.immediate) {\n const url = `${this.normalizeApiUrl()}/events`;\n try {\n void fetch(url, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.config.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(request),\n keepalive: true,\n });\n } catch {\n }\n return;\n }\n\n this.namedEventQueue.push(request);\n if (this.namedEventQueue.length >= this.config.batchSize!) {\n this.flush();\n }\n }\n\n async flush(): Promise<void> {\n const namedEvents = [...this.namedEventQueue];\n this.namedEventQueue = [];\n if (namedEvents.length === 0) return;\n\n try {\n await fetch(`${this.normalizeApiUrl()}/events/batch`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.config.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ events: namedEvents }),\n keepalive: true,\n });\n } catch (error) {\n this.namedEventQueue.unshift(...namedEvents);\n throw error;\n }\n }\n\n async identify(\n anonymousId: string,\n identifiedUserId: string,\n companyId: string,\n mergeReason?: 'user_signup' | 'email_verification' | 'social_login' | 'account_recovery' | 'manual'\n ): Promise<void> {\n const response = await this.makeRequest(\n '/identify',\n 'POST',\n {\n anonymousId,\n identifiedUserId,\n mergeReason: mergeReason || 'manual',\n },\n { 'x-company-id': companyId }\n );\n\n if (!response.success) {\n throw new Error(response.error || 'Failed to identify user');\n }\n }\n\n async label(userId: string, labels: Record<string, string | boolean>): Promise<void> {\n const response = await this.makeRequest('/labels', 'POST', { userId, labels });\n\n if (!response.success) {\n throw new Error(response.error || 'Failed to record labels');\n }\n }\n\n async trackFunnelReward(\n funnelId: string,\n userId: string,\n value: number\n ): Promise<void> {\n const response = await this.makeRequest(\n `/funnels/${funnelId}/reward`,\n 'POST',\n { userId, value }\n );\n\n if (!response.success) {\n throw new Error(response.error || 'Failed to record funnel reward');\n }\n }\n\n async getBundle(\n bundleId: string,\n userId: string,\n context?: ContextualFeatures\n ): Promise<BundleAssignmentResponse> {\n const request: BundleAssignmentRequest = {\n bundleId,\n userId,\n context,\n sessionId: this.sessionId,\n };\n const pageUrl = currentPageUrl();\n if (pageUrl) request.pageUrl = pageUrl;\n\n const response = await this.makeRequest<any>('/bundles/assignment', 'POST', request);\n\n if (!response.success || !response.data) {\n throw new Error(response.error || 'Failed to get bundle assignment');\n }\n\n const payload = response.data?.data ?? response.data;\n if (!payload?.bundleAssignmentId || !payload?.slots) {\n throw new Error('Invalid bundle assignment response');\n }\n\n return {\n bundleAssignmentId: payload.bundleAssignmentId,\n slots: payload.slots,\n };\n }\n\n async trackBundleReward(bundleId: string, userId: string, value: number): Promise<void> {\n const response = await this.makeRequest(`/bundles/${bundleId}/reward`, 'POST', {\n userId,\n value,\n });\n if (!response.success) {\n throw new Error(response.error || 'Failed to record bundle reward');\n }\n }\n\n private generateId(): string {\n return generateId();\n }\n\n private normalizeApiUrl(): string {\n let baseUrl = this.config.apiUrl.replace(/\\/$/, '');\n if (!baseUrl.endsWith('/api')) {\n baseUrl = `${baseUrl}/api`;\n }\n return baseUrl;\n }\n\n private getOrCreateSessionId(): string {\n return getOrCreateSessionId({\n storage: defaultSessionStorage(),\n ttlMs: this.config.sessionTtl,\n });\n }\n\n private async makeRequest<T = any>(\n endpoint: string,\n method: 'GET' | 'POST' | 'PUT' | 'DELETE',\n body?: any,\n extraHeaders?: Record<string, string>\n ): Promise<APIResponse<T>> {\n const url = `${this.normalizeApiUrl()}${endpoint}`;\n\n const headers: Record<string, string> = {\n 'Authorization': `Bearer ${this.config.apiKey}`,\n 'Content-Type': 'application/json',\n ...extraHeaders,\n };\n\n const requestConfig: RequestInit = {\n method,\n headers,\n signal: AbortSignal.timeout(this.config.timeout!)\n };\n\n if (body && method !== 'GET') {\n requestConfig.body = JSON.stringify(body);\n }\n\n try {\n const response = await fetch(url, requestConfig);\n const data = await response.json();\n \n return {\n success: response.ok,\n data: response.ok ? data as T : undefined,\n error: response.ok ? undefined : (data as any)?.message || 'Request failed',\n timestamp: new Date()\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Network error',\n timestamp: new Date()\n };\n }\n }\n\n private startBatchFlush(): void {\n this.flushTimer = setInterval(() => {\n this.flush().catch(console.error);\n }, this.config.flushInterval);\n }\n\n destroy(): void {\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n }\n this.flush().catch(console.error);\n if (typeof window !== 'undefined' && window.localStorage) {\n localStorage.removeItem('bandits_session_id');\n localStorage.removeItem('bandits_session_created_at');\n }\n }\n}","export interface Company {\n id: string;\n name: string;\n subdomain?: string;\n domain?: string;\n plan: string;\n apiKey: string;\n status: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface User {\n id: string;\n email: string;\n name: string;\n image?: string;\n role: string;\n companyId: string | null;\n emailVerified?: boolean;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface Session {\n id: string;\n userId: string;\n token: string;\n expiresAt: Date;\n ipAddress?: string | null;\n userAgent?: string | null;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface Account {\n id: string;\n userId: string;\n provider: string;\n providerAccountId: string;\n accessToken?: string;\n refreshToken?: string;\n accessTokenExpiresAt?: Date;\n refreshTokenExpiresAt?: Date;\n scope?: string;\n idToken?: string;\n sessionState?: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface Experiment {\n id: string;\n name: string;\n description?: string;\n algorithm: BanditAlgorithm;\n forgetting?: ForgettingConfig;\n status: ExperimentStatus;\n contentType: ContentType;\n treatments: Treatment[];\n context?: Record<string, any>;\n bundleId?: string;\n image?: string;\n emoji?: string;\n assignmentTtl: number;\n companyId: string;\n createdAt: Date;\n updatedAt: Date;\n endAt?: Date;\n placement?: Placement;\n}\n\nexport const PLACEMENTS = [\n 'hero_headline',\n 'hero_subhead',\n 'cta_primary',\n 'banner_text',\n 'badge_text',\n] as const;\n\nexport type Placement = (typeof PLACEMENTS)[number];\n\nexport interface Treatment {\n id: string;\n experimentId: string;\n name: string;\n description?: string;\n config: VariantConfig;\n weight?: number;\n companyId: string;\n createdAt: Date;\n}\n\nexport interface AlgorithmResult {\n treatmentId: string;\n confidence: number;\n probabilityBest?: number;\n}\n\nexport interface BanditResult {\n assignmentId: string;\n treatmentId: string;\n confidence: number;\n probabilityBest?: number;\n metadata?: Record<string, any>;\n}\n\nexport interface ContextualFeatures {\n [key: string]: string | number | boolean;\n}\n\nexport enum ExperimentStatus {\n DRAFT = 'draft',\n ACTIVE = 'active',\n PAUSED = 'paused',\n COMPLETED = 'completed',\n ARCHIVED = 'archived'\n}\n\n\nexport interface TreatmentMetadata {\n strategy?: string;\n tone?: string;\n ctaStyle?: string;\n emotionalHook?: string;\n reasoning?: string;\n tags?: string[];\n generated?: boolean;\n generatedAt?: string;\n}\n\nexport interface VariantConfig {\n content: string;\n metadata?: TreatmentMetadata & Record<string, any>;\n}\n\nexport enum BanditAlgorithm {\n THOMPSON_SAMPLING = 'thompson_sampling',\n CONTEXTUAL_LINEAR = 'contextual_linear'\n}\n\nexport enum Posterior {\n BETA = 'beta',\n NORMAL_GAMMA = 'normal_gamma'\n}\n\nexport function normalizePosterior(posterior: string | null | undefined): Posterior {\n return posterior === Posterior.NORMAL_GAMMA ? Posterior.NORMAL_GAMMA : Posterior.BETA;\n}\n\nexport enum ForgettingMode {\n NONE = 'none',\n DISCOUNTED = 'discounted',\n SLIDING_WINDOW = 'sliding_window'\n}\n\nexport interface ForgettingConfig {\n mode: ForgettingMode;\n halfLifeMs?: number;\n windowMs?: number;\n}\n\nexport const DEFAULT_HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000;\n\nexport const DEFAULT_WINDOW_MS = 14 * 24 * 60 * 60 * 1000;\n\nexport const STATIONARY: ForgettingConfig = { mode: ForgettingMode.NONE };\n\nexport function resolveForgetting(config?: Partial<ForgettingConfig> | null): ForgettingConfig {\n switch (config?.mode) {\n case ForgettingMode.DISCOUNTED:\n return {\n mode: ForgettingMode.DISCOUNTED,\n halfLifeMs: config.halfLifeMs && config.halfLifeMs > 0\n ? config.halfLifeMs\n : DEFAULT_HALF_LIFE_MS\n };\n case ForgettingMode.SLIDING_WINDOW:\n return {\n mode: ForgettingMode.SLIDING_WINDOW,\n windowMs: config.windowMs && config.windowMs > 0\n ? config.windowMs\n : DEFAULT_WINDOW_MS\n };\n default:\n return STATIONARY;\n }\n}\n\nconst LEGACY_ALGORITHM_ALIASES: Record<string, BanditAlgorithm> = {\n epsilon_greedy: BanditAlgorithm.THOMPSON_SAMPLING,\n ucb1: BanditAlgorithm.THOMPSON_SAMPLING,\n contextual_neural: BanditAlgorithm.CONTEXTUAL_LINEAR\n};\n\nexport function normalizeAlgorithm(algorithm: string | null | undefined): BanditAlgorithm {\n if (algorithm === BanditAlgorithm.THOMPSON_SAMPLING) return BanditAlgorithm.THOMPSON_SAMPLING;\n if (algorithm === BanditAlgorithm.CONTEXTUAL_LINEAR) return BanditAlgorithm.CONTEXTUAL_LINEAR;\n return LEGACY_ALGORITHM_ALIASES[algorithm ?? ''] ?? BanditAlgorithm.THOMPSON_SAMPLING;\n}\n\nexport enum ContentType {\n PLAIN_TEXT = 'plain_text',\n MARKDOWN = 'markdown',\n HTML = 'html',\n CSS = 'css',\n JAVASCRIPT = 'javascript',\n JSON = 'json',\n IMAGE = 'image',\n VIDEO = 'video',\n AUDIO = 'audio',\n DOCUMENT = 'document',\n}\n\nexport function getContentTypeCategory(contentType: ContentType): 'text' | 'code' | 'file' {\n switch (contentType) {\n case ContentType.PLAIN_TEXT:\n case ContentType.MARKDOWN:\n return 'text';\n case ContentType.HTML:\n case ContentType.CSS:\n case ContentType.JAVASCRIPT:\n case ContentType.JSON:\n return 'code';\n case ContentType.IMAGE:\n case ContentType.VIDEO:\n case ContentType.AUDIO:\n case ContentType.DOCUMENT:\n return 'file';\n }\n}\n\nexport interface BanditConfig {\n algorithm: BanditAlgorithm;\n parameters: Record<string, any>;\n}\n\nexport interface ExperimentStats {\n totalAssignments: number;\n totalConversions: number;\n conversionRate: number;\n treatmentStats: Record<string, TreatmentStats>;\n}\n\nexport interface TreatmentStats {\n assignments: number;\n conversions: number;\n conversionRate: number;\n confidence: number;\n lift?: number;\n}\n\nexport interface APIResponse<T = any> {\n success: boolean;\n data?: T;\n error?: string;\n timestamp: Date;\n}\n\nexport interface CreateExperimentRequest {\n name: string;\n description?: string;\n algorithm: BanditAlgorithm;\n contentType: ContentType;\n algorithmConfig?: Record<string, any>;\n treatments?: Omit<Treatment, 'id' | 'experimentId' | 'createdAt'>[];\n}\n\nexport interface AssignmentRequest {\n experimentId: string;\n userId: string;\n context?: ContextualFeatures;\n sessionId?: string;\n pageUrl?: string;\n}\n\n\nexport interface TrackNamedEventRequest {\n name: string;\n category?: string;\n value?: number;\n metadata?: Record<string, any>;\n sessionId: string;\n}\n\n\nexport enum FunnelStatus {\n DRAFT = 'draft',\n ACTIVE = 'active',\n PAUSED = 'paused',\n COMPLETED = 'completed',\n}\n\nexport interface Funnel {\n id: string;\n name: string;\n description?: string;\n status: FunnelStatus;\n conversionWindowMs: number;\n companyId: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface FunnelRewardRequest {\n funnelId: string;\n userId: string;\n value: number;\n}\n\n\nexport interface ApiKey {\n id: string;\n name: string;\n keyPrefix: string;\n status: 'active' | 'revoked';\n createdBy: string;\n createdAt: Date;\n lastUsedAt?: Date;\n revokedAt?: Date;\n revokedBy?: string;\n}\n\nexport interface CreateApiKeyRequest {\n name: string;\n}\n\nexport interface CreateApiKeyResponse {\n id: string;\n key: string;\n name: string;\n}\n\n\nexport type IdentifyMergeReason =\n | 'user_signup'\n | 'email_verification'\n | 'social_login'\n | 'account_recovery'\n | 'manual';\n\nexport interface IdentifyUserRequest {\n anonymousId: string;\n identifiedUserId: string;\n mergeReason?: IdentifyMergeReason;\n}\n\nexport interface IdentifyUserResponse {\n anonymousId: string;\n identifiedUserId: string;\n alreadyMapped: boolean;\n}\n\nexport interface ResolvedIdentity {\n canonicalId: string;\n anonymousIds: string[];\n isIdentified: boolean;\n}\n\n\nexport type BundleMode = 'factored' | 'joint';\n\nexport interface Bundle {\n id: string;\n name: string;\n description?: string;\n status: ExperimentStatus;\n mode: BundleMode;\n funnelId?: string;\n assignmentTtl: number;\n conversionWindowMs: number;\n companyId: string;\n createdAt: Date;\n updatedAt: Date;\n}\n\nexport interface BundleAssignmentRequest {\n bundleId: string;\n userId: string;\n context?: ContextualFeatures;\n sessionId?: string;\n pageUrl?: string;\n}\n\nexport interface BundleSlotAssignment {\n assignmentId: string;\n experimentId: string;\n treatmentId: string;\n config: VariantConfig | Record<string, unknown>;\n}\n\nexport interface BundleAssignmentResponse {\n bundleAssignmentId: string;\n slots: Record<string, BundleSlotAssignment>;\n}\n\nexport interface SnapshotTreatment {\n id: string;\n name?: string;\n config: unknown;\n pct: number;\n}\n\nexport interface SnapshotExperiment {\n mode: 'snapshot' | 'network';\n assignmentTtl: number;\n treatments: SnapshotTreatment[];\n}\n\nexport interface CompanySnapshot {\n ttl: number;\n generatedAt: number;\n experiments: Record<string, SnapshotExperiment>;\n}\n","import { Treatment } from './types.js';\n\nexport function generateId(): string {\n return Math.random().toString(36).substring(2) + Date.now().toString(36);\n}\n\nexport function validateTreatmentWeights(treatments: Treatment[]): boolean {\n const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);\n return totalWeight > 0;\n}\n\nexport function normalizeTreatmentWeights(treatments: Treatment[]): Treatment[] {\n const totalWeight = treatments.reduce((sum, treatment) => sum + (treatment.weight || 1), 0);\n \n return treatments.map(treatment => ({\n ...treatment,\n weight: (treatment.weight || 1) / totalWeight\n }));\n}\n\nexport function calculateConfidenceInterval(\n successes: number,\n trials: number,\n confidence: number = 0.95\n): { lower: number; upper: number } {\n if (trials === 0) return { lower: 0, upper: 0 };\n \n const z = confidence === 0.95 ? 1.96 : confidence === 0.99 ? 2.576 : 1.645;\n const p = successes / trials;\n const margin = z * Math.sqrt((p * (1 - p)) / trials);\n \n return {\n lower: Math.max(0, p - margin),\n upper: Math.min(1, p + margin)\n };\n}\n\nexport function isValidEmail(email: string): boolean {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return emailRegex.test(email);\n}\n\nexport function sanitizeMetadata(metadata: Record<string, any>): Record<string, any> {\n const sanitized: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n sanitized[key] = value;\n } else if (value !== null && value !== undefined) {\n sanitized[key] = String(value);\n }\n }\n\n return sanitized;\n}\n\nexport function generateSubdomain(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9\\s-]/g, '')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .substring(0, 63);\n}\n\nexport function isValidSubdomain(subdomain: string): boolean {\n if (!subdomain || subdomain.length === 0 || subdomain.length > 63) {\n return false;\n }\n\n const subdomainRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;\n\n const reserved = [\n 'www', 'api', 'app', 'admin', 'dashboard', 'mail', 'ftp', 'localhost',\n 'staging', 'dev', 'test', 'demo', 'blog', 'docs', 'status', 'help',\n 'support', 'cdn', 'static', 'assets', 'secure', 'payment', 'pay'\n ];\n\n return subdomainRegex.test(subdomain) && !reserved.includes(subdomain);\n}\n\nexport function extractSubdomain(hostname: string, baseDomain: string): string | null {\n if (!hostname || !baseDomain) {\n return null;\n }\n\n const cleanHostname = hostname.split(':')[0];\n\n if (!cleanHostname.endsWith(baseDomain)) {\n return null;\n }\n\n const subdomain = cleanHostname.slice(0, -(baseDomain.length + 1));\n\n if (!subdomain || subdomain === 'www') {\n return null;\n }\n\n return subdomain;\n}\n\nexport function buildSubdomainUrl(\n subdomain: string,\n baseDomain: string,\n path: string = '',\n protocol: string = 'https'\n): string {\n const url = `${protocol}://${subdomain}.${baseDomain}`;\n return path ? `${url}${path.startsWith('/') ? path : '/' + path}` : url;\n}","\nexport const EMAIL_TOKEN_PARAM = 'bt';\n\nexport interface SigningKey {\n version: number;\n secret: string;\n}\n\nexport interface EmailTokenPayload {\n c: string;\n r: string;\n k: number[];\n l: number;\n v?: number;\n}\n\nexport type VerifiedToken =\n | { valid: true; payload: Required<EmailTokenPayload> }\n | { valid: false; reason: 'malformed' | 'unknown_key' | 'bad_signature' };\n\n\nconst encoder = new TextEncoder();\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction base64UrlDecode(value: string): Uint8Array {\n const padded = value.replace(/-/g, '+').replace(/_/g, '/');\n const binary = atob(padded + '='.repeat((4 - (padded.length % 4)) % 4));\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\nasync function hmac(secret: string, message: string): Promise<Uint8Array> {\n const key = await crypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n return new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(message)));\n}\n\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n\n\nexport async function hashRecipient(recipientId: string, companySecret: string): Promise<string> {\n const digest = await hmac(companySecret, `recipient:${recipientId}`);\n return base64UrlEncode(digest.slice(0, 16));\n}\n\nexport async function mintEmailToken(\n payload: EmailTokenPayload,\n key: SigningKey\n): Promise<string> {\n const body = base64UrlEncode(\n encoder.encode(\n JSON.stringify({\n c: payload.c,\n r: payload.r,\n k: payload.k,\n l: payload.l,\n v: key.version,\n })\n )\n );\n\n const signature = base64UrlEncode(await hmac(key.secret, body));\n return `${body}.${signature}`;\n}\n\nexport async function verifyEmailToken(\n token: string,\n keys: SigningKey[]\n): Promise<VerifiedToken> {\n const parts = token.split('.');\n if (parts.length !== 2 || !parts[0] || !parts[1]) {\n return { valid: false, reason: 'malformed' };\n }\n const [body, signature] = parts;\n\n let payload: EmailTokenPayload;\n try {\n payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(body)));\n } catch {\n return { valid: false, reason: 'malformed' };\n }\n\n if (\n !payload ||\n typeof payload.c !== 'string' ||\n typeof payload.r !== 'string' ||\n !Array.isArray(payload.k) ||\n !payload.k.every((arm) => Number.isInteger(arm) && arm >= 0) ||\n !Number.isInteger(payload.l) ||\n (payload.l as number) < 0 ||\n !Number.isInteger(payload.v)\n ) {\n return { valid: false, reason: 'malformed' };\n }\n\n const key = keys.find((candidate) => candidate.version === payload.v);\n if (!key) return { valid: false, reason: 'unknown_key' };\n\n const expected = base64UrlEncode(await hmac(key.secret, body));\n if (!timingSafeEqual(expected, signature)) {\n return { valid: false, reason: 'bad_signature' };\n }\n\n return { valid: true, payload: payload as Required<EmailTokenPayload> };\n}\n\nexport function appendEmailToken(url: string, token: string): string {\n const hashIndex = url.indexOf('#');\n const fragment = hashIndex === -1 ? '' : url.slice(hashIndex);\n const base = hashIndex === -1 ? url : url.slice(0, hashIndex);\n const separator = base.includes('?') ? '&' : '?';\n\n return `${base}${separator}${EMAIL_TOKEN_PARAM}=${encodeURIComponent(token)}${fragment}`;\n}\n","\nexport interface PlanArm {\n treatmentId: string;\n armIndex: number;\n}\n\nexport interface PlanSlot {\n experimentId: string;\n name: string;\n arms: PlanArm[];\n probabilities: number[];\n configs: unknown[];\n}\n\nexport interface EmailPlan {\n campaignId: string;\n slots: PlanSlot[];\n linkTable: string[];\n salt: string;\n signingKey: { version: number; secret: string };\n refreshEvery: number;\n expiresAt: number;\n}\n\nexport async function uniformFor(\n salt: string,\n recipientId: string,\n slotIndex: number\n): Promise<number> {\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(salt),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n const mac = new Uint8Array(\n await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(`${recipientId}:${slotIndex}`))\n );\n\n let value = 0;\n for (let i = 0; i < 6; i++) value = value * 256 + mac[i];\n return value / 2 ** 48;\n}\n\nexport function pickArm(probabilities: number[], uniform: number): number {\n if (probabilities.length === 0) return -1;\n\n const total = probabilities.reduce((sum, p) => sum + Math.max(p, 0), 0);\n if (total <= 0) {\n return Math.min(probabilities.length - 1, Math.floor(uniform * probabilities.length));\n }\n\n const target = uniform * total;\n let cumulative = 0;\n for (let i = 0; i < probabilities.length; i++) {\n cumulative += Math.max(probabilities[i], 0);\n if (target < cumulative) return i;\n }\n\n return probabilities.length - 1;\n}\n\nexport async function sampleCombination(\n plan: EmailPlan,\n recipientId: string\n): Promise<number[]> {\n const combination: number[] = [];\n\n for (let i = 0; i < plan.slots.length; i++) {\n const slot = plan.slots[i];\n const uniform = await uniformFor(plan.salt, recipientId, i);\n const chosen = pickArm(slot.probabilities, uniform);\n combination.push(chosen === -1 ? 0 : slot.arms[chosen]?.armIndex ?? 0);\n }\n\n return combination;\n}\n\nexport async function impressionManifest(\n plan: EmailPlan,\n recipientIds: string[]\n): Promise<Array<{ experimentId: string; treatmentId: string; count: number }>> {\n const counts = new Map<string, { experimentId: string; treatmentId: string; count: number }>();\n\n for (const recipientId of recipientIds) {\n for (let i = 0; i < plan.slots.length; i++) {\n const slot = plan.slots[i];\n const uniform = await uniformFor(plan.salt, recipientId, i);\n const chosen = pickArm(slot.probabilities, uniform);\n const arm = slot.arms[chosen === -1 ? 0 : chosen];\n if (!arm) continue;\n\n const key = `${slot.experimentId}:${arm.treatmentId}`;\n const entry = counts.get(key);\n if (entry) entry.count++;\n else\n counts.set(key, {\n experimentId: slot.experimentId,\n treatmentId: arm.treatmentId,\n count: 1,\n });\n }\n }\n\n return Array.from(counts.values());\n}\n","\nexport interface SlotBinding {\n $slot: string;\n}\n\nexport type Bound<T> = T | SlotBinding;\n\nexport function isSlotBinding(value: unknown): value is SlotBinding {\n return typeof value === 'object' && value !== null && typeof (value as SlotBinding).$slot === 'string';\n}\n\nexport interface HeadingBlock {\n type: 'heading';\n text: Bound<string>;\n level?: 1 | 2 | 3;\n align?: 'left' | 'center' | 'right';\n color?: Bound<string>;\n}\n\nexport interface TextBlock {\n type: 'text';\n text: Bound<string>;\n align?: 'left' | 'center' | 'right';\n color?: Bound<string>;\n}\n\nexport interface ButtonBlock {\n type: 'button';\n text: Bound<string>;\n href: Bound<string>;\n backgroundColor?: Bound<string>;\n textColor?: Bound<string>;\n align?: 'left' | 'center' | 'right';\n}\n\nexport interface ImageBlock {\n type: 'image';\n src: Bound<string>;\n alt: Bound<string>;\n href?: Bound<string>;\n width?: number;\n}\n\nexport interface DividerBlock {\n type: 'divider';\n color?: string;\n}\n\nexport interface SpacerBlock {\n type: 'spacer';\n height?: number;\n}\n\nexport type EmailBlock =\n | HeadingBlock\n | TextBlock\n | ButtonBlock\n | ImageBlock\n | DividerBlock\n | SpacerBlock;\n\nexport interface EmailDocument {\n subject: Bound<string>;\n preheader?: Bound<string>;\n blocks: EmailBlock[];\n backgroundColor?: string;\n contentWidth?: number;\n}\n\nconst CONTENT_WIDTH = 600;\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\nfunction resolve(value: Bound<string> | undefined, slots: Record<string, string>, missing: string[]): string {\n if (value === undefined) return '';\n if (!isSlotBinding(value)) return value;\n\n const resolved = slots[value.$slot];\n if (resolved === undefined) {\n missing.push(value.$slot);\n return '';\n }\n return resolved;\n}\n\nexport function extractDocumentSlots(document: EmailDocument): string[] {\n const names: string[] = [];\n const seen = new Set<string>();\n\n const visit = (value: unknown) => {\n if (isSlotBinding(value)) {\n if (!seen.has(value.$slot)) {\n seen.add(value.$slot);\n names.push(value.$slot);\n }\n return;\n }\n if (Array.isArray(value)) value.forEach(visit);\n else if (typeof value === 'object' && value !== null) Object.values(value).forEach(visit);\n };\n\n visit(document.subject);\n visit(document.preheader);\n document.blocks.forEach(visit);\n\n return names;\n}\n\nfunction renderBlock(block: EmailBlock, slots: Record<string, string>, missing: string[]): string {\n const align = (block as { align?: string }).align ?? 'left';\n\n switch (block.type) {\n case 'heading': {\n const size = block.level === 3 ? 18 : block.level === 2 ? 22 : 28;\n const color = escapeHtml(resolve(block.color, slots, missing) || '#111111');\n 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(\n resolve(block.text, slots, missing)\n )}</td></tr>`;\n }\n\n case 'text': {\n const color = escapeHtml(resolve(block.color, slots, missing) || '#333333');\n 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(\n resolve(block.text, slots, missing)\n )}</td></tr>`;\n }\n\n case 'button': {\n const href = escapeHtml(resolve(block.href, slots, missing));\n const label = escapeHtml(resolve(block.text, slots, missing));\n const bg = escapeHtml(resolve(block.backgroundColor, slots, missing) || '#1a73e8');\n const fg = escapeHtml(resolve(block.textColor, slots, missing) || '#ffffff');\n\n return `<tr><td style=\"padding:16px 24px;text-align:${align}\">\n<!--[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]-->\n<!--[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]-->\n</td></tr>`;\n }\n\n case 'image': {\n const src = escapeHtml(resolve(block.src, slots, missing));\n const alt = escapeHtml(resolve(block.alt, slots, missing));\n const width = block.width ?? CONTENT_WIDTH;\n const img = `<img src=\"${src}\" alt=\"${alt}\" width=\"${width}\" style=\"display:block;border:0;outline:none;text-decoration:none;max-width:100%;height:auto\" />`;\n const href = block.href ? escapeHtml(resolve(block.href, slots, missing)) : '';\n\n return `<tr><td style=\"padding:0;text-align:center\">${\n href ? `<a href=\"${href}\">${img}</a>` : img\n }</td></tr>`;\n }\n\n case 'divider':\n 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(\n block.color ?? '#e5e5e5'\n )};font-size:0;line-height:0\"> </td></tr></table></td></tr>`;\n\n case 'spacer':\n return `<tr><td style=\"height:${block.height ?? 24}px;font-size:0;line-height:0\"> </td></tr>`;\n\n default:\n return '';\n }\n}\n\nexport interface RenderedDocument {\n subject: string;\n html: string;\n missingSlots: string[];\n}\n\nexport function renderEmailDocument(\n document: EmailDocument,\n slots: Record<string, string>\n): RenderedDocument {\n const missingSlots: string[] = [];\n const width = document.contentWidth ?? CONTENT_WIDTH;\n const background = escapeHtml(document.backgroundColor ?? '#f4f4f5');\n\n const subject = resolve(document.subject, slots, missingSlots);\n const preheader = resolve(document.preheader, slots, missingSlots);\n\n const body = document.blocks.map((block) => renderBlock(block, slots, missingSlots)).join('\\n');\n\n const html = `<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n<meta name=\"x-apple-disable-message-reformatting\" />\n<title>${escapeHtml(subject)}</title>\n<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->\n</head>\n<body style=\"margin:0;padding:0;background-color:${background}\">\n<div style=\"display:none;font-size:0;line-height:0;max-height:0;overflow:hidden;mso-hide:all\">${escapeHtml(\n preheader\n )}</div>\n<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"background-color:${background}\">\n<tr><td align=\"center\" style=\"padding:24px 12px\">\n<table role=\"presentation\" width=\"${width}\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width:${width}px;max-width:100%;background-color:#ffffff;border-radius:8px\">\n${body}\n</table>\n</td></tr>\n</table>\n</body>\n</html>`;\n\n return { subject, html, missingSlots };\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","import { EMAIL_TOKEN_PARAM } from '@runbandit/shared';\n\n\nconst UNTRACKABLE_SCHEME = /^(mailto:|tel:|sms:|javascript:|data:|#)/i;\n\nconst NOTRACK_ATTRIBUTE = 'data-bandit-notrack';\n\nexport interface RewriteOptions {\n linkTable: string[];\n tokens: string[];\n}\n\nexport interface RewriteResult {\n html: string;\n rewritten: number;\n untracked: string[];\n}\n\ninterface Anchor {\n valueStart: number;\n valueEnd: number;\n url: string;\n}\n\nfunction isTrackable(url: string): boolean {\n const trimmed = url.trim();\n if (!trimmed || UNTRACKABLE_SCHEME.test(trimmed)) return false;\n return /^https?:\\/\\//i.test(trimmed);\n}\n\nfunction appendToken(url: string, token: string): string {\n const hashIndex = url.indexOf('#');\n const fragment = hashIndex === -1 ? '' : url.slice(hashIndex);\n const base = hashIndex === -1 ? url : url.slice(0, hashIndex);\n\n const separator = base.includes('?') ? (base.includes('&') ? '&' : '&') : '?';\n\n return `${base}${separator}${EMAIL_TOKEN_PARAM}=${token}${fragment}`;\n}\n\nfunction scanAnchors(html: string): Anchor[] {\n const anchors: Anchor[] = [];\n let i = 0;\n\n while (i < html.length) {\n const lt = html.indexOf('<', i);\n if (lt === -1) break;\n\n if (html.startsWith('<!--', lt)) {\n if (html.startsWith('<!--[if', lt) || html.startsWith('<!--<![endif]', lt)) {\n const close = html.indexOf('>', lt);\n i = close === -1 ? html.length : close + 1;\n continue;\n }\n\n const end = html.indexOf('-->', lt);\n i = end === -1 ? html.length : end + 3;\n continue;\n }\n\n const rawText = /^<(script|style)\\b/i.exec(html.slice(lt, lt + 8));\n if (rawText) {\n const closing = new RegExp(`</${rawText[1]}\\\\s*>`, 'i');\n const rest = html.slice(lt);\n const match = closing.exec(rest);\n i = match ? lt + match.index + match[0].length : html.length;\n continue;\n }\n\n const isLinkElement =\n /^<a[\\s/>]/i.test(html.slice(lt, lt + 3)) ||\n /^<v:roundrect[\\s/>]/i.test(html.slice(lt, lt + 13));\n if (!isLinkElement) {\n i = lt + 1;\n continue;\n }\n\n const tagEnd = findTagEnd(html, lt);\n if (tagEnd === -1) break;\n\n const tag = html.slice(lt, tagEnd);\n if (!new RegExp(NOTRACK_ATTRIBUTE, 'i').test(tag)) {\n const href = findHref(html, lt, tagEnd);\n if (href && isTrackable(href.url)) anchors.push(href);\n }\n\n i = tagEnd;\n }\n\n return anchors;\n}\n\nfunction findTagEnd(html: string, start: number): number {\n let quote: string | null = null;\n\n for (let i = start + 1; i < html.length; i++) {\n const ch = html[i];\n if (quote) {\n if (ch === quote) quote = null;\n continue;\n }\n if (ch === '\"' || ch === \"'\") {\n quote = ch;\n continue;\n }\n if (ch === '>') return i + 1;\n }\n\n return -1;\n}\n\nfunction findHref(html: string, tagStart: number, tagEnd: number): Anchor | null {\n const tag = html.slice(tagStart, tagEnd);\n const match = /\\shref\\s*=\\s*(\"([^\"]*)\"|'([^']*)'|([^\\s>]+))/i.exec(tag);\n if (!match) return null;\n\n const quoted = match[2] !== undefined || match[3] !== undefined;\n const value = match[2] ?? match[3] ?? match[4] ?? '';\n\n const valueStart = tagStart + match.index + match[0].indexOf(match[1]) + (quoted ? 1 : 0);\n\n return { valueStart, valueEnd: valueStart + value.length, url: value };\n}\n\nexport function extractEmailLinks(html: string): string[] {\n const seen = new Set<string>();\n const urls: string[] = [];\n\n for (const anchor of scanAnchors(html)) {\n if (seen.has(anchor.url)) continue;\n seen.add(anchor.url);\n urls.push(anchor.url);\n }\n\n return urls;\n}\n\nexport function rewriteEmailLinks(html: string, options: RewriteOptions): RewriteResult {\n const { linkTable, tokens } = options;\n\n const indexOfUrl = new Map<string, number>();\n linkTable.forEach((url, index) => {\n if (!indexOfUrl.has(url)) indexOfUrl.set(url, index);\n });\n\n const anchors = scanAnchors(html);\n const untracked: string[] = [];\n let rewritten = 0;\n\n let output = html;\n for (let i = anchors.length - 1; i >= 0; i--) {\n const anchor = anchors[i];\n const index = indexOfUrl.get(anchor.url);\n\n if (index === undefined || tokens[index] === undefined) {\n untracked.push(anchor.url);\n continue;\n }\n\n output =\n output.slice(0, anchor.valueStart) +\n appendToken(anchor.url, tokens[index]) +\n output.slice(anchor.valueEnd);\n rewritten++;\n }\n\n return { html: output, rewritten, untracked: untracked.reverse() };\n}\n","import { rewriteEmailLinks, extractEmailLinks } from './email-links.js';\n\n\nconst SLOT_PATTERN = /\\{\\{\\s*bandit\\.([a-zA-Z0-9_-]+)\\s*\\}\\}/g;\n\nexport interface RenderEmailOptions {\n slots: Record<string, string>;\n linkTable: string[];\n tokens: string[];\n}\n\nexport interface RenderedEmail {\n html: string;\n rewritten: number;\n untracked: string[];\n missingSlots: string[];\n}\n\nexport function renderEmail(templateHtml: string, options: RenderEmailOptions): RenderedEmail {\n const missingSlots: string[] = [];\n\n const substituted = templateHtml.replace(SLOT_PATTERN, (match, name: string) => {\n const value = options.slots[name];\n if (value === undefined) {\n missingSlots.push(name);\n return match;\n }\n return value;\n });\n\n const { html, rewritten, untracked } = rewriteEmailLinks(substituted, {\n linkTable: options.linkTable,\n tokens: options.tokens,\n });\n\n return { html, rewritten, untracked, missingSlots };\n}\n\nexport function extractSlotNames(templateHtml: string): string[] {\n const names = new Set<string>();\n for (const match of templateHtml.matchAll(SLOT_PATTERN)) names.add(match[1]);\n return [...names];\n}\n\nexport { extractEmailLinks, rewriteEmailLinks };\nexport type { RewriteOptions, RewriteResult } from './email-links.js';\n","import {\n mintEmailToken,\n hashRecipient,\n impressionManifest,\n pickArm,\n uniformFor,\n type EmailPlan,\n} from '@runbandit/shared';\nimport { renderEmail, type RenderedEmail } from './email.js';\n\n\nexport interface PlanRenderResult extends RenderedEmail {\n combination: number[];\n tokens: string[];\n}\n\nexport async function renderFromPlan(\n plan: EmailPlan,\n recipientId: string,\n templateHtml: string,\n companySecret: string\n): Promise<PlanRenderResult> {\n const slots: Record<string, string> = {};\n const combination: number[] = [];\n\n for (let i = 0; i < plan.slots.length; i++) {\n const slot = plan.slots[i];\n const chosen = pickArm(slot.probabilities, await uniformFor(plan.salt, recipientId, i));\n const index = chosen === -1 ? 0 : chosen;\n\n combination.push(slot.arms[index]?.armIndex ?? 0);\n\n const config = slot.configs[index];\n slots[slot.name] =\n typeof config === 'string'\n ? config\n : ((config as { content?: string; text?: string } | undefined)?.content ??\n (config as { content?: string; text?: string } | undefined)?.text ??\n '');\n }\n\n const recipientHash = await hashRecipient(recipientId, companySecret);\n\n const tokens = await Promise.all(\n plan.linkTable.map((_, linkIndex) =>\n mintEmailToken(\n { c: plan.campaignId, r: recipientHash, k: combination, l: linkIndex },\n plan.signingKey\n )\n )\n );\n\n const rendered = renderEmail(templateHtml, {\n slots,\n linkTable: plan.linkTable,\n tokens,\n });\n\n return { ...rendered, combination, tokens };\n}\n\nexport function shouldRefreshPlan(\n plan: EmailPlan,\n recipientsRenderedSincePlan: number,\n now: number = Date.now()\n): boolean {\n return recipientsRenderedSincePlan >= plan.refreshEvery || now >= plan.expiresAt;\n}\n\nexport async function reportPlanImpressions(\n plan: EmailPlan,\n recipientIds: string[],\n options: { apiUrl: string; apiKey: string; batchKey?: string }\n): Promise<{ recorded: boolean; alreadyRecorded: boolean }> {\n const counts = await impressionManifest(plan, recipientIds);\n if (counts.length === 0) return { recorded: false, alreadyRecorded: false };\n\n const response = await fetch(`${options.apiUrl}/api/email/impressions`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'x-api-key': options.apiKey },\n body: JSON.stringify({\n campaignId: plan.campaignId,\n batchKey: options.batchKey ?? (await batchKeyFor(plan.campaignId, recipientIds)),\n counts,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Failed to report impressions: ${response.status}`);\n }\n\n const body = (await response.json()) as { data?: { recorded: boolean; alreadyRecorded: boolean } };\n return body.data ?? { recorded: false, alreadyRecorded: false };\n}\n\nasync function batchKeyFor(campaignId: string, recipientIds: string[]): Promise<string> {\n const material = `${campaignId}|${[...recipientIds].sort().join(',')}`;\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(material));\n return Array.from(new Uint8Array(digest).slice(0, 16))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n"],"mappings":";AACO,IAAM,iBAAiB;AACvB,IAAM,yBAAyB;AAE/B,IAAM,yBAAyB;AAE/B,SAAS,aAAqB;AACnC,MAAI,OAAO,WAAW,eAAe,OAAO,YAAY;AACtD,WAAO,OAAO,WAAW;AAAA,EAC3B;AACA,SAAO,uCAAuC,QAAQ,SAAS,CAAC,MAAM;AACpE,UAAM,IAAK,KAAK,OAAO,IAAI,KAAM;AACjC,YAAQ,MAAM,MAAM,IAAK,IAAI,IAAO,GAAK,SAAS,EAAE;AAAA,EACtD,CAAC;AACH;AAQO,SAAS,qBAAqB,UAA0B,CAAC,GAAW;AACzE,QAAM,EAAE,SAAS,MAAM,MAAM,KAAK,IAAI,GAAG,QAAQ,uBAAuB,IAAI;AAC5E,MAAI,CAAC,QAAS,QAAO,WAAW;AAEhC,MAAI;AACF,UAAM,SAAS,QAAQ,QAAQ,cAAc;AAC7C,UAAM,WAAW,QAAQ,QAAQ,sBAAsB;AACvD,QAAI,UAAU,UAAU;AACtB,YAAM,MAAM,IAAI,IAAI,SAAS,UAAU,EAAE;AACzC,UAAI,MAAM,MAAO,QAAO;AAAA,IAC1B;AACA,UAAM,KAAK,WAAW;AACtB,YAAQ,QAAQ,gBAAgB,EAAE;AAClC,YAAQ,QAAQ,wBAAwB,OAAO,IAAI,CAAC,CAAC;AACrD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,WAAW;AAAA,EACpB;AACF;AAEO,SAAS,wBAAwC;AACtD,SAAO,OAAO,WAAW,eAAe,OAAO,eAAe,OAAO,eAAe;AACtF;;;AChBA,SAAS,iBAAqC;AAC5C,MAAI,OAAO,aAAa,eAAe,SAAS,UAAU,SAAS,UAAU;AAC3E,WAAO,SAAS,SAAS,SAAS;AAAA,EACpC;AACA,SAAO;AACT;AAEO,IAAM,gBAAN,MAAoB;AAAA,EACjB;AAAA,EACA,kBAA4C,CAAC;AAAA,EAC7C;AAAA,EACA;AAAA,EAER,YAAY,QAA6B;AACvC,SAAK,SAAS;AAAA,MACZ,SAAS;AAAA,MACT,WAAW;AAAA,MACX,eAAe;AAAA,MACf,YAAY;AAAA,MACZ,GAAG;AAAA,IACL;AACA,SAAK,YAAY,KAAK,qBAAqB;AAC3C,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEA,MAAM,cACJ,cACA,QACA,SACqB;AACrB,UAAM,UAA6B;AAAA,MACjC;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,KAAK;AAAA,IAClB;AACA,UAAM,UAAU,eAAe;AAC/B,QAAI,QAAS,SAAQ,UAAU;AAE/B,UAAM,WAAW,MAAM,KAAK;AAAA,MAC1B;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,QAAI,CAAC,SAAS,WAAW,CAAC,SAAS,MAAM;AACvC,YAAM,IAAI,MAAM,SAAS,SAAS,0BAA0B;AAAA,IAC9D;AAEA,UAAM,aAAa,SAAS,MAAM,QAAQ,SAAS;AAEnD,QAAI,CAAC,YAAY,cAAc;AAC7B,YAAM,IAAI,MAAM,6BAA6B;AAAA,IAC/C;AAEA,WAAO;AAAA,MACL,cAAc,WAAW;AAAA,MACzB,aAAa,WAAW;AAAA,MACxB;AAAA,MACA;AAAA,MACA,QAAQ,WAAW,WAAW,UAAU,CAAC;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,gBACE,OACA,MACM;AACN,UAAM,UAAkC;AAAA,MACtC,MAAM,MAAM;AAAA,MACZ,UAAU,MAAM,YAAY;AAAA,MAC5B,OAAO,MAAM;AAAA,MACb,UAAU,MAAM;AAAA,MAChB,WAAW,KAAK;AAAA,IAClB;AAEA,QAAI,MAAM,WAAW;AACnB,YAAM,MAAM,GAAG,KAAK,gBAAgB,CAAC;AACrC,UAAI;AACF,aAAK,MAAM,KAAK;AAAA,UACd,QAAQ;AAAA,UACR,SAAS;AAAA,YACP,iBAAiB,UAAU,KAAK,OAAO,MAAM;AAAA,YAC7C,gBAAgB;AAAA,UAClB;AAAA,UACA,MAAM,KAAK,UAAU,OAAO;AAAA,UAC5B,WAAW;AAAA,QACb,CAAC;AAAA,MACH,QAAQ;AAAA,MACR;AACA;AAAA,IACF;AAEA,SAAK,gBAAgB,KAAK,OAAO;AACjC,QAAI,KAAK,gBAAgB,UAAU,KAAK,OAAO,WAAY;AACzD,WAAK,MAAM;AAAA,IACb;AAAA,EACF;AAAA,EAEA,MAAM,QAAuB;AAC3B,UAAM,cAAc,CAAC,GAAG,KAAK,eAAe;AAC5C,SAAK,kBAAkB,CAAC;AACxB,QAAI,YAAY,WAAW,EAAG;AAE9B,QAAI;AACF,YAAM,MAAM,GAAG,KAAK,gBAAgB,CAAC,iBAAiB;AAAA,QACpD,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,iBAAiB,UAAU,KAAK,OAAO,MAAM;AAAA,UAC7C,gBAAgB;AAAA,QAClB;AAAA,QACA,MAAM,KAAK,UAAU,EAAE,QAAQ,YAAY,CAAC;AAAA,QAC5C,WAAW;AAAA,MACb,CAAC;AAAA,IACH,SAAS,OAAO;AACd,WAAK,gBAAgB,QAAQ,GAAG,WAAW;AAC3C,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,SACJ,aACA,kBACA,WACA,aACe;AACf,UAAM,WAAW,MAAM,KAAK;AAAA,MAC1B;AAAA,MACA;AAAA,MACA;AAAA,QACE;AAAA,QACA;AAAA,QACA,aAAa,eAAe;AAAA,MAC9B;AAAA,MACA,EAAE,gBAAgB,UAAU;AAAA,IAC9B;AAEA,QAAI,CAAC,SAAS,SAAS;AACrB,YAAM,IAAI,MAAM,SAAS,SAAS,yBAAyB;AAAA,IAC7D;AAAA,EACF;AAAA,EAEA,MAAM,MAAM,QAAgB,QAAyD;AACnF,UAAM,WAAW,MAAM,KAAK,YAAY,WAAW,QAAQ,EAAE,QAAQ,OAAO,CAAC;AAE7E,QAAI,CAAC,SAAS,SAAS;AACrB,YAAM,IAAI,MAAM,SAAS,SAAS,yBAAyB;AAAA,IAC7D;AAAA,EACF;AAAA,EAEA,MAAM,kBACJ,UACA,QACA,OACe;AACf,UAAM,WAAW,MAAM,KAAK;AAAA,MAC1B,YAAY,QAAQ;AAAA,MACpB;AAAA,MACA,EAAE,QAAQ,MAAM;AAAA,IAClB;AAEA,QAAI,CAAC,SAAS,SAAS;AACrB,YAAM,IAAI,MAAM,SAAS,SAAS,gCAAgC;AAAA,IACpE;AAAA,EACF;AAAA,EAEA,MAAM,UACJ,UACA,QACA,SACmC;AACnC,UAAM,UAAmC;AAAA,MACvC;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,KAAK;AAAA,IAClB;AACA,UAAM,UAAU,eAAe;AAC/B,QAAI,QAAS,SAAQ,UAAU;AAE/B,UAAM,WAAW,MAAM,KAAK,YAAiB,uBAAuB,QAAQ,OAAO;AAEnF,QAAI,CAAC,SAAS,WAAW,CAAC,SAAS,MAAM;AACvC,YAAM,IAAI,MAAM,SAAS,SAAS,iCAAiC;AAAA,IACrE;AAEA,UAAM,UAAU,SAAS,MAAM,QAAQ,SAAS;AAChD,QAAI,CAAC,SAAS,sBAAsB,CAAC,SAAS,OAAO;AACnD,YAAM,IAAI,MAAM,oCAAoC;AAAA,IACtD;AAEA,WAAO;AAAA,MACL,oBAAoB,QAAQ;AAAA,MAC5B,OAAO,QAAQ;AAAA,IACjB;AAAA,EACF;AAAA,EAEA,MAAM,kBAAkB,UAAkB,QAAgB,OAA8B;AACtF,UAAM,WAAW,MAAM,KAAK,YAAY,YAAY,QAAQ,WAAW,QAAQ;AAAA,MAC7E;AAAA,MACA;AAAA,IACF,CAAC;AACD,QAAI,CAAC,SAAS,SAAS;AACrB,YAAM,IAAI,MAAM,SAAS,SAAS,gCAAgC;AAAA,IACpE;AAAA,EACF;AAAA,EAEQ,aAAqB;AAC3B,WAAO,WAAW;AAAA,EACpB;AAAA,EAEQ,kBAA0B;AAChC,QAAI,UAAU,KAAK,OAAO,OAAO,QAAQ,OAAO,EAAE;AAClD,QAAI,CAAC,QAAQ,SAAS,MAAM,GAAG;AAC7B,gBAAU,GAAG,OAAO;AAAA,IACtB;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,uBAA+B;AACrC,WAAO,qBAAqB;AAAA,MAC1B,SAAS,sBAAsB;AAAA,MAC/B,OAAO,KAAK,OAAO;AAAA,IACrB,CAAC;AAAA,EACH;AAAA,EAEA,MAAc,YACZ,UACA,QACA,MACA,cACyB;AACzB,UAAM,MAAM,GAAG,KAAK,gBAAgB,CAAC,GAAG,QAAQ;AAEhD,UAAM,UAAkC;AAAA,MACtC,iBAAiB,UAAU,KAAK,OAAO,MAAM;AAAA,MAC7C,gBAAgB;AAAA,MAChB,GAAG;AAAA,IACL;AAEA,UAAM,gBAA6B;AAAA,MACjC;AAAA,MACA;AAAA,MACA,QAAQ,YAAY,QAAQ,KAAK,OAAO,OAAQ;AAAA,IAClD;AAEA,QAAI,QAAQ,WAAW,OAAO;AAC5B,oBAAc,OAAO,KAAK,UAAU,IAAI;AAAA,IAC1C;AAEA,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,KAAK,aAAa;AAC/C,YAAM,OAAO,MAAM,SAAS,KAAK;AAEjC,aAAO;AAAA,QACL,SAAS,SAAS;AAAA,QAClB,MAAM,SAAS,KAAK,OAAY;AAAA,QAChC,OAAO,SAAS,KAAK,SAAa,MAAc,WAAW;AAAA,QAC3D,WAAW,oBAAI,KAAK;AAAA,MACtB;AAAA,IACF,SAAS,OAAO;AACd,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,QAChD,WAAW,oBAAI,KAAK;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,kBAAwB;AAC9B,SAAK,aAAa,YAAY,MAAM;AAClC,WAAK,MAAM,EAAE,MAAM,QAAQ,KAAK;AAAA,IAClC,GAAG,KAAK,OAAO,aAAa;AAAA,EAC9B;AAAA,EAEA,UAAgB;AACd,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAAA,IAC/B;AACA,SAAK,MAAM,EAAE,MAAM,QAAQ,KAAK;AAChC,QAAI,OAAO,WAAW,eAAe,OAAO,cAAc;AACxD,mBAAa,WAAW,oBAAoB;AAC5C,mBAAa,WAAW,4BAA4B;AAAA,IACtD;AAAA,EACF;AACF;;;ACjPO,IAAM,aAAa;EACxB;EACA;EACA;EACA;EACA;;AAkCF,IAAY;CAAZ,SAAYA,mBAAgB;AAC1B,EAAAA,kBAAA,OAAA,IAAA;AACA,EAAAA,kBAAA,QAAA,IAAA;AACA,EAAAA,kBAAA,QAAA,IAAA;AACA,EAAAA,kBAAA,WAAA,IAAA;AACA,EAAAA,kBAAA,UAAA,IAAA;AACF,GANY,qBAAA,mBAAgB,CAAA,EAAA;AAyB5B,IAAY;CAAZ,SAAYC,kBAAe;AACzB,EAAAA,iBAAA,mBAAA,IAAA;AACA,EAAAA,iBAAA,mBAAA,IAAA;AACF,GAHY,oBAAA,kBAAe,CAAA,EAAA;AAK3B,IAAY;CAAZ,SAAYC,YAAS;AACnB,EAAAA,WAAA,MAAA,IAAA;AACA,EAAAA,WAAA,cAAA,IAAA;AACF,GAHY,cAAA,YAAS,CAAA,EAAA;AAKf,SAAU,mBAAmB,WAAoC;AACrE,SAAO,cAAc,UAAU,eAAe,UAAU,eAAe,UAAU;AACnF;AAEA,IAAY;CAAZ,SAAYC,iBAAc;AACxB,EAAAA,gBAAA,MAAA,IAAA;AACA,EAAAA,gBAAA,YAAA,IAAA;AACA,EAAAA,gBAAA,gBAAA,IAAA;AACF,GAJY,mBAAA,iBAAc,CAAA,EAAA;AAYnB,IAAM,uBAAuB,IAAI,KAAK,KAAK,KAAK;AAEhD,IAAM,oBAAoB,KAAK,KAAK,KAAK,KAAK;AAE9C,IAAM,aAA+B,EAAE,MAAM,eAAe,KAAI;AAEjE,SAAU,kBAAkB,QAAyC;AACzE,UAAQ,QAAQ,MAAM;IACpB,KAAK,eAAe;AAClB,aAAO;QACL,MAAM,eAAe;QACrB,YAAY,OAAO,cAAc,OAAO,aAAa,IACjD,OAAO,aACP;;IAER,KAAK,eAAe;AAClB,aAAO;QACL,MAAM,eAAe;QACrB,UAAU,OAAO,YAAY,OAAO,WAAW,IAC3C,OAAO,WACP;;IAER;AACE,aAAO;EACX;AACF;AAEA,IAAM,2BAA4D;EAChE,gBAAgB,gBAAgB;EAChC,MAAM,gBAAgB;EACtB,mBAAmB,gBAAgB;;AAG/B,SAAU,mBAAmB,WAAoC;AACrE,MAAI,cAAc,gBAAgB;AAAmB,WAAO,gBAAgB;AAC5E,MAAI,cAAc,gBAAgB;AAAmB,WAAO,gBAAgB;AAC5E,SAAO,yBAAyB,aAAa,EAAE,KAAK,gBAAgB;AACtE;AAEA,IAAY;CAAZ,SAAYC,cAAW;AACrB,EAAAA,aAAA,YAAA,IAAA;AACA,EAAAA,aAAA,UAAA,IAAA;AACA,EAAAA,aAAA,MAAA,IAAA;AACA,EAAAA,aAAA,KAAA,IAAA;AACA,EAAAA,aAAA,YAAA,IAAA;AACA,EAAAA,aAAA,MAAA,IAAA;AACA,EAAAA,aAAA,OAAA,IAAA;AACA,EAAAA,aAAA,OAAA,IAAA;AACA,EAAAA,aAAA,OAAA,IAAA;AACA,EAAAA,aAAA,UAAA,IAAA;AACF,GAXY,gBAAA,cAAW,CAAA,EAAA;AAajB,SAAU,uBAAuB,aAAwB;AAC7D,UAAQ,aAAa;IACnB,KAAK,YAAY;IACjB,KAAK,YAAY;AACf,aAAO;IACT,KAAK,YAAY;IACjB,KAAK,YAAY;IACjB,KAAK,YAAY;IACjB,KAAK,YAAY;AACf,aAAO;IACT,KAAK,YAAY;IACjB,KAAK,YAAY;IACjB,KAAK,YAAY;IACjB,KAAK,YAAY;AACf,aAAO;EACX;AACF;AAwDA,IAAY;CAAZ,SAAYC,eAAY;AACtB,EAAAA,cAAA,OAAA,IAAA;AACA,EAAAA,cAAA,QAAA,IAAA;AACA,EAAAA,cAAA,QAAA,IAAA;AACA,EAAAA,cAAA,WAAA,IAAA;AACF,GALY,iBAAA,eAAY,CAAA,EAAA;;;AC5RlB,SAAUC,cAAU;AACxB,SAAO,KAAK,OAAM,EAAG,SAAS,EAAE,EAAE,UAAU,CAAC,IAAI,KAAK,IAAG,EAAG,SAAS,EAAE;AACzE;AAEM,SAAU,yBAAyB,YAAuB;AAC9D,QAAM,cAAc,WAAW,OAAO,CAAC,KAAK,cAAc,OAAO,UAAU,UAAU,IAAI,CAAC;AAC1F,SAAO,cAAc;AACvB;AAEM,SAAU,0BAA0B,YAAuB;AAC/D,QAAM,cAAc,WAAW,OAAO,CAAC,KAAK,cAAc,OAAO,UAAU,UAAU,IAAI,CAAC;AAE1F,SAAO,WAAW,IAAI,gBAAc;IAClC,GAAG;IACH,SAAS,UAAU,UAAU,KAAK;IAClC;AACJ;AAEM,SAAU,4BACd,WACA,QACA,aAAqB,MAAI;AAEzB,MAAI,WAAW;AAAG,WAAO,EAAE,OAAO,GAAG,OAAO,EAAC;AAE7C,QAAM,IAAI,eAAe,OAAO,OAAO,eAAe,OAAO,QAAQ;AACrE,QAAM,IAAI,YAAY;AACtB,QAAM,SAAS,IAAI,KAAK,KAAM,KAAK,IAAI,KAAM,MAAM;AAEnD,SAAO;IACL,OAAO,KAAK,IAAI,GAAG,IAAI,MAAM;IAC7B,OAAO,KAAK,IAAI,GAAG,IAAI,MAAM;;AAEjC;AAEM,SAAU,aAAa,OAAa;AACxC,QAAM,aAAa;AACnB,SAAO,WAAW,KAAK,KAAK;AAC9B;AAEM,SAAU,iBAAiB,UAA6B;AAC5D,QAAM,YAAiC,CAAA;AAEvC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACnD,QAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW;AACxF,gBAAU,GAAG,IAAI;IACnB,WAAW,UAAU,QAAQ,UAAU,QAAW;AAChD,gBAAU,GAAG,IAAI,OAAO,KAAK;IAC/B;EACF;AAEA,SAAO;AACT;AAEM,SAAU,kBAAkB,MAAY;AAC5C,SAAO,KACJ,YAAW,EACX,QAAQ,iBAAiB,EAAE,EAC3B,QAAQ,QAAQ,GAAG,EACnB,QAAQ,OAAO,GAAG,EAClB,QAAQ,UAAU,EAAE,EACpB,UAAU,GAAG,EAAE;AACpB;AAEM,SAAU,iBAAiB,WAAiB;AAChD,MAAI,CAAC,aAAa,UAAU,WAAW,KAAK,UAAU,SAAS,IAAI;AACjE,WAAO;EACT;AAEA,QAAM,iBAAiB;AAEvB,QAAM,WAAW;IACf;IAAO;IAAO;IAAO;IAAS;IAAa;IAAQ;IAAO;IAC1D;IAAW;IAAO;IAAQ;IAAQ;IAAQ;IAAQ;IAAU;IAC5D;IAAW;IAAO;IAAU;IAAU;IAAU;IAAW;;AAG7D,SAAO,eAAe,KAAK,SAAS,KAAK,CAAC,SAAS,SAAS,SAAS;AACvE;AAEM,SAAU,iBAAiB,UAAkB,YAAkB;AACnE,MAAI,CAAC,YAAY,CAAC,YAAY;AAC5B,WAAO;EACT;AAEA,QAAM,gBAAgB,SAAS,MAAM,GAAG,EAAE,CAAC;AAE3C,MAAI,CAAC,cAAc,SAAS,UAAU,GAAG;AACvC,WAAO;EACT;AAEA,QAAM,YAAY,cAAc,MAAM,GAAG,EAAE,WAAW,SAAS,EAAE;AAEjE,MAAI,CAAC,aAAa,cAAc,OAAO;AACrC,WAAO;EACT;AAEA,SAAO;AACT;AAEM,SAAU,kBACd,WACA,YACA,OAAe,IACf,WAAmB,SAAO;AAE1B,QAAM,MAAM,GAAG,QAAQ,MAAM,SAAS,IAAI,UAAU;AACpD,SAAO,OAAO,GAAG,GAAG,GAAG,KAAK,WAAW,GAAG,IAAI,OAAO,MAAM,IAAI,KAAK;AACtE;;;AC7GO,IAAM,oBAAoB;AAoBjC,IAAM,UAAU,IAAI,YAAW;AAE/B,SAAS,gBAAgB,OAAiB;AACxC,MAAI,SAAS;AACb,aAAW,QAAQ;AAAO,cAAU,OAAO,aAAa,IAAI;AAC5D,SAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAC/E;AAEA,SAAS,gBAAgB,OAAa;AACpC,QAAM,SAAS,MAAM,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG;AACzD,QAAM,SAAS,KAAK,SAAS,IAAI,QAAQ,IAAK,OAAO,SAAS,KAAM,CAAC,CAAC;AACtE,QAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAC1C,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ;AAAK,UAAM,CAAC,IAAI,OAAO,WAAW,CAAC;AACtE,SAAO;AACT;AAEA,eAAe,KAAK,QAAgB,SAAe;AACjD,QAAM,MAAM,MAAM,OAAO,OAAO,UAC9B,OACA,QAAQ,OAAO,MAAM,GACrB,EAAE,MAAM,QAAQ,MAAM,UAAS,GAC/B,OACA,CAAC,MAAM,CAAC;AAEV,SAAO,IAAI,WAAW,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,QAAQ,OAAO,OAAO,CAAC,CAAC;AACtF;AAEA,SAAS,gBAAgB,GAAW,GAAS;AAC3C,MAAI,EAAE,WAAW,EAAE;AAAQ,WAAO;AAClC,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ;AAAK,YAAQ,EAAE,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC;AAC3E,SAAO,SAAS;AAClB;AAGA,eAAsB,cAAc,aAAqB,eAAqB;AAC5E,QAAM,SAAS,MAAM,KAAK,eAAe,aAAa,WAAW,EAAE;AACnE,SAAO,gBAAgB,OAAO,MAAM,GAAG,EAAE,CAAC;AAC5C;AAEA,eAAsB,eACpB,SACA,KAAe;AAEf,QAAM,OAAO,gBACX,QAAQ,OACN,KAAK,UAAU;IACb,GAAG,QAAQ;IACX,GAAG,QAAQ;IACX,GAAG,QAAQ;IACX,GAAG,QAAQ;IACX,GAAG,IAAI;GACR,CAAC,CACH;AAGH,QAAM,YAAY,gBAAgB,MAAM,KAAK,IAAI,QAAQ,IAAI,CAAC;AAC9D,SAAO,GAAG,IAAI,IAAI,SAAS;AAC7B;AAEA,eAAsB,iBACpB,OACA,MAAkB;AAElB,QAAM,QAAQ,MAAM,MAAM,GAAG;AAC7B,MAAI,MAAM,WAAW,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG;AAChD,WAAO,EAAE,OAAO,OAAO,QAAQ,YAAW;EAC5C;AACA,QAAM,CAAC,MAAM,SAAS,IAAI;AAE1B,MAAI;AACJ,MAAI;AACF,cAAU,KAAK,MAAM,IAAI,YAAW,EAAG,OAAO,gBAAgB,IAAI,CAAC,CAAC;EACtE,QAAQ;AACN,WAAO,EAAE,OAAO,OAAO,QAAQ,YAAW;EAC5C;AAEA,MACE,CAAC,WACD,OAAO,QAAQ,MAAM,YACrB,OAAO,QAAQ,MAAM,YACrB,CAAC,MAAM,QAAQ,QAAQ,CAAC,KACxB,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,OAAO,UAAU,GAAG,KAAK,OAAO,CAAC,KAC3D,CAAC,OAAO,UAAU,QAAQ,CAAC,KAC1B,QAAQ,IAAe,KACxB,CAAC,OAAO,UAAU,QAAQ,CAAC,GAC3B;AACA,WAAO,EAAE,OAAO,OAAO,QAAQ,YAAW;EAC5C;AAEA,QAAM,MAAM,KAAK,KAAK,CAAC,cAAc,UAAU,YAAY,QAAQ,CAAC;AACpE,MAAI,CAAC;AAAK,WAAO,EAAE,OAAO,OAAO,QAAQ,cAAa;AAEtD,QAAM,WAAW,gBAAgB,MAAM,KAAK,IAAI,QAAQ,IAAI,CAAC;AAC7D,MAAI,CAAC,gBAAgB,UAAU,SAAS,GAAG;AACzC,WAAO,EAAE,OAAO,OAAO,QAAQ,gBAAe;EAChD;AAEA,SAAO,EAAE,OAAO,MAAM,QAA+C;AACvE;AAEM,SAAU,iBAAiB,KAAa,OAAa;AACzD,QAAM,YAAY,IAAI,QAAQ,GAAG;AACjC,QAAM,WAAW,cAAc,KAAK,KAAK,IAAI,MAAM,SAAS;AAC5D,QAAM,OAAO,cAAc,KAAK,MAAM,IAAI,MAAM,GAAG,SAAS;AAC5D,QAAM,YAAY,KAAK,SAAS,GAAG,IAAI,MAAM;AAE7C,SAAO,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,IAAI,mBAAmB,KAAK,CAAC,GAAG,QAAQ;AACxF;;;ACzGA,eAAsB,WACpB,MACA,aACA,WAAiB;AAEjB,QAAM,MAAM,MAAM,OAAO,OAAO,UAC9B,OACA,IAAI,YAAW,EAAG,OAAO,IAAI,GAC7B,EAAE,MAAM,QAAQ,MAAM,UAAS,GAC/B,OACA,CAAC,MAAM,CAAC;AAEV,QAAM,MAAM,IAAI,WACd,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,IAAI,YAAW,EAAG,OAAO,GAAG,WAAW,IAAI,SAAS,EAAE,CAAC,CAAC;AAGhG,MAAI,QAAQ;AACZ,WAAS,IAAI,GAAG,IAAI,GAAG;AAAK,YAAQ,QAAQ,MAAM,IAAI,CAAC;AACvD,SAAO,QAAQ,KAAK;AACtB;AAEM,SAAU,QAAQ,eAAyB,SAAe;AAC9D,MAAI,cAAc,WAAW;AAAG,WAAO;AAEvC,QAAM,QAAQ,cAAc,OAAO,CAAC,KAAK,MAAM,MAAM,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC;AACtE,MAAI,SAAS,GAAG;AACd,WAAO,KAAK,IAAI,cAAc,SAAS,GAAG,KAAK,MAAM,UAAU,cAAc,MAAM,CAAC;EACtF;AAEA,QAAM,SAAS,UAAU;AACzB,MAAI,aAAa;AACjB,WAAS,IAAI,GAAG,IAAI,cAAc,QAAQ,KAAK;AAC7C,kBAAc,KAAK,IAAI,cAAc,CAAC,GAAG,CAAC;AAC1C,QAAI,SAAS;AAAY,aAAO;EAClC;AAEA,SAAO,cAAc,SAAS;AAChC;AAEA,eAAsB,kBACpB,MACA,aAAmB;AAEnB,QAAM,cAAwB,CAAA;AAE9B,WAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,UAAM,OAAO,KAAK,MAAM,CAAC;AACzB,UAAM,UAAU,MAAM,WAAW,KAAK,MAAM,aAAa,CAAC;AAC1D,UAAM,SAAS,QAAQ,KAAK,eAAe,OAAO;AAClD,gBAAY,KAAK,WAAW,KAAK,IAAI,KAAK,KAAK,MAAM,GAAG,YAAY,CAAC;EACvE;AAEA,SAAO;AACT;AAEA,eAAsB,mBACpB,MACA,cAAsB;AAEtB,QAAM,SAAS,oBAAI,IAAG;AAEtB,aAAW,eAAe,cAAc;AACtC,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,YAAM,OAAO,KAAK,MAAM,CAAC;AACzB,YAAM,UAAU,MAAM,WAAW,KAAK,MAAM,aAAa,CAAC;AAC1D,YAAM,SAAS,QAAQ,KAAK,eAAe,OAAO;AAClD,YAAM,MAAM,KAAK,KAAK,WAAW,KAAK,IAAI,MAAM;AAChD,UAAI,CAAC;AAAK;AAEV,YAAM,MAAM,GAAG,KAAK,YAAY,IAAI,IAAI,WAAW;AACnD,YAAM,QAAQ,OAAO,IAAI,GAAG;AAC5B,UAAI;AAAO,cAAM;;AAEf,eAAO,IAAI,KAAK;UACd,cAAc,KAAK;UACnB,aAAa,IAAI;UACjB,OAAO;SACR;IACL;EACF;AAEA,SAAO,MAAM,KAAK,OAAO,OAAM,CAAE;AACnC;;;ACnGM,SAAU,cAAc,OAAc;AAC1C,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,OAAQ,MAAsB,UAAU;AAChG;AA4DA,IAAM,gBAAgB;AAEtB,SAAS,WAAW,OAAa;AAC/B,SAAO,MACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ;AAC3B;AAEA,SAAS,QAAQ,OAAkC,OAA+B,SAAiB;AACjG,MAAI,UAAU;AAAW,WAAO;AAChC,MAAI,CAAC,cAAc,KAAK;AAAG,WAAO;AAElC,QAAM,WAAW,MAAM,MAAM,KAAK;AAClC,MAAI,aAAa,QAAW;AAC1B,YAAQ,KAAK,MAAM,KAAK;AACxB,WAAO;EACT;AACA,SAAO;AACT;AAEM,SAAU,qBAAqB,UAAuB;AAC1D,QAAM,QAAkB,CAAA;AACxB,QAAM,OAAO,oBAAI,IAAG;AAEpB,QAAM,QAAQ,CAAC,UAAkB;AAC/B,QAAI,cAAc,KAAK,GAAG;AACxB,UAAI,CAAC,KAAK,IAAI,MAAM,KAAK,GAAG;AAC1B,aAAK,IAAI,MAAM,KAAK;AACpB,cAAM,KAAK,MAAM,KAAK;MACxB;AACA;IACF;AACA,QAAI,MAAM,QAAQ,KAAK;AAAG,YAAM,QAAQ,KAAK;aACpC,OAAO,UAAU,YAAY,UAAU;AAAM,aAAO,OAAO,KAAK,EAAE,QAAQ,KAAK;EAC1F;AAEA,QAAM,SAAS,OAAO;AACtB,QAAM,SAAS,SAAS;AACxB,WAAS,OAAO,QAAQ,KAAK;AAE7B,SAAO;AACT;AAEA,SAAS,YAAY,OAAmB,OAA+B,SAAiB;AACtF,QAAM,QAAS,MAA6B,SAAS;AAErD,UAAQ,MAAM,MAAM;IAClB,KAAK,WAAW;AACd,YAAM,OAAO,MAAM,UAAU,IAAI,KAAK,MAAM,UAAU,IAAI,KAAK;AAC/D,YAAM,QAAQ,WAAW,QAAQ,MAAM,OAAO,OAAO,OAAO,KAAK,SAAS;AAC1E,aAAO,+CAA+C,KAAK,qDAAqD,IAAI,6CAA6C,KAAK,KAAK,WACzK,QAAQ,MAAM,MAAM,OAAO,OAAO,CAAC,CACpC;IACH;IAEA,KAAK,QAAQ;AACX,YAAM,QAAQ,WAAW,QAAQ,MAAM,OAAO,OAAO,OAAO,KAAK,SAAS;AAC1E,aAAO,8CAA8C,KAAK,gFAAgF,KAAK,KAAK,WAClJ,QAAQ,MAAM,MAAM,OAAO,OAAO,CAAC,CACpC;IACH;IAEA,KAAK,UAAU;AACb,YAAM,OAAO,WAAW,QAAQ,MAAM,MAAM,OAAO,OAAO,CAAC;AAC3D,YAAM,QAAQ,WAAW,QAAQ,MAAM,MAAM,OAAO,OAAO,CAAC;AAC5D,YAAM,KAAK,WAAW,QAAQ,MAAM,iBAAiB,OAAO,OAAO,KAAK,SAAS;AACjF,YAAM,KAAK,WAAW,QAAQ,MAAM,WAAW,OAAO,OAAO,KAAK,SAAS;AAE3E,aAAO,+CAA+C,KAAK;0HACyD,IAAI,sFAAsF,EAAE,gBAAgB,EAAE,yCAAyC,EAAE,mEAAmE,KAAK;8BAC7T,IAAI,oEAAoE,EAAE,UAAU,EAAE,mHAAmH,KAAK;;IAExO;IAEA,KAAK,SAAS;AACZ,YAAM,MAAM,WAAW,QAAQ,MAAM,KAAK,OAAO,OAAO,CAAC;AACzD,YAAM,MAAM,WAAW,QAAQ,MAAM,KAAK,OAAO,OAAO,CAAC;AACzD,YAAM,QAAQ,MAAM,SAAS;AAC7B,YAAM,MAAM,aAAa,GAAG,UAAU,GAAG,YAAY,KAAK;AAC1D,YAAM,OAAO,MAAM,OAAO,WAAW,QAAQ,MAAM,MAAM,OAAO,OAAO,CAAC,IAAI;AAE5E,aAAO,+CACL,OAAO,YAAY,IAAI,KAAK,GAAG,SAAS,GAC1C;IACF;IAEA,KAAK;AACH,aAAO,4JAA4J,WACjK,MAAM,SAAS,SAAS,CACzB;IAEH,KAAK;AACH,aAAO,yBAAyB,MAAM,UAAU,EAAE;IAEpD;AACE,aAAO;EACX;AACF;AAQM,SAAU,oBACd,UACA,OAA6B;AAE7B,QAAM,eAAyB,CAAA;AAC/B,QAAM,QAAQ,SAAS,gBAAgB;AACvC,QAAM,aAAa,WAAW,SAAS,mBAAmB,SAAS;AAEnE,QAAM,UAAU,QAAQ,SAAS,SAAS,OAAO,YAAY;AAC7D,QAAM,YAAY,QAAQ,SAAS,WAAW,OAAO,YAAY;AAEjE,QAAM,OAAO,SAAS,OAAO,IAAI,CAAC,UAAU,YAAY,OAAO,OAAO,YAAY,CAAC,EAAE,KAAK,IAAI;AAE9F,QAAM,OAAO;;;;;;SAMN,WAAW,OAAO,CAAC;;;mDAGuB,UAAU;gGACmC,WAC5F,SAAS,CACV;6GAC0G,UAAU;;oCAEnF,KAAK,6DAA6D,KAAK;EACzG,IAAI;;;;;;AAOJ,SAAO,EAAE,SAAS,MAAM,aAAY;AACtC;;;ACpLA,IAAM,cAAc;AACpB,IAAM,mBAAmB;AACzB,IAAM,gBAAgB;AAQf,SAAS,cACd,YACA,MAC0B;AAC1B,MAAI,WAAW,WAAW,EAAG,QAAO;AACpC,MAAI,aAAa;AACjB,aAAW,aAAa,YAAY;AAClC,kBAAc,UAAU;AACxB,QAAI,OAAO,WAAY,QAAO;AAAA,EAChC;AACA,SAAO,WAAW,WAAW,SAAS,CAAC;AACzC;AAEA,IAAM,iCAAiC;AAEvC,SAAS,WAAmB;AAC1B,QAAM,QAAQ;AACd,MAAI,KAAK;AACT,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,UAAM,MAAM,OAAO,KAAK,MAAM,KAAK,OAAO,IAAI,MAAM,MAAM,CAAC;AAAA,EAC7D;AACA,SAAO;AACT;AAEO,IAAM,iBAAN,MAAqB;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAmC;AAAA,EACnC,QAAyB,CAAC;AAAA,EAC1B;AAAA,EACA,WAAiC;AAAA,EAEzC,YAAY,QAA8B;AACxC,SAAK,SAAS;AACd,SAAK,MAAM,OAAO,QAAQ,MAAM,KAAK,IAAI;AACzC,SAAK,YAAY,OAAO,cAAc,IAAI,SAAS,MAAM,GAAG,IAAI;AAChE,SAAK,UACH,OAAO,YACN,OAAO,WAAW,eAAe,OAAO,eAAe,OAAO,eAAe;AAEhF,SAAK,QAAQ,KAAK,UAAU;AAC5B,SAAK,YAAY,qBAAqB;AAAA,MACpC,SAAS,KAAK,WAAW,sBAAsB;AAAA,MAC/C,KAAK,KAAK;AAAA,MACV,OAAO,OAAO;AAAA,IAChB,CAAC;AAED,QAAI,OAAO,gBAAgB;AACzB,WAAK,WAAW,OAAO;AACvB,WAAK,gBAAgB,OAAO,cAAc;AAAA,IAC5C,OAAO;AACL,WAAK,WAAW,KAAK,aAAa;AAAA,IACpC;AAAA,EACF;AAAA,EAEA,kBAAkB,cAAsB,QAAuC;AAC7E,UAAM,WAAW,KAAK,cAAc;AACpC,QAAI,CAAC,SAAU,QAAO;AAEtB,UAAM,aAAa,SAAS,YAAY,YAAY;AACpD,QAAI,CAAC,cAAc,WAAW,SAAS,WAAY,QAAO;AAE1D,UAAM,MAAM,GAAG,YAAY,IAAI,MAAM;AACrC,UAAM,YAAY,KAAK,cAAc;AACrC,UAAM,SAAS,UAAU,GAAG;AAC5B,QAAI,UAAU,OAAO,YAAY,KAAK,IAAI,GAAG;AAC3C,YAAM,OAAO,WAAW,WAAW,KAAK,CAAC,MAAM,EAAE,OAAO,OAAO,WAAW;AAC1E,UAAI,MAAM;AACR,eAAO;AAAA,UACL,oBAAoB,OAAO;AAAA,UAC3B;AAAA,UACA,aAAa,KAAK;AAAA,UAClB,QAAQ,KAAK;AAAA,QACf;AAAA,MACF;AAAA,IACF;AAEA,UAAM,YAAY,cAAc,WAAW,YAAY,KAAK,OAAO,CAAC;AACpE,QAAI,CAAC,UAAW,QAAO;AAEvB,UAAM,qBAAqB,SAAS;AACpC,UAAM,aAAa,WAAW,iBAAiB;AAC/C,cAAU,GAAG,IAAI;AAAA,MACf;AAAA,MACA,aAAa,UAAU;AAAA,MACvB,WAAW,KAAK,IAAI,IAAI,aAAa;AAAA,IACvC;AACA,SAAK,eAAe,SAAS;AAE7B,SAAK,MAAM,KAAK;AAAA,MACd;AAAA,MACA;AAAA,MACA,aAAa,UAAU;AAAA,MACvB;AAAA,MACA,WAAW,KAAK;AAAA,IAClB,CAAC;AACD,SAAK,aAAa;AAElB,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,aAAa,UAAU;AAAA,MACvB,QAAQ,UAAU;AAAA,IACpB;AAAA,EACF;AAAA,EAEA,MAAM,UAAyB;AAC7B,UAAM,WAAW,MAAM,KAAK,UAAU,GAAG,KAAK,OAAO,MAAM,aAAa;AAAA,MACtE,QAAQ;AAAA,MACR,SAAS,EAAE,aAAa,KAAK,OAAO,eAAe;AAAA,IACrD,CAAC;AACD,QAAI,CAAC,SAAS,GAAI;AAClB,UAAM,WAAY,MAAM,SAAS,KAAK;AACtC,SAAK,WAAW;AAChB,SAAK,gBAAgB,QAAQ;AAAA,EAC/B;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI,KAAK,SAAU,QAAO,KAAK;AAC/B,SAAK,WAAW,KAAK,QAAQ,EAAE,QAAQ,MAAM;AAC3C,WAAK,WAAW;AAAA,IAClB,CAAC;AACD,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,UAAyB;AACrC,QAAI,KAAK,MAAM,WAAW,EAAG;AAC7B,UAAM,UAAU,KAAK,MAAM,MAAM,GAAG,GAAI;AAExC,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,UAAU,GAAG,KAAK,OAAO,MAAM,uBAAuB;AAAA,QAChF,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,aAAa,KAAK,OAAO;AAAA,QAC3B;AAAA,QACA,MAAM,KAAK,UAAU,EAAE,QAAQ,CAAC;AAAA,QAChC,WAAW;AAAA,MACb,CAAC;AACD,UAAI,CAAC,SAAS,GAAI;AAAA,IACpB,QAAQ;AACN;AAAA,IACF;AAEA,SAAK,QAAQ,KAAK,MAAM,MAAM,QAAQ,MAAM;AAC5C,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,eAAuB;AACrB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,qBAA6B;AAC3B,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA,EAEQ,gBAAwC;AAC9C,QAAI,CAAC,KAAK,SAAU,QAAO;AAC3B,QAAI,KAAK,SAAS,QAAQ,EAAG,QAAO;AACpC,UAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,SAAS;AACzC,QAAI,QAAQ,KAAK,SAAS,MAAM,IAAM,QAAO;AAC7C,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,eAAuC;AAC7C,QAAI,CAAC,KAAK,QAAS,QAAO;AAC1B,QAAI;AACF,YAAM,MAAM,KAAK,QAAQ,QAAQ,WAAW;AAC5C,aAAO,MAAO,KAAK,MAAM,GAAG,IAAwB;AAAA,IACtD,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEQ,gBAAgB,UAAiC;AACvD,QAAI,CAAC,KAAK,QAAS;AACnB,QAAI;AACF,WAAK,QAAQ,QAAQ,aAAa,KAAK,UAAU,QAAQ,CAAC;AAAA,IAC5D,QAAQ;AAAA,IAAC;AAAA,EACX;AAAA,EAEQ,YAA6B;AACnC,QAAI,CAAC,KAAK,QAAS,QAAO,CAAC;AAC3B,QAAI;AACF,YAAM,MAAM,KAAK,QAAQ,QAAQ,gBAAgB;AACjD,aAAO,MAAO,KAAK,MAAM,GAAG,IAAwB,CAAC;AAAA,IACvD,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA,EAEQ,gBAAgD;AACtD,QAAI,CAAC,KAAK,QAAS,QAAO,CAAC;AAC3B,QAAI;AACF,YAAM,MAAM,KAAK,QAAQ,QAAQ,aAAa;AAC9C,UAAI,CAAC,IAAK,QAAO,CAAC;AAClB,YAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,OAAuC,CAAC;AAC9C,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,YAAI,KAAK,EAAE,YAAY,IAAK,MAAK,CAAC,IAAI;AAAA,MACxC;AACA,aAAO;AAAA,IACT,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA,EAEQ,eAAe,WAAiD;AACtE,QAAI,CAAC,KAAK,QAAS;AACnB,QAAI;AACF,WAAK,QAAQ,QAAQ,eAAe,KAAK,UAAU,SAAS,CAAC;AAAA,IAC/D,QAAQ;AAAA,IAAC;AAAA,EACX;AAAA,EAEQ,eAAqB;AAC3B,QAAI,CAAC,KAAK,QAAS;AACnB,QAAI;AACF,WAAK,QAAQ,QAAQ,kBAAkB,KAAK,UAAU,KAAK,KAAK,CAAC;AAAA,IACnE,QAAQ;AAAA,IAAC;AAAA,EACX;AACF;;;ACvQA,IAAM,qBAAqB;AAE3B,IAAM,oBAAoB;AAmB1B,SAAS,YAAY,KAAsB;AACzC,QAAM,UAAU,IAAI,KAAK;AACzB,MAAI,CAAC,WAAW,mBAAmB,KAAK,OAAO,EAAG,QAAO;AACzD,SAAO,gBAAgB,KAAK,OAAO;AACrC;AAEA,SAAS,YAAY,KAAa,OAAuB;AACvD,QAAM,YAAY,IAAI,QAAQ,GAAG;AACjC,QAAM,WAAW,cAAc,KAAK,KAAK,IAAI,MAAM,SAAS;AAC5D,QAAM,OAAO,cAAc,KAAK,MAAM,IAAI,MAAM,GAAG,SAAS;AAE5D,QAAM,YAAY,KAAK,SAAS,GAAG,IAAK,KAAK,SAAS,OAAO,IAAI,UAAU,MAAO;AAElF,SAAO,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,IAAI,KAAK,GAAG,QAAQ;AACpE;AAEA,SAAS,YAAY,MAAwB;AAC3C,QAAM,UAAoB,CAAC;AAC3B,MAAI,IAAI;AAER,SAAO,IAAI,KAAK,QAAQ;AACtB,UAAM,KAAK,KAAK,QAAQ,KAAK,CAAC;AAC9B,QAAI,OAAO,GAAI;AAEf,QAAI,KAAK,WAAW,QAAQ,EAAE,GAAG;AAC/B,UAAI,KAAK,WAAW,WAAW,EAAE,KAAK,KAAK,WAAW,iBAAiB,EAAE,GAAG;AAC1E,cAAM,QAAQ,KAAK,QAAQ,KAAK,EAAE;AAClC,YAAI,UAAU,KAAK,KAAK,SAAS,QAAQ;AACzC;AAAA,MACF;AAEA,YAAM,MAAM,KAAK,QAAQ,OAAO,EAAE;AAClC,UAAI,QAAQ,KAAK,KAAK,SAAS,MAAM;AACrC;AAAA,IACF;AAEA,UAAM,UAAU,sBAAsB,KAAK,KAAK,MAAM,IAAI,KAAK,CAAC,CAAC;AACjE,QAAI,SAAS;AACX,YAAM,UAAU,IAAI,OAAO,KAAK,QAAQ,CAAC,CAAC,SAAS,GAAG;AACtD,YAAM,OAAO,KAAK,MAAM,EAAE;AAC1B,YAAM,QAAQ,QAAQ,KAAK,IAAI;AAC/B,UAAI,QAAQ,KAAK,MAAM,QAAQ,MAAM,CAAC,EAAE,SAAS,KAAK;AACtD;AAAA,IACF;AAEA,UAAM,gBACJ,aAAa,KAAK,KAAK,MAAM,IAAI,KAAK,CAAC,CAAC,KACxC,uBAAuB,KAAK,KAAK,MAAM,IAAI,KAAK,EAAE,CAAC;AACrD,QAAI,CAAC,eAAe;AAClB,UAAI,KAAK;AACT;AAAA,IACF;AAEA,UAAM,SAAS,WAAW,MAAM,EAAE;AAClC,QAAI,WAAW,GAAI;AAEnB,UAAM,MAAM,KAAK,MAAM,IAAI,MAAM;AACjC,QAAI,CAAC,IAAI,OAAO,mBAAmB,GAAG,EAAE,KAAK,GAAG,GAAG;AACjD,YAAM,OAAO,SAAS,MAAM,IAAI,MAAM;AACtC,UAAI,QAAQ,YAAY,KAAK,GAAG,EAAG,SAAQ,KAAK,IAAI;AAAA,IACtD;AAEA,QAAI;AAAA,EACN;AAEA,SAAO;AACT;AAEA,SAAS,WAAW,MAAc,OAAuB;AACvD,MAAI,QAAuB;AAE3B,WAAS,IAAI,QAAQ,GAAG,IAAI,KAAK,QAAQ,KAAK;AAC5C,UAAM,KAAK,KAAK,CAAC;AACjB,QAAI,OAAO;AACT,UAAI,OAAO,MAAO,SAAQ;AAC1B;AAAA,IACF;AACA,QAAI,OAAO,OAAO,OAAO,KAAK;AAC5B,cAAQ;AACR;AAAA,IACF;AACA,QAAI,OAAO,IAAK,QAAO,IAAI;AAAA,EAC7B;AAEA,SAAO;AACT;AAEA,SAAS,SAAS,MAAc,UAAkB,QAA+B;AAC/E,QAAM,MAAM,KAAK,MAAM,UAAU,MAAM;AACvC,QAAM,QAAQ,gDAAgD,KAAK,GAAG;AACtE,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,SAAS,MAAM,CAAC,MAAM,UAAa,MAAM,CAAC,MAAM;AACtD,QAAM,QAAQ,MAAM,CAAC,KAAK,MAAM,CAAC,KAAK,MAAM,CAAC,KAAK;AAElD,QAAM,aAAa,WAAW,MAAM,QAAQ,MAAM,CAAC,EAAE,QAAQ,MAAM,CAAC,CAAC,KAAK,SAAS,IAAI;AAEvF,SAAO,EAAE,YAAY,UAAU,aAAa,MAAM,QAAQ,KAAK,MAAM;AACvE;AAEO,SAAS,kBAAkB,MAAwB;AACxD,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,OAAiB,CAAC;AAExB,aAAW,UAAU,YAAY,IAAI,GAAG;AACtC,QAAI,KAAK,IAAI,OAAO,GAAG,EAAG;AAC1B,SAAK,IAAI,OAAO,GAAG;AACnB,SAAK,KAAK,OAAO,GAAG;AAAA,EACtB;AAEA,SAAO;AACT;AAEO,SAAS,kBAAkB,MAAc,SAAwC;AACtF,QAAM,EAAE,WAAW,OAAO,IAAI;AAE9B,QAAM,aAAa,oBAAI,IAAoB;AAC3C,YAAU,QAAQ,CAAC,KAAK,UAAU;AAChC,QAAI,CAAC,WAAW,IAAI,GAAG,EAAG,YAAW,IAAI,KAAK,KAAK;AAAA,EACrD,CAAC;AAED,QAAM,UAAU,YAAY,IAAI;AAChC,QAAM,YAAsB,CAAC;AAC7B,MAAI,YAAY;AAEhB,MAAI,SAAS;AACb,WAAS,IAAI,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;AAC5C,UAAM,SAAS,QAAQ,CAAC;AACxB,UAAM,QAAQ,WAAW,IAAI,OAAO,GAAG;AAEvC,QAAI,UAAU,UAAa,OAAO,KAAK,MAAM,QAAW;AACtD,gBAAU,KAAK,OAAO,GAAG;AACzB;AAAA,IACF;AAEA,aACE,OAAO,MAAM,GAAG,OAAO,UAAU,IACjC,YAAY,OAAO,KAAK,OAAO,KAAK,CAAC,IACrC,OAAO,MAAM,OAAO,QAAQ;AAC9B;AAAA,EACF;AAEA,SAAO,EAAE,MAAM,QAAQ,WAAW,WAAW,UAAU,QAAQ,EAAE;AACnE;;;ACpKA,IAAM,eAAe;AAed,SAAS,YAAY,cAAsB,SAA4C;AAC5F,QAAM,eAAyB,CAAC;AAEhC,QAAM,cAAc,aAAa,QAAQ,cAAc,CAAC,OAAO,SAAiB;AAC9E,UAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAI,UAAU,QAAW;AACvB,mBAAa,KAAK,IAAI;AACtB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,CAAC;AAED,QAAM,EAAE,MAAM,WAAW,UAAU,IAAI,kBAAkB,aAAa;AAAA,IACpE,WAAW,QAAQ;AAAA,IACnB,QAAQ,QAAQ;AAAA,EAClB,CAAC;AAED,SAAO,EAAE,MAAM,WAAW,WAAW,aAAa;AACpD;AAEO,SAAS,iBAAiB,cAAgC;AAC/D,QAAM,QAAQ,oBAAI,IAAY;AAC9B,aAAW,SAAS,aAAa,SAAS,YAAY,EAAG,OAAM,IAAI,MAAM,CAAC,CAAC;AAC3E,SAAO,CAAC,GAAG,KAAK;AAClB;;;AC1BA,eAAsB,eACpB,MACA,aACA,cACA,eAC2B;AAC3B,QAAM,QAAgC,CAAC;AACvC,QAAM,cAAwB,CAAC;AAE/B,WAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,UAAM,OAAO,KAAK,MAAM,CAAC;AACzB,UAAM,SAAS,QAAQ,KAAK,eAAe,MAAM,WAAW,KAAK,MAAM,aAAa,CAAC,CAAC;AACtF,UAAM,QAAQ,WAAW,KAAK,IAAI;AAElC,gBAAY,KAAK,KAAK,KAAK,KAAK,GAAG,YAAY,CAAC;AAEhD,UAAM,SAAS,KAAK,QAAQ,KAAK;AACjC,UAAM,KAAK,IAAI,IACb,OAAO,WAAW,WACd,SACE,QAA4D,WAC7D,QAA4D,QAC7D;AAAA,EACR;AAEA,QAAM,gBAAgB,MAAM,cAAc,aAAa,aAAa;AAEpE,QAAM,SAAS,MAAM,QAAQ;AAAA,IAC3B,KAAK,UAAU;AAAA,MAAI,CAAC,GAAG,cACrB;AAAA,QACE,EAAE,GAAG,KAAK,YAAY,GAAG,eAAe,GAAG,aAAa,GAAG,UAAU;AAAA,QACrE,KAAK;AAAA,MACP;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAW,YAAY,cAAc;AAAA,IACzC;AAAA,IACA,WAAW,KAAK;AAAA,IAChB;AAAA,EACF,CAAC;AAED,SAAO,EAAE,GAAG,UAAU,aAAa,OAAO;AAC5C;AAEO,SAAS,kBACd,MACA,6BACA,MAAc,KAAK,IAAI,GACd;AACT,SAAO,+BAA+B,KAAK,gBAAgB,OAAO,KAAK;AACzE;AAEA,eAAsB,sBACpB,MACA,cACA,SAC0D;AAC1D,QAAM,SAAS,MAAM,mBAAmB,MAAM,YAAY;AAC1D,MAAI,OAAO,WAAW,EAAG,QAAO,EAAE,UAAU,OAAO,iBAAiB,MAAM;AAE1E,QAAM,WAAW,MAAM,MAAM,GAAG,QAAQ,MAAM,0BAA0B;AAAA,IACtE,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,oBAAoB,aAAa,QAAQ,OAAO;AAAA,IAC3E,MAAM,KAAK,UAAU;AAAA,MACnB,YAAY,KAAK;AAAA,MACjB,UAAU,QAAQ,YAAa,MAAM,YAAY,KAAK,YAAY,YAAY;AAAA,MAC9E;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAED,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,iCAAiC,SAAS,MAAM,EAAE;AAAA,EACpE;AAEA,QAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,SAAO,KAAK,QAAQ,EAAE,UAAU,OAAO,iBAAiB,MAAM;AAChE;AAEA,eAAe,YAAY,YAAoB,cAAyC;AACtF,QAAM,WAAW,GAAG,UAAU,IAAI,CAAC,GAAG,YAAY,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC;AACpE,QAAM,SAAS,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI,YAAY,EAAE,OAAO,QAAQ,CAAC;AACvF,SAAO,MAAM,KAAK,IAAI,WAAW,MAAM,EAAE,MAAM,GAAG,EAAE,CAAC,EAClD,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAC1C,KAAK,EAAE;AACZ;","names":["ExperimentStatus","BanditAlgorithm","Posterior","ForgettingMode","ContentType","FunnelStatus","generateId"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runbandit/sdk",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@runbandit/shared": "workspace:*",
|
|
27
27
|
"tsup": "^8.0.0",
|
|
28
|
-
"typescript": "^5.
|
|
28
|
+
"typescript": "^5.9.3",
|
|
29
29
|
"vitest": "^4.0.18"
|
|
30
30
|
}
|
|
31
31
|
}
|