@xenia-martech/sdk-js 1.0.0-dev.26 → 1.0.0-dev.28
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/CSS_VARIABLES.md +7 -0
- package/README.md +71 -2
- package/dist/{actions-DFLoSXO_.d.cts → actions-CcBYV--3.d.cts} +242 -6
- package/dist/{actions-DFLoSXO_.d.ts → actions-CcBYV--3.d.ts} +242 -6
- package/dist/{chunk-3KYKKIIG.cjs → chunk-WYM4CQVG.cjs} +126 -4
- package/dist/chunk-WYM4CQVG.cjs.map +1 -0
- package/dist/{chunk-D37RD3UC.js → chunk-XXNDHYAS.js} +125 -5
- package/dist/chunk-XXNDHYAS.js.map +1 -0
- package/dist/client/index.cjs +613 -35
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +112 -3
- package/dist/client/index.d.ts +112 -3
- package/dist/client/index.js +610 -36
- package/dist/client/index.js.map +1 -1
- package/dist/client/react/index.cjs +609 -35
- 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 +609 -35
- package/dist/client/react/index.js.map +1 -1
- package/dist/{cookie-names-LgNDAreS.d.cts → cookie-names-B3b0XNc2.d.cts} +187 -1
- package/dist/{cookie-names-LgNDAreS.d.ts → cookie-names-B3b0XNc2.d.ts} +187 -1
- package/dist/index.cjs +23 -19
- package/dist/index.d.cts +23 -3
- package/dist/index.d.ts +23 -3
- package/dist/index.js +1 -1
- package/dist/server/index.cjs +53 -22
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +31 -11
- package/dist/server/index.d.ts +31 -11
- package/dist/server/index.js +43 -12
- package/dist/server/index.js.map +1 -1
- package/dist/xenia-sdk.iife.js +37 -3
- package/dist/xenia-sdk.iife.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-3KYKKIIG.cjs.map +0 -1
- package/dist/chunk-D37RD3UC.js.map +0 -1
package/dist/server/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkWYM4CQVG_cjs = require('../chunk-WYM4CQVG.cjs');
|
|
4
4
|
|
|
5
5
|
// src/core/event-payload.ts
|
|
6
6
|
var RESERVED = /* @__PURE__ */ new Set([
|
|
@@ -21,7 +21,7 @@ var RESERVED = /* @__PURE__ */ new Set([
|
|
|
21
21
|
"path",
|
|
22
22
|
"query"
|
|
23
23
|
]);
|
|
24
|
-
function
|
|
24
|
+
function compact2(obj) {
|
|
25
25
|
const out = {};
|
|
26
26
|
for (const [k, v] of Object.entries(obj)) {
|
|
27
27
|
if (v !== void 0) out[k] = v;
|
|
@@ -45,8 +45,8 @@ function buildEventPayload(input) {
|
|
|
45
45
|
// what they reliably know (real page url, visitor/device id), so a stray prop
|
|
46
46
|
// can't override the true page context — see merge order below.
|
|
47
47
|
...reserved,
|
|
48
|
-
...
|
|
49
|
-
...
|
|
48
|
+
...compact2(input.context ?? {}),
|
|
49
|
+
...compact2(input.identity ?? {}),
|
|
50
50
|
eventId: input.eventId,
|
|
51
51
|
...input.eventType ? { eventType: input.eventType } : {},
|
|
52
52
|
...input.journeyId ? { journeyId: input.journeyId } : {}
|
|
@@ -59,9 +59,12 @@ var XeniaServer = class {
|
|
|
59
59
|
api;
|
|
60
60
|
/** SDK-owned visitor anchor for this (per-request) client — see {@link XeniaConfig.visitorId}. */
|
|
61
61
|
visitorId;
|
|
62
|
+
/** Ambient request context attached to every call — see {@link XeniaConfig.context}. */
|
|
63
|
+
context;
|
|
62
64
|
constructor(config) {
|
|
63
|
-
this.api = new
|
|
65
|
+
this.api = new chunkWYM4CQVG_cjs.XeniaApi(config);
|
|
64
66
|
this.visitorId = config.visitorId;
|
|
67
|
+
this.context = config.context;
|
|
65
68
|
}
|
|
66
69
|
/**
|
|
67
70
|
* Send a unified event server-side (`POST /api/events/v1`) and return the
|
|
@@ -72,7 +75,7 @@ var XeniaServer = class {
|
|
|
72
75
|
async track(eventName, opts = {}) {
|
|
73
76
|
const event = {
|
|
74
77
|
eventName,
|
|
75
|
-
eventId: opts.eventId ?
|
|
78
|
+
eventId: opts.eventId ? chunkWYM4CQVG_cjs.toEventId(opts.eventId) : chunkWYM4CQVG_cjs.generateUlid(),
|
|
76
79
|
eventVersion: opts.eventVersion ?? 1,
|
|
77
80
|
occurredAtMs: opts.occurredAtMs ?? Date.now(),
|
|
78
81
|
source: opts.source ?? "xenia-server-sdk",
|
|
@@ -103,7 +106,9 @@ var XeniaServer = class {
|
|
|
103
106
|
// cookie per request) is attached automatically. Falls back to a
|
|
104
107
|
// per-call visitorId only when none is bound.
|
|
105
108
|
identity: { ...opts.identity, visitorId: this.visitorId ?? opts.identity?.visitorId },
|
|
106
|
-
context
|
|
109
|
+
// Ambient request context (config.context, bound once per request) under a per-call
|
|
110
|
+
// override — same precedence resolve() uses, so both surfaces carry the same fields.
|
|
111
|
+
context: { ...this.context, ...opts.context },
|
|
107
112
|
props: data
|
|
108
113
|
});
|
|
109
114
|
return this.api.webhook(triggerId, payload, opts.method ?? "POST");
|
|
@@ -117,6 +122,13 @@ var XeniaServer = class {
|
|
|
117
122
|
* (`POST /api/personalization/resolve`). Requires a configured SDK token
|
|
118
123
|
* (`token`/`getToken`) — resolve is auth-required. With `merge: true` the
|
|
119
124
|
* returned `content` is server-spliced.
|
|
125
|
+
*
|
|
126
|
+
* Pass `opts.identity`/`opts.context` to attach the same SDK-provided
|
|
127
|
+
* fields (`deviceId`, `customerId`, `email`, `path`, `url`, `referrer`,
|
|
128
|
+
* `utm*`, …) the browser client collects ambiently — there's no ambient
|
|
129
|
+
* `window`/`document` server-side, so a server caller must supply whatever
|
|
130
|
+
* it knows (e.g. from the current request) explicitly. See
|
|
131
|
+
* {@link ResolveOptions}.
|
|
120
132
|
*/
|
|
121
133
|
async resolve(contentKey, opts) {
|
|
122
134
|
return this.api.resolve(contentKey, this.withVisitor(opts));
|
|
@@ -126,14 +138,33 @@ var XeniaServer = class {
|
|
|
126
138
|
return this.api.resolveBatch(items, this.withVisitor(opts));
|
|
127
139
|
}
|
|
128
140
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
141
|
+
* Merge the ambient `config.context` and `opts.identity`/`opts.context` into
|
|
142
|
+
* the resolve `data`, then stamp the SDK-owned visitor id
|
|
143
|
+
* (`config.visitorId`) under both the reserved `_xenia_vid` key (read by the
|
|
144
|
+
* backend for holdout/exposure bucketing) and the flat `visitorId` field —
|
|
145
|
+
* mirrors `webhook()`'s merge order/precedence exactly: caller `data` <
|
|
146
|
+
* ambient `config.context` < caller `context`/`identity` < SDK-owned
|
|
147
|
+
* `visitorId`/`_xenia_vid`, set LAST so neither can be overridden per call.
|
|
148
|
+
*
|
|
149
|
+
* `config.context` is what makes the SDK-provided fields automatic on the
|
|
150
|
+
* server: bound once per request, every `resolve()` carries `path`/`query`/
|
|
151
|
+
* `url`/`referrer`/UTMs with no per-call wiring, matching what the browser
|
|
152
|
+
* client collects from `window` on its own. A no-op when nothing is bound and
|
|
153
|
+
* nothing was passed — preserving today's behavior for callers using neither.
|
|
133
154
|
*/
|
|
134
155
|
withVisitor(opts) {
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
const identity = chunkWYM4CQVG_cjs.compact({
|
|
157
|
+
...opts?.identity,
|
|
158
|
+
visitorId: this.visitorId ?? opts?.identity?.visitorId
|
|
159
|
+
});
|
|
160
|
+
const merged = {
|
|
161
|
+
...chunkWYM4CQVG_cjs.compact(this.context ?? {}),
|
|
162
|
+
...chunkWYM4CQVG_cjs.compact(opts?.context ?? {}),
|
|
163
|
+
...identity
|
|
164
|
+
};
|
|
165
|
+
if (Object.keys(merged).length === 0 && !this.visitorId) return opts;
|
|
166
|
+
if (this.visitorId) merged._xenia_vid = this.visitorId;
|
|
167
|
+
return { ...opts, data: { ...opts?.data ?? {}, ...merged } };
|
|
137
168
|
}
|
|
138
169
|
};
|
|
139
170
|
function createXeniaServer(config) {
|
|
@@ -142,7 +173,7 @@ function createXeniaServer(config) {
|
|
|
142
173
|
|
|
143
174
|
// src/server/token.ts
|
|
144
175
|
async function refreshSdkToken(opts) {
|
|
145
|
-
const http = new
|
|
176
|
+
const http = new chunkWYM4CQVG_cjs.HttpClient({ baseUrl: opts.baseUrl, timeout: opts.timeout });
|
|
146
177
|
const { data } = await http.request({
|
|
147
178
|
method: "POST",
|
|
148
179
|
path: "/api/personalization/token/refresh",
|
|
@@ -187,10 +218,10 @@ function parseCookies(header) {
|
|
|
187
218
|
return out;
|
|
188
219
|
}
|
|
189
220
|
function getVisitorId(cookieHeader, prefix) {
|
|
190
|
-
return parseCookies(cookieHeader)[
|
|
221
|
+
return parseCookies(cookieHeader)[chunkWYM4CQVG_cjs.visitorCookieName(prefix)] ?? null;
|
|
191
222
|
}
|
|
192
223
|
function getConsent(cookieHeader, prefix) {
|
|
193
|
-
const raw = parseCookies(cookieHeader)[
|
|
224
|
+
const raw = parseCookies(cookieHeader)[chunkWYM4CQVG_cjs.consentCookieName(prefix)];
|
|
194
225
|
if (!raw) return { analytics: false, marketing: false, personalization: false };
|
|
195
226
|
try {
|
|
196
227
|
const parsed = JSON.parse(raw);
|
|
@@ -207,27 +238,27 @@ function getConsent(cookieHeader, prefix) {
|
|
|
207
238
|
|
|
208
239
|
Object.defineProperty(exports, "CONSENT_COOKIE", {
|
|
209
240
|
enumerable: true,
|
|
210
|
-
get: function () { return
|
|
241
|
+
get: function () { return chunkWYM4CQVG_cjs.CONSENT_COOKIE; }
|
|
211
242
|
});
|
|
212
243
|
Object.defineProperty(exports, "VISITOR_COOKIE", {
|
|
213
244
|
enumerable: true,
|
|
214
|
-
get: function () { return
|
|
245
|
+
get: function () { return chunkWYM4CQVG_cjs.VISITOR_COOKIE; }
|
|
215
246
|
});
|
|
216
247
|
Object.defineProperty(exports, "XeniaApi", {
|
|
217
248
|
enumerable: true,
|
|
218
|
-
get: function () { return
|
|
249
|
+
get: function () { return chunkWYM4CQVG_cjs.XeniaApi; }
|
|
219
250
|
});
|
|
220
251
|
Object.defineProperty(exports, "consentCookieName", {
|
|
221
252
|
enumerable: true,
|
|
222
|
-
get: function () { return
|
|
253
|
+
get: function () { return chunkWYM4CQVG_cjs.consentCookieName; }
|
|
223
254
|
});
|
|
224
255
|
Object.defineProperty(exports, "parseActions", {
|
|
225
256
|
enumerable: true,
|
|
226
|
-
get: function () { return
|
|
257
|
+
get: function () { return chunkWYM4CQVG_cjs.parseActions; }
|
|
227
258
|
});
|
|
228
259
|
Object.defineProperty(exports, "visitorCookieName", {
|
|
229
260
|
enumerable: true,
|
|
230
|
-
get: function () { return
|
|
261
|
+
get: function () { return chunkWYM4CQVG_cjs.visitorCookieName; }
|
|
231
262
|
});
|
|
232
263
|
exports.XeniaServer = XeniaServer;
|
|
233
264
|
exports.createTokenRoute = createTokenRoute;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/event-payload.ts","../../src/server/xenia-server.ts","../../src/server/token.ts","../../src/server/cookies.ts"],"names":["XeniaApi","toEventId","generateUlid","HttpClient","visitorCookieName","consentCookieName"],"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,SAAS,QAAQ,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,GAAG,OAAA,CAAS,KAAA,CAAM,OAAA,IAAW,EAA4C,CAAA;AAAA,IACzE,GAAG,OAAA,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;;;ACfO,IAAM,cAAN,MAAkB;AAAA,EACd,GAAA;AAAA;AAAA,EAEQ,SAAA;AAAA,EAEjB,YAAY,MAAA,EAAqB;AAC/B,IAAA,IAAA,CAAK,GAAA,GAAM,IAAIA,0BAAA,CAAS,MAAM,CAAA;AAC9B,IAAA,IAAA,CAAK,YAAY,MAAA,CAAO,SAAA;AAAA,EAC1B;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,GAAUC,4BAAU,IAAA,CAAK,OAAO,IAAIC,8BAAA,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,EAQA,MAAM,OAAA,CACJ,SAAA,EACA,IAAA,EACA,IAAA,GAA6B,EAAC,EACJ;AAC1B,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,MACpF,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,KAAA,EAAO;AAAA,KACR,CAAA;AACD,IAAA,OAAO,KAAK,GAAA,CAAI,OAAA,CAAQ,WAAW,OAAA,EAAsB,IAAA,CAAK,UAAU,MAAM,CAAA;AAAA,EAChF;AAAA;AAAA,EAGA,MAAM,SAAS,IAAA,EAAkD;AAC/D,IAAA,OAAO,IAAA,CAAK,GAAA,CAAI,QAAA,CAAS,IAAI,CAAA;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,OAAA,CAAQ,UAAA,EAAoB,IAAA,EAA+C;AAC/E,IAAA,OAAO,KAAK,GAAA,CAAI,OAAA,CAAQ,YAAY,IAAA,CAAK,WAAA,CAAY,IAAI,CAAC,CAAA;AAAA,EAC5D;AAAA;AAAA,EAGA,MAAM,YAAA,CACJ,KAAA,EACA,IAAA,EAC+B;AAC/B,IAAA,OAAO,KAAK,GAAA,CAAI,YAAA,CAAa,OAAO,IAAA,CAAK,WAAA,CAAY,IAAI,CAAC,CAAA;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQQ,YAAY,IAAA,EAAmD;AACrE,IAAA,IAAI,CAAC,IAAA,CAAK,SAAA,EAAW,OAAO,IAAA;AAC5B,IAAA,OAAO,EAAE,GAAG,IAAA,EAAM,IAAA,EAAM,EAAE,GAAI,IAAA,EAAM,IAAA,IAAQ,EAAC,EAAI,UAAA,EAAY,IAAA,CAAK,WAAU,EAAE;AAAA,EAChF;AACF;AAGO,SAAS,kBAAkB,MAAA,EAAkC;AAClE,EAAA,OAAO,IAAI,YAAY,MAAM,CAAA;AAC/B;;;ACvIA,eAAsB,gBAAgB,IAAA,EAAoD;AACxF,EAAA,MAAM,IAAA,GAAO,IAAIC,4BAAA,CAAW,EAAE,OAAA,EAAS,KAAK,OAAA,EAAS,OAAA,EAAS,IAAA,CAAK,OAAA,EAAS,CAAA;AAC5E,EAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,KAAK,OAAA,CAAQ;AAAA,IAClC,MAAA,EAAQ,MAAA;AAAA,IACR,IAAA,EAAM,oCAAA;AAAA,IACN,SAAS,EAAE,aAAA,EAAe,CAAA,OAAA,EAAU,IAAA,CAAK,QAAQ,CAAA,CAAA,EAAG;AAAA,IACpD,IAAA,EAAM,EAAE,UAAA,EAAY,IAAA,CAAK,cAAc,IAAA;AAAK,GAC7C,CAAA;AACD,EAAA,MAAM,GAAA,GAAO,QAAQ,EAAC;AACtB,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,MAAA,CAAO,GAAA,CAAI,KAAA,IAAS,EAAE,CAAA;AAAA,IAC7B,WAAW,OAAO,GAAA,CAAI,SAAA,KAAc,QAAA,GAAW,IAAI,SAAA,GAAY;AAAA,GACjE;AACF;AAiBO,SAAS,iBAAiB,MAAA,EAAsE;AACrG,EAAA,OAAO,YAA+B;AACpC,IAAA,IAAI;AACF,MAAA,MAAM,SAAA,GAAY,MAAM,eAAA,CAAgB,MAAM,CAAA;AAC9C,MAAA,OAAO,IAAI,QAAA,CAAS,IAAA,CAAK,SAAA,CAAU,SAAS,CAAA,EAAG;AAAA,QAC7C,MAAA,EAAQ,GAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAoB,iBAAiB,UAAA;AAAW,OAC5E,CAAA;AAAA,IACH,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,OAAA,GAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,sBAAA;AACrD,MAAA,OAAO,IAAI,SAAS,IAAA,CAAK,SAAA,CAAU,EAAE,KAAA,EAAO,OAAA,EAAS,CAAA,EAAG;AAAA,QACtD,MAAA,EAAQ,GAAA;AAAA,QACR,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA;AAAmB,OAC/C,CAAA;AAAA,IACH;AAAA,EACF,CAAA;AACF;;;ACzDO,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,CAAEC,mCAAA,CAAkB,MAAM,CAAC,CAAA,IAAK,IAAA;AAClE;AAQO,SAAS,UAAA,CACd,cACA,MAAA,EACc;AACd,EAAA,MAAM,MAAM,YAAA,CAAa,YAAY,CAAA,CAAEC,mCAAA,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":"index.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 { 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. There's no\n * ambient `window` on the server to auto-collect `path`/`query`/`url`/UTM\n * params the way the browser client does — pass whatever's known at the call\n * site (e.g. the current request's pathname from Next.js `headers()`/route\n * params) via `context`, and `deviceId`/`visitorId`/etc. via `identity`.\n */\nexport interface ServerWebhookOptions {\n method?: \"GET\" | \"POST\";\n journeyId?: string;\n identity?: EventIdentity;\n context?: EventContext;\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 private readonly visitorId?: string;\n\n constructor(config: XeniaConfig) {\n this.api = new XeniaApi(config);\n this.visitorId = config.visitorId;\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`.\n */\n async webhook(\n triggerId: string,\n data?: Record<string, JsonValue | undefined>,\n opts: ServerWebhookOptions = {},\n ): Promise<WebhookResponse> {\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 context: opts.context,\n props: data,\n });\n return this.api.webhook(triggerId, payload as JsonValue, opts.method ?? \"POST\");\n }\n\n /** Bind visitor→user server-side — `POST /api/events/v1/identify`. */\n async identify(body: IdentifyRequest): Promise<IdentifyResponse> {\n return this.api.identify(body);\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 async resolve(contentKey: string, opts?: ResolveOptions): Promise<ResolveResult> {\n return this.api.resolve(contentKey, this.withVisitor(opts));\n }\n\n /** Resolve several content keys in one round-trip (`POST /api/personalization/resolve-batch`). */\n async resolveBatch(\n items: ResolveBatchItem[],\n opts?: ResolveOptions,\n ): Promise<ResolveBatchResult[]> {\n return this.api.resolveBatch(items, this.withVisitor(opts));\n }\n\n /**\n * Stamp the SDK-owned visitor id (`config.visitorId`) into the resolve `data`\n * under the reserved `_xenia_vid` key so the backend can bucket holdout\n * experiments and record exposure per visitor. Set last so a caller can't\n * override it; a no-op when no `visitorId` was bound on this client.\n */\n private withVisitor(opts?: ResolveOptions): ResolveOptions | undefined {\n if (!this.visitorId) return opts;\n return { ...opts, data: { ...(opts?.data ?? {}), _xenia_vid: this.visitorId } };\n }\n}\n\n/** Create a server-native client. */\nexport function createXeniaServer(config: XeniaConfig): XeniaServer {\n return new XeniaServer(config);\n}\n","import { HttpClient } from \"../core/http-client\";\nimport type { JsonObject } from \"../core/types\";\n\nexport interface RefreshTokenOptions {\n /**\n * Xenia base URL. Defaults to the hosted engine,\n * `https://engine.xeniamartech.com/xenia` — omit unless self-hosting.\n */\n baseUrl?: string;\n /**\n * The personalization SDK token created once in the dashboard (API Tokens →\n * Create token). Stays server-side; the browser never sees it — only the\n * short-lived token this exchanges it for does.\n */\n sdkToken: string;\n /** Lifetime of the returned token, in minutes. Default 15, capped server-side at 60. */\n ttlMinutes?: number;\n timeout?: number;\n}\n\nexport interface RefreshedToken {\n token: string;\n expiresAt?: string;\n}\n\n/**\n * Exchange the long-lived personalization SDK token for a short-lived one with\n * the same tenant/env scope, via `POST /api/personalization/token/refresh`.\n * Self-service — no dashboard credential needed, just the SDK token you\n * already have. Call this from your backend and hand the result to the\n * browser (e.g. from a token route the client SDK calls via `getToken`).\n */\nexport async function refreshSdkToken(opts: RefreshTokenOptions): Promise<RefreshedToken> {\n const http = new HttpClient({ baseUrl: opts.baseUrl, timeout: opts.timeout });\n const { data } = await http.request({\n method: \"POST\",\n path: \"/api/personalization/token/refresh\",\n headers: { Authorization: `Bearer ${opts.sdkToken}` },\n body: { ttlMinutes: opts.ttlMinutes ?? null },\n });\n const obj = (data ?? {}) as JsonObject;\n return {\n token: String(obj.token ?? \"\"),\n expiresAt: typeof obj.expiresAt === \"string\" ? obj.expiresAt : undefined,\n };\n}\n\n/**\n * Build a framework-agnostic request handler that hands the browser SDK a\n * personalization token — mount it at the endpoint `getToken` calls. Needs\n * only the SDK token you already created in the dashboard; mints a fresh\n * short-lived token per request so the long-lived one never reaches the\n * browser.\n *\n * @example\n * ```ts\n * export const GET = createTokenRoute({\n * sdkToken: process.env.XENIA_SDK_TOKEN!,\n * // baseUrl: process.env.XENIA_URL, // only to override the default hosted engine\n * });\n * ```\n */\nexport function createTokenRoute(config: RefreshTokenOptions): (request: Request) => Promise<Response> {\n return async (): Promise<Response> => {\n try {\n const refreshed = await refreshSdkToken(config);\n return new Response(JSON.stringify(refreshed), {\n status: 200,\n headers: { \"Content-Type\": \"application/json\", \"Cache-Control\": \"no-store\" },\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"token refresh failed\";\n return new Response(JSON.stringify({ error: message }), {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n };\n}\n","import {\n CONSENT_COOKIE,\n VISITOR_COOKIE,\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, 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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/event-payload.ts","../../src/server/xenia-server.ts","../../src/server/token.ts","../../src/server/cookies.ts"],"names":["compact","XeniaApi","toEventId","generateUlid","HttpClient","visitorCookieName","consentCookieName"],"mappings":"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","file":"index.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 {\n method?: \"GET\" | \"POST\";\n journeyId?: string;\n identity?: EventIdentity;\n context?: EventContext;\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 private 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`.\n */\n async webhook(\n triggerId: string,\n data?: Record<string, JsonValue | undefined>,\n opts: ServerWebhookOptions = {},\n ): Promise<WebhookResponse> {\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 this.api.webhook(triggerId, payload as JsonValue, opts.method ?? \"POST\");\n }\n\n /** Bind visitor→user server-side — `POST /api/events/v1/identify`. */\n async identify(body: IdentifyRequest): Promise<IdentifyResponse> {\n return this.api.identify(body);\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}.\n */\n async resolve(contentKey: string, opts?: ResolveOptions): Promise<ResolveResult> {\n return this.api.resolve(contentKey, this.withVisitor(opts));\n }\n\n /** Resolve several content keys in one round-trip (`POST /api/personalization/resolve-batch`). */\n async resolveBatch(\n items: ResolveBatchItem[],\n opts?: ResolveOptions,\n ): Promise<ResolveBatchResult[]> {\n return this.api.resolveBatch(items, this.withVisitor(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 { HttpClient } from \"../core/http-client\";\nimport type { JsonObject } from \"../core/types\";\n\nexport interface RefreshTokenOptions {\n /**\n * Xenia base URL. Defaults to the hosted engine,\n * `https://engine.xeniamartech.com/xenia` — omit unless self-hosting.\n */\n baseUrl?: string;\n /**\n * The personalization SDK token created once in the dashboard (API Tokens →\n * Create token). Stays server-side; the browser never sees it — only the\n * short-lived token this exchanges it for does.\n */\n sdkToken: string;\n /** Lifetime of the returned token, in minutes. Default 15, capped server-side at 60. */\n ttlMinutes?: number;\n timeout?: number;\n}\n\nexport interface RefreshedToken {\n token: string;\n expiresAt?: string;\n}\n\n/**\n * Exchange the long-lived personalization SDK token for a short-lived one with\n * the same tenant/env scope, via `POST /api/personalization/token/refresh`.\n * Self-service — no dashboard credential needed, just the SDK token you\n * already have. Call this from your backend and hand the result to the\n * browser (e.g. from a token route the client SDK calls via `getToken`).\n */\nexport async function refreshSdkToken(opts: RefreshTokenOptions): Promise<RefreshedToken> {\n const http = new HttpClient({ baseUrl: opts.baseUrl, timeout: opts.timeout });\n const { data } = await http.request({\n method: \"POST\",\n path: \"/api/personalization/token/refresh\",\n headers: { Authorization: `Bearer ${opts.sdkToken}` },\n body: { ttlMinutes: opts.ttlMinutes ?? null },\n });\n const obj = (data ?? {}) as JsonObject;\n return {\n token: String(obj.token ?? \"\"),\n expiresAt: typeof obj.expiresAt === \"string\" ? obj.expiresAt : undefined,\n };\n}\n\n/**\n * Build a framework-agnostic request handler that hands the browser SDK a\n * personalization token — mount it at the endpoint `getToken` calls. Needs\n * only the SDK token you already created in the dashboard; mints a fresh\n * short-lived token per request so the long-lived one never reaches the\n * browser.\n *\n * @example\n * ```ts\n * export const GET = createTokenRoute({\n * sdkToken: process.env.XENIA_SDK_TOKEN!,\n * // baseUrl: process.env.XENIA_URL, // only to override the default hosted engine\n * });\n * ```\n */\nexport function createTokenRoute(config: RefreshTokenOptions): (request: Request) => Promise<Response> {\n return async (): Promise<Response> => {\n try {\n const refreshed = await refreshSdkToken(config);\n return new Response(JSON.stringify(refreshed), {\n status: 200,\n headers: { \"Content-Type\": \"application/json\", \"Cache-Control\": \"no-store\" },\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : \"token refresh failed\";\n return new Response(JSON.stringify({ error: message }), {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n };\n}\n","import {\n CONSENT_COOKIE,\n VISITOR_COOKIE,\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, 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"]}
|
package/dist/server/index.d.cts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { b as AnalyticsIdentity, C as CONSENT_COOKIE, P as PersonalizationSwap, V as VISITOR_COOKIE,
|
|
1
|
+
import { y as CallerIdentity, A as AnalyticsContext, J as JsonValue, d as ConsentState, e as EventIdentity, E as EventContext, s as XeniaApi, t as XeniaConfig, a as AnalyticsEventName, c as AnalyticsIngestResponse, W as WebhookResponse, I as IdentifyRequest, f as IdentifyResponse, l as ResolveOptions, m as ResolveResult, j as ResolveBatchItem, k as ResolveBatchResult } from '../cookie-names-B3b0XNc2.cjs';
|
|
2
|
+
export { b as AnalyticsIdentity, C as CONSENT_COOKIE, P as PersonalizationSwap, V as VISITOR_COOKIE, u as XeniaEvent, v as consentCookieName, w as parseActions, x as visitorCookieName } from '../cookie-names-B3b0XNc2.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Extra per-call identity/context for server-side webhook calls
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Extra per-call identity/context for server-side webhook calls — an override on top of whatever the
|
|
6
|
+
* client already carries.
|
|
7
|
+
*
|
|
8
|
+
* There's no ambient `window` on the server to auto-collect `path`/`query`/`url`/UTM params the way
|
|
9
|
+
* the browser client does, so bind them ONCE per request via {@link XeniaConfig.context} (see
|
|
10
|
+
* `contextFromUrl`) and every call attaches them automatically. These per-call fields are for the
|
|
11
|
+
* remainder: values known only at this call site, or a deliberate override of the ambient ones.
|
|
10
12
|
*/
|
|
11
13
|
interface ServerWebhookOptions {
|
|
12
14
|
method?: "GET" | "POST";
|
|
@@ -45,6 +47,8 @@ declare class XeniaServer {
|
|
|
45
47
|
readonly api: XeniaApi;
|
|
46
48
|
/** SDK-owned visitor anchor for this (per-request) client — see {@link XeniaConfig.visitorId}. */
|
|
47
49
|
private readonly visitorId?;
|
|
50
|
+
/** Ambient request context attached to every call — see {@link XeniaConfig.context}. */
|
|
51
|
+
private readonly context?;
|
|
48
52
|
constructor(config: XeniaConfig);
|
|
49
53
|
/**
|
|
50
54
|
* Send a unified event server-side (`POST /api/events/v1`) and return the
|
|
@@ -67,15 +71,31 @@ declare class XeniaServer {
|
|
|
67
71
|
* (`POST /api/personalization/resolve`). Requires a configured SDK token
|
|
68
72
|
* (`token`/`getToken`) — resolve is auth-required. With `merge: true` the
|
|
69
73
|
* returned `content` is server-spliced.
|
|
74
|
+
*
|
|
75
|
+
* Pass `opts.identity`/`opts.context` to attach the same SDK-provided
|
|
76
|
+
* fields (`deviceId`, `customerId`, `email`, `path`, `url`, `referrer`,
|
|
77
|
+
* `utm*`, …) the browser client collects ambiently — there's no ambient
|
|
78
|
+
* `window`/`document` server-side, so a server caller must supply whatever
|
|
79
|
+
* it knows (e.g. from the current request) explicitly. See
|
|
80
|
+
* {@link ResolveOptions}.
|
|
70
81
|
*/
|
|
71
82
|
resolve(contentKey: string, opts?: ResolveOptions): Promise<ResolveResult>;
|
|
72
83
|
/** Resolve several content keys in one round-trip (`POST /api/personalization/resolve-batch`). */
|
|
73
84
|
resolveBatch(items: ResolveBatchItem[], opts?: ResolveOptions): Promise<ResolveBatchResult[]>;
|
|
74
85
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
86
|
+
* Merge the ambient `config.context` and `opts.identity`/`opts.context` into
|
|
87
|
+
* the resolve `data`, then stamp the SDK-owned visitor id
|
|
88
|
+
* (`config.visitorId`) under both the reserved `_xenia_vid` key (read by the
|
|
89
|
+
* backend for holdout/exposure bucketing) and the flat `visitorId` field —
|
|
90
|
+
* mirrors `webhook()`'s merge order/precedence exactly: caller `data` <
|
|
91
|
+
* ambient `config.context` < caller `context`/`identity` < SDK-owned
|
|
92
|
+
* `visitorId`/`_xenia_vid`, set LAST so neither can be overridden per call.
|
|
93
|
+
*
|
|
94
|
+
* `config.context` is what makes the SDK-provided fields automatic on the
|
|
95
|
+
* server: bound once per request, every `resolve()` carries `path`/`query`/
|
|
96
|
+
* `url`/`referrer`/UTMs with no per-call wiring, matching what the browser
|
|
97
|
+
* client collects from `window` on its own. A no-op when nothing is bound and
|
|
98
|
+
* nothing was passed — preserving today's behavior for callers using neither.
|
|
79
99
|
*/
|
|
80
100
|
private withVisitor;
|
|
81
101
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { b as AnalyticsIdentity, C as CONSENT_COOKIE, P as PersonalizationSwap, V as VISITOR_COOKIE,
|
|
1
|
+
import { y as CallerIdentity, A as AnalyticsContext, J as JsonValue, d as ConsentState, e as EventIdentity, E as EventContext, s as XeniaApi, t as XeniaConfig, a as AnalyticsEventName, c as AnalyticsIngestResponse, W as WebhookResponse, I as IdentifyRequest, f as IdentifyResponse, l as ResolveOptions, m as ResolveResult, j as ResolveBatchItem, k as ResolveBatchResult } from '../cookie-names-B3b0XNc2.js';
|
|
2
|
+
export { b as AnalyticsIdentity, C as CONSENT_COOKIE, P as PersonalizationSwap, V as VISITOR_COOKIE, u as XeniaEvent, v as consentCookieName, w as parseActions, x as visitorCookieName } from '../cookie-names-B3b0XNc2.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Extra per-call identity/context for server-side webhook calls
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Extra per-call identity/context for server-side webhook calls — an override on top of whatever the
|
|
6
|
+
* client already carries.
|
|
7
|
+
*
|
|
8
|
+
* There's no ambient `window` on the server to auto-collect `path`/`query`/`url`/UTM params the way
|
|
9
|
+
* the browser client does, so bind them ONCE per request via {@link XeniaConfig.context} (see
|
|
10
|
+
* `contextFromUrl`) and every call attaches them automatically. These per-call fields are for the
|
|
11
|
+
* remainder: values known only at this call site, or a deliberate override of the ambient ones.
|
|
10
12
|
*/
|
|
11
13
|
interface ServerWebhookOptions {
|
|
12
14
|
method?: "GET" | "POST";
|
|
@@ -45,6 +47,8 @@ declare class XeniaServer {
|
|
|
45
47
|
readonly api: XeniaApi;
|
|
46
48
|
/** SDK-owned visitor anchor for this (per-request) client — see {@link XeniaConfig.visitorId}. */
|
|
47
49
|
private readonly visitorId?;
|
|
50
|
+
/** Ambient request context attached to every call — see {@link XeniaConfig.context}. */
|
|
51
|
+
private readonly context?;
|
|
48
52
|
constructor(config: XeniaConfig);
|
|
49
53
|
/**
|
|
50
54
|
* Send a unified event server-side (`POST /api/events/v1`) and return the
|
|
@@ -67,15 +71,31 @@ declare class XeniaServer {
|
|
|
67
71
|
* (`POST /api/personalization/resolve`). Requires a configured SDK token
|
|
68
72
|
* (`token`/`getToken`) — resolve is auth-required. With `merge: true` the
|
|
69
73
|
* returned `content` is server-spliced.
|
|
74
|
+
*
|
|
75
|
+
* Pass `opts.identity`/`opts.context` to attach the same SDK-provided
|
|
76
|
+
* fields (`deviceId`, `customerId`, `email`, `path`, `url`, `referrer`,
|
|
77
|
+
* `utm*`, …) the browser client collects ambiently — there's no ambient
|
|
78
|
+
* `window`/`document` server-side, so a server caller must supply whatever
|
|
79
|
+
* it knows (e.g. from the current request) explicitly. See
|
|
80
|
+
* {@link ResolveOptions}.
|
|
70
81
|
*/
|
|
71
82
|
resolve(contentKey: string, opts?: ResolveOptions): Promise<ResolveResult>;
|
|
72
83
|
/** Resolve several content keys in one round-trip (`POST /api/personalization/resolve-batch`). */
|
|
73
84
|
resolveBatch(items: ResolveBatchItem[], opts?: ResolveOptions): Promise<ResolveBatchResult[]>;
|
|
74
85
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
86
|
+
* Merge the ambient `config.context` and `opts.identity`/`opts.context` into
|
|
87
|
+
* the resolve `data`, then stamp the SDK-owned visitor id
|
|
88
|
+
* (`config.visitorId`) under both the reserved `_xenia_vid` key (read by the
|
|
89
|
+
* backend for holdout/exposure bucketing) and the flat `visitorId` field —
|
|
90
|
+
* mirrors `webhook()`'s merge order/precedence exactly: caller `data` <
|
|
91
|
+
* ambient `config.context` < caller `context`/`identity` < SDK-owned
|
|
92
|
+
* `visitorId`/`_xenia_vid`, set LAST so neither can be overridden per call.
|
|
93
|
+
*
|
|
94
|
+
* `config.context` is what makes the SDK-provided fields automatic on the
|
|
95
|
+
* server: bound once per request, every `resolve()` carries `path`/`query`/
|
|
96
|
+
* `url`/`referrer`/UTMs with no per-call wiring, matching what the browser
|
|
97
|
+
* client collects from `window` on its own. A no-op when nothing is bound and
|
|
98
|
+
* nothing was passed — preserving today's behavior for callers using neither.
|
|
79
99
|
*/
|
|
80
100
|
private withVisitor;
|
|
81
101
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { XeniaApi, toEventId, generateUlid, HttpClient, visitorCookieName, consentCookieName } from '../chunk-
|
|
2
|
-
export { CONSENT_COOKIE, VISITOR_COOKIE, XeniaApi, consentCookieName, parseActions, visitorCookieName } from '../chunk-
|
|
1
|
+
import { XeniaApi, toEventId, generateUlid, compact, HttpClient, visitorCookieName, consentCookieName } from '../chunk-XXNDHYAS.js';
|
|
2
|
+
export { CONSENT_COOKIE, VISITOR_COOKIE, XeniaApi, consentCookieName, parseActions, visitorCookieName } from '../chunk-XXNDHYAS.js';
|
|
3
3
|
|
|
4
4
|
// src/core/event-payload.ts
|
|
5
5
|
var RESERVED = /* @__PURE__ */ new Set([
|
|
@@ -20,7 +20,7 @@ var RESERVED = /* @__PURE__ */ new Set([
|
|
|
20
20
|
"path",
|
|
21
21
|
"query"
|
|
22
22
|
]);
|
|
23
|
-
function
|
|
23
|
+
function compact2(obj) {
|
|
24
24
|
const out = {};
|
|
25
25
|
for (const [k, v] of Object.entries(obj)) {
|
|
26
26
|
if (v !== void 0) out[k] = v;
|
|
@@ -44,8 +44,8 @@ function buildEventPayload(input) {
|
|
|
44
44
|
// what they reliably know (real page url, visitor/device id), so a stray prop
|
|
45
45
|
// can't override the true page context — see merge order below.
|
|
46
46
|
...reserved,
|
|
47
|
-
...
|
|
48
|
-
...
|
|
47
|
+
...compact2(input.context ?? {}),
|
|
48
|
+
...compact2(input.identity ?? {}),
|
|
49
49
|
eventId: input.eventId,
|
|
50
50
|
...input.eventType ? { eventType: input.eventType } : {},
|
|
51
51
|
...input.journeyId ? { journeyId: input.journeyId } : {}
|
|
@@ -58,9 +58,12 @@ var XeniaServer = class {
|
|
|
58
58
|
api;
|
|
59
59
|
/** SDK-owned visitor anchor for this (per-request) client — see {@link XeniaConfig.visitorId}. */
|
|
60
60
|
visitorId;
|
|
61
|
+
/** Ambient request context attached to every call — see {@link XeniaConfig.context}. */
|
|
62
|
+
context;
|
|
61
63
|
constructor(config) {
|
|
62
64
|
this.api = new XeniaApi(config);
|
|
63
65
|
this.visitorId = config.visitorId;
|
|
66
|
+
this.context = config.context;
|
|
64
67
|
}
|
|
65
68
|
/**
|
|
66
69
|
* Send a unified event server-side (`POST /api/events/v1`) and return the
|
|
@@ -102,7 +105,9 @@ var XeniaServer = class {
|
|
|
102
105
|
// cookie per request) is attached automatically. Falls back to a
|
|
103
106
|
// per-call visitorId only when none is bound.
|
|
104
107
|
identity: { ...opts.identity, visitorId: this.visitorId ?? opts.identity?.visitorId },
|
|
105
|
-
context
|
|
108
|
+
// Ambient request context (config.context, bound once per request) under a per-call
|
|
109
|
+
// override — same precedence resolve() uses, so both surfaces carry the same fields.
|
|
110
|
+
context: { ...this.context, ...opts.context },
|
|
106
111
|
props: data
|
|
107
112
|
});
|
|
108
113
|
return this.api.webhook(triggerId, payload, opts.method ?? "POST");
|
|
@@ -116,6 +121,13 @@ var XeniaServer = class {
|
|
|
116
121
|
* (`POST /api/personalization/resolve`). Requires a configured SDK token
|
|
117
122
|
* (`token`/`getToken`) — resolve is auth-required. With `merge: true` the
|
|
118
123
|
* returned `content` is server-spliced.
|
|
124
|
+
*
|
|
125
|
+
* Pass `opts.identity`/`opts.context` to attach the same SDK-provided
|
|
126
|
+
* fields (`deviceId`, `customerId`, `email`, `path`, `url`, `referrer`,
|
|
127
|
+
* `utm*`, …) the browser client collects ambiently — there's no ambient
|
|
128
|
+
* `window`/`document` server-side, so a server caller must supply whatever
|
|
129
|
+
* it knows (e.g. from the current request) explicitly. See
|
|
130
|
+
* {@link ResolveOptions}.
|
|
119
131
|
*/
|
|
120
132
|
async resolve(contentKey, opts) {
|
|
121
133
|
return this.api.resolve(contentKey, this.withVisitor(opts));
|
|
@@ -125,14 +137,33 @@ var XeniaServer = class {
|
|
|
125
137
|
return this.api.resolveBatch(items, this.withVisitor(opts));
|
|
126
138
|
}
|
|
127
139
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
140
|
+
* Merge the ambient `config.context` and `opts.identity`/`opts.context` into
|
|
141
|
+
* the resolve `data`, then stamp the SDK-owned visitor id
|
|
142
|
+
* (`config.visitorId`) under both the reserved `_xenia_vid` key (read by the
|
|
143
|
+
* backend for holdout/exposure bucketing) and the flat `visitorId` field —
|
|
144
|
+
* mirrors `webhook()`'s merge order/precedence exactly: caller `data` <
|
|
145
|
+
* ambient `config.context` < caller `context`/`identity` < SDK-owned
|
|
146
|
+
* `visitorId`/`_xenia_vid`, set LAST so neither can be overridden per call.
|
|
147
|
+
*
|
|
148
|
+
* `config.context` is what makes the SDK-provided fields automatic on the
|
|
149
|
+
* server: bound once per request, every `resolve()` carries `path`/`query`/
|
|
150
|
+
* `url`/`referrer`/UTMs with no per-call wiring, matching what the browser
|
|
151
|
+
* client collects from `window` on its own. A no-op when nothing is bound and
|
|
152
|
+
* nothing was passed — preserving today's behavior for callers using neither.
|
|
132
153
|
*/
|
|
133
154
|
withVisitor(opts) {
|
|
134
|
-
|
|
135
|
-
|
|
155
|
+
const identity = compact({
|
|
156
|
+
...opts?.identity,
|
|
157
|
+
visitorId: this.visitorId ?? opts?.identity?.visitorId
|
|
158
|
+
});
|
|
159
|
+
const merged = {
|
|
160
|
+
...compact(this.context ?? {}),
|
|
161
|
+
...compact(opts?.context ?? {}),
|
|
162
|
+
...identity
|
|
163
|
+
};
|
|
164
|
+
if (Object.keys(merged).length === 0 && !this.visitorId) return opts;
|
|
165
|
+
if (this.visitorId) merged._xenia_vid = this.visitorId;
|
|
166
|
+
return { ...opts, data: { ...opts?.data ?? {}, ...merged } };
|
|
136
167
|
}
|
|
137
168
|
};
|
|
138
169
|
function createXeniaServer(config) {
|