@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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/app-router.cjs +325 -0
- package/dist/app-router.cjs.map +1 -0
- package/dist/app-router.d.cts +41 -0
- package/dist/app-router.d.ts +41 -0
- package/dist/app-router.js +309 -0
- package/dist/app-router.js.map +1 -0
- package/dist/client/hooks.cjs +191 -0
- package/dist/client/hooks.cjs.map +1 -0
- package/dist/client/hooks.d.cts +62 -0
- package/dist/client/hooks.d.ts +62 -0
- package/dist/client/hooks.js +178 -0
- package/dist/client/hooks.js.map +1 -0
- package/dist/index.cjs +319 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +318 -0
- package/dist/index.d.ts +318 -0
- package/dist/index.js +304 -0
- package/dist/index.js.map +1 -0
- package/dist/pages-router.cjs +325 -0
- package/dist/pages-router.cjs.map +1 -0
- package/dist/pages-router.d.cts +26 -0
- package/dist/pages-router.d.ts +26 -0
- package/dist/pages-router.js +309 -0
- package/dist/pages-router.js.map +1 -0
- package/package.json +105 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 variantlab contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @variantlab/next
|
|
2
|
+
|
|
3
|
+
Next.js 14 and 15 bindings for variantlab — App Router, Pages Router, and Edge runtime.
|
|
4
|
+
|
|
5
|
+
> **Status:** Phase 1 — Pre-alpha. Not ready for production use.
|
|
6
|
+
|
|
7
|
+
## What this package gives you
|
|
8
|
+
|
|
9
|
+
- **SSR-correct variant resolution.** Resolve variants on the server, seed them into the client engine, and render the same tree twice — no hydration mismatches.
|
|
10
|
+
- **Sticky cookies.** A base64url-encoded `__variantlab_sticky` cookie persists the `userId` (and, optionally, pre-resolved assignments) across requests so every refresh shows the same variants.
|
|
11
|
+
- **Edge-runtime compatible.** No Node-only APIs, no `process.env`, no `cookie` package, no `crypto.createHash`. Runs on Vercel Edge, Cloudflare Workers, and Deno Deploy.
|
|
12
|
+
- **Four subpath entrypoints.** Server-only helpers (`.`), Client Component provider + hooks (`/client`), and router-scoped convenience helpers (`/app-router`, `/pages-router`).
|
|
13
|
+
- **≤ 2 KB gzipped** for the server entrypoint, **≤ 1 KB gzipped** for `/client`.
|
|
14
|
+
|
|
15
|
+
## Peer dependencies
|
|
16
|
+
|
|
17
|
+
- `next` `^14.0.0 || ^15.0.0`
|
|
18
|
+
- `react` `^18.2.0 || ^19.0.0`
|
|
19
|
+
|
|
20
|
+
## Quick start — App Router
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
// middleware.ts
|
|
24
|
+
import { NextResponse } from "next/server";
|
|
25
|
+
import { variantLabMiddleware } from "@variantlab/next";
|
|
26
|
+
import experiments from "./experiments.json";
|
|
27
|
+
|
|
28
|
+
export const runtime = "edge";
|
|
29
|
+
export const config = { matcher: ["/((?!_next|.*\\..*).*)"] };
|
|
30
|
+
|
|
31
|
+
const apply = variantLabMiddleware(experiments);
|
|
32
|
+
|
|
33
|
+
export default function middleware(req) {
|
|
34
|
+
return apply(req, NextResponse.next());
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// app/layout.tsx (Server Component)
|
|
40
|
+
import { cookies } from "next/headers";
|
|
41
|
+
import { createVariantLabServer } from "@variantlab/next";
|
|
42
|
+
import { VariantLabProvider } from "@variantlab/next/client";
|
|
43
|
+
import experiments from "./experiments.json";
|
|
44
|
+
|
|
45
|
+
const server = createVariantLabServer(experiments);
|
|
46
|
+
|
|
47
|
+
export default function RootLayout({ children }) {
|
|
48
|
+
const props = server.toProviderProps(cookies());
|
|
49
|
+
return (
|
|
50
|
+
<html lang="en">
|
|
51
|
+
<body>
|
|
52
|
+
<VariantLabProvider config={experiments} {...props}>
|
|
53
|
+
{children}
|
|
54
|
+
</VariantLabProvider>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// app/page.tsx (Server Component)
|
|
63
|
+
import { cookies } from "next/headers";
|
|
64
|
+
import { getVariantValueSSR } from "@variantlab/next";
|
|
65
|
+
import experiments from "./experiments.json";
|
|
66
|
+
|
|
67
|
+
export default function Page() {
|
|
68
|
+
const hero = getVariantValueSSR<string>("hero-copy", cookies(), experiments);
|
|
69
|
+
return <h1>{hero}</h1>;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
// app/client-demo/page.tsx
|
|
75
|
+
"use client";
|
|
76
|
+
import { useVariantValue, Variant } from "@variantlab/next/client";
|
|
77
|
+
|
|
78
|
+
export default function ClientDemo() {
|
|
79
|
+
const cta = useVariantValue<string>("cta-color");
|
|
80
|
+
return (
|
|
81
|
+
<main>
|
|
82
|
+
<button style={{ background: cta }}>Go</button>
|
|
83
|
+
<Variant experimentId="layout">
|
|
84
|
+
{{
|
|
85
|
+
compact: <p>compact</p>,
|
|
86
|
+
expanded: <p>expanded</p>,
|
|
87
|
+
}}
|
|
88
|
+
</Variant>
|
|
89
|
+
</main>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Subpath exports
|
|
95
|
+
|
|
96
|
+
| Subpath | Contents | `"use client"` |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `@variantlab/next` | Server helpers, middleware factory, cookie helpers, shared types | ❌ |
|
|
99
|
+
| `@variantlab/next/client` | `VariantLabProvider`, all React hooks + components | ✅ |
|
|
100
|
+
| `@variantlab/next/app-router` | App Router-scoped re-exports + `readPayloadFromCookies()` | ❌ |
|
|
101
|
+
| `@variantlab/next/pages-router` | Pages Router-scoped re-exports + `readPayloadFromReq(req)` | ❌ |
|
|
102
|
+
|
|
103
|
+
## Notes on the spec
|
|
104
|
+
|
|
105
|
+
- `getVariantSSR` / `getVariantValueSSR` are **synchronous**, matching the canonical contract in [`API.md`](../../API.md). The underlying engine is synchronous, so an async wrapper would only add Promise allocation. If you're on Next 15 where `cookies()` is async, `await` it at the call site and pass the resolved store to `getVariantSSR`.
|
|
106
|
+
- The middleware writes only `{ v, u, a: {} }` on first visit — it does not compute assignments at the edge. Server Components / Route Handlers can use `server.writePayload(...)` to persist computed assignments back into the cookie if they want to avoid a re-evaluation on the next request.
|
|
107
|
+
|
|
108
|
+
See the [root README](../../README.md) for project overview, motivation, and roadmap.
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@variantlab/core');
|
|
4
|
+
|
|
5
|
+
// src/types.ts
|
|
6
|
+
var DEFAULT_COOKIE_NAME = "__variantlab_sticky";
|
|
7
|
+
var DEFAULT_MAX_AGE = 60 * 60 * 24 * 365;
|
|
8
|
+
|
|
9
|
+
// src/server/cookie.ts
|
|
10
|
+
var MAX_COOKIE_HEADER_BYTES = 8192;
|
|
11
|
+
var MAX_PAYLOAD_BYTES = 4096;
|
|
12
|
+
var RESERVED_NAMES = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
13
|
+
var textEncoder = new TextEncoder();
|
|
14
|
+
var textDecoder = new TextDecoder();
|
|
15
|
+
function base64urlEncode(input) {
|
|
16
|
+
const bytes = textEncoder.encode(input);
|
|
17
|
+
let binary = "";
|
|
18
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
19
|
+
binary += String.fromCharCode(bytes[i]);
|
|
20
|
+
}
|
|
21
|
+
const b64 = btoa(binary);
|
|
22
|
+
return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
23
|
+
}
|
|
24
|
+
function base64urlDecode(input) {
|
|
25
|
+
if (!/^[A-Za-z0-9\-_]*$/.test(input)) return null;
|
|
26
|
+
const pad = input.length % 4;
|
|
27
|
+
const padded = input.replace(/-/g, "+").replace(/_/g, "/") + "====".slice(pad === 0 ? 4 : pad);
|
|
28
|
+
try {
|
|
29
|
+
const binary = atob(padded);
|
|
30
|
+
const bytes = new Uint8Array(binary.length);
|
|
31
|
+
for (let i = 0; i < binary.length; i++) {
|
|
32
|
+
bytes[i] = binary.charCodeAt(i);
|
|
33
|
+
}
|
|
34
|
+
return textDecoder.decode(bytes);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function encodePayload(payload) {
|
|
40
|
+
const json = JSON.stringify({ v: payload.v, u: payload.u, a: payload.a });
|
|
41
|
+
return base64urlEncode(json);
|
|
42
|
+
}
|
|
43
|
+
function decodePayload(raw) {
|
|
44
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
45
|
+
if (raw.length > MAX_PAYLOAD_BYTES) return null;
|
|
46
|
+
const json = base64urlDecode(raw);
|
|
47
|
+
if (json === null) return null;
|
|
48
|
+
if (json.length > MAX_PAYLOAD_BYTES) return null;
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(json);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
56
|
+
const obj = parsed;
|
|
57
|
+
if (obj["v"] !== 1) return null;
|
|
58
|
+
if (typeof obj["u"] !== "string" || obj["u"].length === 0 || obj["u"].length > 256) return null;
|
|
59
|
+
const rawA = obj["a"];
|
|
60
|
+
if (rawA === null || typeof rawA !== "object" || Array.isArray(rawA)) return null;
|
|
61
|
+
const src = rawA;
|
|
62
|
+
const a = /* @__PURE__ */ Object.create(null);
|
|
63
|
+
for (const key of Object.keys(src)) {
|
|
64
|
+
if (RESERVED_NAMES.has(key)) continue;
|
|
65
|
+
if (key.length === 0 || key.length > 128) continue;
|
|
66
|
+
const val = src[key];
|
|
67
|
+
if (typeof val !== "string" || val.length === 0 || val.length > 128) continue;
|
|
68
|
+
a[key] = val;
|
|
69
|
+
}
|
|
70
|
+
return { v: 1, u: obj["u"], a };
|
|
71
|
+
}
|
|
72
|
+
function parseCookieHeader(header) {
|
|
73
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
74
|
+
if (typeof header !== "string" || header.length === 0) return out;
|
|
75
|
+
if (header.length > MAX_COOKIE_HEADER_BYTES) return out;
|
|
76
|
+
let i = 0;
|
|
77
|
+
const n = header.length;
|
|
78
|
+
while (i < n) {
|
|
79
|
+
while (i < n && (header.charCodeAt(i) === 32 || header.charCodeAt(i) === 9)) i++;
|
|
80
|
+
const start = i;
|
|
81
|
+
while (i < n && header.charCodeAt(i) !== 59) i++;
|
|
82
|
+
const segment = header.slice(start, i);
|
|
83
|
+
if (i < n) i++;
|
|
84
|
+
if (segment.length === 0) continue;
|
|
85
|
+
const eq = segment.indexOf("=");
|
|
86
|
+
if (eq <= 0) continue;
|
|
87
|
+
const name = segment.slice(0, eq).trim();
|
|
88
|
+
if (name.length === 0) continue;
|
|
89
|
+
if (RESERVED_NAMES.has(name)) continue;
|
|
90
|
+
let value = segment.slice(eq + 1).trim();
|
|
91
|
+
if (value.length >= 2 && value.charCodeAt(0) === 34 && value.charCodeAt(value.length - 1) === 34) {
|
|
92
|
+
value = value.slice(1, -1);
|
|
93
|
+
}
|
|
94
|
+
if (out[name] === void 0) {
|
|
95
|
+
out[name] = safeDecodeURIComponent(value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
function safeDecodeURIComponent(s) {
|
|
101
|
+
try {
|
|
102
|
+
return decodeURIComponent(s);
|
|
103
|
+
} catch {
|
|
104
|
+
return s;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function serializeCookie(name, value, options = {}) {
|
|
108
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
109
|
+
const maxAge = options.maxAge ?? DEFAULT_MAX_AGE;
|
|
110
|
+
if (maxAge > 0) parts.push(`Max-Age=${Math.floor(maxAge)}`);
|
|
111
|
+
parts.push(`Path=${options.path ?? "/"}`);
|
|
112
|
+
const sameSite = options.sameSite ?? "lax";
|
|
113
|
+
parts.push(`SameSite=${capitalize(sameSite)}`);
|
|
114
|
+
if (options.httpOnly !== false) parts.push("HttpOnly");
|
|
115
|
+
if (options.secure === true) parts.push("Secure");
|
|
116
|
+
if (options.domain !== void 0) parts.push(`Domain=${options.domain}`);
|
|
117
|
+
return parts.join("; ");
|
|
118
|
+
}
|
|
119
|
+
function capitalize(s) {
|
|
120
|
+
if (s.length === 0) return s;
|
|
121
|
+
return s[0].toUpperCase() + s.slice(1);
|
|
122
|
+
}
|
|
123
|
+
function readCookieFromSource(source, name = DEFAULT_COOKIE_NAME) {
|
|
124
|
+
if (source === null || source === void 0) return void 0;
|
|
125
|
+
if (typeof source === "string") {
|
|
126
|
+
const parsed = parseCookieHeader(source);
|
|
127
|
+
return parsed[name];
|
|
128
|
+
}
|
|
129
|
+
if (typeof source.get === "function") {
|
|
130
|
+
const entry = source.get(name);
|
|
131
|
+
return entry === void 0 ? void 0 : entry.value;
|
|
132
|
+
}
|
|
133
|
+
if (typeof source.headers?.get === "function") {
|
|
134
|
+
const header = source.headers.get("cookie");
|
|
135
|
+
if (header === null) return void 0;
|
|
136
|
+
return parseCookieHeader(header)[name];
|
|
137
|
+
}
|
|
138
|
+
const pagesReq = source;
|
|
139
|
+
if (pagesReq.cookies !== void 0) {
|
|
140
|
+
const fromBag = pagesReq.cookies[name];
|
|
141
|
+
if (typeof fromBag === "string" && fromBag.length > 0) return fromBag;
|
|
142
|
+
}
|
|
143
|
+
if (pagesReq.headers !== void 0) {
|
|
144
|
+
const header = pagesReq.headers["cookie"];
|
|
145
|
+
if (typeof header === "string") return parseCookieHeader(header)[name];
|
|
146
|
+
if (Array.isArray(header) && typeof header[0] === "string") {
|
|
147
|
+
return parseCookieHeader(header[0])[name];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return void 0;
|
|
151
|
+
}
|
|
152
|
+
function readPayloadFromSource(source, name = DEFAULT_COOKIE_NAME) {
|
|
153
|
+
const raw = readCookieFromSource(source, name);
|
|
154
|
+
return decodePayload(raw);
|
|
155
|
+
}
|
|
156
|
+
function generateUserId() {
|
|
157
|
+
const g = globalThis;
|
|
158
|
+
if (g.crypto?.randomUUID !== void 0) return g.crypto.randomUUID();
|
|
159
|
+
if (g.crypto?.getRandomValues !== void 0) {
|
|
160
|
+
const bytes = new Uint8Array(16);
|
|
161
|
+
g.crypto.getRandomValues(bytes);
|
|
162
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
163
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
164
|
+
const hex = [];
|
|
165
|
+
for (let i = 0; i < 16; i++) {
|
|
166
|
+
hex.push(bytes[i].toString(16).padStart(2, "0"));
|
|
167
|
+
}
|
|
168
|
+
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("")}`;
|
|
169
|
+
}
|
|
170
|
+
return `u-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;
|
|
171
|
+
}
|
|
172
|
+
function createVariantLabServer(rawConfig, options = {}) {
|
|
173
|
+
const config = core.validateConfig(rawConfig);
|
|
174
|
+
const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
175
|
+
const onError = options.onError ?? (() => {
|
|
176
|
+
});
|
|
177
|
+
function readPayload(source) {
|
|
178
|
+
try {
|
|
179
|
+
return readPayloadFromSource(source, cookieName);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
onError(error);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function buildContext(payload, extras) {
|
|
186
|
+
const userId = payload?.u ?? extras?.userId;
|
|
187
|
+
if (userId === void 0) return { ...extras };
|
|
188
|
+
return { ...extras, userId };
|
|
189
|
+
}
|
|
190
|
+
function getVariant(experimentId, source, context) {
|
|
191
|
+
try {
|
|
192
|
+
const payload = readPayload(source);
|
|
193
|
+
const engine = core.createEngine(config, {
|
|
194
|
+
context: buildContext(payload, context),
|
|
195
|
+
...payload?.a !== void 0 ? { initialAssignments: payload.a } : {}
|
|
196
|
+
});
|
|
197
|
+
return engine.getVariant(experimentId);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
onError(error);
|
|
200
|
+
return experimentDefault(config, experimentId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function getVariantValue(experimentId, source, context) {
|
|
204
|
+
try {
|
|
205
|
+
const payload = readPayload(source);
|
|
206
|
+
const engine = core.createEngine(config, {
|
|
207
|
+
context: buildContext(payload, context),
|
|
208
|
+
...payload?.a !== void 0 ? { initialAssignments: payload.a } : {}
|
|
209
|
+
});
|
|
210
|
+
return engine.getVariantValue(experimentId);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
onError(error);
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function writePayload(payload, secure) {
|
|
217
|
+
const value = encodePayload(payload);
|
|
218
|
+
return serializeCookie(cookieName, value, {
|
|
219
|
+
...options,
|
|
220
|
+
...secure !== void 0 ? { secure } : {}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function toProviderProps(source, contextExtras) {
|
|
224
|
+
const payload = readPayload(source);
|
|
225
|
+
const initialContext = buildContext(payload, contextExtras);
|
|
226
|
+
const initialVariants = payload?.a ? { ...payload.a } : {};
|
|
227
|
+
return { initialContext, initialVariants };
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
config,
|
|
231
|
+
getVariant,
|
|
232
|
+
getVariantValue,
|
|
233
|
+
readPayload,
|
|
234
|
+
writePayload,
|
|
235
|
+
toProviderProps
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function experimentDefault(config, experimentId) {
|
|
239
|
+
const exp = config.experiments.find((e) => e.id === experimentId);
|
|
240
|
+
return exp?.default ?? "";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/server/get-variant-ssr.ts
|
|
244
|
+
var cache = /* @__PURE__ */ new WeakMap();
|
|
245
|
+
function resolveServer(rawConfig, options) {
|
|
246
|
+
if (rawConfig !== null && typeof rawConfig === "object") {
|
|
247
|
+
const hit = cache.get(rawConfig);
|
|
248
|
+
if (hit !== void 0) return hit;
|
|
249
|
+
}
|
|
250
|
+
const server = createVariantLabServer(rawConfig, options);
|
|
251
|
+
if (rawConfig !== null && typeof rawConfig === "object") {
|
|
252
|
+
cache.set(rawConfig, server);
|
|
253
|
+
}
|
|
254
|
+
return server;
|
|
255
|
+
}
|
|
256
|
+
function getVariantSSR(experimentId, source, config, options) {
|
|
257
|
+
const server = resolveServer(config, options);
|
|
258
|
+
return server.getVariant(experimentId, source, options?.context);
|
|
259
|
+
}
|
|
260
|
+
function getVariantValueSSR(experimentId, source, config, options) {
|
|
261
|
+
const server = resolveServer(config, options);
|
|
262
|
+
return server.getVariantValue(experimentId, source, options?.context);
|
|
263
|
+
}
|
|
264
|
+
function variantLabMiddleware(rawConfig, options = {}) {
|
|
265
|
+
let frozen = true;
|
|
266
|
+
try {
|
|
267
|
+
core.validateConfig(rawConfig);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
options.onError?.(error);
|
|
270
|
+
frozen = false;
|
|
271
|
+
}
|
|
272
|
+
const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
273
|
+
const onError = options.onError ?? (() => {
|
|
274
|
+
});
|
|
275
|
+
return function apply(req, response) {
|
|
276
|
+
if (!frozen) return response;
|
|
277
|
+
try {
|
|
278
|
+
const header = req.headers.get("cookie");
|
|
279
|
+
const cookies = parseCookieHeader(header ?? "");
|
|
280
|
+
const existing = decodePayload(cookies[cookieName]);
|
|
281
|
+
if (existing !== null) return response;
|
|
282
|
+
const payload = {
|
|
283
|
+
v: 1,
|
|
284
|
+
u: generateUserId(),
|
|
285
|
+
a: {}
|
|
286
|
+
};
|
|
287
|
+
const secure = req.nextUrl.protocol === "https:";
|
|
288
|
+
const setCookie = serializeCookie(cookieName, encodePayload(payload), {
|
|
289
|
+
...options,
|
|
290
|
+
secure
|
|
291
|
+
});
|
|
292
|
+
response.headers.append("set-cookie", setCookie);
|
|
293
|
+
return response;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
onError(error);
|
|
296
|
+
return response;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/index.ts
|
|
302
|
+
var VERSION = "0.0.0";
|
|
303
|
+
|
|
304
|
+
// src/app-router.ts
|
|
305
|
+
function readPayloadFromCookies(store, name = DEFAULT_COOKIE_NAME) {
|
|
306
|
+
return readPayloadFromSource(store, name);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
exports.DEFAULT_COOKIE_NAME = DEFAULT_COOKIE_NAME;
|
|
310
|
+
exports.DEFAULT_MAX_AGE = DEFAULT_MAX_AGE;
|
|
311
|
+
exports.VERSION = VERSION;
|
|
312
|
+
exports.createVariantLabServer = createVariantLabServer;
|
|
313
|
+
exports.decodePayload = decodePayload;
|
|
314
|
+
exports.encodePayload = encodePayload;
|
|
315
|
+
exports.generateUserId = generateUserId;
|
|
316
|
+
exports.getVariantSSR = getVariantSSR;
|
|
317
|
+
exports.getVariantValueSSR = getVariantValueSSR;
|
|
318
|
+
exports.parseCookieHeader = parseCookieHeader;
|
|
319
|
+
exports.readCookieFromSource = readCookieFromSource;
|
|
320
|
+
exports.readPayloadFromCookies = readPayloadFromCookies;
|
|
321
|
+
exports.readPayloadFromSource = readPayloadFromSource;
|
|
322
|
+
exports.serializeCookie = serializeCookie;
|
|
323
|
+
exports.variantLabMiddleware = variantLabMiddleware;
|
|
324
|
+
//# sourceMappingURL=app-router.cjs.map
|
|
325
|
+
//# sourceMappingURL=app-router.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/server/cookie.ts","../src/server/create-variant-lab-server.ts","../src/server/get-variant-ssr.ts","../src/server/middleware.ts","../src/index.ts","../src/app-router.ts"],"names":["validateConfig","createEngine"],"mappings":";;;;;AAgGO,IAAM,mBAAA,GAAsB;AAG5B,IAAM,eAAA,GAAkB,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK;;;ACzE9C,IAAM,uBAAA,GAA0B,IAAA;AAEhC,IAAM,iBAAA,GAAoB,IAAA;AAE1B,IAAM,iCAAiB,IAAI,GAAA,CAAI,CAAC,WAAA,EAAa,aAAA,EAAe,WAAW,CAAC,CAAA;AAMxE,IAAM,WAAA,GAAc,IAAI,WAAA,EAAY;AACpC,IAAM,WAAA,GAAc,IAAI,WAAA,EAAY;AAEpC,SAAS,gBAAgB,KAAA,EAAuB;AAC9C,EAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,MAAA,CAAO,KAAK,CAAA;AAEtC,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,MAAA,IAAU,MAAA,CAAO,YAAA,CAAa,KAAA,CAAM,CAAC,CAAW,CAAA;AAAA,EAClD;AACA,EAAA,MAAM,GAAA,GAAM,KAAK,MAAM,CAAA;AACvB,EAAA,OAAO,GAAA,CAAI,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AACtE;AAEA,SAAS,gBAAgB,KAAA,EAA8B;AAErD,EAAA,IAAI,CAAC,mBAAA,CAAoB,IAAA,CAAK,KAAK,GAAG,OAAO,IAAA;AAC7C,EAAA,MAAM,GAAA,GAAM,MAAM,MAAA,GAAS,CAAA;AAC3B,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA,CAAE,OAAA,CAAQ,IAAA,EAAM,GAAG,IAAI,MAAA,CAAO,KAAA,CAAM,GAAA,KAAQ,CAAA,GAAI,IAAI,GAAG,CAAA;AAC7F,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,KAAK,MAAM,CAAA;AAC1B,IAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,MAAA,CAAO,MAAM,CAAA;AAC1C,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,MAAA,CAAO,QAAQ,CAAA,EAAA,EAAK;AACtC,MAAA,KAAA,CAAM,CAAC,CAAA,GAAI,MAAA,CAAO,UAAA,CAAW,CAAC,CAAA;AAAA,IAChC;AACA,IAAA,OAAO,WAAA,CAAY,OAAO,KAAK,CAAA;AAAA,EACjC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAUO,SAAS,cAAc,OAAA,EAAsC;AAClE,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,EAAE,CAAA,EAAG,OAAA,CAAQ,CAAA,EAAG,CAAA,EAAG,OAAA,CAAQ,CAAA,EAAG,CAAA,EAAG,OAAA,CAAQ,GAAG,CAAA;AACxE,EAAA,OAAO,gBAAgB,IAAI,CAAA;AAC7B;AAMO,SAAS,cAAc,GAAA,EAA4D;AACxF,EAAA,IAAI,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,CAAI,MAAA,KAAW,GAAG,OAAO,IAAA;AACxD,EAAA,IAAI,GAAA,CAAI,MAAA,GAAS,iBAAA,EAAmB,OAAO,IAAA;AAC3C,EAAA,MAAM,IAAA,GAAO,gBAAgB,GAAG,CAAA;AAChC,EAAA,IAAI,IAAA,KAAS,MAAM,OAAO,IAAA;AAC1B,EAAA,IAAI,IAAA,CAAK,MAAA,GAAS,iBAAA,EAAmB,OAAO,IAAA;AAC5C,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI;AACF,IAAA,MAAA,GAAS,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,MAAA,KAAW,QAAQ,OAAO,MAAA,KAAW,YAAY,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,EAAG,OAAO,IAAA;AACnF,EAAA,MAAM,GAAA,GAAM,MAAA;AACZ,EAAA,IAAI,GAAA,CAAI,GAAG,CAAA,KAAM,CAAA,EAAG,OAAO,IAAA;AAC3B,EAAA,IAAI,OAAO,GAAA,CAAI,GAAG,CAAA,KAAM,YAAY,GAAA,CAAI,GAAG,CAAA,CAAE,MAAA,KAAW,KAAK,GAAA,CAAI,GAAG,CAAA,CAAE,MAAA,GAAS,KAAK,OAAO,IAAA;AAC3F,EAAA,MAAM,IAAA,GAAO,IAAI,GAAG,CAAA;AACpB,EAAA,IAAI,IAAA,KAAS,QAAQ,OAAO,IAAA,KAAS,YAAY,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,EAAG,OAAO,IAAA;AAC7E,EAAA,MAAM,GAAA,GAAM,IAAA;AACZ,EAAA,MAAM,CAAA,mBAA4B,MAAA,CAAO,MAAA,CAAO,IAAI,CAAA;AACpD,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,IAAA,CAAK,GAAG,CAAA,EAAG;AAClC,IAAA,IAAI,cAAA,CAAe,GAAA,CAAI,GAAG,CAAA,EAAG;AAC7B,IAAA,IAAI,GAAA,CAAI,MAAA,KAAW,CAAA,IAAK,GAAA,CAAI,SAAS,GAAA,EAAK;AAC1C,IAAA,MAAM,GAAA,GAAM,IAAI,GAAG,CAAA;AACnB,IAAA,IAAI,OAAO,QAAQ,QAAA,IAAY,GAAA,CAAI,WAAW,CAAA,IAAK,GAAA,CAAI,SAAS,GAAA,EAAK;AACrE,IAAA,CAAA,CAAE,GAAG,CAAA,GAAI,GAAA;AAAA,EACX;AACA,EAAA,OAAO,EAAE,CAAA,EAAG,CAAA,EAAG,GAAG,GAAA,CAAI,GAAG,GAAa,CAAA,EAAE;AAC1C;AAWO,SAAS,kBAAkB,MAAA,EAA2D;AAC3F,EAAA,MAAM,GAAA,mBAA8B,MAAA,CAAO,MAAA,CAAO,IAAI,CAAA;AACtD,EAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,CAAO,MAAA,KAAW,GAAG,OAAO,GAAA;AAC9D,EAAA,IAAI,MAAA,CAAO,MAAA,GAAS,uBAAA,EAAyB,OAAO,GAAA;AAEpD,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,MAAM,IAAI,MAAA,CAAO,MAAA;AACjB,EAAA,OAAO,IAAI,CAAA,EAAG;AAEZ,IAAA,OAAO,CAAA,GAAI,CAAA,KAAM,MAAA,CAAO,UAAA,CAAW,CAAC,CAAA,KAAM,EAAA,IAAQ,MAAA,CAAO,UAAA,CAAW,CAAC,CAAA,KAAM,CAAA,CAAA,EAAO,CAAA,EAAA;AAClF,IAAA,MAAM,KAAA,GAAQ,CAAA;AACd,IAAA,OAAO,IAAI,CAAA,IAAK,MAAA,CAAO,UAAA,CAAW,CAAC,MAAM,EAAA,EAAc,CAAA,EAAA;AACvD,IAAA,MAAM,OAAA,GAAU,MAAA,CAAO,KAAA,CAAM,KAAA,EAAO,CAAC,CAAA;AACrC,IAAA,IAAI,IAAI,CAAA,EAAG,CAAA,EAAA;AACX,IAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AAC1B,IAAA,MAAM,EAAA,GAAK,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AAC9B,IAAA,IAAI,MAAM,CAAA,EAAG;AACb,IAAA,MAAM,OAAO,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,EAAE,EAAE,IAAA,EAAK;AACvC,IAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACvB,IAAA,IAAI,cAAA,CAAe,GAAA,CAAI,IAAI,CAAA,EAAG;AAC9B,IAAA,IAAI,QAAQ,OAAA,CAAQ,KAAA,CAAM,EAAA,GAAK,CAAC,EAAE,IAAA,EAAK;AAEvC,IAAA,IACE,KAAA,CAAM,MAAA,IAAU,CAAA,IAChB,KAAA,CAAM,WAAW,CAAC,CAAA,KAAM,EAAA,IACxB,KAAA,CAAM,UAAA,CAAW,KAAA,CAAM,MAAA,GAAS,CAAC,MAAM,EAAA,EACvC;AACA,MAAA,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAAA,IAC3B;AAEA,IAAA,IAAI,GAAA,CAAI,IAAI,CAAA,KAAM,MAAA,EAAW;AAC3B,MAAA,GAAA,CAAI,IAAI,CAAA,GAAI,sBAAA,CAAuB,KAAK,CAAA;AAAA,IAC1C;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;AAEA,SAAS,uBAAuB,CAAA,EAAmB;AACjD,EAAA,IAAI;AACF,IAAA,OAAO,mBAAmB,CAAC,CAAA;AAAA,EAC7B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,CAAA;AAAA,EACT;AACF;AAWO,SAAS,eAAA,CACd,IAAA,EACA,KAAA,EACA,OAAA,GAAmE,EAAC,EAC5D;AACR,EAAA,MAAM,KAAA,GAAkB,CAAC,CAAA,EAAG,IAAI,IAAI,kBAAA,CAAmB,KAAK,CAAC,CAAA,CAAE,CAAA;AAC/D,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,eAAA;AACjC,EAAA,IAAI,MAAA,GAAS,GAAG,KAAA,CAAM,IAAA,CAAK,WAAW,IAAA,CAAK,KAAA,CAAM,MAAM,CAAC,CAAA,CAAE,CAAA;AAC1D,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,KAAA,EAAQ,OAAA,CAAQ,IAAA,IAAQ,GAAG,CAAA,CAAE,CAAA;AACxC,EAAA,MAAM,QAAA,GAAW,QAAQ,QAAA,IAAY,KAAA;AACrC,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,SAAA,EAAY,UAAA,CAAW,QAAQ,CAAC,CAAA,CAAE,CAAA;AAC7C,EAAA,IAAI,OAAA,CAAQ,QAAA,KAAa,KAAA,EAAO,KAAA,CAAM,KAAK,UAAU,CAAA;AACrD,EAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,IAAA,EAAM,KAAA,CAAM,KAAK,QAAQ,CAAA;AAChD,EAAA,IAAI,OAAA,CAAQ,WAAW,MAAA,EAAW,KAAA,CAAM,KAAK,CAAA,OAAA,EAAU,OAAA,CAAQ,MAAM,CAAA,CAAE,CAAA;AACvE,EAAA,OAAO,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB;AAEA,SAAS,WAAW,CAAA,EAAmB;AACrC,EAAA,IAAI,CAAA,CAAE,MAAA,KAAW,CAAA,EAAG,OAAO,CAAA;AAC3B,EAAA,OAAO,EAAE,CAAC,CAAA,CAAG,aAAY,GAAI,CAAA,CAAE,MAAM,CAAC,CAAA;AACxC;AAWO,SAAS,oBAAA,CACd,MAAA,EACA,IAAA,GAAe,mBAAA,EACK;AACpB,EAAA,IAAI,MAAA,KAAW,IAAA,IAAQ,MAAA,KAAW,MAAA,EAAW,OAAO,MAAA;AAEpD,EAAA,IAAI,OAAO,WAAW,QAAA,EAAU;AAC9B,IAAA,MAAM,MAAA,GAAS,kBAAkB,MAAM,CAAA;AACvC,IAAA,OAAO,OAAO,IAAI,CAAA;AAAA,EACpB;AAGA,EAAA,IAAI,OAAQ,MAAA,CAA4B,GAAA,KAAQ,UAAA,EAAY;AAC1D,IAAA,MAAM,KAAA,GAAS,MAAA,CAA4B,GAAA,CAAI,IAAI,CAAA;AACnD,IAAA,OAAO,KAAA,KAAU,MAAA,GAAY,MAAA,GAAY,KAAA,CAAM,KAAA;AAAA,EACjD;AAGA,EAAA,IAAI,OAAQ,MAAA,CAAmB,OAAA,EAAS,GAAA,KAAQ,UAAA,EAAY;AAC1D,IAAA,MAAM,MAAA,GAAU,MAAA,CAAmB,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AACvD,IAAA,IAAI,MAAA,KAAW,MAAM,OAAO,MAAA;AAC5B,IAAA,OAAO,iBAAA,CAAkB,MAAM,CAAA,CAAE,IAAI,CAAA;AAAA,EACvC;AAGA,EAAA,MAAM,QAAA,GAAW,MAAA;AACjB,EAAA,IAAI,QAAA,CAAS,YAAY,MAAA,EAAW;AAClC,IAAA,MAAM,OAAA,GAAU,QAAA,CAAS,OAAA,CAAQ,IAAI,CAAA;AACrC,IAAA,IAAI,OAAO,OAAA,KAAY,QAAA,IAAY,OAAA,CAAQ,MAAA,GAAS,GAAG,OAAO,OAAA;AAAA,EAChE;AACA,EAAA,IAAI,QAAA,CAAS,YAAY,MAAA,EAAW;AAClC,IAAA,MAAM,MAAA,GAAS,QAAA,CAAS,OAAA,CAAQ,QAAQ,CAAA;AACxC,IAAA,IAAI,OAAO,MAAA,KAAW,QAAA,SAAiB,iBAAA,CAAkB,MAAM,EAAE,IAAI,CAAA;AACrE,IAAA,IAAI,KAAA,CAAM,QAAQ,MAAM,CAAA,IAAK,OAAO,MAAA,CAAO,CAAC,MAAM,QAAA,EAAU;AAC1D,MAAA,OAAO,iBAAA,CAAkB,MAAA,CAAO,CAAC,CAAC,EAAE,IAAI,CAAA;AAAA,IAC1C;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAMO,SAAS,qBAAA,CACd,MAAA,EACA,IAAA,GAAe,mBAAA,EACa;AAC5B,EAAA,MAAM,GAAA,GAAM,oBAAA,CAAqB,MAAA,EAAQ,IAAI,CAAA;AAC7C,EAAA,OAAO,cAAc,GAAG,CAAA;AAC1B;AAWO,SAAS,cAAA,GAAyB;AACvC,EAAA,MAAM,CAAA,GAAI,UAAA;AAMV,EAAA,IAAI,EAAE,MAAA,EAAQ,UAAA,KAAe,QAAW,OAAO,CAAA,CAAE,OAAO,UAAA,EAAW;AACnE,EAAA,IAAI,CAAA,CAAE,MAAA,EAAQ,eAAA,KAAoB,MAAA,EAAW;AAC3C,IAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,EAAE,CAAA;AAC/B,IAAA,CAAA,CAAE,MAAA,CAAO,gBAAgB,KAAK,CAAA;AAE9B,IAAA,KAAA,CAAM,CAAC,CAAA,GAAK,KAAA,CAAM,CAAC,IAAK,EAAA,GAAQ,EAAA;AAChC,IAAA,KAAA,CAAM,CAAC,CAAA,GAAK,KAAA,CAAM,CAAC,IAAK,EAAA,GAAQ,GAAA;AAChC,IAAA,MAAM,MAAgB,EAAC;AACvB,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,EAAA,EAAI,CAAA,EAAA,EAAK;AAC3B,MAAA,GAAA,CAAI,IAAA,CAAM,KAAA,CAAM,CAAC,CAAA,CAAa,QAAA,CAAS,EAAE,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,IAC7D;AACA,IAAA,OAAO,CAAA,EAAG,IAAI,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,CAAE,IAAA,CAAK,EAAE,CAAC,CAAA,CAAA,EAAI,IAAI,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,CAAE,IAAA,CAAK,EAAE,CAAC,CAAA,CAAA,EAAI,IAAI,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,CAAE,IAAA,CAAK,EAAE,CAAC,CAAA,CAAA,EAAI,IAAI,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,CAAE,IAAA,CAAK,EAAE,CAAC,CAAA,CAAA,EAAI,IAAI,KAAA,CAAM,EAAA,EAAI,EAAE,CAAA,CAAE,IAAA,CAAK,EAAE,CAAC,CAAA,CAAA;AAAA,EACvJ;AAGA,EAAA,OAAO,KAAK,IAAA,CAAK,GAAA,EAAI,CAAE,QAAA,CAAS,EAAE,CAAC,CAAA,CAAA,EAAI,IAAA,CAAK,KAAA,CAAM,KAAK,MAAA,EAAO,GAAI,GAAG,CAAA,CAAE,QAAA,CAAS,EAAE,CAAC,CAAA,CAAA;AACrF;ACzMO,SAAS,sBAAA,CACd,SAAA,EACA,OAAA,GAAyC,EAAC,EACxB;AAClB,EAAA,MAAM,MAAA,GAASA,oBAAe,SAAS,CAAA;AACvC,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,mBAAA;AACzC,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,KAAY,MAAM;AAAA,EAAC,CAAA,CAAA;AAE3C,EAAA,SAAS,YAAY,MAAA,EAAkD;AACrE,IAAA,IAAI;AACF,MAAA,OAAO,qBAAA,CAAsB,QAAQ,UAAU,CAAA;AAAA,IACjD,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAc,CAAA;AACtB,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AAEA,EAAA,SAAS,YAAA,CACP,SACA,MAAA,EACgB;AAChB,IAAA,MAAM,MAAA,GAAS,OAAA,EAAS,CAAA,IAAK,MAAA,EAAQ,MAAA;AACrC,IAAA,IAAI,MAAA,KAAW,MAAA,EAAW,OAAO,EAAE,GAAG,MAAA,EAAO;AAC7C,IAAA,OAAO,EAAE,GAAG,MAAA,EAAQ,MAAA,EAAO;AAAA,EAC7B;AAEA,EAAA,SAAS,UAAA,CACP,YAAA,EACA,MAAA,EACA,OAAA,EACQ;AACR,IAAA,IAAI;AACF,MAAA,MAAM,OAAA,GAAU,YAAY,MAAM,CAAA;AAClC,MAAA,MAAM,MAAA,GAASC,kBAAa,MAAA,EAAQ;AAAA,QAClC,OAAA,EAAS,YAAA,CAAa,OAAA,EAAS,OAAO,CAAA;AAAA,QACtC,GAAI,SAAS,CAAA,KAAM,KAAA,CAAA,GAAY,EAAE,kBAAA,EAAoB,OAAA,CAAQ,CAAA,EAAE,GAAI;AAAC,OACrE,CAAA;AACD,MAAA,OAAO,MAAA,CAAO,WAAW,YAAY,CAAA;AAAA,IACvC,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAc,CAAA;AACtB,MAAA,OAAO,iBAAA,CAAkB,QAAQ,YAAY,CAAA;AAAA,IAC/C;AAAA,EACF;AAEA,EAAA,SAAS,eAAA,CACP,YAAA,EACA,MAAA,EACA,OAAA,EACG;AACH,IAAA,IAAI;AACF,MAAA,MAAM,OAAA,GAAU,YAAY,MAAM,CAAA;AAClC,MAAA,MAAM,MAAA,GAASA,kBAAa,MAAA,EAAQ;AAAA,QAClC,OAAA,EAAS,YAAA,CAAa,OAAA,EAAS,OAAO,CAAA;AAAA,QACtC,GAAI,SAAS,CAAA,KAAM,KAAA,CAAA,GAAY,EAAE,kBAAA,EAAoB,OAAA,CAAQ,CAAA,EAAE,GAAI;AAAC,OACrE,CAAA;AACD,MAAA,OAAO,MAAA,CAAO,gBAAmB,YAAY,CAAA;AAAA,IAC/C,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAc,CAAA;AACtB,MAAA,OAAO,MAAA;AAAA,IACT;AAAA,EACF;AAEA,EAAA,SAAS,YAAA,CAAa,SAA8B,MAAA,EAA0B;AAC5E,IAAA,MAAM,KAAA,GAAQ,cAAc,OAAO,CAAA;AACnC,IAAA,OAAO,eAAA,CAAgB,YAAY,KAAA,EAAO;AAAA,MACxC,GAAG,OAAA;AAAA,MACH,GAAI,MAAA,KAAW,MAAA,GAAY,EAAE,MAAA,KAAW;AAAC,KAC1C,CAAA;AAAA,EACH;AAEA,EAAA,SAAS,eAAA,CACP,QACA,aAAA,EACsD;AACtD,IAAA,MAAM,OAAA,GAAU,YAAY,MAAM,CAAA;AAClC,IAAA,MAAM,cAAA,GAAiB,YAAA,CAAa,OAAA,EAAS,aAAa,CAAA;AAO1D,IAAA,MAAM,eAAA,GAA0C,SAAS,CAAA,GAAI,EAAE,GAAG,OAAA,CAAQ,CAAA,KAAM,EAAC;AACjF,IAAA,OAAO,EAAE,gBAAgB,eAAA,EAAgB;AAAA,EAC3C;AAEA,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,UAAA;AAAA,IACA,eAAA;AAAA,IACA,WAAA;AAAA,IACA,YAAA;AAAA,IACA;AAAA,GACF;AACF;AAEA,SAAS,iBAAA,CAAkB,QAA2B,YAAA,EAA8B;AAClF,EAAA,MAAM,GAAA,GAAM,OAAO,WAAA,CAAY,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,YAAY,CAAA;AAChE,EAAA,OAAO,KAAK,OAAA,IAAW,EAAA;AACzB;;;ACzKA,IAAM,KAAA,uBAAY,OAAA,EAAkC;AAEpD,SAAS,aAAA,CACP,WACA,OAAA,EACkB;AAClB,EAAA,IAAI,SAAA,KAAc,IAAA,IAAQ,OAAO,SAAA,KAAc,QAAA,EAAU;AACvD,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,GAAA,CAAI,SAAmB,CAAA;AACzC,IAAA,IAAI,GAAA,KAAQ,QAAW,OAAO,GAAA;AAAA,EAChC;AACA,EAAA,MAAM,MAAA,GAAS,sBAAA,CAAuB,SAAA,EAAW,OAAO,CAAA;AACxD,EAAA,IAAI,SAAA,KAAc,IAAA,IAAQ,OAAO,SAAA,KAAc,QAAA,EAAU;AACvD,IAAA,KAAA,CAAM,GAAA,CAAI,WAAqB,MAAM,CAAA;AAAA,EACvC;AACA,EAAA,OAAO,MAAA;AACT;AAUO,SAAS,aAAA,CACd,YAAA,EACA,MAAA,EACA,MAAA,EACA,OAAA,EACQ;AACR,EAAA,MAAM,MAAA,GAAS,aAAA,CAAc,MAAA,EAAQ,OAAO,CAAA;AAC5C,EAAA,OAAO,MAAA,CAAO,UAAA,CAAW,YAAA,EAAc,MAAA,EAAQ,SAAS,OAAO,CAAA;AACjE;AAGO,SAAS,kBAAA,CACd,YAAA,EACA,MAAA,EACA,MAAA,EACA,OAAA,EACG;AACH,EAAA,MAAM,MAAA,GAAS,aAAA,CAAc,MAAA,EAAQ,OAAO,CAAA;AAC5C,EAAA,OAAO,MAAA,CAAO,eAAA,CAAmB,YAAA,EAAc,MAAA,EAAQ,SAAS,OAAO,CAAA;AACzE;ACSO,SAAS,oBAAA,CACd,SAAA,EACA,OAAA,GAAuC,EAAC,EACxC;AAIA,EAAA,IAAI,MAAA,GAAS,IAAA;AACb,EAAA,IAAI;AACF,IAAAD,oBAAe,SAAS,CAAA;AAAA,EAC1B,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,UAAU,KAAc,CAAA;AAChC,IAAA,MAAA,GAAS,KAAA;AAAA,EACX;AAEA,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,mBAAA;AACzC,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,KAAY,MAAM;AAAA,EAAC,CAAA,CAAA;AAE3C,EAAA,OAAO,SAAS,KAAA,CACd,GAAA,EACA,QAAA,EACW;AACX,IAAA,IAAI,CAAC,QAAQ,OAAO,QAAA;AACpB,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AACvC,MAAA,MAAM,OAAA,GAAU,iBAAA,CAAkB,MAAA,IAAU,EAAE,CAAA;AAC9C,MAAA,MAAM,QAAA,GAAW,aAAA,CAAc,OAAA,CAAQ,UAAU,CAAC,CAAA;AAClD,MAAA,IAAI,QAAA,KAAa,MAAM,OAAO,QAAA;AAE9B,MAAA,MAAM,OAAA,GAA+B;AAAA,QACnC,CAAA,EAAG,CAAA;AAAA,QACH,GAAG,cAAA,EAAe;AAAA,QAClB,GAAG;AAAC,OACN;AACA,MAAA,MAAM,MAAA,GAAS,GAAA,CAAI,OAAA,CAAQ,QAAA,KAAa,QAAA;AACxC,MAAA,MAAM,SAAA,GAAY,eAAA,CAAgB,UAAA,EAAY,aAAA,CAAc,OAAO,CAAA,EAAG;AAAA,QACpE,GAAG,OAAA;AAAA,QACH;AAAA,OACD,CAAA;AACD,MAAA,QAAA,CAAS,OAAA,CAAQ,MAAA,CAAO,YAAA,EAAc,SAAS,CAAA;AAC/C,MAAA,OAAO,QAAA;AAAA,IACT,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAc,CAAA;AACtB,MAAA,OAAO,QAAA;AAAA,IACT;AAAA,EACF,CAAA;AACF;;;AC5GO,IAAM,OAAA,GAAU;;;AC0BhB,SAAS,sBAAA,CACd,KAAA,EACA,IAAA,GAAe,mBAAA,EACa;AAC5B,EAAA,OAAO,qBAAA,CAAsB,OAAO,IAAI,CAAA;AAC1C","file":"app-router.cjs","sourcesContent":["/**\n * Shared types for `@variantlab/next`.\n *\n * Kept in a single tiny file so every entrypoint (server barrel,\n * app-router subpath, pages-router subpath, client provider) can\n * import without pulling in code.\n */\n\nimport type { ExperimentsConfig, VariantContext } from \"@variantlab/core\";\nimport type { ReactNode } from \"react\";\n\n/**\n * On-wire cookie payload shape. Short keys to minimize cookie bytes.\n *\n * v — schema version (currently 1)\n * u — userId (required; generated on first visit by middleware)\n * a — assignments: { [experimentId]: variantId } (may be empty)\n */\nexport interface StickyCookiePayload {\n readonly v: 1;\n readonly u: string;\n readonly a: Readonly<Record<string, string>>;\n}\n\n/**\n * Anything from which we can read an HTTP Cookie header. Supports:\n *\n * - Fetch-style `Request` (App Router Route Handlers, middleware)\n * - `NextApiRequest` / Pages Router `req` (has `.cookies` and `.headers`)\n * - Next 14+ `ReadonlyRequestCookies` from `cookies()` in `next/headers`\n * - A plain cookie header string\n */\nexport type CookieSource =\n | Request\n | RequestCookieJar\n | PagesRouterRequestLike\n | string\n | null\n | undefined;\n\n/**\n * Shape we rely on from Next's `cookies()` return value. Kept minimal\n * so we don't depend on `next/headers` types at compile time — the\n * server barrel must be framework-agnostic enough to tree-shake.\n */\nexport interface RequestCookieJar {\n get(name: string): { readonly value: string } | undefined;\n}\n\n/**\n * Minimal Pages Router / `NextApiRequest` shape. Avoids importing\n * `next` types in the base entrypoint.\n */\nexport interface PagesRouterRequestLike {\n readonly cookies?: Readonly<Record<string, string | undefined>>;\n readonly headers?: Readonly<Record<string, string | string[] | undefined>>;\n}\n\n/**\n * Options accepted by `createVariantLabServer` and the SSR helpers.\n */\nexport interface VariantLabServerOptions {\n /** Cookie name. Defaults to `__variantlab_sticky`. */\n readonly cookieName?: string;\n /** Max age in seconds. Defaults to 365 days. */\n readonly maxAge?: number;\n /** Cookie path. Defaults to `/`. */\n readonly path?: string;\n /** `SameSite` attribute. Defaults to `\"lax\"`. */\n readonly sameSite?: \"strict\" | \"lax\" | \"none\";\n /** Override for the `Secure` flag. When `undefined`, derived from the request URL. */\n readonly secure?: boolean;\n /** `HttpOnly` flag. Defaults to `true`. */\n readonly httpOnly?: boolean;\n /** `Domain` attribute. Defaults to undefined (host-only cookie). */\n readonly domain?: string;\n}\n\n/**\n * Props accepted by the Next `VariantLabProvider` Client Component.\n */\nexport interface VariantLabProviderProps {\n /** Validated `ExperimentsConfig` or a raw JSON module import. */\n readonly config: unknown | ExperimentsConfig;\n /** Runtime context (userId, locale, platform, …) applied before first render. */\n readonly initialContext?: VariantContext;\n /**\n * Assignments computed on the server. Seeded into the engine cache so\n * the first `getVariant` call on the client returns the same variant\n * that was server-rendered, without re-evaluating targeting.\n */\n readonly initialVariants?: Readonly<Record<string, string>>;\n readonly children?: ReactNode;\n}\n\n/** Default cookie name. Shared between server helpers and middleware. */\nexport const DEFAULT_COOKIE_NAME = \"__variantlab_sticky\";\n\n/** Default max age (365 days, in seconds). */\nexport const DEFAULT_MAX_AGE = 60 * 60 * 24 * 365;\n","/**\n * Hand-rolled cookie codec + parser for `@variantlab/next`.\n *\n * Goals:\n * 1. Zero runtime dependencies. No `cookie` package, no `js-base64`.\n * 2. Edge-runtime safe. Uses only `TextEncoder`/`TextDecoder`,\n * `globalThis.crypto`, standard `atob`/`btoa`, and `Object.create(null)`\n * — all available on Vercel Edge, Cloudflare Workers, Deno, Bun, Node 18+.\n * 3. Prototype-pollution hardened. Mirrors the guards in\n * `packages/core/src/config/validator.ts`: cookie parser rejects\n * `__proto__`, `constructor`, `prototype` as cookie names, and the\n * JSON payload is parsed through `Object.create(null)` sanitization.\n * 4. Size-capped. A malicious client sending a 10 MB Cookie header gets\n * rejected before allocation by `MAX_COOKIE_HEADER_BYTES`.\n */\n\nimport type {\n CookieSource,\n PagesRouterRequestLike,\n RequestCookieJar,\n StickyCookiePayload,\n VariantLabServerOptions,\n} from \"../types.js\";\nimport { DEFAULT_COOKIE_NAME, DEFAULT_MAX_AGE } from \"../types.js\";\n\n/** Hard cap on cookie header length (4 KB × 2 for long headers). */\nconst MAX_COOKIE_HEADER_BYTES = 8192;\n/** Hard cap on a single decoded payload. */\nconst MAX_PAYLOAD_BYTES = 4096;\n/** Cookie names that are never read (prototype-pollution guard). */\nconst RESERVED_NAMES = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\n\n// ---------------------------------------------------------------------------\n// base64url codec (hand-rolled, 16 LOC)\n// ---------------------------------------------------------------------------\n\nconst textEncoder = new TextEncoder();\nconst textDecoder = new TextDecoder();\n\nfunction base64urlEncode(input: string): string {\n const bytes = textEncoder.encode(input);\n // `btoa` takes a latin-1 string, so map bytes → chars first.\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i] as number);\n }\n const b64 = btoa(binary);\n return b64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction base64urlDecode(input: string): string | null {\n // Reject anything that isn't legal base64url up front.\n if (!/^[A-Za-z0-9\\-_]*$/.test(input)) return null;\n const pad = input.length % 4;\n const padded = input.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"====\".slice(pad === 0 ? 4 : pad);\n try {\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return textDecoder.decode(bytes);\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Payload encode / decode\n// ---------------------------------------------------------------------------\n\n/**\n * Encode a `StickyCookiePayload` to its on-wire value. Does NOT include\n * the cookie name or attributes — see {@link serializeCookie}.\n */\nexport function encodePayload(payload: StickyCookiePayload): string {\n const json = JSON.stringify({ v: payload.v, u: payload.u, a: payload.a });\n return base64urlEncode(json);\n}\n\n/**\n * Decode a base64url-encoded cookie value back to a `StickyCookiePayload`.\n * Returns `null` for anything that fails validation. Never throws.\n */\nexport function decodePayload(raw: string | undefined | null): StickyCookiePayload | null {\n if (typeof raw !== \"string\" || raw.length === 0) return null;\n if (raw.length > MAX_PAYLOAD_BYTES) return null;\n const json = base64urlDecode(raw);\n if (json === null) return null;\n if (json.length > MAX_PAYLOAD_BYTES) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n return null;\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n const obj = parsed as Record<string, unknown>;\n if (obj[\"v\"] !== 1) return null;\n if (typeof obj[\"u\"] !== \"string\" || obj[\"u\"].length === 0 || obj[\"u\"].length > 256) return null;\n const rawA = obj[\"a\"];\n if (rawA === null || typeof rawA !== \"object\" || Array.isArray(rawA)) return null;\n const src = rawA as Record<string, unknown>;\n const a: Record<string, string> = Object.create(null);\n for (const key of Object.keys(src)) {\n if (RESERVED_NAMES.has(key)) continue;\n if (key.length === 0 || key.length > 128) continue;\n const val = src[key];\n if (typeof val !== \"string\" || val.length === 0 || val.length > 128) continue;\n a[key] = val;\n }\n return { v: 1, u: obj[\"u\"] as string, a };\n}\n\n// ---------------------------------------------------------------------------\n// Cookie header parsing (hand-rolled tokenizer)\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a raw `Cookie:` header into a `null`-prototype map of\n * `name → value`. Tolerates leading whitespace, empty segments,\n * missing `=`, and rejects reserved names.\n */\nexport function parseCookieHeader(header: string | undefined | null): Record<string, string> {\n const out: Record<string, string> = Object.create(null);\n if (typeof header !== \"string\" || header.length === 0) return out;\n if (header.length > MAX_COOKIE_HEADER_BYTES) return out;\n\n let i = 0;\n const n = header.length;\n while (i < n) {\n // Skip leading whitespace in this segment.\n while (i < n && (header.charCodeAt(i) === 0x20 || header.charCodeAt(i) === 0x09)) i++;\n const start = i;\n while (i < n && header.charCodeAt(i) !== 0x3b /* ; */) i++;\n const segment = header.slice(start, i);\n if (i < n) i++; // consume `;`\n if (segment.length === 0) continue;\n const eq = segment.indexOf(\"=\");\n if (eq <= 0) continue;\n const name = segment.slice(0, eq).trim();\n if (name.length === 0) continue;\n if (RESERVED_NAMES.has(name)) continue;\n let value = segment.slice(eq + 1).trim();\n // Cookies sometimes come wrapped in double quotes.\n if (\n value.length >= 2 &&\n value.charCodeAt(0) === 0x22 &&\n value.charCodeAt(value.length - 1) === 0x22\n ) {\n value = value.slice(1, -1);\n }\n // Keep first occurrence of a duplicate name.\n if (out[name] === undefined) {\n out[name] = safeDecodeURIComponent(value);\n }\n }\n return out;\n}\n\nfunction safeDecodeURIComponent(s: string): string {\n try {\n return decodeURIComponent(s);\n } catch {\n return s;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Cookie serialization (Set-Cookie value builder)\n// ---------------------------------------------------------------------------\n\n/**\n * Build a `Set-Cookie` header value: `name=value; attr=...; ...`.\n * Cookie value is URL-encoded so any non-ASCII bytes round-trip through\n * a standards-compliant parser.\n */\nexport function serializeCookie(\n name: string,\n value: string,\n options: VariantLabServerOptions & { readonly secure?: boolean } = {},\n): string {\n const parts: string[] = [`${name}=${encodeURIComponent(value)}`];\n const maxAge = options.maxAge ?? DEFAULT_MAX_AGE;\n if (maxAge > 0) parts.push(`Max-Age=${Math.floor(maxAge)}`);\n parts.push(`Path=${options.path ?? \"/\"}`);\n const sameSite = options.sameSite ?? \"lax\";\n parts.push(`SameSite=${capitalize(sameSite)}`);\n if (options.httpOnly !== false) parts.push(\"HttpOnly\");\n if (options.secure === true) parts.push(\"Secure\");\n if (options.domain !== undefined) parts.push(`Domain=${options.domain}`);\n return parts.join(\"; \");\n}\n\nfunction capitalize(s: string): string {\n if (s.length === 0) return s;\n return s[0]!.toUpperCase() + s.slice(1);\n}\n\n// ---------------------------------------------------------------------------\n// Request-source adapters\n// ---------------------------------------------------------------------------\n\n/**\n * Read a named cookie from any supported source: `Request`, Next's\n * `ReadonlyRequestCookies`, a Pages Router `NextApiRequest`, or a\n * raw header string. Returns `undefined` when missing.\n */\nexport function readCookieFromSource(\n source: CookieSource,\n name: string = DEFAULT_COOKIE_NAME,\n): string | undefined {\n if (source === null || source === undefined) return undefined;\n\n if (typeof source === \"string\") {\n const parsed = parseCookieHeader(source);\n return parsed[name];\n }\n\n // `ReadonlyRequestCookies` shape.\n if (typeof (source as RequestCookieJar).get === \"function\") {\n const entry = (source as RequestCookieJar).get(name);\n return entry === undefined ? undefined : entry.value;\n }\n\n // Fetch `Request`.\n if (typeof (source as Request).headers?.get === \"function\") {\n const header = (source as Request).headers.get(\"cookie\");\n if (header === null) return undefined;\n return parseCookieHeader(header)[name];\n }\n\n // Pages Router `req` shape.\n const pagesReq = source as PagesRouterRequestLike;\n if (pagesReq.cookies !== undefined) {\n const fromBag = pagesReq.cookies[name];\n if (typeof fromBag === \"string\" && fromBag.length > 0) return fromBag;\n }\n if (pagesReq.headers !== undefined) {\n const header = pagesReq.headers[\"cookie\"];\n if (typeof header === \"string\") return parseCookieHeader(header)[name];\n if (Array.isArray(header) && typeof header[0] === \"string\") {\n return parseCookieHeader(header[0])[name];\n }\n }\n return undefined;\n}\n\n/**\n * Full helper: read the sticky payload from a source, returning\n * `null` if the cookie is missing, malformed, or fails validation.\n */\nexport function readPayloadFromSource(\n source: CookieSource,\n name: string = DEFAULT_COOKIE_NAME,\n): StickyCookiePayload | null {\n const raw = readCookieFromSource(source, name);\n return decodePayload(raw);\n}\n\n// ---------------------------------------------------------------------------\n// User ID generation (Web Crypto — available on every target runtime)\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a v4 UUID via `crypto.randomUUID`, falling back to a\n * hand-formatted v4 built from `crypto.getRandomValues`. Both APIs\n * are Web Crypto and are available on every Phase 1 target runtime.\n */\nexport function generateUserId(): string {\n const g = globalThis as {\n crypto?: {\n randomUUID?: () => string;\n getRandomValues?: <T extends ArrayBufferView>(arr: T) => T;\n };\n };\n if (g.crypto?.randomUUID !== undefined) return g.crypto.randomUUID();\n if (g.crypto?.getRandomValues !== undefined) {\n const bytes = new Uint8Array(16);\n g.crypto.getRandomValues(bytes);\n // Set version (4) and variant (10xx) bits.\n bytes[6] = (bytes[6]! & 0x0f) | 0x40;\n bytes[8] = (bytes[8]! & 0x3f) | 0x80;\n const hex: string[] = [];\n for (let i = 0; i < 16; i++) {\n hex.push((bytes[i] as number).toString(16).padStart(2, \"0\"));\n }\n 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(\"\")}`;\n }\n // Last-ditch fallback: timestamp + Math.random. Not cryptographically\n // strong, but deterministic per-visit user IDs are not a security boundary.\n return `u-${Date.now().toString(36)}-${Math.floor(Math.random() * 1e9).toString(36)}`;\n}\n\n/** Exported so adapters can override the default name. */\nexport { DEFAULT_COOKIE_NAME, DEFAULT_MAX_AGE } from \"../types.js\";\n","/**\n * `createVariantLabServer(config, options?)` — factory that validates the\n * config once and returns request-scoped helpers. Each call to\n * `getVariant` / `getVariantValue` constructs a short-lived engine\n * seeded from the request's cookie so concurrent HTTP requests never\n * share mutable state.\n *\n * Validation is expensive; engine construction is cheap (a few Maps,\n * no I/O, no allocation beyond what's already in the frozen config).\n */\n\nimport {\n createEngine,\n type ExperimentsConfig,\n type VariantContext,\n validateConfig,\n} from \"@variantlab/core\";\nimport type {\n CookieSource,\n StickyCookiePayload,\n VariantLabProviderProps,\n VariantLabServerOptions,\n} from \"../types.js\";\nimport { DEFAULT_COOKIE_NAME } from \"../types.js\";\nimport {\n decodePayload,\n encodePayload,\n generateUserId,\n readPayloadFromSource,\n serializeCookie,\n} from \"./cookie.js\";\n\nexport interface VariantLabServer {\n /** The frozen, validated config this server was built with. */\n readonly config: ExperimentsConfig;\n\n /**\n * Resolve a variant id using the given cookie source + optional\n * context extras. Always returns the default if anything fails.\n */\n getVariant(experimentId: string, source: CookieSource, context?: VariantContext): string;\n\n /**\n * Resolve a variant's `value` payload. Equivalent to calling\n * `getVariant` + looking up the variant by id.\n */\n getVariantValue<T = unknown>(\n experimentId: string,\n source: CookieSource,\n context?: VariantContext,\n ): T;\n\n /**\n * Decode the sticky cookie. Returns `null` when missing or invalid.\n * Layouts pass this directly to `<VariantLabProvider initialVariants={...}>`.\n */\n readPayload(source: CookieSource): StickyCookiePayload | null;\n\n /**\n * Build a `Set-Cookie` header value for a freshly-computed payload.\n * Caller is responsible for attaching it to the outgoing response.\n */\n writePayload(payload: StickyCookiePayload, secure?: boolean): string;\n\n /**\n * Build the `initialContext` + `initialVariants` props that the\n * Next `<VariantLabProvider>` needs. Convenience for layouts:\n *\n * const props = server.toProviderProps(cookies(), { locale: \"en\" });\n * return <VariantLabProvider {...props}>{children}</VariantLabProvider>;\n */\n toProviderProps(\n source: CookieSource,\n contextExtras?: VariantContext,\n ): Omit<VariantLabProviderProps, \"children\" | \"config\">;\n}\n\nexport interface CreateVariantLabServerOptions extends VariantLabServerOptions {\n /**\n * Called when cookie reads or engine construction fails catastrophically.\n * Defaults to a no-op (fail-open).\n */\n readonly onError?: (error: Error) => void;\n}\n\n/**\n * Validate the supplied config once and return a factory whose methods\n * build a new `VariantEngine` per call. This is the entry point that\n * layouts, server components, route handlers, and `getServerSideProps`\n * should use when they want to resolve variants at SSR time.\n */\nexport function createVariantLabServer(\n rawConfig: unknown,\n options: CreateVariantLabServerOptions = {},\n): VariantLabServer {\n const config = validateConfig(rawConfig);\n const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;\n const onError = options.onError ?? (() => {});\n\n function readPayload(source: CookieSource): StickyCookiePayload | null {\n try {\n return readPayloadFromSource(source, cookieName);\n } catch (error) {\n onError(error as Error);\n return null;\n }\n }\n\n function buildContext(\n payload: StickyCookiePayload | null,\n extras: VariantContext | undefined,\n ): VariantContext {\n const userId = payload?.u ?? extras?.userId;\n if (userId === undefined) return { ...extras };\n return { ...extras, userId };\n }\n\n function getVariant(\n experimentId: string,\n source: CookieSource,\n context?: VariantContext,\n ): string {\n try {\n const payload = readPayload(source);\n const engine = createEngine(config, {\n context: buildContext(payload, context),\n ...(payload?.a !== undefined ? { initialAssignments: payload.a } : {}),\n });\n return engine.getVariant(experimentId);\n } catch (error) {\n onError(error as Error);\n return experimentDefault(config, experimentId);\n }\n }\n\n function getVariantValue<T = unknown>(\n experimentId: string,\n source: CookieSource,\n context?: VariantContext,\n ): T {\n try {\n const payload = readPayload(source);\n const engine = createEngine(config, {\n context: buildContext(payload, context),\n ...(payload?.a !== undefined ? { initialAssignments: payload.a } : {}),\n });\n return engine.getVariantValue<T>(experimentId);\n } catch (error) {\n onError(error as Error);\n return undefined as T;\n }\n }\n\n function writePayload(payload: StickyCookiePayload, secure?: boolean): string {\n const value = encodePayload(payload);\n return serializeCookie(cookieName, value, {\n ...options,\n ...(secure !== undefined ? { secure } : {}),\n });\n }\n\n function toProviderProps(\n source: CookieSource,\n contextExtras?: VariantContext,\n ): Omit<VariantLabProviderProps, \"children\" | \"config\"> {\n const payload = readPayload(source);\n const initialContext = buildContext(payload, contextExtras);\n // `payload.a` is a null-prototype object (see `decodePayload`) for\n // prototype-pollution safety during internal lookups. React Server\n // Components refuse to serialize null-prototype objects when passing\n // props across the Server→Client boundary, so we shallow-copy into a\n // plain object here. Values are strings-only per the decoder, so a\n // spread is sufficient.\n const initialVariants: Record<string, string> = payload?.a ? { ...payload.a } : {};\n return { initialContext, initialVariants };\n }\n\n return {\n config,\n getVariant,\n getVariantValue,\n readPayload,\n writePayload,\n toProviderProps,\n };\n}\n\nfunction experimentDefault(config: ExperimentsConfig, experimentId: string): string {\n const exp = config.experiments.find((e) => e.id === experimentId);\n return exp?.default ?? \"\";\n}\n\n/** Re-exported helpers for advanced users who only want the codecs. */\nexport { decodePayload, encodePayload, generateUserId };\n","/**\n * `getVariantSSR` / `getVariantValueSSR` — per-request SSR helpers that\n * don't require the caller to hold a `VariantLabServer` instance.\n *\n * Internally they use a `WeakMap` keyed on the raw config object identity\n * so repeat calls with the same imported JSON module don't re-validate\n * the config. The WeakMap doesn't retain anything — if the caller drops\n * the config reference, the cached server is GC'd.\n *\n * Signature matches `API.md` lines 569–582 (synchronous). See the\n * package README for notes on spec drift vs. the Session 7 prompt.\n */\n\nimport type { ExperimentsConfig, VariantContext } from \"@variantlab/core\";\nimport type { CookieSource } from \"../types.js\";\nimport {\n type CreateVariantLabServerOptions,\n createVariantLabServer,\n type VariantLabServer,\n} from \"./create-variant-lab-server.js\";\n\nconst cache = new WeakMap<object, VariantLabServer>();\n\nfunction resolveServer(\n rawConfig: unknown,\n options: CreateVariantLabServerOptions | undefined,\n): VariantLabServer {\n if (rawConfig !== null && typeof rawConfig === \"object\") {\n const hit = cache.get(rawConfig as object);\n if (hit !== undefined) return hit;\n }\n const server = createVariantLabServer(rawConfig, options);\n if (rawConfig !== null && typeof rawConfig === \"object\") {\n cache.set(rawConfig as object, server);\n }\n return server;\n}\n\n/**\n * Resolve a variant for the current request. Reads the sticky cookie\n * from the supplied source (`Request`, `ReadonlyRequestCookies`,\n * `NextApiRequest`, or a raw cookie header string).\n *\n * Synchronous. In Next 15, `cookies()` is async — await it and pass the\n * resolved store into this function.\n */\nexport function getVariantSSR(\n experimentId: string,\n source: CookieSource,\n config: unknown | ExperimentsConfig,\n options?: CreateVariantLabServerOptions & { readonly context?: VariantContext },\n): string {\n const server = resolveServer(config, options);\n return server.getVariant(experimentId, source, options?.context);\n}\n\n/** Variant-value equivalent of {@link getVariantSSR}. */\nexport function getVariantValueSSR<T = unknown>(\n experimentId: string,\n source: CookieSource,\n config: unknown | ExperimentsConfig,\n options?: CreateVariantLabServerOptions & { readonly context?: VariantContext },\n): T {\n const server = resolveServer(config, options);\n return server.getVariantValue<T>(experimentId, source, options?.context);\n}\n","/**\n * `variantLabMiddleware(config, options?)` — Next.js middleware factory.\n *\n * Responsibilities (Phase 1):\n * 1. Read the sticky cookie from the incoming request.\n * 2. If missing or malformed, mint a fresh userId and write an\n * empty payload `{ v: 1, u: userId, a: {} }` on the outgoing\n * response. Assignments are NOT computed at the edge.\n * 3. Fail-open: any error → pass through unmodified.\n *\n * The factory takes the raw config only so it can validate it once at\n * import time — middleware is instantiated per process, not per request.\n * It does not resolve any experiments during middleware execution.\n *\n * Designed to run on the Vercel Edge runtime (`export const runtime = \"edge\"`).\n * No Node-only APIs. No `process.env` access.\n */\n\nimport { validateConfig } from \"@variantlab/core\";\nimport type { StickyCookiePayload, VariantLabServerOptions } from \"../types.js\";\nimport { DEFAULT_COOKIE_NAME } from \"../types.js\";\nimport {\n decodePayload,\n encodePayload,\n generateUserId,\n parseCookieHeader,\n serializeCookie,\n} from \"./cookie.js\";\n\n/**\n * The minimal shape we need from `NextRequest`. Avoids a hard\n * dependency on `next/server` at type-check time.\n */\ninterface NextRequestLike {\n readonly headers: { get(name: string): string | null };\n readonly nextUrl: { readonly protocol: string };\n}\n\n/**\n * The minimal shape we produce / consume for `NextResponse`. Matches\n * `NextResponse.next()` / `NextResponse.redirect()` return values.\n */\ninterface NextResponseLike {\n readonly headers: { append(name: string, value: string): void };\n}\n\nexport interface VariantLabMiddlewareOptions extends VariantLabServerOptions {\n /**\n * Called on any caught error. Defaults to a no-op so the middleware\n * remains fail-open. Provide a logger here if you want visibility.\n */\n readonly onError?: (error: Error) => void;\n}\n\n/**\n * Build a middleware function. Accepts the raw response factory from\n * the caller so we don't need to import `NextResponse` directly (that\n * would drag `next/server` into every consumer's bundle).\n *\n * Typical usage in `middleware.ts`:\n *\n * import { NextResponse } from \"next/server\";\n * import experiments from \"./experiments.json\";\n * import { variantLabMiddleware } from \"@variantlab/next\";\n *\n * const middleware = variantLabMiddleware(experiments);\n *\n * export default function (req) {\n * return middleware(req, NextResponse.next());\n * }\n *\n * The factory also exports `middleware.handle(req, nextResponseFactory)`\n * for the more idiomatic style where NextResponse is only imported once.\n */\nexport function variantLabMiddleware(\n rawConfig: unknown,\n options: VariantLabMiddlewareOptions = {},\n) {\n // Validate once at import time so schema errors surface at build time,\n // not on the first request. The validated value is unused by the\n // per-request path, but we keep it captured to hold a strong reference.\n let frozen = true;\n try {\n validateConfig(rawConfig);\n } catch (error) {\n options.onError?.(error as Error);\n frozen = false;\n }\n\n const cookieName = options.cookieName ?? DEFAULT_COOKIE_NAME;\n const onError = options.onError ?? (() => {});\n\n return function apply<TResponse extends NextResponseLike>(\n req: NextRequestLike,\n response: TResponse,\n ): TResponse {\n if (!frozen) return response;\n try {\n const header = req.headers.get(\"cookie\");\n const cookies = parseCookieHeader(header ?? \"\");\n const existing = decodePayload(cookies[cookieName]);\n if (existing !== null) return response;\n\n const payload: StickyCookiePayload = {\n v: 1,\n u: generateUserId(),\n a: {},\n };\n const secure = req.nextUrl.protocol === \"https:\";\n const setCookie = serializeCookie(cookieName, encodePayload(payload), {\n ...options,\n secure,\n });\n response.headers.append(\"set-cookie\", setCookie);\n return response;\n } catch (error) {\n onError(error as Error);\n return response;\n }\n };\n}\n","/**\n * `@variantlab/next` — server barrel.\n *\n * This file is the default entrypoint when consumers write\n * `import { ... } from \"@variantlab/next\"`. It exposes only\n * server-safe helpers (no `\"use client\"`, no React). Use\n * `@variantlab/next/client` for the provider and hooks.\n *\n * Edge-runtime compatible: no Node-only APIs, no `process.env`, and\n * no runtime dependencies beyond `@variantlab/core`.\n */\n\nexport const VERSION = \"0.0.0\";\n\n// Re-export core types so consumers can `import type { ExperimentsConfig }\n// from \"@variantlab/next\"` without reaching into core directly.\nexport type {\n AssignmentStrategy,\n Experiment,\n ExperimentsConfig,\n Variant,\n VariantContext,\n} from \"@variantlab/core\";\n// Cookie codec + request adapters\nexport {\n decodePayload,\n encodePayload,\n generateUserId,\n parseCookieHeader,\n readCookieFromSource,\n readPayloadFromSource,\n serializeCookie,\n} from \"./server/cookie.js\";\n// Server-side resolution\nexport {\n type CreateVariantLabServerOptions,\n createVariantLabServer,\n type VariantLabServer,\n} from \"./server/create-variant-lab-server.js\";\nexport { getVariantSSR, getVariantValueSSR } from \"./server/get-variant-ssr.js\";\n// Middleware factory\nexport {\n type VariantLabMiddlewareOptions,\n variantLabMiddleware,\n} from \"./server/middleware.js\";\n// Types\nexport type {\n CookieSource,\n PagesRouterRequestLike,\n RequestCookieJar,\n StickyCookiePayload,\n VariantLabProviderProps,\n VariantLabServerOptions,\n} from \"./types.js\";\nexport { DEFAULT_COOKIE_NAME, DEFAULT_MAX_AGE } from \"./types.js\";\n","/**\n * `@variantlab/next/app-router` — App Router-scoped entrypoint.\n *\n * Re-exports everything in the main barrel, plus a narrow helper for\n * reading the sticky cookie from a Next `cookies()` store. Using a\n * dedicated subpath lets the App Router build a slightly smaller\n * bundle than consumers who mix routers.\n */\n\nexport * from \"./index.js\";\n\nimport { readPayloadFromSource } from \"./server/cookie.js\";\nimport type { StickyCookiePayload } from \"./types.js\";\nimport { DEFAULT_COOKIE_NAME } from \"./types.js\";\n\n/**\n * Minimal shape of the object returned by Next's `cookies()` helper.\n * Typed locally so we don't pull `next/headers` into the type surface\n * of every importer — Next itself will provide the real types at the\n * call site.\n */\nexport interface AppRouterCookieStore {\n get(name: string): { readonly value: string } | undefined;\n}\n\n/**\n * Decode the sticky payload from an App Router cookie store. Accepts\n * both Next 14's synchronous `cookies()` return value and Next 15's\n * awaited `cookies()`.\n *\n * import { cookies } from \"next/headers\";\n * import { readPayloadFromCookies } from \"@variantlab/next/app-router\";\n *\n * // Next 14:\n * const payload = readPayloadFromCookies(cookies());\n * // Next 15:\n * const payload = readPayloadFromCookies(await cookies());\n */\nexport function readPayloadFromCookies(\n store: AppRouterCookieStore,\n name: string = DEFAULT_COOKIE_NAME,\n): StickyCookiePayload | null {\n return readPayloadFromSource(store, name);\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { StickyCookiePayload } from './index.cjs';
|
|
2
|
+
export { CookieSource, CreateVariantLabServerOptions, DEFAULT_COOKIE_NAME, DEFAULT_MAX_AGE, PagesRouterRequestLike, RequestCookieJar, VERSION, VariantLabMiddlewareOptions, VariantLabProviderProps, VariantLabServer, VariantLabServerOptions, createVariantLabServer, decodePayload, encodePayload, generateUserId, getVariantSSR, getVariantValueSSR, parseCookieHeader, readCookieFromSource, readPayloadFromSource, serializeCookie, variantLabMiddleware } from './index.cjs';
|
|
3
|
+
export { AssignmentStrategy, Experiment, ExperimentsConfig, Variant, VariantContext } from '@variantlab/core';
|
|
4
|
+
import 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `@variantlab/next/app-router` — App Router-scoped entrypoint.
|
|
8
|
+
*
|
|
9
|
+
* Re-exports everything in the main barrel, plus a narrow helper for
|
|
10
|
+
* reading the sticky cookie from a Next `cookies()` store. Using a
|
|
11
|
+
* dedicated subpath lets the App Router build a slightly smaller
|
|
12
|
+
* bundle than consumers who mix routers.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Minimal shape of the object returned by Next's `cookies()` helper.
|
|
17
|
+
* Typed locally so we don't pull `next/headers` into the type surface
|
|
18
|
+
* of every importer — Next itself will provide the real types at the
|
|
19
|
+
* call site.
|
|
20
|
+
*/
|
|
21
|
+
interface AppRouterCookieStore {
|
|
22
|
+
get(name: string): {
|
|
23
|
+
readonly value: string;
|
|
24
|
+
} | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Decode the sticky payload from an App Router cookie store. Accepts
|
|
28
|
+
* both Next 14's synchronous `cookies()` return value and Next 15's
|
|
29
|
+
* awaited `cookies()`.
|
|
30
|
+
*
|
|
31
|
+
* import { cookies } from "next/headers";
|
|
32
|
+
* import { readPayloadFromCookies } from "@variantlab/next/app-router";
|
|
33
|
+
*
|
|
34
|
+
* // Next 14:
|
|
35
|
+
* const payload = readPayloadFromCookies(cookies());
|
|
36
|
+
* // Next 15:
|
|
37
|
+
* const payload = readPayloadFromCookies(await cookies());
|
|
38
|
+
*/
|
|
39
|
+
declare function readPayloadFromCookies(store: AppRouterCookieStore, name?: string): StickyCookiePayload | null;
|
|
40
|
+
|
|
41
|
+
export { type AppRouterCookieStore, StickyCookiePayload, readPayloadFromCookies };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { StickyCookiePayload } from './index.js';
|
|
2
|
+
export { CookieSource, CreateVariantLabServerOptions, DEFAULT_COOKIE_NAME, DEFAULT_MAX_AGE, PagesRouterRequestLike, RequestCookieJar, VERSION, VariantLabMiddlewareOptions, VariantLabProviderProps, VariantLabServer, VariantLabServerOptions, createVariantLabServer, decodePayload, encodePayload, generateUserId, getVariantSSR, getVariantValueSSR, parseCookieHeader, readCookieFromSource, readPayloadFromSource, serializeCookie, variantLabMiddleware } from './index.js';
|
|
3
|
+
export { AssignmentStrategy, Experiment, ExperimentsConfig, Variant, VariantContext } from '@variantlab/core';
|
|
4
|
+
import 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `@variantlab/next/app-router` — App Router-scoped entrypoint.
|
|
8
|
+
*
|
|
9
|
+
* Re-exports everything in the main barrel, plus a narrow helper for
|
|
10
|
+
* reading the sticky cookie from a Next `cookies()` store. Using a
|
|
11
|
+
* dedicated subpath lets the App Router build a slightly smaller
|
|
12
|
+
* bundle than consumers who mix routers.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Minimal shape of the object returned by Next's `cookies()` helper.
|
|
17
|
+
* Typed locally so we don't pull `next/headers` into the type surface
|
|
18
|
+
* of every importer — Next itself will provide the real types at the
|
|
19
|
+
* call site.
|
|
20
|
+
*/
|
|
21
|
+
interface AppRouterCookieStore {
|
|
22
|
+
get(name: string): {
|
|
23
|
+
readonly value: string;
|
|
24
|
+
} | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Decode the sticky payload from an App Router cookie store. Accepts
|
|
28
|
+
* both Next 14's synchronous `cookies()` return value and Next 15's
|
|
29
|
+
* awaited `cookies()`.
|
|
30
|
+
*
|
|
31
|
+
* import { cookies } from "next/headers";
|
|
32
|
+
* import { readPayloadFromCookies } from "@variantlab/next/app-router";
|
|
33
|
+
*
|
|
34
|
+
* // Next 14:
|
|
35
|
+
* const payload = readPayloadFromCookies(cookies());
|
|
36
|
+
* // Next 15:
|
|
37
|
+
* const payload = readPayloadFromCookies(await cookies());
|
|
38
|
+
*/
|
|
39
|
+
declare function readPayloadFromCookies(store: AppRouterCookieStore, name?: string): StickyCookiePayload | null;
|
|
40
|
+
|
|
41
|
+
export { type AppRouterCookieStore, StickyCookiePayload, readPayloadFromCookies };
|