@xenia-martech/sdk-js 1.1.0 → 1.2.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.
Files changed (63) hide show
  1. package/README.md +50 -0
  2. package/assets/coverage.json +1 -0
  3. package/dist/{actions-CKeiofxJ.d.cts → actions-BcdlokGO.d.cts} +192 -4
  4. package/dist/{actions-CKeiofxJ.d.ts → actions-BcdlokGO.d.ts} +192 -4
  5. package/dist/api-BEPW0Uan.d.cts +88 -0
  6. package/dist/api-CktVQ6Gl.d.ts +88 -0
  7. package/dist/{chunk-U2WBBOYT.cjs → chunk-3PYPPIUM.cjs} +35 -10
  8. package/dist/chunk-3PYPPIUM.cjs.map +1 -0
  9. package/dist/{chunk-FYSEKFIY.js → chunk-7NGJC7CF.js} +249 -3
  10. package/dist/chunk-7NGJC7CF.js.map +1 -0
  11. package/dist/{chunk-WYAMPEVV.js → chunk-DWFVDNU5.js} +28 -3
  12. package/dist/chunk-DWFVDNU5.js.map +1 -0
  13. package/dist/{chunk-JEKVN7HK.cjs → chunk-IBNIRT7J.cjs} +251 -2
  14. package/dist/chunk-IBNIRT7J.cjs.map +1 -0
  15. package/dist/{chunk-TLTG3BTC.js → chunk-OUZU4Z5B.js} +2 -2
  16. package/dist/{chunk-TLTG3BTC.js.map → chunk-OUZU4Z5B.js.map} +1 -1
  17. package/dist/{chunk-5D7SJIO4.cjs → chunk-PKWANLT7.cjs} +2 -2
  18. package/dist/{chunk-5D7SJIO4.cjs.map → chunk-PKWANLT7.cjs.map} +1 -1
  19. package/dist/client/index.cjs +314 -12
  20. package/dist/client/index.cjs.map +1 -1
  21. package/dist/client/index.d.cts +2 -2
  22. package/dist/client/index.d.ts +2 -2
  23. package/dist/client/index.js +312 -13
  24. package/dist/client/index.js.map +1 -1
  25. package/dist/client/react/index.cjs +402 -16
  26. package/dist/client/react/index.cjs.map +1 -1
  27. package/dist/client/react/index.d.cts +53 -3
  28. package/dist/client/react/index.d.ts +53 -3
  29. package/dist/client/react/index.js +396 -17
  30. package/dist/client/react/index.js.map +1 -1
  31. package/dist/cms/contentful.cjs +138 -0
  32. package/dist/cms/contentful.cjs.map +1 -0
  33. package/dist/cms/contentful.d.cts +57 -0
  34. package/dist/cms/contentful.d.ts +57 -0
  35. package/dist/cms/contentful.js +134 -0
  36. package/dist/cms/contentful.js.map +1 -0
  37. package/dist/{cookie-names-QYMiEMAq.d.cts → cookie-names-BACctagz.d.cts} +1 -1
  38. package/dist/{cookie-names-8FxbGKGS.d.ts → cookie-names-CLqtmGBM.d.ts} +1 -1
  39. package/dist/index.cjs +35 -23
  40. package/dist/index.d.cts +4 -3
  41. package/dist/index.d.ts +4 -3
  42. package/dist/index.js +2 -2
  43. package/dist/next/index.cjs +41 -9
  44. package/dist/next/index.cjs.map +1 -1
  45. package/dist/next/index.d.cts +32 -3
  46. package/dist/next/index.d.ts +32 -3
  47. package/dist/next/index.js +14 -3
  48. package/dist/next/index.js.map +1 -1
  49. package/dist/{api-URhxxi4x.d.cts → personalize-content-Clc1oMYY.d.cts} +152 -69
  50. package/dist/{api-URhxxi4x.d.ts → personalize-content-Clc1oMYY.d.ts} +152 -69
  51. package/dist/server/index.cjs +29 -17
  52. package/dist/server/index.d.cts +5 -4
  53. package/dist/server/index.d.ts +5 -4
  54. package/dist/server/index.js +4 -4
  55. package/dist/xenia-sdk.iife.js +3 -3
  56. package/dist/xenia-sdk.iife.js.map +1 -1
  57. package/dist/{xenia-server-Cti94W49.d.cts → xenia-server-BWhWD0La.d.ts} +15 -1
  58. package/dist/{xenia-server-DDtQp6Wi.d.ts → xenia-server-o_wDOcei.d.cts} +15 -1
  59. package/package.json +15 -1
  60. package/dist/chunk-FYSEKFIY.js.map +0 -1
  61. package/dist/chunk-JEKVN7HK.cjs.map +0 -1
  62. package/dist/chunk-U2WBBOYT.cjs.map +0 -1
  63. package/dist/chunk-WYAMPEVV.js.map +0 -1
@@ -0,0 +1,88 @@
1
+ import { L as XeniaConfig, J as JsonValue, W as WebhookResponse, F as WidgetSubmitResponse, N as XeniaEvent, c as AnalyticsIngestResponse, I as IdentifyRequest, m as IdentifyResponse, f as ConsentState, w as ResolveOptions, x as ResolveResult, R as ResolveBatchItem, u as ResolveBatchResult, v as ResolveEntriesResult } from './personalize-content-Clc1oMYY.js';
2
+
3
+ /**
4
+ * Low-level, isomorphic Xenia API client. Wraps the raw endpoints; identity,
5
+ * UTM capture, and action rendering are layered on top by the client runtime.
6
+ * All paths are relative to `baseUrl`, which defaults to the hosted engine
7
+ * (`https://engine.xeniamartech.com/xenia`) when omitted from config.
8
+ */
9
+ declare class XeniaApi {
10
+ private readonly http;
11
+ private readonly tenantId;
12
+ private readonly environmentKey;
13
+ private readonly tokenProvider;
14
+ constructor(config: XeniaConfig);
15
+ /** Tenant-scope headers required by header-scoped endpoints. */
16
+ private tenantHeaders;
17
+ /**
18
+ * Tenant headers plus a resolved `Authorization: Bearer` header when
19
+ * `config.token`/`config.getToken` is set — e.g. required by a webhook
20
+ * trigger node configured with its own auth token. No-op (tenant headers
21
+ * only) when no token source is configured, so this is safe to call
22
+ * unconditionally on every request.
23
+ */
24
+ private authHeaders;
25
+ /**
26
+ * Webhook request/response — `GET|POST /flow?id={triggerId}`. Held open by the
27
+ * backend until a `webhook-response` node resolves. Returns the normalized
28
+ * response, including any `xeniaActions` parsed from the body.
29
+ *
30
+ * Not retried by default: the backend long-polls this call, so a retry would
31
+ * duplicate the journey run.
32
+ */
33
+ webhook(triggerId: string, data?: JsonValue, method?: "GET" | "POST"): Promise<WebhookResponse>;
34
+ /**
35
+ * Widget form submission — `POST /widget-submissions?ref={bindingRef}`. Used whenever the
36
+ * action's `formSubmit.requiresResolve` is true (spin-wheel, which must have a
37
+ * server-resolved outcome before anything renders) or `mode === "endpoint"` — the common
38
+ * plain-popup case (`mode === "journey" && !requiresResolve`) bypasses this entirely and calls
39
+ * {@link webhook} against `flowRef` instead (see `XeniaClient.submitWidgetForm`'s endpoint
40
+ * selection).
41
+ *
42
+ * Not retried (`noRetry: true`), styled like {@link webhook} — a retried POST must never
43
+ * double-fire a journey or double-resolve a prize server-side.
44
+ */
45
+ submitWidget(ref: string, body?: JsonValue): Promise<WidgetSubmitResponse>;
46
+ /**
47
+ * Unified event batch (max 100/request per the backend) — `POST /api/events/v1`.
48
+ * Records analytics for every event and matches/advances a journey for any
49
+ * event with `trigger: true`. Replaces the old `sendEvent()` (journey trigger)
50
+ * and `sendAnalytics()` (CDP) calls.
51
+ */
52
+ sendEvents(events: XeniaEvent[], opts?: {
53
+ keepalive?: boolean;
54
+ }): Promise<AnalyticsIngestResponse>;
55
+ /** Bind visitor→user — `POST /api/events/v1/identify`. */
56
+ identify(body: IdentifyRequest): Promise<IdentifyResponse>;
57
+ /**
58
+ * Update consent — `POST /api/events/v1/consent`.
59
+ *
60
+ * The three scope flags are required (non-null) by the backend, so an omitted
61
+ * one is sent as an explicit `false` rather than left out: a partial body like
62
+ * `consent({ analytics: true })` — the shape most callers actually write — is
63
+ * otherwise rejected with a 400 and the consent record is never stored.
64
+ */
65
+ consent(xeniaVisitorId: string, consent: ConsentState): Promise<JsonValue>;
66
+ /**
67
+ * Resolve in-entry content personalization for one content key —
68
+ * `POST /api/personalization/resolve?merge=<bool>`. Requires the SDK Bearer
69
+ * token (same credential as events; resolve is auth-required). `merge=true`
70
+ * returns server-spliced `content`; otherwise splice `swaps` client-side.
71
+ */
72
+ resolve(contentKey: string, opts?: ResolveOptions): Promise<ResolveResult>;
73
+ /**
74
+ * Resolve several content keys in one round-trip (≤50) —
75
+ * `POST /api/personalization/resolve-batch?merge=<bool>`. Same auth/merge
76
+ * semantics as {@link resolve}.
77
+ */
78
+ resolveBatch(items: ResolveBatchItem[], opts?: ResolveOptions): Promise<ResolveBatchResult[]>;
79
+ /**
80
+ * Resolve personalized CMS entries by id for one visitor (≤200) —
81
+ * `POST /api/personalization/resolve-entries`. The page resolver
82
+ * ({@link personalizeContent}) calls this once per round with the runtime
83
+ * targets it found; callers rarely need it directly.
84
+ */
85
+ resolveEntries(entryIds: string[], opts?: ResolveOptions): Promise<ResolveEntriesResult>;
86
+ }
87
+
88
+ export { XeniaApi as X };
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkJEKVN7HK_cjs = require('./chunk-JEKVN7HK.cjs');
3
+ var chunkIBNIRT7J_cjs = require('./chunk-IBNIRT7J.cjs');
4
4
 
5
5
  // src/core/event-payload.ts
6
6
  var RESERVED = /* @__PURE__ */ new Set([
@@ -70,7 +70,7 @@ var XeniaServer = class {
70
70
  /** Ambient request context attached to every call — see {@link XeniaConfig.context}. */
71
71
  context;
72
72
  constructor(config) {
73
- this.api = new chunkJEKVN7HK_cjs.XeniaApi(config);
73
+ this.api = new chunkIBNIRT7J_cjs.XeniaApi(config);
74
74
  this.visitorId = config.visitorId;
75
75
  this.context = config.context;
76
76
  }
@@ -83,7 +83,7 @@ var XeniaServer = class {
83
83
  async track(eventName, opts = {}) {
84
84
  const event = {
85
85
  eventName,
86
- eventId: opts.eventId ? chunkJEKVN7HK_cjs.toEventId(opts.eventId) : chunkJEKVN7HK_cjs.generateUlid(),
86
+ eventId: opts.eventId ? chunkIBNIRT7J_cjs.toEventId(opts.eventId) : chunkIBNIRT7J_cjs.generateUlid(),
87
87
  eventVersion: opts.eventVersion ?? 1,
88
88
  occurredAtMs: opts.occurredAtMs ?? Date.now(),
89
89
  source: opts.source ?? "xenia-server-sdk",
@@ -158,6 +158,31 @@ var XeniaServer = class {
158
158
  async resolveBatch(items, opts) {
159
159
  return orFallback(() => this.api.resolveBatch(items, this.withVisitor(opts)), opts);
160
160
  }
161
+ /**
162
+ * Personalize a fetched CMS page server-side — see `XeniaClient.personalizeContent`. Never
163
+ * throws: on error or timeout the page renders with whatever was decided (base content otherwise).
164
+ * Pass `opts.identity`/`opts.context` like `resolve()`.
165
+ */
166
+ async personalizeContent(page, opts) {
167
+ return chunkIBNIRT7J_cjs.personalizeContent(page, { ...opts, resolve: this.entryResolver(opts) });
168
+ }
169
+ /**
170
+ * Fetch a page through `opts.cms` and personalize it — the app passes only the page's identity
171
+ * (entry id, or `{ contentType, fields: { slug } }`). Never throws: a failed fetch is `content:
172
+ * null` with `error`, a missing page `content: null`.
173
+ */
174
+ async personalizePage(ref, opts) {
175
+ return chunkIBNIRT7J_cjs.personalizePage(ref, { ...opts, resolve: this.entryResolver(opts) });
176
+ }
177
+ entryResolver(opts) {
178
+ const resolveOpts = this.withVisitor({
179
+ data: opts.data,
180
+ locale: opts.locale,
181
+ identity: opts.identity,
182
+ context: opts.context
183
+ });
184
+ return async (ids) => (await this.api.resolveEntries(ids, resolveOpts)).decisions ?? [];
185
+ }
161
186
  /**
162
187
  * Merge the ambient `config.context` and `opts.identity`/`opts.context` into
163
188
  * the resolve `data`, then stamp the SDK-owned visitor id
@@ -174,13 +199,13 @@ var XeniaServer = class {
174
199
  * nothing was passed — preserving today's behavior for callers using neither.
175
200
  */
176
201
  withVisitor(opts) {
177
- const identity = chunkJEKVN7HK_cjs.compact({
202
+ const identity = chunkIBNIRT7J_cjs.compact({
178
203
  ...opts?.identity,
179
204
  visitorId: this.visitorId ?? opts?.identity?.visitorId
180
205
  });
181
206
  const merged = {
182
- ...chunkJEKVN7HK_cjs.compact(this.context ?? {}),
183
- ...chunkJEKVN7HK_cjs.compact(opts?.context ?? {}),
207
+ ...chunkIBNIRT7J_cjs.compact(this.context ?? {}),
208
+ ...chunkIBNIRT7J_cjs.compact(opts?.context ?? {}),
184
209
  ...identity
185
210
  };
186
211
  if (Object.keys(merged).length === 0 && !this.visitorId) return opts;
@@ -206,10 +231,10 @@ function parseCookies(header) {
206
231
  return out;
207
232
  }
208
233
  function getVisitorId(cookieHeader, prefix) {
209
- return parseCookies(cookieHeader)[chunkJEKVN7HK_cjs.visitorCookieName(prefix)] ?? null;
234
+ return parseCookies(cookieHeader)[chunkIBNIRT7J_cjs.visitorCookieName(prefix)] ?? null;
210
235
  }
211
236
  function getConsent(cookieHeader, prefix) {
212
- const raw = parseCookies(cookieHeader)[chunkJEKVN7HK_cjs.consentCookieName(prefix)];
237
+ const raw = parseCookies(cookieHeader)[chunkIBNIRT7J_cjs.consentCookieName(prefix)];
213
238
  if (!raw) return { analytics: false, marketing: false, personalization: false };
214
239
  try {
215
240
  const parsed = JSON.parse(raw);
@@ -229,5 +254,5 @@ exports.createXeniaServer = createXeniaServer;
229
254
  exports.getConsent = getConsent;
230
255
  exports.getVisitorId = getVisitorId;
231
256
  exports.parseCookies = parseCookies;
232
- //# sourceMappingURL=chunk-U2WBBOYT.cjs.map
233
- //# sourceMappingURL=chunk-U2WBBOYT.cjs.map
257
+ //# sourceMappingURL=chunk-3PYPPIUM.cjs.map
258
+ //# sourceMappingURL=chunk-3PYPPIUM.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","personalizeContent","personalizePage","visitorCookieName","consentCookieName"],"mappings":"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","file":"chunk-3PYPPIUM.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 { personalizeContent, personalizePage } from \"../core/personalize-content\";\nimport type { EntryResolver, PageRef, PagePersonalizeOptions, PersonalizeContentResult } from \"../core/personalize-content\";\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 * Personalize a fetched CMS page server-side — see `XeniaClient.personalizeContent`. Never\n * throws: on error or timeout the page renders with whatever was decided (base content otherwise).\n * Pass `opts.identity`/`opts.context` like `resolve()`.\n */\n async personalizeContent<T>(page: T, opts: PagePersonalizeOptions): Promise<PersonalizeContentResult<T>> {\n return personalizeContent(page, { ...opts, resolve: this.entryResolver(opts) });\n }\n\n /**\n * Fetch a page through `opts.cms` and personalize it — the app passes only the page's identity\n * (entry id, or `{ contentType, fields: { slug } }`). Never throws: a failed fetch is `content:\n * null` with `error`, a missing page `content: null`.\n */\n async personalizePage<T = unknown>(ref: PageRef, opts: PagePersonalizeOptions): Promise<PersonalizeContentResult<T | null>> {\n return personalizePage<T>(ref, { ...opts, resolve: this.entryResolver(opts) });\n }\n\n private entryResolver(opts: PagePersonalizeOptions): EntryResolver {\n const resolveOpts = this.withVisitor({\n data: opts.data, locale: opts.locale, identity: opts.identity, context: opts.context,\n });\n return async (ids) => (await this.api.resolveEntries(ids, resolveOpts)).decisions ?? [];\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"]}
@@ -540,6 +540,25 @@ var XeniaApi = class {
540
540
  });
541
541
  return data;
542
542
  }
543
+ /**
544
+ * Resolve personalized CMS entries by id for one visitor (≤200) —
545
+ * `POST /api/personalization/resolve-entries`. The page resolver
546
+ * ({@link personalizeContent}) calls this once per round with the runtime
547
+ * targets it found; callers rarely need it directly.
548
+ */
549
+ async resolveEntries(entryIds, opts = {}) {
550
+ const { data } = await this.http.request({
551
+ method: "POST",
552
+ path: "/api/personalization/resolve-entries",
553
+ headers: await this.authHeaders(),
554
+ body: compact({
555
+ entryIds,
556
+ data: opts.data,
557
+ locale: opts.locale
558
+ })
559
+ });
560
+ return data;
561
+ }
543
562
  };
544
563
 
545
564
  // src/core/id.ts
@@ -632,6 +651,233 @@ function contextFromUrl(url, referrer) {
632
651
  return ctx;
633
652
  }
634
653
 
635
- export { CONSENT_COOKIE, CircuitOpenError, DEFAULT_PREFIX, HttpClient, HttpError, MAX_EVENT_ID_LENGTH, VISITOR_COOKIE, XeniaApi, buildUrl, compact, consentCookie, consentCookieName, contextFromUrl, generateUlid, isBrowser, parseActions, toEventId, visitorCookieName };
636
- //# sourceMappingURL=chunk-FYSEKFIY.js.map
637
- //# sourceMappingURL=chunk-FYSEKFIY.js.map
654
+ // src/core/personalize-content.ts
655
+ var DEFAULT_TIMEOUT_MS = 300;
656
+ var DEFAULT_MAX_ROUNDS = 3;
657
+ var copyOf = (v) => v && typeof v === "object" && typeof structuredClone === "function" ? structuredClone(v) : v;
658
+ async function personalizeContent(payload, options) {
659
+ const { cms } = options;
660
+ const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
661
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
662
+ const applied = [];
663
+ const report = (swap) => {
664
+ applied.push(swap);
665
+ try {
666
+ options.onApplied?.(swap);
667
+ } catch {
668
+ }
669
+ };
670
+ let content = payload;
671
+ try {
672
+ if (!options.inPlace && typeof structuredClone === "function") content = structuredClone(content);
673
+ content = cms.prepare ? cms.prepare(content) : content;
674
+ } catch (error) {
675
+ return { content: payload, applied, complete: false, error };
676
+ }
677
+ const box = { root: content };
678
+ const settled = /* @__PURE__ */ new WeakSet();
679
+ const decided = /* @__PURE__ */ new Map();
680
+ let frontier = [{ parent: box, key: "root", value: box.root }];
681
+ const settle = (target, d, round) => {
682
+ settled.add(target.entry);
683
+ cms.clearMarker?.(target.entry);
684
+ const items = d.items.map(copyOf);
685
+ if (!d.decision || items.length === 0) {
686
+ return deferredSlots(target, cms);
687
+ }
688
+ for (const item of items) if (item && typeof item === "object") settled.add(item);
689
+ const op = d.decision.op ?? target.marker.op ?? "replace";
690
+ const next = apply(target, op, d.decision.field ?? target.marker.field, items, cms);
691
+ report({
692
+ target: target.id,
693
+ kind: "runtime",
694
+ op,
695
+ entryIds: items.map((e) => cms.entryId(e)).filter((id) => !!id),
696
+ contentKey: d.decision.contentKey,
697
+ bucket: d.decision.bucket,
698
+ round
699
+ });
700
+ return next;
701
+ };
702
+ for (let round = 0; ; round++) {
703
+ const pending = walk(frontier, cms, settled, round, report, (t) => {
704
+ const d = decided.get(t.id);
705
+ return d ? settle(t, d, round) : void 0;
706
+ });
707
+ if (pending.size === 0) return { content: box.root, applied, complete: true };
708
+ if (!options.resolve || round >= maxRounds) {
709
+ return { content: box.root, applied, complete: !options.resolve };
710
+ }
711
+ progress(options, { content: box.root, round, pending: [...pending.keys()] });
712
+ let decisions;
713
+ let entriesById;
714
+ let queried;
715
+ try {
716
+ decisions = await withDeadline(options.resolve([...pending.keys()]), deadline);
717
+ ({ entriesById, queried } = await withDeadline(loadReplacements(decisions, cms, options.locale), deadline));
718
+ } catch (error) {
719
+ return { content: box.root, applied, complete: false, error: error === TIMEOUT ? void 0 : error };
720
+ }
721
+ const byTarget = new Map(decisions.map((d) => [d.target, d]));
722
+ frontier = [];
723
+ for (const target of pending.values()) {
724
+ const decision = byTarget.get(target.id);
725
+ const items = decision ? decision.ids ? decision.ids.map((id) => entriesById.get(id)).filter((e) => e !== void 0) : queried.get(decision) ?? [] : [];
726
+ const d = { decision, items };
727
+ decided.set(target.id, d);
728
+ frontier.push(...settle(target, d, round));
729
+ }
730
+ progress(options, { content: box.root, round, pending: [] });
731
+ }
732
+ }
733
+ function progress(options, p) {
734
+ try {
735
+ options.onProgress?.(p);
736
+ } catch {
737
+ }
738
+ }
739
+ function walk(frontier, cms, settled, round, report, reuse) {
740
+ const pending = /* @__PURE__ */ new Map();
741
+ const seen = /* @__PURE__ */ new WeakSet();
742
+ const deferred = /* @__PURE__ */ new WeakSet();
743
+ const queue = [...frontier];
744
+ while (queue.length > 0) {
745
+ const slot = queue.shift();
746
+ const value = slot.value;
747
+ if (!value || typeof value !== "object" || deferred.has(value)) continue;
748
+ const id = settled.has(value) ? void 0 : cms.entryId(value);
749
+ const marker = id ? cms.marker(value) : null;
750
+ if (id && marker?.kind === "runtime") {
751
+ const again = reuse({ id, marker, entry: value, slots: [slot] });
752
+ if (again) {
753
+ queue.push(...again);
754
+ continue;
755
+ }
756
+ const existing = pending.get(id);
757
+ if (existing) {
758
+ existing.slots.push(slot);
759
+ } else {
760
+ pending.set(id, { id, marker, entry: value, slots: [slot] });
761
+ if (marker.op === "items" && marker.field) {
762
+ const held = cms.getField(value, marker.field);
763
+ if (held && typeof held === "object") deferred.add(held);
764
+ queue.push(...children(value));
765
+ }
766
+ }
767
+ continue;
768
+ }
769
+ if (id && marker?.kind === "static") {
770
+ const variants = cms.staticVariants(value).filter((v) => v && typeof v === "object");
771
+ settled.add(value);
772
+ cms.clearMarker?.(value);
773
+ if (variants.length > 0) {
774
+ for (const v of variants) settled.add(v);
775
+ const op = marker.op ?? "replace";
776
+ const target = { entry: value, slots: [slot] };
777
+ queue.push(...apply(target, op, marker.field, variants, cms));
778
+ report({
779
+ target: id,
780
+ kind: "static",
781
+ op,
782
+ entryIds: variants.map((v) => cms.entryId(v)).filter((x) => !!x),
783
+ round
784
+ });
785
+ continue;
786
+ }
787
+ }
788
+ if (seen.has(value)) continue;
789
+ seen.add(value);
790
+ queue.push(...children(value));
791
+ }
792
+ return pending;
793
+ }
794
+ function apply(target, op, field, items, cms) {
795
+ if (op === "items") {
796
+ if (field) {
797
+ cms.setField(target.entry, field, items);
798
+ return [{ parent: target.entry, key: field, value: cms.getField(target.entry, field) }];
799
+ }
800
+ if (cms.mergeItems) {
801
+ cms.mergeItems(target.entry, items[0]);
802
+ return target.slots.map((s) => ({ ...s, value: target.entry }));
803
+ }
804
+ op = "replace";
805
+ }
806
+ const next = [];
807
+ for (const slot of target.slots) {
808
+ const placed = op === "expand" && Array.isArray(slot.parent) ? items : [items[0]];
809
+ if (Array.isArray(slot.parent)) {
810
+ const index = slot.parent.indexOf(target.entry);
811
+ if (index < 0) continue;
812
+ slot.parent.splice(index, 1, ...placed);
813
+ } else {
814
+ slot.parent[slot.key] = placed[0];
815
+ }
816
+ for (const value of placed) next.push({ parent: slot.parent, key: slot.key, value });
817
+ }
818
+ return next;
819
+ }
820
+ function deferredSlots(target, cms) {
821
+ if (target.marker.op === "items" && target.marker.field) {
822
+ return [{ parent: target.entry, key: target.marker.field, value: cms.getField(target.entry, target.marker.field) }];
823
+ }
824
+ return target.slots.map((s) => ({ ...s, value: target.entry }));
825
+ }
826
+ function children(value) {
827
+ if (Array.isArray(value)) return value.map((v, i) => ({ parent: value, key: i, value: v }));
828
+ return Object.keys(value).map((k) => ({ parent: value, key: k, value: value[k] }));
829
+ }
830
+ async function loadReplacements(decisions, cms, locale) {
831
+ const ids = [...new Set(decisions.flatMap((d) => d.ids ?? []))];
832
+ const queries = decisions.filter((d) => !d.ids && d.query);
833
+ const [fetched, ...results] = await Promise.all([
834
+ ids.length > 0 ? cms.fetchEntries(ids, locale) : Promise.resolve([]),
835
+ ...queries.map((d) => cms.queryEntries(d.query, locale).catch(() => []))
836
+ ]);
837
+ const entriesById = /* @__PURE__ */ new Map();
838
+ for (const entry of fetched) {
839
+ const id = cms.entryId(entry);
840
+ if (id) entriesById.set(id, entry);
841
+ }
842
+ const queried = /* @__PURE__ */ new Map();
843
+ queries.forEach((d, i) => queried.set(d, results[i]));
844
+ return { entriesById, queried };
845
+ }
846
+ var TIMEOUT = /* @__PURE__ */ Symbol("timeout");
847
+ function withDeadline(promise, deadline) {
848
+ const remaining = deadline - Date.now();
849
+ if (remaining <= 0) return Promise.reject(TIMEOUT);
850
+ let timer;
851
+ return Promise.race([
852
+ promise,
853
+ new Promise((_, reject) => {
854
+ timer = setTimeout(() => reject(TIMEOUT), remaining);
855
+ })
856
+ ]).finally(() => clearTimeout(timer));
857
+ }
858
+ function pendingTargets(payload, cms) {
859
+ try {
860
+ let content = typeof structuredClone === "function" ? structuredClone(payload) : payload;
861
+ content = cms.prepare ? cms.prepare(content) : content;
862
+ const root = { parent: { root: content }, key: "root", value: content };
863
+ return [...walk([root], cms, /* @__PURE__ */ new WeakSet(), 0, () => {
864
+ }, () => void 0).keys()];
865
+ } catch {
866
+ return [];
867
+ }
868
+ }
869
+ async function personalizePage(ref, options) {
870
+ let page;
871
+ try {
872
+ if (!options.cms.fetchPage) throw new Error("This CMS adapter cannot fetch pages; use personalizeContent(page)");
873
+ page = await options.cms.fetchPage(ref, options.locale);
874
+ } catch (error) {
875
+ return { content: null, applied: [], complete: false, error };
876
+ }
877
+ if (page == null) return { content: null, applied: [], complete: true };
878
+ return personalizeContent(page, options);
879
+ }
880
+
881
+ export { CONSENT_COOKIE, CircuitOpenError, DEFAULT_PREFIX, HttpClient, HttpError, MAX_EVENT_ID_LENGTH, VISITOR_COOKIE, XeniaApi, buildUrl, compact, consentCookie, consentCookieName, contextFromUrl, generateUlid, isBrowser, parseActions, pendingTargets, personalizeContent, personalizePage, toEventId, visitorCookieName };
882
+ //# sourceMappingURL=chunk-7NGJC7CF.js.map
883
+ //# sourceMappingURL=chunk-7NGJC7CF.js.map