@xenia-martech/sdk-js 1.0.1-dev.1 → 1.1.0-dev.2
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/README.md +54 -4
- package/dist/{actions-CcBYV--3.d.cts → actions-CKeiofxJ.d.cts} +19 -1
- package/dist/{actions-CcBYV--3.d.ts → actions-CKeiofxJ.d.ts} +19 -1
- package/dist/{cookie-names-B3b0XNc2.d.cts → api-URhxxi4x.d.cts} +19 -42
- package/dist/{cookie-names-B3b0XNc2.d.ts → api-URhxxi4x.d.ts} +19 -42
- package/dist/chunk-5D7SJIO4.cjs +109 -0
- package/dist/chunk-5D7SJIO4.cjs.map +1 -0
- package/dist/{chunk-XXNDHYAS.js → chunk-FYSEKFIY.js} +45 -103
- package/dist/chunk-FYSEKFIY.js.map +1 -0
- package/dist/{chunk-WYM4CQVG.cjs → chunk-JEKVN7HK.cjs} +46 -106
- package/dist/chunk-JEKVN7HK.cjs.map +1 -0
- package/dist/chunk-TLTG3BTC.js +104 -0
- package/dist/chunk-TLTG3BTC.js.map +1 -0
- package/dist/chunk-U2WBBOYT.cjs +233 -0
- package/dist/chunk-U2WBBOYT.cjs.map +1 -0
- package/dist/chunk-WYAMPEVV.js +227 -0
- package/dist/chunk-WYAMPEVV.js.map +1 -0
- package/dist/client/index.cjs +60 -6
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +25 -3
- package/dist/client/index.d.ts +25 -3
- package/dist/client/index.js +59 -7
- package/dist/client/index.js.map +1 -1
- package/dist/client/react/index.cjs +58 -6
- package/dist/client/react/index.cjs.map +1 -1
- package/dist/client/react/index.d.cts +2 -2
- package/dist/client/react/index.d.ts +2 -2
- package/dist/client/react/index.js +58 -6
- package/dist/client/react/index.js.map +1 -1
- package/dist/cookie-names-8FxbGKGS.d.ts +127 -0
- package/dist/cookie-names-QYMiEMAq.d.cts +127 -0
- package/dist/index.cjs +41 -32
- package/dist/index.d.cts +4 -71
- package/dist/index.d.ts +4 -71
- package/dist/index.js +2 -1
- package/dist/next/index.cjs +59 -0
- package/dist/next/index.cjs.map +1 -0
- package/dist/next/index.d.cts +45 -0
- package/dist/next/index.d.ts +45 -0
- package/dist/next/index.js +54 -0
- package/dist/next/index.js.map +1 -0
- package/dist/server/index.cjs +38 -214
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +5 -104
- package/dist/server/index.d.ts +5 -104
- package/dist/server/index.js +5 -204
- package/dist/server/index.js.map +1 -1
- package/dist/xenia-sdk.iife.js +3 -3
- package/dist/xenia-sdk.iife.js.map +1 -1
- package/dist/xenia-server-D-ozqSgv.d.ts +126 -0
- package/dist/xenia-server-rs3Gpbw7.d.cts +126 -0
- package/package.json +26 -7
- package/dist/chunk-WYM4CQVG.cjs.map +0 -1
- package/dist/chunk-XXNDHYAS.js.map +0 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkJEKVN7HK_cjs = require('./chunk-JEKVN7HK.cjs');
|
|
4
|
+
|
|
5
|
+
// src/core/event-payload.ts
|
|
6
|
+
var RESERVED = /* @__PURE__ */ new Set([
|
|
7
|
+
"eventId",
|
|
8
|
+
"eventType",
|
|
9
|
+
"journeyId",
|
|
10
|
+
"deviceId",
|
|
11
|
+
"visitorId",
|
|
12
|
+
"customerId",
|
|
13
|
+
"email",
|
|
14
|
+
"utmSource",
|
|
15
|
+
"utmMedium",
|
|
16
|
+
"utmCampaign",
|
|
17
|
+
"utmContent",
|
|
18
|
+
"utmId",
|
|
19
|
+
"url",
|
|
20
|
+
"referrer",
|
|
21
|
+
"path",
|
|
22
|
+
"query"
|
|
23
|
+
]);
|
|
24
|
+
function compact2(obj) {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
27
|
+
if (v !== void 0) out[k] = v;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function buildEventPayload(input) {
|
|
32
|
+
const passthrough = {};
|
|
33
|
+
const reserved = {};
|
|
34
|
+
if (input.props) {
|
|
35
|
+
for (const [k, v] of Object.entries(input.props)) {
|
|
36
|
+
if (v === void 0) continue;
|
|
37
|
+
if (RESERVED.has(k)) reserved[k] = v;
|
|
38
|
+
else passthrough[k] = v;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const payload = {
|
|
42
|
+
...passthrough,
|
|
43
|
+
// Reserved-named props fill first-class fields (e.g. `email`, `customerId`)
|
|
44
|
+
// the SDK didn't auto-collect. Auto-collected context/identity still win for
|
|
45
|
+
// what they reliably know (real page url, visitor/device id), so a stray prop
|
|
46
|
+
// can't override the true page context — see merge order below.
|
|
47
|
+
...reserved,
|
|
48
|
+
...compact2(input.context ?? {}),
|
|
49
|
+
...compact2(input.identity ?? {}),
|
|
50
|
+
eventId: input.eventId,
|
|
51
|
+
...input.eventType ? { eventType: input.eventType } : {},
|
|
52
|
+
...input.journeyId ? { journeyId: input.journeyId } : {}
|
|
53
|
+
};
|
|
54
|
+
return payload;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/server/xenia-server.ts
|
|
58
|
+
async function orFallback(call, opts) {
|
|
59
|
+
if (!opts || !("fallback" in opts)) return call();
|
|
60
|
+
try {
|
|
61
|
+
return await call();
|
|
62
|
+
} catch {
|
|
63
|
+
return opts.fallback;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
var XeniaServer = class {
|
|
67
|
+
api;
|
|
68
|
+
/** SDK-owned visitor anchor for this (per-request) client — see {@link XeniaConfig.visitorId}. */
|
|
69
|
+
visitorId;
|
|
70
|
+
/** Ambient request context attached to every call — see {@link XeniaConfig.context}. */
|
|
71
|
+
context;
|
|
72
|
+
constructor(config) {
|
|
73
|
+
this.api = new chunkJEKVN7HK_cjs.XeniaApi(config);
|
|
74
|
+
this.visitorId = config.visitorId;
|
|
75
|
+
this.context = config.context;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Send a unified event server-side (`POST /api/events/v1`) and return the
|
|
79
|
+
* ingest response (including per-result `triggered`). `opts.trigger: true`
|
|
80
|
+
* also matches/advances a journey; pass `opts.eventId` (e.g. `"ord-1001"`)
|
|
81
|
+
* as a stable dedup key for idempotent conversions.
|
|
82
|
+
*/
|
|
83
|
+
async track(eventName, opts = {}) {
|
|
84
|
+
const event = {
|
|
85
|
+
eventName,
|
|
86
|
+
eventId: opts.eventId ? chunkJEKVN7HK_cjs.toEventId(opts.eventId) : chunkJEKVN7HK_cjs.generateUlid(),
|
|
87
|
+
eventVersion: opts.eventVersion ?? 1,
|
|
88
|
+
occurredAtMs: opts.occurredAtMs ?? Date.now(),
|
|
89
|
+
source: opts.source ?? "xenia-server-sdk",
|
|
90
|
+
identity: {
|
|
91
|
+
...opts.identity,
|
|
92
|
+
// SDK-owned, set LAST so it can't be overridden per call. Comes from
|
|
93
|
+
// `config.visitorId` (bind it per request from the `xenia_vid` cookie).
|
|
94
|
+
xeniaVisitorId: this.visitorId
|
|
95
|
+
},
|
|
96
|
+
context: opts.context,
|
|
97
|
+
properties: opts.properties,
|
|
98
|
+
consent: opts.consent,
|
|
99
|
+
trigger: opts.trigger === true ? true : void 0
|
|
100
|
+
};
|
|
101
|
+
return this.api.sendEvents([event]);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Send a webhook request and await the synchronous response. Merges
|
|
105
|
+
* `opts.identity`/`opts.context` into the payload the same way the browser
|
|
106
|
+
* client's auto-attached fields do (`path`, `query`, `url`, `referrer`,
|
|
107
|
+
* `deviceId`, `visitorId`, etc.) — see `ServerWebhookOptions`. Pass `opts.fallback` to get it
|
|
108
|
+
* back instead of an error (see {@link WithFallback}).
|
|
109
|
+
*/
|
|
110
|
+
async webhook(triggerId, data, opts = {}) {
|
|
111
|
+
const payload = buildEventPayload({
|
|
112
|
+
eventId: triggerId,
|
|
113
|
+
journeyId: opts.journeyId,
|
|
114
|
+
// SDK-owned visitor anchor wins: config.visitorId (bind the `xenia_vid`
|
|
115
|
+
// cookie per request) is attached automatically. Falls back to a
|
|
116
|
+
// per-call visitorId only when none is bound.
|
|
117
|
+
identity: { ...opts.identity, visitorId: this.visitorId ?? opts.identity?.visitorId },
|
|
118
|
+
// Ambient request context (config.context, bound once per request) under a per-call
|
|
119
|
+
// override — same precedence resolve() uses, so both surfaces carry the same fields.
|
|
120
|
+
context: { ...this.context, ...opts.context },
|
|
121
|
+
props: data
|
|
122
|
+
});
|
|
123
|
+
return orFallback(() => this.api.webhook(triggerId, payload, opts.method ?? "POST"), opts);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Bind visitor→user server-side — `POST /api/events/v1/identify`. `xeniaVisitorId` defaults to
|
|
127
|
+
* the bound `config.visitorId`, so a per-request client only passes what it learned (e.g. an email
|
|
128
|
+
* from a submitted form).
|
|
129
|
+
*/
|
|
130
|
+
async identify(body) {
|
|
131
|
+
const xeniaVisitorId = body.xeniaVisitorId ?? this.visitorId;
|
|
132
|
+
if (!xeniaVisitorId) {
|
|
133
|
+
throw new Error("identify() needs a visitor: bind config.visitorId or pass xeniaVisitorId");
|
|
134
|
+
}
|
|
135
|
+
return this.api.identify({ ...body, xeniaVisitorId });
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Resolve in-entry content personalization for one content key
|
|
139
|
+
* (`POST /api/personalization/resolve`). Requires a configured SDK token
|
|
140
|
+
* (`token`/`getToken`) — resolve is auth-required. With `merge: true` the
|
|
141
|
+
* returned `content` is server-spliced.
|
|
142
|
+
*
|
|
143
|
+
* Pass `opts.identity`/`opts.context` to attach the same SDK-provided
|
|
144
|
+
* fields (`deviceId`, `customerId`, `email`, `path`, `url`, `referrer`,
|
|
145
|
+
* `utm*`, …) the browser client collects ambiently — there's no ambient
|
|
146
|
+
* `window`/`document` server-side, so a server caller must supply whatever
|
|
147
|
+
* it knows (e.g. from the current request) explicitly. See
|
|
148
|
+
* {@link ResolveOptions}. Pass `opts.fallback` to get it back instead of an
|
|
149
|
+
* error (see {@link WithFallback}).
|
|
150
|
+
*/
|
|
151
|
+
async resolve(contentKey, opts) {
|
|
152
|
+
return orFallback(() => this.api.resolve(contentKey, this.withVisitor(opts)), opts);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolve several content keys in one round-trip (`POST /api/personalization/resolve-batch`).
|
|
156
|
+
* `opts.fallback` as in {@link resolve}.
|
|
157
|
+
*/
|
|
158
|
+
async resolveBatch(items, opts) {
|
|
159
|
+
return orFallback(() => this.api.resolveBatch(items, this.withVisitor(opts)), opts);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Merge the ambient `config.context` and `opts.identity`/`opts.context` into
|
|
163
|
+
* the resolve `data`, then stamp the SDK-owned visitor id
|
|
164
|
+
* (`config.visitorId`) under both the reserved `_xenia_vid` key (read by the
|
|
165
|
+
* backend for holdout/exposure bucketing) and the flat `visitorId` field —
|
|
166
|
+
* mirrors `webhook()`'s merge order/precedence exactly: caller `data` <
|
|
167
|
+
* ambient `config.context` < caller `context`/`identity` < SDK-owned
|
|
168
|
+
* `visitorId`/`_xenia_vid`, set LAST so neither can be overridden per call.
|
|
169
|
+
*
|
|
170
|
+
* `config.context` is what makes the SDK-provided fields automatic on the
|
|
171
|
+
* server: bound once per request, every `resolve()` carries `path`/`query`/
|
|
172
|
+
* `url`/`referrer`/UTMs with no per-call wiring, matching what the browser
|
|
173
|
+
* client collects from `window` on its own. A no-op when nothing is bound and
|
|
174
|
+
* nothing was passed — preserving today's behavior for callers using neither.
|
|
175
|
+
*/
|
|
176
|
+
withVisitor(opts) {
|
|
177
|
+
const identity = chunkJEKVN7HK_cjs.compact({
|
|
178
|
+
...opts?.identity,
|
|
179
|
+
visitorId: this.visitorId ?? opts?.identity?.visitorId
|
|
180
|
+
});
|
|
181
|
+
const merged = {
|
|
182
|
+
...chunkJEKVN7HK_cjs.compact(this.context ?? {}),
|
|
183
|
+
...chunkJEKVN7HK_cjs.compact(opts?.context ?? {}),
|
|
184
|
+
...identity
|
|
185
|
+
};
|
|
186
|
+
if (Object.keys(merged).length === 0 && !this.visitorId) return opts;
|
|
187
|
+
if (this.visitorId) merged._xenia_vid = this.visitorId;
|
|
188
|
+
return { ...opts, data: { ...opts?.data ?? {}, ...merged } };
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
function createXeniaServer(config) {
|
|
192
|
+
return new XeniaServer(config);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/server/cookies.ts
|
|
196
|
+
function parseCookies(header) {
|
|
197
|
+
const out = {};
|
|
198
|
+
if (!header) return out;
|
|
199
|
+
for (const part of header.split(";")) {
|
|
200
|
+
const idx = part.indexOf("=");
|
|
201
|
+
if (idx === -1) continue;
|
|
202
|
+
const key = part.slice(0, idx).trim();
|
|
203
|
+
const value = part.slice(idx + 1).trim();
|
|
204
|
+
if (key) out[key] = decodeURIComponent(value);
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
function getVisitorId(cookieHeader, prefix) {
|
|
209
|
+
return parseCookies(cookieHeader)[chunkJEKVN7HK_cjs.visitorCookieName(prefix)] ?? null;
|
|
210
|
+
}
|
|
211
|
+
function getConsent(cookieHeader, prefix) {
|
|
212
|
+
const raw = parseCookies(cookieHeader)[chunkJEKVN7HK_cjs.consentCookieName(prefix)];
|
|
213
|
+
if (!raw) return { analytics: false, marketing: false, personalization: false };
|
|
214
|
+
try {
|
|
215
|
+
const parsed = JSON.parse(raw);
|
|
216
|
+
return parsed;
|
|
217
|
+
} catch {
|
|
218
|
+
const scopes = new Set(raw.split(",").map((s) => s.trim()));
|
|
219
|
+
return {
|
|
220
|
+
analytics: scopes.has("analytics"),
|
|
221
|
+
marketing: scopes.has("marketing"),
|
|
222
|
+
personalization: scopes.has("personalization")
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
exports.XeniaServer = XeniaServer;
|
|
228
|
+
exports.createXeniaServer = createXeniaServer;
|
|
229
|
+
exports.getConsent = getConsent;
|
|
230
|
+
exports.getVisitorId = getVisitorId;
|
|
231
|
+
exports.parseCookies = parseCookies;
|
|
232
|
+
//# sourceMappingURL=chunk-U2WBBOYT.cjs.map
|
|
233
|
+
//# sourceMappingURL=chunk-U2WBBOYT.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/event-payload.ts","../src/server/xenia-server.ts","../src/server/cookies.ts"],"names":["compact","XeniaApi","toEventId","generateUlid","visitorCookieName","consentCookieName"],"mappings":"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","file":"chunk-U2WBBOYT.cjs","sourcesContent":["import type { EventContext, EventIdentity, EventPayload, JsonValue } from \"./types\";\n\n/** Reserved keys that map to first-class fields (not passthrough data). */\nconst RESERVED = new Set([\n \"eventId\",\n \"eventType\",\n \"journeyId\",\n \"deviceId\",\n \"visitorId\",\n \"customerId\",\n \"email\",\n \"utmSource\",\n \"utmMedium\",\n \"utmCampaign\",\n \"utmContent\",\n \"utmId\",\n \"url\",\n \"referrer\",\n \"path\",\n \"query\",\n]);\n\nexport interface BuildEventInput {\n eventId: string;\n eventType?: string;\n journeyId?: string;\n identity?: EventIdentity;\n context?: EventContext;\n /** Arbitrary extra properties merged as passthrough data. */\n props?: Record<string, JsonValue | undefined>;\n}\n\n/** Drop `undefined` values so the JSON body stays clean. */\nfunction compact(obj: Record<string, JsonValue | undefined>): Record<string, JsonValue> {\n const out: Record<string, JsonValue> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (v !== undefined) out[k] = v;\n }\n return out;\n}\n\n/**\n * Build the request body for the webhook `/flow` call (`XeniaApi.webhook`).\n *\n * Merge order (later wins): passthrough props < context < identity < core.\n * Field names match the backend `EventTriggerRequest` DTO exactly.\n *\n * @internal Used only by the webhook request/response path — the unified event\n * surface (`track()` → `POST /api/events/v1`) builds {@link XeniaEvent} instead.\n */\nexport function buildEventPayload(input: BuildEventInput): EventPayload {\n const passthrough: Record<string, JsonValue> = {};\n const reserved: Record<string, JsonValue> = {};\n if (input.props) {\n for (const [k, v] of Object.entries(input.props)) {\n if (v === undefined) continue;\n // A prop whose key matches a reserved field (e.g. `email`, `customerId`,\n // `utmSource`) is promoted to that first-class field; everything else is\n // custom passthrough data. Previously reserved-named props were dropped\n // outright — so `track(\"sign_up\", { email })` never sent `email` at all.\n if (RESERVED.has(k)) reserved[k] = v;\n else passthrough[k] = v;\n }\n }\n\n const payload = {\n ...passthrough,\n // Reserved-named props fill first-class fields (e.g. `email`, `customerId`)\n // the SDK didn't auto-collect. Auto-collected context/identity still win for\n // what they reliably know (real page url, visitor/device id), so a stray prop\n // can't override the true page context — see merge order below.\n ...reserved,\n ...compact((input.context ?? {}) as Record<string, JsonValue | undefined>),\n ...compact((input.identity ?? {}) as Record<string, JsonValue | undefined>),\n eventId: input.eventId,\n ...(input.eventType ? { eventType: input.eventType } : {}),\n ...(input.journeyId ? { journeyId: input.journeyId } : {}),\n };\n\n return payload as EventPayload;\n}\n","import { compact, XeniaApi } from \"../core/api\";\nimport { buildEventPayload } from \"../core/event-payload\";\nimport { generateUlid, toEventId } from \"../core/id\";\nimport type {\n AnalyticsContext,\n AnalyticsEventName,\n CallerIdentity,\n AnalyticsIngestResponse,\n ConsentState,\n EventContext,\n EventIdentity,\n IdentifyRequest,\n IdentifyResponse,\n JsonValue,\n ResolveBatchItem,\n ResolveBatchResult,\n ResolveOptions,\n ResolveResult,\n WebhookResponse,\n XeniaConfig,\n XeniaEvent,\n} from \"../core/types\";\n\n/**\n * Extra per-call identity/context for server-side webhook calls — an override on top of whatever the\n * client already carries.\n *\n * There's no ambient `window` on the server to auto-collect `path`/`query`/`url`/UTM params the way\n * the browser client does, so bind them ONCE per request via {@link XeniaConfig.context} (see\n * `contextFromUrl`) and every call attaches them automatically. These per-call fields are for the\n * remainder: values known only at this call site, or a deliberate override of the ambient ones.\n */\nexport interface ServerWebhookOptions<F = never> extends WithFallback<F> {\n method?: \"GET\" | \"POST\";\n journeyId?: string;\n identity?: EventIdentity;\n context?: EventContext;\n}\n\n/**\n * Opt-in \"never throw\" for calls a page renders from. When `fallback` is set (even to `null`), any\n * failure (timeout, network error, non-2xx, open circuit) resolves to it instead of throwing, so a\n * caller can render its default content without a `try/catch`. Errors are swallowed silently: leave\n * it unset where you need to see them.\n */\nexport interface WithFallback<F> {\n fallback?: F;\n}\n\n/** {@link ResolveOptions} plus {@link WithFallback}. */\nexport type ServerResolveOptions<F = never> = ResolveOptions & WithFallback<F>;\n\n/** Runs `call`, or resolves to `opts.fallback` on any error when one was given. */\nasync function orFallback<T, F>(call: () => Promise<T>, opts?: WithFallback<F>): Promise<T | F> {\n if (!opts || !(\"fallback\" in opts)) return call();\n try {\n return await call();\n } catch {\n return opts.fallback as F;\n }\n}\n\n/**\n * Options for a server-side unified event (`POST /api/events/v1`). There's no\n * ambient request context on the server — pass whatever's known at the call\n * site (e.g. pathname/user-agent from the current request, session id from a\n * cookie) via `identity`/`context`.\n */\nexport interface ServerTrackOptions {\n /** When true, also match/advance a journey (never sampled/shed). Default false. */\n trigger?: boolean;\n /** Stable dedup key (e.g. `\"ord-1001\"`) for idempotent conversions. */\n eventId?: string;\n eventVersion?: number;\n occurredAtMs?: number;\n /** Origin tag. Default `\"xenia-server-sdk\"`. */\n source?: string;\n /** Identity fields for this event (except the SDK-owned `xeniaVisitorId`,\n * which comes from `config.visitorId` and can't be set per call). */\n identity?: CallerIdentity;\n context?: AnalyticsContext;\n properties?: Record<string, JsonValue | undefined>;\n consent?: ConsentState;\n}\n\n/**\n * Server-native Xenia client. Same method names as the browser client, but\n * every call is stateless: identity and context are passed explicitly rather\n * than read from the DOM/localStorage. Uses global `fetch` (Node ≥18).\n */\nexport class XeniaServer {\n readonly api: XeniaApi;\n /** SDK-owned visitor anchor for this (per-request) client — see {@link XeniaConfig.visitorId}. */\n readonly visitorId?: string;\n /** Ambient request context attached to every call — see {@link XeniaConfig.context}. */\n private readonly context?: EventContext;\n\n constructor(config: XeniaConfig) {\n this.api = new XeniaApi(config);\n this.visitorId = config.visitorId;\n this.context = config.context;\n }\n\n /**\n * Send a unified event server-side (`POST /api/events/v1`) and return the\n * ingest response (including per-result `triggered`). `opts.trigger: true`\n * also matches/advances a journey; pass `opts.eventId` (e.g. `\"ord-1001\"`)\n * as a stable dedup key for idempotent conversions.\n */\n async track(\n eventName: AnalyticsEventName,\n opts: ServerTrackOptions = {},\n ): Promise<AnalyticsIngestResponse> {\n const event: XeniaEvent = {\n eventName,\n eventId: opts.eventId ? toEventId(opts.eventId) : generateUlid(),\n eventVersion: opts.eventVersion ?? 1,\n occurredAtMs: opts.occurredAtMs ?? Date.now(),\n source: opts.source ?? \"xenia-server-sdk\",\n identity: {\n ...opts.identity,\n // SDK-owned, set LAST so it can't be overridden per call. Comes from\n // `config.visitorId` (bind it per request from the `xenia_vid` cookie).\n xeniaVisitorId: this.visitorId,\n },\n context: opts.context,\n properties: opts.properties,\n consent: opts.consent,\n trigger: opts.trigger === true ? true : undefined,\n };\n return this.api.sendEvents([event]);\n }\n\n /**\n * Send a webhook request and await the synchronous response. Merges\n * `opts.identity`/`opts.context` into the payload the same way the browser\n * client's auto-attached fields do (`path`, `query`, `url`, `referrer`,\n * `deviceId`, `visitorId`, etc.) — see `ServerWebhookOptions`. Pass `opts.fallback` to get it\n * back instead of an error (see {@link WithFallback}).\n */\n async webhook<F = never>(\n triggerId: string,\n data?: Record<string, JsonValue | undefined>,\n opts: ServerWebhookOptions<F> = {},\n ): Promise<WebhookResponse | F> {\n const payload = buildEventPayload({\n eventId: triggerId,\n journeyId: opts.journeyId,\n // SDK-owned visitor anchor wins: config.visitorId (bind the `xenia_vid`\n // cookie per request) is attached automatically. Falls back to a\n // per-call visitorId only when none is bound.\n identity: { ...opts.identity, visitorId: this.visitorId ?? opts.identity?.visitorId },\n // Ambient request context (config.context, bound once per request) under a per-call\n // override — same precedence resolve() uses, so both surfaces carry the same fields.\n context: { ...this.context, ...opts.context },\n props: data,\n });\n return orFallback(() => this.api.webhook(triggerId, payload as JsonValue, opts.method ?? \"POST\"), opts);\n }\n\n /**\n * Bind visitor→user server-side — `POST /api/events/v1/identify`. `xeniaVisitorId` defaults to\n * the bound `config.visitorId`, so a per-request client only passes what it learned (e.g. an email\n * from a submitted form).\n */\n async identify(\n body: Omit<IdentifyRequest, \"xeniaVisitorId\"> & { xeniaVisitorId?: string },\n ): Promise<IdentifyResponse> {\n const xeniaVisitorId = body.xeniaVisitorId ?? this.visitorId;\n if (!xeniaVisitorId) {\n throw new Error(\"identify() needs a visitor: bind config.visitorId or pass xeniaVisitorId\");\n }\n return this.api.identify({ ...body, xeniaVisitorId });\n }\n\n /**\n * Resolve in-entry content personalization for one content key\n * (`POST /api/personalization/resolve`). Requires a configured SDK token\n * (`token`/`getToken`) — resolve is auth-required. With `merge: true` the\n * returned `content` is server-spliced.\n *\n * Pass `opts.identity`/`opts.context` to attach the same SDK-provided\n * fields (`deviceId`, `customerId`, `email`, `path`, `url`, `referrer`,\n * `utm*`, …) the browser client collects ambiently — there's no ambient\n * `window`/`document` server-side, so a server caller must supply whatever\n * it knows (e.g. from the current request) explicitly. See\n * {@link ResolveOptions}. Pass `opts.fallback` to get it back instead of an\n * error (see {@link WithFallback}).\n */\n async resolve<F = never>(contentKey: string, opts?: ServerResolveOptions<F>): Promise<ResolveResult | F> {\n return orFallback(() => this.api.resolve(contentKey, this.withVisitor(opts)), opts);\n }\n\n /**\n * Resolve several content keys in one round-trip (`POST /api/personalization/resolve-batch`).\n * `opts.fallback` as in {@link resolve}.\n */\n async resolveBatch<F = never>(\n items: ResolveBatchItem[],\n opts?: ServerResolveOptions<F>,\n ): Promise<ResolveBatchResult[] | F> {\n return orFallback(() => this.api.resolveBatch(items, this.withVisitor(opts)), opts);\n }\n\n /**\n * Merge the ambient `config.context` and `opts.identity`/`opts.context` into\n * the resolve `data`, then stamp the SDK-owned visitor id\n * (`config.visitorId`) under both the reserved `_xenia_vid` key (read by the\n * backend for holdout/exposure bucketing) and the flat `visitorId` field —\n * mirrors `webhook()`'s merge order/precedence exactly: caller `data` <\n * ambient `config.context` < caller `context`/`identity` < SDK-owned\n * `visitorId`/`_xenia_vid`, set LAST so neither can be overridden per call.\n *\n * `config.context` is what makes the SDK-provided fields automatic on the\n * server: bound once per request, every `resolve()` carries `path`/`query`/\n * `url`/`referrer`/UTMs with no per-call wiring, matching what the browser\n * client collects from `window` on its own. A no-op when nothing is bound and\n * nothing was passed — preserving today's behavior for callers using neither.\n */\n private withVisitor(opts?: ResolveOptions): ResolveOptions | undefined {\n const identity = compact({\n ...opts?.identity,\n visitorId: this.visitorId ?? opts?.identity?.visitorId,\n });\n const merged = {\n ...compact((this.context ?? {}) as Record<string, JsonValue | undefined>),\n ...compact((opts?.context ?? {}) as Record<string, JsonValue | undefined>),\n ...identity,\n };\n if (Object.keys(merged).length === 0 && !this.visitorId) return opts;\n if (this.visitorId) merged._xenia_vid = this.visitorId;\n return { ...opts, data: { ...(opts?.data ?? {}), ...merged } };\n }\n}\n\n/** Create a server-native client. */\nexport function createXeniaServer(config: XeniaConfig): XeniaServer {\n return new XeniaServer(config);\n}\n","import {\n CONSENT_COOKIE,\n VISITOR_COOKIE,\n consentCookie,\n consentCookieName,\n visitorCookieName,\n} from \"../core/cookie-names\";\nimport type { ConsentState } from \"../core/types\";\n\n/**\n * Cookie names the SDK reads server-side (set by the browser SDK / your app).\n *\n * The constants are the DEFAULT-prefix names; the functions build the name for any prefix. If you\n * pass `config.prefix` to the browser client, pass the same prefix here — the browser namespaces\n * the cookie by it, so the default name won't be there to find.\n */\nexport { CONSENT_COOKIE, VISITOR_COOKIE, consentCookie, consentCookieName, visitorCookieName };\n\n/**\n * Parse a raw `Cookie` header into a map. Framework-agnostic so it works with\n * `Request.headers.get(\"cookie\")`, Node `req.headers.cookie`, etc.\n */\nexport function parseCookies(header: string | null | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!header) return out;\n for (const part of header.split(\";\")) {\n const idx = part.indexOf(\"=\");\n if (idx === -1) continue;\n const key = part.slice(0, idx).trim();\n const value = part.slice(idx + 1).trim();\n if (key) out[key] = decodeURIComponent(value);\n }\n return out;\n}\n\n/**\n * Read the visitor id the browser SDK wrote, so a server-side event can be bound to the same\n * visitor (`createXeniaServer({ visitorId })`) and attribute to a browser-entered journey.\n *\n * @param prefix Must match the browser client's `config.prefix`. The cookie is namespaced by it, so\n * a mismatch finds nothing and quietly leaves the server with no visitor.\n */\nexport function getVisitorId(\n cookieHeader: string | null | undefined,\n prefix?: string,\n): string | null {\n return parseCookies(cookieHeader)[visitorCookieName(prefix)] ?? null;\n}\n\n/**\n * Read consent from a cookie header. Accepts a JSON blob or a comma-list of\n * granted scopes (e.g. `analytics,personalization`). Absent → all denied.\n *\n * @param prefix Must match the browser client's `config.prefix` — see {@link getVisitorId}.\n */\nexport function getConsent(\n cookieHeader: string | null | undefined,\n prefix?: string,\n): ConsentState {\n const raw = parseCookies(cookieHeader)[consentCookieName(prefix)];\n if (!raw) return { analytics: false, marketing: false, personalization: false };\n try {\n const parsed = JSON.parse(raw) as ConsentState;\n return parsed;\n } catch {\n const scopes = new Set(raw.split(\",\").map((s) => s.trim()));\n return {\n analytics: scopes.has(\"analytics\"),\n marketing: scopes.has(\"marketing\"),\n personalization: scopes.has(\"personalization\"),\n };\n }\n}\n"]}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { XeniaApi, toEventId, generateUlid, compact, visitorCookieName, consentCookieName } from './chunk-FYSEKFIY.js';
|
|
2
|
+
|
|
3
|
+
// src/core/event-payload.ts
|
|
4
|
+
var RESERVED = /* @__PURE__ */ new Set([
|
|
5
|
+
"eventId",
|
|
6
|
+
"eventType",
|
|
7
|
+
"journeyId",
|
|
8
|
+
"deviceId",
|
|
9
|
+
"visitorId",
|
|
10
|
+
"customerId",
|
|
11
|
+
"email",
|
|
12
|
+
"utmSource",
|
|
13
|
+
"utmMedium",
|
|
14
|
+
"utmCampaign",
|
|
15
|
+
"utmContent",
|
|
16
|
+
"utmId",
|
|
17
|
+
"url",
|
|
18
|
+
"referrer",
|
|
19
|
+
"path",
|
|
20
|
+
"query"
|
|
21
|
+
]);
|
|
22
|
+
function compact2(obj) {
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
25
|
+
if (v !== void 0) out[k] = v;
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function buildEventPayload(input) {
|
|
30
|
+
const passthrough = {};
|
|
31
|
+
const reserved = {};
|
|
32
|
+
if (input.props) {
|
|
33
|
+
for (const [k, v] of Object.entries(input.props)) {
|
|
34
|
+
if (v === void 0) continue;
|
|
35
|
+
if (RESERVED.has(k)) reserved[k] = v;
|
|
36
|
+
else passthrough[k] = v;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const payload = {
|
|
40
|
+
...passthrough,
|
|
41
|
+
// Reserved-named props fill first-class fields (e.g. `email`, `customerId`)
|
|
42
|
+
// the SDK didn't auto-collect. Auto-collected context/identity still win for
|
|
43
|
+
// what they reliably know (real page url, visitor/device id), so a stray prop
|
|
44
|
+
// can't override the true page context — see merge order below.
|
|
45
|
+
...reserved,
|
|
46
|
+
...compact2(input.context ?? {}),
|
|
47
|
+
...compact2(input.identity ?? {}),
|
|
48
|
+
eventId: input.eventId,
|
|
49
|
+
...input.eventType ? { eventType: input.eventType } : {},
|
|
50
|
+
...input.journeyId ? { journeyId: input.journeyId } : {}
|
|
51
|
+
};
|
|
52
|
+
return payload;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/server/xenia-server.ts
|
|
56
|
+
async function orFallback(call, opts) {
|
|
57
|
+
if (!opts || !("fallback" in opts)) return call();
|
|
58
|
+
try {
|
|
59
|
+
return await call();
|
|
60
|
+
} catch {
|
|
61
|
+
return opts.fallback;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
var XeniaServer = class {
|
|
65
|
+
api;
|
|
66
|
+
/** SDK-owned visitor anchor for this (per-request) client — see {@link XeniaConfig.visitorId}. */
|
|
67
|
+
visitorId;
|
|
68
|
+
/** Ambient request context attached to every call — see {@link XeniaConfig.context}. */
|
|
69
|
+
context;
|
|
70
|
+
constructor(config) {
|
|
71
|
+
this.api = new XeniaApi(config);
|
|
72
|
+
this.visitorId = config.visitorId;
|
|
73
|
+
this.context = config.context;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Send a unified event server-side (`POST /api/events/v1`) and return the
|
|
77
|
+
* ingest response (including per-result `triggered`). `opts.trigger: true`
|
|
78
|
+
* also matches/advances a journey; pass `opts.eventId` (e.g. `"ord-1001"`)
|
|
79
|
+
* as a stable dedup key for idempotent conversions.
|
|
80
|
+
*/
|
|
81
|
+
async track(eventName, opts = {}) {
|
|
82
|
+
const event = {
|
|
83
|
+
eventName,
|
|
84
|
+
eventId: opts.eventId ? toEventId(opts.eventId) : generateUlid(),
|
|
85
|
+
eventVersion: opts.eventVersion ?? 1,
|
|
86
|
+
occurredAtMs: opts.occurredAtMs ?? Date.now(),
|
|
87
|
+
source: opts.source ?? "xenia-server-sdk",
|
|
88
|
+
identity: {
|
|
89
|
+
...opts.identity,
|
|
90
|
+
// SDK-owned, set LAST so it can't be overridden per call. Comes from
|
|
91
|
+
// `config.visitorId` (bind it per request from the `xenia_vid` cookie).
|
|
92
|
+
xeniaVisitorId: this.visitorId
|
|
93
|
+
},
|
|
94
|
+
context: opts.context,
|
|
95
|
+
properties: opts.properties,
|
|
96
|
+
consent: opts.consent,
|
|
97
|
+
trigger: opts.trigger === true ? true : void 0
|
|
98
|
+
};
|
|
99
|
+
return this.api.sendEvents([event]);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Send a webhook request and await the synchronous response. Merges
|
|
103
|
+
* `opts.identity`/`opts.context` into the payload the same way the browser
|
|
104
|
+
* client's auto-attached fields do (`path`, `query`, `url`, `referrer`,
|
|
105
|
+
* `deviceId`, `visitorId`, etc.) — see `ServerWebhookOptions`. Pass `opts.fallback` to get it
|
|
106
|
+
* back instead of an error (see {@link WithFallback}).
|
|
107
|
+
*/
|
|
108
|
+
async webhook(triggerId, data, opts = {}) {
|
|
109
|
+
const payload = buildEventPayload({
|
|
110
|
+
eventId: triggerId,
|
|
111
|
+
journeyId: opts.journeyId,
|
|
112
|
+
// SDK-owned visitor anchor wins: config.visitorId (bind the `xenia_vid`
|
|
113
|
+
// cookie per request) is attached automatically. Falls back to a
|
|
114
|
+
// per-call visitorId only when none is bound.
|
|
115
|
+
identity: { ...opts.identity, visitorId: this.visitorId ?? opts.identity?.visitorId },
|
|
116
|
+
// Ambient request context (config.context, bound once per request) under a per-call
|
|
117
|
+
// override — same precedence resolve() uses, so both surfaces carry the same fields.
|
|
118
|
+
context: { ...this.context, ...opts.context },
|
|
119
|
+
props: data
|
|
120
|
+
});
|
|
121
|
+
return orFallback(() => this.api.webhook(triggerId, payload, opts.method ?? "POST"), opts);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Bind visitor→user server-side — `POST /api/events/v1/identify`. `xeniaVisitorId` defaults to
|
|
125
|
+
* the bound `config.visitorId`, so a per-request client only passes what it learned (e.g. an email
|
|
126
|
+
* from a submitted form).
|
|
127
|
+
*/
|
|
128
|
+
async identify(body) {
|
|
129
|
+
const xeniaVisitorId = body.xeniaVisitorId ?? this.visitorId;
|
|
130
|
+
if (!xeniaVisitorId) {
|
|
131
|
+
throw new Error("identify() needs a visitor: bind config.visitorId or pass xeniaVisitorId");
|
|
132
|
+
}
|
|
133
|
+
return this.api.identify({ ...body, xeniaVisitorId });
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolve in-entry content personalization for one content key
|
|
137
|
+
* (`POST /api/personalization/resolve`). Requires a configured SDK token
|
|
138
|
+
* (`token`/`getToken`) — resolve is auth-required. With `merge: true` the
|
|
139
|
+
* returned `content` is server-spliced.
|
|
140
|
+
*
|
|
141
|
+
* Pass `opts.identity`/`opts.context` to attach the same SDK-provided
|
|
142
|
+
* fields (`deviceId`, `customerId`, `email`, `path`, `url`, `referrer`,
|
|
143
|
+
* `utm*`, …) the browser client collects ambiently — there's no ambient
|
|
144
|
+
* `window`/`document` server-side, so a server caller must supply whatever
|
|
145
|
+
* it knows (e.g. from the current request) explicitly. See
|
|
146
|
+
* {@link ResolveOptions}. Pass `opts.fallback` to get it back instead of an
|
|
147
|
+
* error (see {@link WithFallback}).
|
|
148
|
+
*/
|
|
149
|
+
async resolve(contentKey, opts) {
|
|
150
|
+
return orFallback(() => this.api.resolve(contentKey, this.withVisitor(opts)), opts);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Resolve several content keys in one round-trip (`POST /api/personalization/resolve-batch`).
|
|
154
|
+
* `opts.fallback` as in {@link resolve}.
|
|
155
|
+
*/
|
|
156
|
+
async resolveBatch(items, opts) {
|
|
157
|
+
return orFallback(() => this.api.resolveBatch(items, this.withVisitor(opts)), opts);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Merge the ambient `config.context` and `opts.identity`/`opts.context` into
|
|
161
|
+
* the resolve `data`, then stamp the SDK-owned visitor id
|
|
162
|
+
* (`config.visitorId`) under both the reserved `_xenia_vid` key (read by the
|
|
163
|
+
* backend for holdout/exposure bucketing) and the flat `visitorId` field —
|
|
164
|
+
* mirrors `webhook()`'s merge order/precedence exactly: caller `data` <
|
|
165
|
+
* ambient `config.context` < caller `context`/`identity` < SDK-owned
|
|
166
|
+
* `visitorId`/`_xenia_vid`, set LAST so neither can be overridden per call.
|
|
167
|
+
*
|
|
168
|
+
* `config.context` is what makes the SDK-provided fields automatic on the
|
|
169
|
+
* server: bound once per request, every `resolve()` carries `path`/`query`/
|
|
170
|
+
* `url`/`referrer`/UTMs with no per-call wiring, matching what the browser
|
|
171
|
+
* client collects from `window` on its own. A no-op when nothing is bound and
|
|
172
|
+
* nothing was passed — preserving today's behavior for callers using neither.
|
|
173
|
+
*/
|
|
174
|
+
withVisitor(opts) {
|
|
175
|
+
const identity = compact({
|
|
176
|
+
...opts?.identity,
|
|
177
|
+
visitorId: this.visitorId ?? opts?.identity?.visitorId
|
|
178
|
+
});
|
|
179
|
+
const merged = {
|
|
180
|
+
...compact(this.context ?? {}),
|
|
181
|
+
...compact(opts?.context ?? {}),
|
|
182
|
+
...identity
|
|
183
|
+
};
|
|
184
|
+
if (Object.keys(merged).length === 0 && !this.visitorId) return opts;
|
|
185
|
+
if (this.visitorId) merged._xenia_vid = this.visitorId;
|
|
186
|
+
return { ...opts, data: { ...opts?.data ?? {}, ...merged } };
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
function createXeniaServer(config) {
|
|
190
|
+
return new XeniaServer(config);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/server/cookies.ts
|
|
194
|
+
function parseCookies(header) {
|
|
195
|
+
const out = {};
|
|
196
|
+
if (!header) return out;
|
|
197
|
+
for (const part of header.split(";")) {
|
|
198
|
+
const idx = part.indexOf("=");
|
|
199
|
+
if (idx === -1) continue;
|
|
200
|
+
const key = part.slice(0, idx).trim();
|
|
201
|
+
const value = part.slice(idx + 1).trim();
|
|
202
|
+
if (key) out[key] = decodeURIComponent(value);
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
function getVisitorId(cookieHeader, prefix) {
|
|
207
|
+
return parseCookies(cookieHeader)[visitorCookieName(prefix)] ?? null;
|
|
208
|
+
}
|
|
209
|
+
function getConsent(cookieHeader, prefix) {
|
|
210
|
+
const raw = parseCookies(cookieHeader)[consentCookieName(prefix)];
|
|
211
|
+
if (!raw) return { analytics: false, marketing: false, personalization: false };
|
|
212
|
+
try {
|
|
213
|
+
const parsed = JSON.parse(raw);
|
|
214
|
+
return parsed;
|
|
215
|
+
} catch {
|
|
216
|
+
const scopes = new Set(raw.split(",").map((s) => s.trim()));
|
|
217
|
+
return {
|
|
218
|
+
analytics: scopes.has("analytics"),
|
|
219
|
+
marketing: scopes.has("marketing"),
|
|
220
|
+
personalization: scopes.has("personalization")
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export { XeniaServer, createXeniaServer, getConsent, getVisitorId, parseCookies };
|
|
226
|
+
//# sourceMappingURL=chunk-WYAMPEVV.js.map
|
|
227
|
+
//# sourceMappingURL=chunk-WYAMPEVV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/event-payload.ts","../src/server/xenia-server.ts","../src/server/cookies.ts"],"names":["compact"],"mappings":";;;AAGA,IAAM,QAAA,uBAAe,GAAA,CAAI;AAAA,EACvB,SAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA,UAAA;AAAA,EACA,WAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA,aAAA;AAAA,EACA,YAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA;AAAA,EACA,UAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAC,CAAA;AAaD,SAASA,SAAQ,GAAA,EAAuE;AACtF,EAAA,MAAM,MAAiC,EAAC;AACxC,EAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAA,EAAG;AACxC,IAAA,IAAI,CAAA,KAAM,MAAA,EAAW,GAAA,CAAI,CAAC,CAAA,GAAI,CAAA;AAAA,EAChC;AACA,EAAA,OAAO,GAAA;AACT;AAWO,SAAS,kBAAkB,KAAA,EAAsC;AACtE,EAAA,MAAM,cAAyC,EAAC;AAChD,EAAA,MAAM,WAAsC,EAAC;AAC7C,EAAA,IAAI,MAAM,KAAA,EAAO;AACf,IAAA,KAAA,MAAW,CAAC,GAAG,CAAC,CAAA,IAAK,OAAO,OAAA,CAAQ,KAAA,CAAM,KAAK,CAAA,EAAG;AAChD,MAAA,IAAI,MAAM,MAAA,EAAW;AAKrB,MAAA,IAAI,SAAS,GAAA,CAAI,CAAC,CAAA,EAAG,QAAA,CAAS,CAAC,CAAA,GAAI,CAAA;AAAA,WAC9B,WAAA,CAAY,CAAC,CAAA,GAAI,CAAA;AAAA,IACxB;AAAA,EACF;AAEA,EAAA,MAAM,OAAA,GAAU;AAAA,IACd,GAAG,WAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKH,GAAG,QAAA;AAAA,IACH,GAAGA,QAAAA,CAAS,KAAA,CAAM,OAAA,IAAW,EAA4C,CAAA;AAAA,IACzE,GAAGA,QAAAA,CAAS,KAAA,CAAM,QAAA,IAAY,EAA4C,CAAA;AAAA,IAC1E,SAAS,KAAA,CAAM,OAAA;AAAA,IACf,GAAI,MAAM,SAAA,GAAY,EAAE,WAAW,KAAA,CAAM,SAAA,KAAc,EAAC;AAAA,IACxD,GAAI,MAAM,SAAA,GAAY,EAAE,WAAW,KAAA,CAAM,SAAA,KAAc;AAAC,GAC1D;AAEA,EAAA,OAAO,OAAA;AACT;;;AC3BA,eAAe,UAAA,CAAiB,MAAwB,IAAA,EAAwC;AAC9F,EAAA,IAAI,CAAC,IAAA,IAAQ,EAAE,UAAA,IAAc,IAAA,CAAA,SAAc,IAAA,EAAK;AAChD,EAAA,IAAI;AACF,IAAA,OAAO,MAAM,IAAA,EAAK;AAAA,EACpB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA,CAAK,QAAA;AAAA,EACd;AACF;AA8BO,IAAM,cAAN,MAAkB;AAAA,EACd,GAAA;AAAA;AAAA,EAEA,SAAA;AAAA;AAAA,EAEQ,OAAA;AAAA,EAEjB,YAAY,MAAA,EAAqB;AAC/B,IAAA,IAAA,CAAK,GAAA,GAAM,IAAI,QAAA,CAAS,MAAM,CAAA;AAC9B,IAAA,IAAA,CAAK,YAAY,MAAA,CAAO,SAAA;AACxB,IAAA,IAAA,CAAK,UAAU,MAAA,CAAO,OAAA;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,KAAA,CACJ,SAAA,EACA,IAAA,GAA2B,EAAC,EACM;AAClC,IAAA,MAAM,KAAA,GAAoB;AAAA,MACxB,SAAA;AAAA,MACA,SAAS,IAAA,CAAK,OAAA,GAAU,UAAU,IAAA,CAAK,OAAO,IAAI,YAAA,EAAa;AAAA,MAC/D,YAAA,EAAc,KAAK,YAAA,IAAgB,CAAA;AAAA,MACnC,YAAA,EAAc,IAAA,CAAK,YAAA,IAAgB,IAAA,CAAK,GAAA,EAAI;AAAA,MAC5C,MAAA,EAAQ,KAAK,MAAA,IAAU,kBAAA;AAAA,MACvB,QAAA,EAAU;AAAA,QACR,GAAG,IAAA,CAAK,QAAA;AAAA;AAAA;AAAA,QAGR,gBAAgB,IAAA,CAAK;AAAA,OACvB;AAAA,MACA,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,YAAY,IAAA,CAAK,UAAA;AAAA,MACjB,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,OAAA,EAAS,IAAA,CAAK,OAAA,KAAY,IAAA,GAAO,IAAA,GAAO;AAAA,KAC1C;AACA,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,UAAA,CAAW,CAAC,KAAK,CAAC,CAAA;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,OAAA,CACJ,SAAA,EACA,IAAA,EACA,IAAA,GAAgC,EAAC,EACH;AAC9B,IAAA,MAAM,UAAU,iBAAA,CAAkB;AAAA,MAChC,OAAA,EAAS,SAAA;AAAA,MACT,WAAW,IAAA,CAAK,SAAA;AAAA;AAAA;AAAA;AAAA,MAIhB,QAAA,EAAU,EAAE,GAAG,IAAA,CAAK,QAAA,EAAU,WAAW,IAAA,CAAK,SAAA,IAAa,IAAA,CAAK,QAAA,EAAU,SAAA,EAAU;AAAA;AAAA;AAAA,MAGpF,SAAS,EAAE,GAAG,KAAK,OAAA,EAAS,GAAG,KAAK,OAAA,EAAQ;AAAA,MAC5C,KAAA,EAAO;AAAA,KACR,CAAA;AACD,IAAA,OAAO,UAAA,CAAW,MAAM,IAAA,CAAK,GAAA,CAAI,OAAA,CAAQ,SAAA,EAAW,OAAA,EAAsB,IAAA,CAAK,MAAA,IAAU,MAAM,CAAA,EAAG,IAAI,CAAA;AAAA,EACxG;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,SACJ,IAAA,EAC2B;AAC3B,IAAA,MAAM,cAAA,GAAiB,IAAA,CAAK,cAAA,IAAkB,IAAA,CAAK,SAAA;AACnD,IAAA,IAAI,CAAC,cAAA,EAAgB;AACnB,MAAA,MAAM,IAAI,MAAM,0EAA0E,CAAA;AAAA,IAC5F;AACA,IAAA,OAAO,KAAK,GAAA,CAAI,QAAA,CAAS,EAAE,GAAG,IAAA,EAAM,gBAAgB,CAAA;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBA,MAAM,OAAA,CAAmB,UAAA,EAAoB,IAAA,EAA4D;AACvG,IAAA,OAAO,UAAA,CAAW,MAAM,IAAA,CAAK,GAAA,CAAI,OAAA,CAAQ,UAAA,EAAY,IAAA,CAAK,WAAA,CAAY,IAAI,CAAC,CAAA,EAAG,IAAI,CAAA;AAAA,EACpF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,YAAA,CACJ,KAAA,EACA,IAAA,EACmC;AACnC,IAAA,OAAO,UAAA,CAAW,MAAM,IAAA,CAAK,GAAA,CAAI,YAAA,CAAa,KAAA,EAAO,IAAA,CAAK,WAAA,CAAY,IAAI,CAAC,CAAA,EAAG,IAAI,CAAA;AAAA,EACpF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBQ,YAAY,IAAA,EAAmD;AACrE,IAAA,MAAM,WAAW,OAAA,CAAQ;AAAA,MACvB,GAAG,IAAA,EAAM,QAAA;AAAA,MACT,SAAA,EAAW,IAAA,CAAK,SAAA,IAAa,IAAA,EAAM,QAAA,EAAU;AAAA,KAC9C,CAAA;AACD,IAAA,MAAM,MAAA,GAAS;AAAA,MACb,GAAG,OAAA,CAAS,IAAA,CAAK,OAAA,IAAW,EAA4C,CAAA;AAAA,MACxE,GAAG,OAAA,CAAS,IAAA,EAAM,OAAA,IAAW,EAA4C,CAAA;AAAA,MACzE,GAAG;AAAA,KACL;AACA,IAAA,IAAI,MAAA,CAAO,KAAK,MAAM,CAAA,CAAE,WAAW,CAAA,IAAK,CAAC,IAAA,CAAK,SAAA,EAAW,OAAO,IAAA;AAChE,IAAA,IAAI,IAAA,CAAK,SAAA,EAAW,MAAA,CAAO,UAAA,GAAa,IAAA,CAAK,SAAA;AAC7C,IAAA,OAAO,EAAE,GAAG,IAAA,EAAM,IAAA,EAAM,EAAE,GAAI,IAAA,EAAM,IAAA,IAAQ,EAAC,EAAI,GAAG,MAAA,EAAO,EAAE;AAAA,EAC/D;AACF;AAGO,SAAS,kBAAkB,MAAA,EAAkC;AAClE,EAAA,OAAO,IAAI,YAAY,MAAM,CAAA;AAC/B;;;ACxNO,SAAS,aAAa,MAAA,EAA2D;AACtF,EAAA,MAAM,MAA8B,EAAC;AACrC,EAAA,IAAI,CAAC,QAAQ,OAAO,GAAA;AACpB,EAAA,KAAA,MAAW,IAAA,IAAQ,MAAA,CAAO,KAAA,CAAM,GAAG,CAAA,EAAG;AACpC,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,OAAA,CAAQ,GAAG,CAAA;AAC5B,IAAA,IAAI,QAAQ,EAAA,EAAI;AAChB,IAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,GAAG,EAAE,IAAA,EAAK;AACpC,IAAA,MAAM,QAAQ,IAAA,CAAK,KAAA,CAAM,GAAA,GAAM,CAAC,EAAE,IAAA,EAAK;AACvC,IAAA,IAAI,GAAA,EAAK,GAAA,CAAI,GAAG,CAAA,GAAI,mBAAmB,KAAK,CAAA;AAAA,EAC9C;AACA,EAAA,OAAO,GAAA;AACT;AASO,SAAS,YAAA,CACd,cACA,MAAA,EACe;AACf,EAAA,OAAO,aAAa,YAAY,CAAA,CAAE,iBAAA,CAAkB,MAAM,CAAC,CAAA,IAAK,IAAA;AAClE;AAQO,SAAS,UAAA,CACd,cACA,MAAA,EACc;AACd,EAAA,MAAM,MAAM,YAAA,CAAa,YAAY,CAAA,CAAE,iBAAA,CAAkB,MAAM,CAAC,CAAA;AAChE,EAAA,IAAI,CAAC,KAAK,OAAO,EAAE,WAAW,KAAA,EAAO,SAAA,EAAW,KAAA,EAAO,eAAA,EAAiB,KAAA,EAAM;AAC9E,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC7B,IAAA,OAAO,MAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAA,CAAI,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAM,CAAC,CAAA;AAC1D,IAAA,OAAO;AAAA,MACL,SAAA,EAAW,MAAA,CAAO,GAAA,CAAI,WAAW,CAAA;AAAA,MACjC,SAAA,EAAW,MAAA,CAAO,GAAA,CAAI,WAAW,CAAA;AAAA,MACjC,eAAA,EAAiB,MAAA,CAAO,GAAA,CAAI,iBAAiB;AAAA,KAC/C;AAAA,EACF;AACF","file":"chunk-WYAMPEVV.js","sourcesContent":["import type { EventContext, EventIdentity, EventPayload, JsonValue } from \"./types\";\n\n/** Reserved keys that map to first-class fields (not passthrough data). */\nconst RESERVED = new Set([\n \"eventId\",\n \"eventType\",\n \"journeyId\",\n \"deviceId\",\n \"visitorId\",\n \"customerId\",\n \"email\",\n \"utmSource\",\n \"utmMedium\",\n \"utmCampaign\",\n \"utmContent\",\n \"utmId\",\n \"url\",\n \"referrer\",\n \"path\",\n \"query\",\n]);\n\nexport interface BuildEventInput {\n eventId: string;\n eventType?: string;\n journeyId?: string;\n identity?: EventIdentity;\n context?: EventContext;\n /** Arbitrary extra properties merged as passthrough data. */\n props?: Record<string, JsonValue | undefined>;\n}\n\n/** Drop `undefined` values so the JSON body stays clean. */\nfunction compact(obj: Record<string, JsonValue | undefined>): Record<string, JsonValue> {\n const out: Record<string, JsonValue> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (v !== undefined) out[k] = v;\n }\n return out;\n}\n\n/**\n * Build the request body for the webhook `/flow` call (`XeniaApi.webhook`).\n *\n * Merge order (later wins): passthrough props < context < identity < core.\n * Field names match the backend `EventTriggerRequest` DTO exactly.\n *\n * @internal Used only by the webhook request/response path — the unified event\n * surface (`track()` → `POST /api/events/v1`) builds {@link XeniaEvent} instead.\n */\nexport function buildEventPayload(input: BuildEventInput): EventPayload {\n const passthrough: Record<string, JsonValue> = {};\n const reserved: Record<string, JsonValue> = {};\n if (input.props) {\n for (const [k, v] of Object.entries(input.props)) {\n if (v === undefined) continue;\n // A prop whose key matches a reserved field (e.g. `email`, `customerId`,\n // `utmSource`) is promoted to that first-class field; everything else is\n // custom passthrough data. Previously reserved-named props were dropped\n // outright — so `track(\"sign_up\", { email })` never sent `email` at all.\n if (RESERVED.has(k)) reserved[k] = v;\n else passthrough[k] = v;\n }\n }\n\n const payload = {\n ...passthrough,\n // Reserved-named props fill first-class fields (e.g. `email`, `customerId`)\n // the SDK didn't auto-collect. Auto-collected context/identity still win for\n // what they reliably know (real page url, visitor/device id), so a stray prop\n // can't override the true page context — see merge order below.\n ...reserved,\n ...compact((input.context ?? {}) as Record<string, JsonValue | undefined>),\n ...compact((input.identity ?? {}) as Record<string, JsonValue | undefined>),\n eventId: input.eventId,\n ...(input.eventType ? { eventType: input.eventType } : {}),\n ...(input.journeyId ? { journeyId: input.journeyId } : {}),\n };\n\n return payload as EventPayload;\n}\n","import { compact, XeniaApi } from \"../core/api\";\nimport { buildEventPayload } from \"../core/event-payload\";\nimport { generateUlid, toEventId } from \"../core/id\";\nimport type {\n AnalyticsContext,\n AnalyticsEventName,\n CallerIdentity,\n AnalyticsIngestResponse,\n ConsentState,\n EventContext,\n EventIdentity,\n IdentifyRequest,\n IdentifyResponse,\n JsonValue,\n ResolveBatchItem,\n ResolveBatchResult,\n ResolveOptions,\n ResolveResult,\n WebhookResponse,\n XeniaConfig,\n XeniaEvent,\n} from \"../core/types\";\n\n/**\n * Extra per-call identity/context for server-side webhook calls — an override on top of whatever the\n * client already carries.\n *\n * There's no ambient `window` on the server to auto-collect `path`/`query`/`url`/UTM params the way\n * the browser client does, so bind them ONCE per request via {@link XeniaConfig.context} (see\n * `contextFromUrl`) and every call attaches them automatically. These per-call fields are for the\n * remainder: values known only at this call site, or a deliberate override of the ambient ones.\n */\nexport interface ServerWebhookOptions<F = never> extends WithFallback<F> {\n method?: \"GET\" | \"POST\";\n journeyId?: string;\n identity?: EventIdentity;\n context?: EventContext;\n}\n\n/**\n * Opt-in \"never throw\" for calls a page renders from. When `fallback` is set (even to `null`), any\n * failure (timeout, network error, non-2xx, open circuit) resolves to it instead of throwing, so a\n * caller can render its default content without a `try/catch`. Errors are swallowed silently: leave\n * it unset where you need to see them.\n */\nexport interface WithFallback<F> {\n fallback?: F;\n}\n\n/** {@link ResolveOptions} plus {@link WithFallback}. */\nexport type ServerResolveOptions<F = never> = ResolveOptions & WithFallback<F>;\n\n/** Runs `call`, or resolves to `opts.fallback` on any error when one was given. */\nasync function orFallback<T, F>(call: () => Promise<T>, opts?: WithFallback<F>): Promise<T | F> {\n if (!opts || !(\"fallback\" in opts)) return call();\n try {\n return await call();\n } catch {\n return opts.fallback as F;\n }\n}\n\n/**\n * Options for a server-side unified event (`POST /api/events/v1`). There's no\n * ambient request context on the server — pass whatever's known at the call\n * site (e.g. pathname/user-agent from the current request, session id from a\n * cookie) via `identity`/`context`.\n */\nexport interface ServerTrackOptions {\n /** When true, also match/advance a journey (never sampled/shed). Default false. */\n trigger?: boolean;\n /** Stable dedup key (e.g. `\"ord-1001\"`) for idempotent conversions. */\n eventId?: string;\n eventVersion?: number;\n occurredAtMs?: number;\n /** Origin tag. Default `\"xenia-server-sdk\"`. */\n source?: string;\n /** Identity fields for this event (except the SDK-owned `xeniaVisitorId`,\n * which comes from `config.visitorId` and can't be set per call). */\n identity?: CallerIdentity;\n context?: AnalyticsContext;\n properties?: Record<string, JsonValue | undefined>;\n consent?: ConsentState;\n}\n\n/**\n * Server-native Xenia client. Same method names as the browser client, but\n * every call is stateless: identity and context are passed explicitly rather\n * than read from the DOM/localStorage. Uses global `fetch` (Node ≥18).\n */\nexport class XeniaServer {\n readonly api: XeniaApi;\n /** SDK-owned visitor anchor for this (per-request) client — see {@link XeniaConfig.visitorId}. */\n readonly visitorId?: string;\n /** Ambient request context attached to every call — see {@link XeniaConfig.context}. */\n private readonly context?: EventContext;\n\n constructor(config: XeniaConfig) {\n this.api = new XeniaApi(config);\n this.visitorId = config.visitorId;\n this.context = config.context;\n }\n\n /**\n * Send a unified event server-side (`POST /api/events/v1`) and return the\n * ingest response (including per-result `triggered`). `opts.trigger: true`\n * also matches/advances a journey; pass `opts.eventId` (e.g. `\"ord-1001\"`)\n * as a stable dedup key for idempotent conversions.\n */\n async track(\n eventName: AnalyticsEventName,\n opts: ServerTrackOptions = {},\n ): Promise<AnalyticsIngestResponse> {\n const event: XeniaEvent = {\n eventName,\n eventId: opts.eventId ? toEventId(opts.eventId) : generateUlid(),\n eventVersion: opts.eventVersion ?? 1,\n occurredAtMs: opts.occurredAtMs ?? Date.now(),\n source: opts.source ?? \"xenia-server-sdk\",\n identity: {\n ...opts.identity,\n // SDK-owned, set LAST so it can't be overridden per call. Comes from\n // `config.visitorId` (bind it per request from the `xenia_vid` cookie).\n xeniaVisitorId: this.visitorId,\n },\n context: opts.context,\n properties: opts.properties,\n consent: opts.consent,\n trigger: opts.trigger === true ? true : undefined,\n };\n return this.api.sendEvents([event]);\n }\n\n /**\n * Send a webhook request and await the synchronous response. Merges\n * `opts.identity`/`opts.context` into the payload the same way the browser\n * client's auto-attached fields do (`path`, `query`, `url`, `referrer`,\n * `deviceId`, `visitorId`, etc.) — see `ServerWebhookOptions`. Pass `opts.fallback` to get it\n * back instead of an error (see {@link WithFallback}).\n */\n async webhook<F = never>(\n triggerId: string,\n data?: Record<string, JsonValue | undefined>,\n opts: ServerWebhookOptions<F> = {},\n ): Promise<WebhookResponse | F> {\n const payload = buildEventPayload({\n eventId: triggerId,\n journeyId: opts.journeyId,\n // SDK-owned visitor anchor wins: config.visitorId (bind the `xenia_vid`\n // cookie per request) is attached automatically. Falls back to a\n // per-call visitorId only when none is bound.\n identity: { ...opts.identity, visitorId: this.visitorId ?? opts.identity?.visitorId },\n // Ambient request context (config.context, bound once per request) under a per-call\n // override — same precedence resolve() uses, so both surfaces carry the same fields.\n context: { ...this.context, ...opts.context },\n props: data,\n });\n return orFallback(() => this.api.webhook(triggerId, payload as JsonValue, opts.method ?? \"POST\"), opts);\n }\n\n /**\n * Bind visitor→user server-side — `POST /api/events/v1/identify`. `xeniaVisitorId` defaults to\n * the bound `config.visitorId`, so a per-request client only passes what it learned (e.g. an email\n * from a submitted form).\n */\n async identify(\n body: Omit<IdentifyRequest, \"xeniaVisitorId\"> & { xeniaVisitorId?: string },\n ): Promise<IdentifyResponse> {\n const xeniaVisitorId = body.xeniaVisitorId ?? this.visitorId;\n if (!xeniaVisitorId) {\n throw new Error(\"identify() needs a visitor: bind config.visitorId or pass xeniaVisitorId\");\n }\n return this.api.identify({ ...body, xeniaVisitorId });\n }\n\n /**\n * Resolve in-entry content personalization for one content key\n * (`POST /api/personalization/resolve`). Requires a configured SDK token\n * (`token`/`getToken`) — resolve is auth-required. With `merge: true` the\n * returned `content` is server-spliced.\n *\n * Pass `opts.identity`/`opts.context` to attach the same SDK-provided\n * fields (`deviceId`, `customerId`, `email`, `path`, `url`, `referrer`,\n * `utm*`, …) the browser client collects ambiently — there's no ambient\n * `window`/`document` server-side, so a server caller must supply whatever\n * it knows (e.g. from the current request) explicitly. See\n * {@link ResolveOptions}. Pass `opts.fallback` to get it back instead of an\n * error (see {@link WithFallback}).\n */\n async resolve<F = never>(contentKey: string, opts?: ServerResolveOptions<F>): Promise<ResolveResult | F> {\n return orFallback(() => this.api.resolve(contentKey, this.withVisitor(opts)), opts);\n }\n\n /**\n * Resolve several content keys in one round-trip (`POST /api/personalization/resolve-batch`).\n * `opts.fallback` as in {@link resolve}.\n */\n async resolveBatch<F = never>(\n items: ResolveBatchItem[],\n opts?: ServerResolveOptions<F>,\n ): Promise<ResolveBatchResult[] | F> {\n return orFallback(() => this.api.resolveBatch(items, this.withVisitor(opts)), opts);\n }\n\n /**\n * Merge the ambient `config.context` and `opts.identity`/`opts.context` into\n * the resolve `data`, then stamp the SDK-owned visitor id\n * (`config.visitorId`) under both the reserved `_xenia_vid` key (read by the\n * backend for holdout/exposure bucketing) and the flat `visitorId` field —\n * mirrors `webhook()`'s merge order/precedence exactly: caller `data` <\n * ambient `config.context` < caller `context`/`identity` < SDK-owned\n * `visitorId`/`_xenia_vid`, set LAST so neither can be overridden per call.\n *\n * `config.context` is what makes the SDK-provided fields automatic on the\n * server: bound once per request, every `resolve()` carries `path`/`query`/\n * `url`/`referrer`/UTMs with no per-call wiring, matching what the browser\n * client collects from `window` on its own. A no-op when nothing is bound and\n * nothing was passed — preserving today's behavior for callers using neither.\n */\n private withVisitor(opts?: ResolveOptions): ResolveOptions | undefined {\n const identity = compact({\n ...opts?.identity,\n visitorId: this.visitorId ?? opts?.identity?.visitorId,\n });\n const merged = {\n ...compact((this.context ?? {}) as Record<string, JsonValue | undefined>),\n ...compact((opts?.context ?? {}) as Record<string, JsonValue | undefined>),\n ...identity,\n };\n if (Object.keys(merged).length === 0 && !this.visitorId) return opts;\n if (this.visitorId) merged._xenia_vid = this.visitorId;\n return { ...opts, data: { ...(opts?.data ?? {}), ...merged } };\n }\n}\n\n/** Create a server-native client. */\nexport function createXeniaServer(config: XeniaConfig): XeniaServer {\n return new XeniaServer(config);\n}\n","import {\n CONSENT_COOKIE,\n VISITOR_COOKIE,\n consentCookie,\n consentCookieName,\n visitorCookieName,\n} from \"../core/cookie-names\";\nimport type { ConsentState } from \"../core/types\";\n\n/**\n * Cookie names the SDK reads server-side (set by the browser SDK / your app).\n *\n * The constants are the DEFAULT-prefix names; the functions build the name for any prefix. If you\n * pass `config.prefix` to the browser client, pass the same prefix here — the browser namespaces\n * the cookie by it, so the default name won't be there to find.\n */\nexport { CONSENT_COOKIE, VISITOR_COOKIE, consentCookie, consentCookieName, visitorCookieName };\n\n/**\n * Parse a raw `Cookie` header into a map. Framework-agnostic so it works with\n * `Request.headers.get(\"cookie\")`, Node `req.headers.cookie`, etc.\n */\nexport function parseCookies(header: string | null | undefined): Record<string, string> {\n const out: Record<string, string> = {};\n if (!header) return out;\n for (const part of header.split(\";\")) {\n const idx = part.indexOf(\"=\");\n if (idx === -1) continue;\n const key = part.slice(0, idx).trim();\n const value = part.slice(idx + 1).trim();\n if (key) out[key] = decodeURIComponent(value);\n }\n return out;\n}\n\n/**\n * Read the visitor id the browser SDK wrote, so a server-side event can be bound to the same\n * visitor (`createXeniaServer({ visitorId })`) and attribute to a browser-entered journey.\n *\n * @param prefix Must match the browser client's `config.prefix`. The cookie is namespaced by it, so\n * a mismatch finds nothing and quietly leaves the server with no visitor.\n */\nexport function getVisitorId(\n cookieHeader: string | null | undefined,\n prefix?: string,\n): string | null {\n return parseCookies(cookieHeader)[visitorCookieName(prefix)] ?? null;\n}\n\n/**\n * Read consent from a cookie header. Accepts a JSON blob or a comma-list of\n * granted scopes (e.g. `analytics,personalization`). Absent → all denied.\n *\n * @param prefix Must match the browser client's `config.prefix` — see {@link getVisitorId}.\n */\nexport function getConsent(\n cookieHeader: string | null | undefined,\n prefix?: string,\n): ConsentState {\n const raw = parseCookies(cookieHeader)[consentCookieName(prefix)];\n if (!raw) return { analytics: false, marketing: false, personalization: false };\n try {\n const parsed = JSON.parse(raw) as ConsentState;\n return parsed;\n } catch {\n const scopes = new Set(raw.split(\",\").map((s) => s.trim()));\n return {\n analytics: scopes.has(\"analytics\"),\n marketing: scopes.has(\"marketing\"),\n personalization: scopes.has(\"personalization\"),\n };\n }\n}\n"]}
|