@variantlab/next 0.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.
@@ -0,0 +1,318 @@
1
+ import { ExperimentsConfig, VariantContext } from '@variantlab/core';
2
+ export { AssignmentStrategy, Experiment, ExperimentsConfig, Variant, VariantContext } from '@variantlab/core';
3
+ import { ReactNode } from 'react';
4
+
5
+ /**
6
+ * Shared types for `@variantlab/next`.
7
+ *
8
+ * Kept in a single tiny file so every entrypoint (server barrel,
9
+ * app-router subpath, pages-router subpath, client provider) can
10
+ * import without pulling in code.
11
+ */
12
+
13
+ /**
14
+ * On-wire cookie payload shape. Short keys to minimize cookie bytes.
15
+ *
16
+ * v — schema version (currently 1)
17
+ * u — userId (required; generated on first visit by middleware)
18
+ * a — assignments: { [experimentId]: variantId } (may be empty)
19
+ */
20
+ interface StickyCookiePayload {
21
+ readonly v: 1;
22
+ readonly u: string;
23
+ readonly a: Readonly<Record<string, string>>;
24
+ }
25
+ /**
26
+ * Anything from which we can read an HTTP Cookie header. Supports:
27
+ *
28
+ * - Fetch-style `Request` (App Router Route Handlers, middleware)
29
+ * - `NextApiRequest` / Pages Router `req` (has `.cookies` and `.headers`)
30
+ * - Next 14+ `ReadonlyRequestCookies` from `cookies()` in `next/headers`
31
+ * - A plain cookie header string
32
+ */
33
+ type CookieSource = Request | RequestCookieJar | PagesRouterRequestLike | string | null | undefined;
34
+ /**
35
+ * Shape we rely on from Next's `cookies()` return value. Kept minimal
36
+ * so we don't depend on `next/headers` types at compile time — the
37
+ * server barrel must be framework-agnostic enough to tree-shake.
38
+ */
39
+ interface RequestCookieJar {
40
+ get(name: string): {
41
+ readonly value: string;
42
+ } | undefined;
43
+ }
44
+ /**
45
+ * Minimal Pages Router / `NextApiRequest` shape. Avoids importing
46
+ * `next` types in the base entrypoint.
47
+ */
48
+ interface PagesRouterRequestLike {
49
+ readonly cookies?: Readonly<Record<string, string | undefined>>;
50
+ readonly headers?: Readonly<Record<string, string | string[] | undefined>>;
51
+ }
52
+ /**
53
+ * Options accepted by `createVariantLabServer` and the SSR helpers.
54
+ */
55
+ interface VariantLabServerOptions {
56
+ /** Cookie name. Defaults to `__variantlab_sticky`. */
57
+ readonly cookieName?: string;
58
+ /** Max age in seconds. Defaults to 365 days. */
59
+ readonly maxAge?: number;
60
+ /** Cookie path. Defaults to `/`. */
61
+ readonly path?: string;
62
+ /** `SameSite` attribute. Defaults to `"lax"`. */
63
+ readonly sameSite?: "strict" | "lax" | "none";
64
+ /** Override for the `Secure` flag. When `undefined`, derived from the request URL. */
65
+ readonly secure?: boolean;
66
+ /** `HttpOnly` flag. Defaults to `true`. */
67
+ readonly httpOnly?: boolean;
68
+ /** `Domain` attribute. Defaults to undefined (host-only cookie). */
69
+ readonly domain?: string;
70
+ }
71
+ /**
72
+ * Props accepted by the Next `VariantLabProvider` Client Component.
73
+ */
74
+ interface VariantLabProviderProps {
75
+ /** Validated `ExperimentsConfig` or a raw JSON module import. */
76
+ readonly config: unknown | ExperimentsConfig;
77
+ /** Runtime context (userId, locale, platform, …) applied before first render. */
78
+ readonly initialContext?: VariantContext;
79
+ /**
80
+ * Assignments computed on the server. Seeded into the engine cache so
81
+ * the first `getVariant` call on the client returns the same variant
82
+ * that was server-rendered, without re-evaluating targeting.
83
+ */
84
+ readonly initialVariants?: Readonly<Record<string, string>>;
85
+ readonly children?: ReactNode;
86
+ }
87
+ /** Default cookie name. Shared between server helpers and middleware. */
88
+ declare const DEFAULT_COOKIE_NAME = "__variantlab_sticky";
89
+ /** Default max age (365 days, in seconds). */
90
+ declare const DEFAULT_MAX_AGE: number;
91
+
92
+ /**
93
+ * Hand-rolled cookie codec + parser for `@variantlab/next`.
94
+ *
95
+ * Goals:
96
+ * 1. Zero runtime dependencies. No `cookie` package, no `js-base64`.
97
+ * 2. Edge-runtime safe. Uses only `TextEncoder`/`TextDecoder`,
98
+ * `globalThis.crypto`, standard `atob`/`btoa`, and `Object.create(null)`
99
+ * — all available on Vercel Edge, Cloudflare Workers, Deno, Bun, Node 18+.
100
+ * 3. Prototype-pollution hardened. Mirrors the guards in
101
+ * `packages/core/src/config/validator.ts`: cookie parser rejects
102
+ * `__proto__`, `constructor`, `prototype` as cookie names, and the
103
+ * JSON payload is parsed through `Object.create(null)` sanitization.
104
+ * 4. Size-capped. A malicious client sending a 10 MB Cookie header gets
105
+ * rejected before allocation by `MAX_COOKIE_HEADER_BYTES`.
106
+ */
107
+
108
+ /**
109
+ * Encode a `StickyCookiePayload` to its on-wire value. Does NOT include
110
+ * the cookie name or attributes — see {@link serializeCookie}.
111
+ */
112
+ declare function encodePayload(payload: StickyCookiePayload): string;
113
+ /**
114
+ * Decode a base64url-encoded cookie value back to a `StickyCookiePayload`.
115
+ * Returns `null` for anything that fails validation. Never throws.
116
+ */
117
+ declare function decodePayload(raw: string | undefined | null): StickyCookiePayload | null;
118
+ /**
119
+ * Parse a raw `Cookie:` header into a `null`-prototype map of
120
+ * `name → value`. Tolerates leading whitespace, empty segments,
121
+ * missing `=`, and rejects reserved names.
122
+ */
123
+ declare function parseCookieHeader(header: string | undefined | null): Record<string, string>;
124
+ /**
125
+ * Build a `Set-Cookie` header value: `name=value; attr=...; ...`.
126
+ * Cookie value is URL-encoded so any non-ASCII bytes round-trip through
127
+ * a standards-compliant parser.
128
+ */
129
+ declare function serializeCookie(name: string, value: string, options?: VariantLabServerOptions & {
130
+ readonly secure?: boolean;
131
+ }): string;
132
+ /**
133
+ * Read a named cookie from any supported source: `Request`, Next's
134
+ * `ReadonlyRequestCookies`, a Pages Router `NextApiRequest`, or a
135
+ * raw header string. Returns `undefined` when missing.
136
+ */
137
+ declare function readCookieFromSource(source: CookieSource, name?: string): string | undefined;
138
+ /**
139
+ * Full helper: read the sticky payload from a source, returning
140
+ * `null` if the cookie is missing, malformed, or fails validation.
141
+ */
142
+ declare function readPayloadFromSource(source: CookieSource, name?: string): StickyCookiePayload | null;
143
+ /**
144
+ * Generate a v4 UUID via `crypto.randomUUID`, falling back to a
145
+ * hand-formatted v4 built from `crypto.getRandomValues`. Both APIs
146
+ * are Web Crypto and are available on every Phase 1 target runtime.
147
+ */
148
+ declare function generateUserId(): string;
149
+
150
+ /**
151
+ * `createVariantLabServer(config, options?)` — factory that validates the
152
+ * config once and returns request-scoped helpers. Each call to
153
+ * `getVariant` / `getVariantValue` constructs a short-lived engine
154
+ * seeded from the request's cookie so concurrent HTTP requests never
155
+ * share mutable state.
156
+ *
157
+ * Validation is expensive; engine construction is cheap (a few Maps,
158
+ * no I/O, no allocation beyond what's already in the frozen config).
159
+ */
160
+
161
+ interface VariantLabServer {
162
+ /** The frozen, validated config this server was built with. */
163
+ readonly config: ExperimentsConfig;
164
+ /**
165
+ * Resolve a variant id using the given cookie source + optional
166
+ * context extras. Always returns the default if anything fails.
167
+ */
168
+ getVariant(experimentId: string, source: CookieSource, context?: VariantContext): string;
169
+ /**
170
+ * Resolve a variant's `value` payload. Equivalent to calling
171
+ * `getVariant` + looking up the variant by id.
172
+ */
173
+ getVariantValue<T = unknown>(experimentId: string, source: CookieSource, context?: VariantContext): T;
174
+ /**
175
+ * Decode the sticky cookie. Returns `null` when missing or invalid.
176
+ * Layouts pass this directly to `<VariantLabProvider initialVariants={...}>`.
177
+ */
178
+ readPayload(source: CookieSource): StickyCookiePayload | null;
179
+ /**
180
+ * Build a `Set-Cookie` header value for a freshly-computed payload.
181
+ * Caller is responsible for attaching it to the outgoing response.
182
+ */
183
+ writePayload(payload: StickyCookiePayload, secure?: boolean): string;
184
+ /**
185
+ * Build the `initialContext` + `initialVariants` props that the
186
+ * Next `<VariantLabProvider>` needs. Convenience for layouts:
187
+ *
188
+ * const props = server.toProviderProps(cookies(), { locale: "en" });
189
+ * return <VariantLabProvider {...props}>{children}</VariantLabProvider>;
190
+ */
191
+ toProviderProps(source: CookieSource, contextExtras?: VariantContext): Omit<VariantLabProviderProps, "children" | "config">;
192
+ }
193
+ interface CreateVariantLabServerOptions extends VariantLabServerOptions {
194
+ /**
195
+ * Called when cookie reads or engine construction fails catastrophically.
196
+ * Defaults to a no-op (fail-open).
197
+ */
198
+ readonly onError?: (error: Error) => void;
199
+ }
200
+ /**
201
+ * Validate the supplied config once and return a factory whose methods
202
+ * build a new `VariantEngine` per call. This is the entry point that
203
+ * layouts, server components, route handlers, and `getServerSideProps`
204
+ * should use when they want to resolve variants at SSR time.
205
+ */
206
+ declare function createVariantLabServer(rawConfig: unknown, options?: CreateVariantLabServerOptions): VariantLabServer;
207
+
208
+ /**
209
+ * `getVariantSSR` / `getVariantValueSSR` — per-request SSR helpers that
210
+ * don't require the caller to hold a `VariantLabServer` instance.
211
+ *
212
+ * Internally they use a `WeakMap` keyed on the raw config object identity
213
+ * so repeat calls with the same imported JSON module don't re-validate
214
+ * the config. The WeakMap doesn't retain anything — if the caller drops
215
+ * the config reference, the cached server is GC'd.
216
+ *
217
+ * Signature matches `API.md` lines 569–582 (synchronous). See the
218
+ * package README for notes on spec drift vs. the Session 7 prompt.
219
+ */
220
+
221
+ /**
222
+ * Resolve a variant for the current request. Reads the sticky cookie
223
+ * from the supplied source (`Request`, `ReadonlyRequestCookies`,
224
+ * `NextApiRequest`, or a raw cookie header string).
225
+ *
226
+ * Synchronous. In Next 15, `cookies()` is async — await it and pass the
227
+ * resolved store into this function.
228
+ */
229
+ declare function getVariantSSR(experimentId: string, source: CookieSource, config: unknown | ExperimentsConfig, options?: CreateVariantLabServerOptions & {
230
+ readonly context?: VariantContext;
231
+ }): string;
232
+ /** Variant-value equivalent of {@link getVariantSSR}. */
233
+ declare function getVariantValueSSR<T = unknown>(experimentId: string, source: CookieSource, config: unknown | ExperimentsConfig, options?: CreateVariantLabServerOptions & {
234
+ readonly context?: VariantContext;
235
+ }): T;
236
+
237
+ /**
238
+ * `variantLabMiddleware(config, options?)` — Next.js middleware factory.
239
+ *
240
+ * Responsibilities (Phase 1):
241
+ * 1. Read the sticky cookie from the incoming request.
242
+ * 2. If missing or malformed, mint a fresh userId and write an
243
+ * empty payload `{ v: 1, u: userId, a: {} }` on the outgoing
244
+ * response. Assignments are NOT computed at the edge.
245
+ * 3. Fail-open: any error → pass through unmodified.
246
+ *
247
+ * The factory takes the raw config only so it can validate it once at
248
+ * import time — middleware is instantiated per process, not per request.
249
+ * It does not resolve any experiments during middleware execution.
250
+ *
251
+ * Designed to run on the Vercel Edge runtime (`export const runtime = "edge"`).
252
+ * No Node-only APIs. No `process.env` access.
253
+ */
254
+
255
+ /**
256
+ * The minimal shape we need from `NextRequest`. Avoids a hard
257
+ * dependency on `next/server` at type-check time.
258
+ */
259
+ interface NextRequestLike {
260
+ readonly headers: {
261
+ get(name: string): string | null;
262
+ };
263
+ readonly nextUrl: {
264
+ readonly protocol: string;
265
+ };
266
+ }
267
+ /**
268
+ * The minimal shape we produce / consume for `NextResponse`. Matches
269
+ * `NextResponse.next()` / `NextResponse.redirect()` return values.
270
+ */
271
+ interface NextResponseLike {
272
+ readonly headers: {
273
+ append(name: string, value: string): void;
274
+ };
275
+ }
276
+ interface VariantLabMiddlewareOptions extends VariantLabServerOptions {
277
+ /**
278
+ * Called on any caught error. Defaults to a no-op so the middleware
279
+ * remains fail-open. Provide a logger here if you want visibility.
280
+ */
281
+ readonly onError?: (error: Error) => void;
282
+ }
283
+ /**
284
+ * Build a middleware function. Accepts the raw response factory from
285
+ * the caller so we don't need to import `NextResponse` directly (that
286
+ * would drag `next/server` into every consumer's bundle).
287
+ *
288
+ * Typical usage in `middleware.ts`:
289
+ *
290
+ * import { NextResponse } from "next/server";
291
+ * import experiments from "./experiments.json";
292
+ * import { variantLabMiddleware } from "@variantlab/next";
293
+ *
294
+ * const middleware = variantLabMiddleware(experiments);
295
+ *
296
+ * export default function (req) {
297
+ * return middleware(req, NextResponse.next());
298
+ * }
299
+ *
300
+ * The factory also exports `middleware.handle(req, nextResponseFactory)`
301
+ * for the more idiomatic style where NextResponse is only imported once.
302
+ */
303
+ declare function variantLabMiddleware(rawConfig: unknown, options?: VariantLabMiddlewareOptions): <TResponse extends NextResponseLike>(req: NextRequestLike, response: TResponse) => TResponse;
304
+
305
+ /**
306
+ * `@variantlab/next` — server barrel.
307
+ *
308
+ * This file is the default entrypoint when consumers write
309
+ * `import { ... } from "@variantlab/next"`. It exposes only
310
+ * server-safe helpers (no `"use client"`, no React). Use
311
+ * `@variantlab/next/client` for the provider and hooks.
312
+ *
313
+ * Edge-runtime compatible: no Node-only APIs, no `process.env`, and
314
+ * no runtime dependencies beyond `@variantlab/core`.
315
+ */
316
+ declare const VERSION = "0.0.0";
317
+
318
+ export { type CookieSource, type CreateVariantLabServerOptions, DEFAULT_COOKIE_NAME, DEFAULT_MAX_AGE, type PagesRouterRequestLike, type RequestCookieJar, type StickyCookiePayload, VERSION, type VariantLabMiddlewareOptions, type VariantLabProviderProps, type VariantLabServer, type VariantLabServerOptions, createVariantLabServer, decodePayload, encodePayload, generateUserId, getVariantSSR, getVariantValueSSR, parseCookieHeader, readCookieFromSource, readPayloadFromSource, serializeCookie, variantLabMiddleware };
package/dist/index.js ADDED
@@ -0,0 +1,304 @@
1
+ import { validateConfig, createEngine } from '@variantlab/core';
2
+
3
+ // src/types.ts
4
+ var DEFAULT_COOKIE_NAME = "__variantlab_sticky";
5
+ var DEFAULT_MAX_AGE = 60 * 60 * 24 * 365;
6
+
7
+ // src/server/cookie.ts
8
+ var MAX_COOKIE_HEADER_BYTES = 8192;
9
+ var MAX_PAYLOAD_BYTES = 4096;
10
+ var RESERVED_NAMES = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
11
+ var textEncoder = new TextEncoder();
12
+ var textDecoder = new TextDecoder();
13
+ function base64urlEncode(input) {
14
+ const bytes = textEncoder.encode(input);
15
+ let binary = "";
16
+ for (let i = 0; i < bytes.length; i++) {
17
+ binary += String.fromCharCode(bytes[i]);
18
+ }
19
+ const b64 = btoa(binary);
20
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
21
+ }
22
+ function base64urlDecode(input) {
23
+ if (!/^[A-Za-z0-9\-_]*$/.test(input)) return null;
24
+ const pad = input.length % 4;
25
+ const padded = input.replace(/-/g, "+").replace(/_/g, "/") + "====".slice(pad === 0 ? 4 : pad);
26
+ try {
27
+ const binary = atob(padded);
28
+ const bytes = new Uint8Array(binary.length);
29
+ for (let i = 0; i < binary.length; i++) {
30
+ bytes[i] = binary.charCodeAt(i);
31
+ }
32
+ return textDecoder.decode(bytes);
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function encodePayload(payload) {
38
+ const json = JSON.stringify({ v: payload.v, u: payload.u, a: payload.a });
39
+ return base64urlEncode(json);
40
+ }
41
+ function decodePayload(raw) {
42
+ if (typeof raw !== "string" || raw.length === 0) return null;
43
+ if (raw.length > MAX_PAYLOAD_BYTES) return null;
44
+ const json = base64urlDecode(raw);
45
+ if (json === null) return null;
46
+ if (json.length > MAX_PAYLOAD_BYTES) return null;
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(json);
50
+ } catch {
51
+ return null;
52
+ }
53
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
54
+ const obj = parsed;
55
+ if (obj["v"] !== 1) return null;
56
+ if (typeof obj["u"] !== "string" || obj["u"].length === 0 || obj["u"].length > 256) return null;
57
+ const rawA = obj["a"];
58
+ if (rawA === null || typeof rawA !== "object" || Array.isArray(rawA)) return null;
59
+ const src = rawA;
60
+ const a = /* @__PURE__ */ Object.create(null);
61
+ for (const key of Object.keys(src)) {
62
+ if (RESERVED_NAMES.has(key)) continue;
63
+ if (key.length === 0 || key.length > 128) continue;
64
+ const val = src[key];
65
+ if (typeof val !== "string" || val.length === 0 || val.length > 128) continue;
66
+ a[key] = val;
67
+ }
68
+ return { v: 1, u: obj["u"], a };
69
+ }
70
+ function parseCookieHeader(header) {
71
+ const out = /* @__PURE__ */ Object.create(null);
72
+ if (typeof header !== "string" || header.length === 0) return out;
73
+ if (header.length > MAX_COOKIE_HEADER_BYTES) return out;
74
+ let i = 0;
75
+ const n = header.length;
76
+ while (i < n) {
77
+ while (i < n && (header.charCodeAt(i) === 32 || header.charCodeAt(i) === 9)) i++;
78
+ const start = i;
79
+ while (i < n && header.charCodeAt(i) !== 59) i++;
80
+ const segment = header.slice(start, i);
81
+ if (i < n) i++;
82
+ if (segment.length === 0) continue;
83
+ const eq = segment.indexOf("=");
84
+ if (eq <= 0) continue;
85
+ const name = segment.slice(0, eq).trim();
86
+ if (name.length === 0) continue;
87
+ if (RESERVED_NAMES.has(name)) continue;
88
+ let value = segment.slice(eq + 1).trim();
89
+ if (value.length >= 2 && value.charCodeAt(0) === 34 && value.charCodeAt(value.length - 1) === 34) {
90
+ value = value.slice(1, -1);
91
+ }
92
+ if (out[name] === void 0) {
93
+ out[name] = safeDecodeURIComponent(value);
94
+ }
95
+ }
96
+ return out;
97
+ }
98
+ function safeDecodeURIComponent(s) {
99
+ try {
100
+ return decodeURIComponent(s);
101
+ } catch {
102
+ return s;
103
+ }
104
+ }
105
+ function serializeCookie(name, value, options = {}) {
106
+ const parts = [`${name}=${encodeURIComponent(value)}`];
107
+ const maxAge = options.maxAge ?? DEFAULT_MAX_AGE;
108
+ if (maxAge > 0) parts.push(`Max-Age=${Math.floor(maxAge)}`);
109
+ parts.push(`Path=${options.path ?? "/"}`);
110
+ const sameSite = options.sameSite ?? "lax";
111
+ parts.push(`SameSite=${capitalize(sameSite)}`);
112
+ if (options.httpOnly !== false) parts.push("HttpOnly");
113
+ if (options.secure === true) parts.push("Secure");
114
+ if (options.domain !== void 0) parts.push(`Domain=${options.domain}`);
115
+ return parts.join("; ");
116
+ }
117
+ function capitalize(s) {
118
+ if (s.length === 0) return s;
119
+ return s[0].toUpperCase() + s.slice(1);
120
+ }
121
+ function readCookieFromSource(source, name = DEFAULT_COOKIE_NAME) {
122
+ if (source === null || source === void 0) return void 0;
123
+ if (typeof source === "string") {
124
+ const parsed = parseCookieHeader(source);
125
+ return parsed[name];
126
+ }
127
+ if (typeof source.get === "function") {
128
+ const entry = source.get(name);
129
+ return entry === void 0 ? void 0 : entry.value;
130
+ }
131
+ if (typeof source.headers?.get === "function") {
132
+ const header = source.headers.get("cookie");
133
+ if (header === null) return void 0;
134
+ return parseCookieHeader(header)[name];
135
+ }
136
+ const pagesReq = source;
137
+ if (pagesReq.cookies !== void 0) {
138
+ const fromBag = pagesReq.cookies[name];
139
+ if (typeof fromBag === "string" && fromBag.length > 0) return fromBag;
140
+ }
141
+ if (pagesReq.headers !== void 0) {
142
+ const header = pagesReq.headers["cookie"];
143
+ if (typeof header === "string") return parseCookieHeader(header)[name];
144
+ if (Array.isArray(header) && typeof header[0] === "string") {
145
+ return parseCookieHeader(header[0])[name];
146
+ }
147
+ }
148
+ return void 0;
149
+ }
150
+ function readPayloadFromSource(source, name = DEFAULT_COOKIE_NAME) {
151
+ const raw = readCookieFromSource(source, name);
152
+ return decodePayload(raw);
153
+ }
154
+ function generateUserId() {
155
+ const g = globalThis;
156
+ if (g.crypto?.randomUUID !== void 0) return g.crypto.randomUUID();
157
+ if (g.crypto?.getRandomValues !== void 0) {
158
+ const bytes = new Uint8Array(16);
159
+ g.crypto.getRandomValues(bytes);
160
+ bytes[6] = bytes[6] & 15 | 64;
161
+ bytes[8] = bytes[8] & 63 | 128;
162
+ const hex = [];
163
+ for (let i = 0; i < 16; i++) {
164
+ hex.push(bytes[i].toString(16).padStart(2, "0"));
165
+ }
166
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
167
+ }
168
+ return `u-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
169
+ }
170
+ function createVariantLabServer(rawConfig, options = {}) {
171
+ const config = validateConfig(rawConfig);
172
+ const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;
173
+ const onError = options.onError ?? (() => {
174
+ });
175
+ function readPayload(source) {
176
+ try {
177
+ return readPayloadFromSource(source, cookieName);
178
+ } catch (error) {
179
+ onError(error);
180
+ return null;
181
+ }
182
+ }
183
+ function buildContext(payload, extras) {
184
+ const userId = payload?.u ?? extras?.userId;
185
+ if (userId === void 0) return { ...extras };
186
+ return { ...extras, userId };
187
+ }
188
+ function getVariant(experimentId, source, context) {
189
+ try {
190
+ const payload = readPayload(source);
191
+ const engine = createEngine(config, {
192
+ context: buildContext(payload, context),
193
+ ...payload?.a !== void 0 ? { initialAssignments: payload.a } : {}
194
+ });
195
+ return engine.getVariant(experimentId);
196
+ } catch (error) {
197
+ onError(error);
198
+ return experimentDefault(config, experimentId);
199
+ }
200
+ }
201
+ function getVariantValue(experimentId, source, context) {
202
+ try {
203
+ const payload = readPayload(source);
204
+ const engine = createEngine(config, {
205
+ context: buildContext(payload, context),
206
+ ...payload?.a !== void 0 ? { initialAssignments: payload.a } : {}
207
+ });
208
+ return engine.getVariantValue(experimentId);
209
+ } catch (error) {
210
+ onError(error);
211
+ return void 0;
212
+ }
213
+ }
214
+ function writePayload(payload, secure) {
215
+ const value = encodePayload(payload);
216
+ return serializeCookie(cookieName, value, {
217
+ ...options,
218
+ ...secure !== void 0 ? { secure } : {}
219
+ });
220
+ }
221
+ function toProviderProps(source, contextExtras) {
222
+ const payload = readPayload(source);
223
+ const initialContext = buildContext(payload, contextExtras);
224
+ const initialVariants = payload?.a ? { ...payload.a } : {};
225
+ return { initialContext, initialVariants };
226
+ }
227
+ return {
228
+ config,
229
+ getVariant,
230
+ getVariantValue,
231
+ readPayload,
232
+ writePayload,
233
+ toProviderProps
234
+ };
235
+ }
236
+ function experimentDefault(config, experimentId) {
237
+ const exp = config.experiments.find((e) => e.id === experimentId);
238
+ return exp?.default ?? "";
239
+ }
240
+
241
+ // src/server/get-variant-ssr.ts
242
+ var cache = /* @__PURE__ */ new WeakMap();
243
+ function resolveServer(rawConfig, options) {
244
+ if (rawConfig !== null && typeof rawConfig === "object") {
245
+ const hit = cache.get(rawConfig);
246
+ if (hit !== void 0) return hit;
247
+ }
248
+ const server = createVariantLabServer(rawConfig, options);
249
+ if (rawConfig !== null && typeof rawConfig === "object") {
250
+ cache.set(rawConfig, server);
251
+ }
252
+ return server;
253
+ }
254
+ function getVariantSSR(experimentId, source, config, options) {
255
+ const server = resolveServer(config, options);
256
+ return server.getVariant(experimentId, source, options?.context);
257
+ }
258
+ function getVariantValueSSR(experimentId, source, config, options) {
259
+ const server = resolveServer(config, options);
260
+ return server.getVariantValue(experimentId, source, options?.context);
261
+ }
262
+ function variantLabMiddleware(rawConfig, options = {}) {
263
+ let frozen = true;
264
+ try {
265
+ validateConfig(rawConfig);
266
+ } catch (error) {
267
+ options.onError?.(error);
268
+ frozen = false;
269
+ }
270
+ const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;
271
+ const onError = options.onError ?? (() => {
272
+ });
273
+ return function apply(req, response) {
274
+ if (!frozen) return response;
275
+ try {
276
+ const header = req.headers.get("cookie");
277
+ const cookies = parseCookieHeader(header ?? "");
278
+ const existing = decodePayload(cookies[cookieName]);
279
+ if (existing !== null) return response;
280
+ const payload = {
281
+ v: 1,
282
+ u: generateUserId(),
283
+ a: {}
284
+ };
285
+ const secure = req.nextUrl.protocol === "https:";
286
+ const setCookie = serializeCookie(cookieName, encodePayload(payload), {
287
+ ...options,
288
+ secure
289
+ });
290
+ response.headers.append("set-cookie", setCookie);
291
+ return response;
292
+ } catch (error) {
293
+ onError(error);
294
+ return response;
295
+ }
296
+ };
297
+ }
298
+
299
+ // src/index.ts
300
+ var VERSION = "0.0.0";
301
+
302
+ export { DEFAULT_COOKIE_NAME, DEFAULT_MAX_AGE, VERSION, createVariantLabServer, decodePayload, encodePayload, generateUserId, getVariantSSR, getVariantValueSSR, parseCookieHeader, readCookieFromSource, readPayloadFromSource, serializeCookie, variantLabMiddleware };
303
+ //# sourceMappingURL=index.js.map
304
+ //# sourceMappingURL=index.js.map