@xenia-martech/sdk-js 1.2.0-dev.7 → 1.2.0-dev.9

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 (96) hide show
  1. package/README.md +70 -6
  2. package/dist/{actions-35V37IT0.d.cts → actions--1_9Z8ig.d.cts} +13 -2
  3. package/dist/{actions-35V37IT0.d.ts → actions--1_9Z8ig.d.ts} +13 -2
  4. package/dist/{api-0IycTYJR.d.cts → api-DbUxT38T.d.ts} +1 -1
  5. package/dist/{api-COXWLL5L.d.ts → api-DgzJ5s-k.d.cts} +1 -1
  6. package/dist/chunk-DPXINHQR.cjs +34 -0
  7. package/dist/chunk-DPXINHQR.cjs.map +1 -0
  8. package/dist/chunk-FSJMHXS6.js +432 -0
  9. package/dist/chunk-FSJMHXS6.js.map +1 -0
  10. package/dist/chunk-GXJLU3RE.cjs +448 -0
  11. package/dist/chunk-GXJLU3RE.cjs.map +1 -0
  12. package/dist/{chunk-2DR5AVPX.cjs → chunk-I5EHLRSR.cjs} +11 -10
  13. package/dist/chunk-I5EHLRSR.cjs.map +1 -0
  14. package/dist/{chunk-LW5AEJ6K.js → chunk-MEULT5X6.js} +4 -241
  15. package/dist/chunk-MEULT5X6.js.map +1 -0
  16. package/dist/{chunk-GWRDZIVA.js → chunk-NDRRSRTU.js} +4 -3
  17. package/dist/chunk-NDRRSRTU.js.map +1 -0
  18. package/dist/chunk-RC46KXDM.js +32 -0
  19. package/dist/chunk-RC46KXDM.js.map +1 -0
  20. package/dist/{chunk-RWE2OCQL.cjs → chunk-SU25SELD.cjs} +4 -244
  21. package/dist/chunk-SU25SELD.cjs.map +1 -0
  22. package/dist/{chunk-WHGK55JP.cjs → chunk-UUIKUU5W.cjs} +5 -5
  23. package/dist/{chunk-WHGK55JP.cjs.map → chunk-UUIKUU5W.cjs.map} +1 -1
  24. package/dist/{chunk-QEYONKWY.js → chunk-UVTFGQR6.js} +3 -3
  25. package/dist/{chunk-QEYONKWY.js.map → chunk-UVTFGQR6.js.map} +1 -1
  26. package/dist/{chunk-OMMIRSPK.js → chunk-V4EGU3PM.js} +48 -11
  27. package/dist/chunk-V4EGU3PM.js.map +1 -0
  28. package/dist/{chunk-YLMMTKQL.cjs → chunk-WMGCQPHY.cjs} +52 -13
  29. package/dist/chunk-WMGCQPHY.cjs.map +1 -0
  30. package/dist/client/index.cjs +26 -10
  31. package/dist/client/index.cjs.map +1 -1
  32. package/dist/client/index.d.cts +2 -2
  33. package/dist/client/index.d.ts +2 -2
  34. package/dist/client/index.js +26 -10
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/client/react/index.cjs +73 -15
  37. package/dist/client/react/index.cjs.map +1 -1
  38. package/dist/client/react/index.d.cts +9 -3
  39. package/dist/client/react/index.d.ts +9 -3
  40. package/dist/client/react/index.js +73 -15
  41. package/dist/client/react/index.js.map +1 -1
  42. package/dist/cms/contentful.cjs +7 -24
  43. package/dist/cms/contentful.cjs.map +1 -1
  44. package/dist/cms/contentful.d.cts +5 -4
  45. package/dist/cms/contentful.d.ts +5 -4
  46. package/dist/cms/contentful.js +4 -23
  47. package/dist/cms/contentful.js.map +1 -1
  48. package/dist/cms/contentstack.cjs +120 -0
  49. package/dist/cms/contentstack.cjs.map +1 -0
  50. package/dist/cms/contentstack.d.cts +41 -0
  51. package/dist/cms/contentstack.d.ts +41 -0
  52. package/dist/cms/contentstack.js +117 -0
  53. package/dist/cms/contentstack.js.map +1 -0
  54. package/dist/{consent-DRCV0N0d.d.ts → consent-Dtiqzz2B.d.cts} +71 -11
  55. package/dist/{consent-BgAETCkL.d.cts → consent-byYiDMFr.d.ts} +71 -11
  56. package/dist/{cookie-names-DU3MtcNG.d.ts → cookie-names-B4hda2rX.d.cts} +1 -1
  57. package/dist/{cookie-names-DFItnSLK.d.cts → cookie-names-DioZXuV0.d.ts} +1 -1
  58. package/dist/edge/index.cjs +122 -29
  59. package/dist/edge/index.cjs.map +1 -1
  60. package/dist/edge/index.d.cts +42 -6
  61. package/dist/edge/index.d.ts +42 -6
  62. package/dist/edge/index.js +92 -9
  63. package/dist/edge/index.js.map +1 -1
  64. package/dist/index.cjs +31 -31
  65. package/dist/index.d.cts +4 -5
  66. package/dist/index.d.ts +4 -5
  67. package/dist/index.js +2 -2
  68. package/dist/next/index.cjs +39 -34
  69. package/dist/next/index.cjs.map +1 -1
  70. package/dist/next/index.d.cts +2 -3
  71. package/dist/next/index.d.ts +2 -3
  72. package/dist/next/index.js +13 -8
  73. package/dist/next/index.js.map +1 -1
  74. package/dist/{types-DgVHS4Qw.d.ts → personalize-content-D6xeBI7-.d.cts} +150 -1
  75. package/dist/{types-DgVHS4Qw.d.cts → personalize-content-D6xeBI7-.d.ts} +150 -1
  76. package/dist/server/index.cjs +30 -30
  77. package/dist/server/index.d.cts +5 -6
  78. package/dist/server/index.d.ts +5 -6
  79. package/dist/server/index.js +5 -5
  80. package/dist/xenia-sdk.iife.js +3 -3
  81. package/dist/xenia-sdk.iife.js.map +1 -1
  82. package/dist/{xenia-server-Cf24mL0c.d.ts → xenia-server-BcgeUHX4.d.ts} +2 -3
  83. package/dist/{xenia-server-D-bx14lw.d.cts → xenia-server-lv6LtJZP.d.cts} +2 -3
  84. package/package.json +14 -1
  85. package/dist/chunk-2DR5AVPX.cjs.map +0 -1
  86. package/dist/chunk-BQ5PJCZ7.js +0 -179
  87. package/dist/chunk-BQ5PJCZ7.js.map +0 -1
  88. package/dist/chunk-GWRDZIVA.js.map +0 -1
  89. package/dist/chunk-LW5AEJ6K.js.map +0 -1
  90. package/dist/chunk-N5M4PI7C.cjs +0 -192
  91. package/dist/chunk-N5M4PI7C.cjs.map +0 -1
  92. package/dist/chunk-OMMIRSPK.js.map +0 -1
  93. package/dist/chunk-RWE2OCQL.cjs.map +0 -1
  94. package/dist/chunk-YLMMTKQL.cjs.map +0 -1
  95. package/dist/personalize-content-Cwc2AomC.d.ts +0 -141
  96. package/dist/personalize-content-CxSdplMV.d.cts +0 -141
package/README.md CHANGED
@@ -142,6 +142,7 @@ Building a plain HTML site (no npm, no bundler)? Skip the install and pull the p
142
142
  | `@xenia-martech/sdk-js/edge` | any (Node, edge functions, workers) | [Edge personalization](#edge-personalization): the manifest/decide client, page matching, signed variant URLs, webhook verification, request facts, page trail and consent for a storefront's proxy. No Next.js, React or DOM dependency. |
143
143
  | `@xenia-martech/sdk-js/next` | Next.js 15+ (App Router) | `xeniaProxy()` for `proxy.ts`/`middleware.ts` and `xeniaServerFromNext()` — the visitor cookie and URL context wired for you. `xeniaEdgeProxy()`, `createManifestRoute()` and `createRevalidateRoute()` for [edge personalization](#nextjs-one-call). Needs `next` (optional peer). |
144
144
  | `@xenia-martech/sdk-js/cms/contentful` | any | `contentfulAdapter()` for [CMS page personalization](#cms-page-personalization). |
145
+ | `@xenia-martech/sdk-js/cms/contentstack` | any | `contentstackAdapter()` for [CMS page personalization](#cms-page-personalization). |
145
146
  | `@xenia-martech/sdk-js/iife` | `<script>` tag | Prebuilt `window.Xenia` for sites without a bundler. |
146
147
 
147
148
  ---
@@ -727,12 +728,24 @@ const { content } = await xenia.personalizePage({ contentType: "page", fields: {
727
728
  // content is the page entry, or null when it doesn't exist / the CMS fetch failed (`error` says which)
728
729
  ```
729
730
 
730
- Already fetching the page yourself (your own caching, a CMS client you keep)? Pass it in instead: `xenia.personalizeContent(page, { cms })`.
731
+ On Contentstack, the same call with `contentstackAdapter`. A Contentstack page is addressed by its content type and a field, never by uid alone:
732
+
733
+ ```ts
734
+ import { contentstackAdapter } from "@xenia-martech/sdk-js/cms/contentstack";
735
+
736
+ const cms = contentstackAdapter({ apiKey, deliveryToken, environment, region: "eu", includeDepth: 3 });
737
+ const { content } = await xenia.personalizePage({ contentType: "landing_page", fields: { url: "/" } }, {
738
+ cms,
739
+ contentKey: "home-main",
740
+ });
741
+ ```
742
+
743
+ Already fetching the page yourself (your own caching, a CMS client you keep)? Pass it in instead: `xenia.personalizeContent(page, { cms })`. On Contentstack, fetch it with `include_all=true` so references come back as entries.
731
744
 
732
745
  How it works:
733
746
 
734
- - **Marking.** Xenia writes two fields onto personalized entries: `xenia` (the marker: `kind`, `op`, `field`) and `xeniaVariants` (static replacements).
735
- - **Static swaps cost nothing.** A static swap's replacement comes back in your own CMS fetch, so it is applied with no Xenia call.
747
+ - **Marking.** Xenia writes the marker onto personalized entries when a journey is published. On Contentful it uses two fields: `xenia` (the marker: `kind`, `op`, `field`) and `xeniaVariants` (static replacements). On Contentstack it uses one text field, `xenia`, holding the marker JSON; static replacements are listed in it (`ids`, `ct`). The connector's **publish stage** decides how far Xenia takes that change in the CMS: published, saved as a draft, or moved to a Contentstack workflow stage. Your site only sees a marker once it is published.
748
+ - **Static swaps cost no Xenia call.** On Contentful a static swap's replacement comes back in your own CMS fetch. On Contentstack it is fetched from the delivery API in the same round as the runtime replacements.
736
749
  - **Runtime swaps cost one call per nesting level.** All runtime targets the walk finds go to Xenia in one `POST /api/personalization/resolve-entries` call. Their replacements are then loaded in one CMS call.
737
750
  - **Nested targets.** A runtime target's own subtree is not walked until it has been decided, because it may be replaced. So targets inside it, or inside its replacement, are decided in the next round.
738
751
  - **One-to-many.** `op: "expand"` splices N entries where one stood. `op: "items"` replaces an array field, e.g. a carousel's slides, with explicit entries or a content-type query (for example, "4 products in the visitor's category").
@@ -872,21 +885,72 @@ export const proxy = (request: NextRequest) =>
872
885
  | `onDecision(info)` | Every exit (`skipped`, `not-targeted`, `no-consent`, `decided`, `error`) with the context, facts, outcome and response — debug output, extra cookies. |
873
886
  | `onError(error)` | Anything that threw — a hook, the edge client. The page is served with base content either way. |
874
887
 
875
- Two route factories serve the rest: `createManifestRoute({ token, baseUrl, revalidate })` for the public manifest copy (`export const GET`, with `export const revalidate = …` beside it), and `createRevalidateRoute({ secret, paths: ["/api/xenia/manifest"], onManifestChanged })` for Xenia's storefront webhook (`export const POST`), which regenerates the manifest the moment a journey changes.
888
+ Two route factories serve the rest: `createManifestRoute({ token, baseUrl, revalidate })` for the public manifest copy (`export const GET`, with `export const revalidate = …` beside it), and `createRevalidateRoute({ secret, paths: ["/api/xenia/manifest"], onManifestChanged })` for Xenia's storefront webhook (`export const POST`), which regenerates the manifest the moment a journey changes. Each build and server process fetches its own copy, so a deploy never serves a manifest cached by an earlier build. Point `createXeniaEdge`'s `manifestUrl` at that route: its copy lists pages, not content keys, so the proxy asks `/decide` by path and Xenia decides every live journey on it, including one published since the last manifest refresh. Read Xenia's full manifest instead and the proxy asks only for the keys it already knew.
876
889
 
877
890
  The page behind the rewrite reads the swaps back with `decodeVariant(token, secret)` — `null` for a token that is malformed or not signed with your secret, so a crafted URL can never render arbitrary entries — and applies them to its CMS content. The token holds only the swaps (plus your optional `extra`), identical for every visitor in the same variant, so each variant renders once and is then served from your ISR/CDN cache.
878
891
 
892
+ **Multi-level CMS pages at the edge.** For a journey that personalizes CMS entries at several levels, `/decide` also returns `entryDecisions`: the decision for every target of the page's content key, at every level. The proxy puts them in the token, so the variant page resolves the whole page with **no Xenia call** — only CMS fetches:
893
+
894
+ ```ts
895
+ // app/xv/[token]/[[...slug]]/page.tsx — cached per variant
896
+ export const revalidate = 60;
897
+ export async function generateStaticParams() { return []; }
898
+
899
+ export default async function Page({ params }) {
900
+ const { token, slug } = await params;
901
+ const { content } = await personalizeVariantPage(token, { contentType: "page", fields: { slug } }, {
902
+ secret: process.env.XENIA_VARIANT_SECRET!,
903
+ cms, // contentfulAdapter / contentstackAdapter
904
+ contentKey: "home-main", // omit to apply every journey the token decided
905
+ });
906
+ if (!content) notFound();
907
+ return <PageView page={content} />;
908
+ }
909
+ ```
910
+
911
+ The token holds no visitor data, so each variant renders once and is served from cache to everyone in it — measured on the Contentful example, about 180 ms for a new visitor against 740 ms for the server path, and a few ms for a returning one. Fetching the page yourself? `personalizeVariantContent(page, await decodeVariant(token, secret), { cms })`.
912
+
913
+ The decisions travel compressed. If a token would still be too long, it drops `extra` first, then the entry decisions (`entriesDropped`): those targets keep their base content and the result's `complete` is false.
914
+
879
915
  | Export | What it does |
880
916
  |---|---|
881
- | `createXeniaEdge(config)` | `match(host, path)` from the manifest (stale-while-revalidate, ETag; seed it with `initialManifest` so a cold start never waits), `decide(keys, input)` with `budgetMs` (default 150 ms), a per-visitor decision cache (`memoryDecisionCache()` by default, any `DecisionCache` for a shared store) and in-flight de-duplication. `manifestUrl` reads a storefront's own CDN copy instead. |
917
+ | `createXeniaEdge(config)` | `match(host, path)` from the manifest (stale-while-revalidate, ETag; seed it with `initialManifest` so a cold start never waits), `decide(keys, input)` with `budgetMs` (default 150 ms), a per-visitor decision cache (`memoryDecisionCache()` by default; `tieredDecisionCache(shared)` puts a shared store, sync or async, behind it) and in-flight de-duplication. `manifestUrl` reads a storefront's own CDN copy instead. |
882
918
  | `matchesAny(patterns, host, path)` | The manifest's page patterns: `/women`, `/women/*`, `/*`, `uk.example.com/women`. |
883
919
  | `fetchManifest(opts)` / `trimManifest(m)` | Xenia's manifest, and the public copy a storefront may serve from its CDN (version + page patterns only). |
884
920
  | `verifyWebhook({ headers, body }, secret)` | Checks Xenia's storefront webhook (`X-Xenia-Signature`), refusing anything older than 5 minutes. |
885
921
  | `swapGroups` / `encodeVariant` / `decodeVariant` | The signed variant token. `extra` that would make the token too long is left out. |
922
+ | `personalizeVariantPage` / `personalizeVariantContent` | A CMS page journey's variant page in one call: verify the token, fetch the page, apply every level from the token's decisions. |
923
+ | `variantEntries` / `variantEntryResolver(entries, contentKey)` | The lower-level pieces: the entry decisions a token carries, and a `resolve` that answers from them. |
886
924
  | `viewerGeo`, `deviceOf`, `isBot`, `isPrefetch`, `isPageLoad`, `landingContext`, `localTime`, `seasonOf` | Request facts journey rules read. Geo must come from your edge, never Xenia's: `/decide` is server to server. `viewerGeo` always uses Netlify's runtime context and a runtime-attached `request.geo`, but reads a location **header** only when you list it in `trust` — a visitor can send any header, so name only the ones your host sets and overwrites (`x-vercel-ip-country` on Vercel, `cf-ipcountry` behind Cloudflare, `cloudfront-viewer-country` on CloudFront, `x-nf-geo`/`x-country` on Netlify, `x-xn-geo` from your own edge function). |
887
925
  | `readTrail` / `withView` / `trailCookieValue` | The visitor's recent page views (`xenia_trail`), sent with `/decide` so profile rules see views whose events haven't landed yet. |
888
926
  | `edgeConsent(headers)` | What the edge flow may do (below). |
889
927
 
928
+ **Several instances (serverless, multiple regions).** A visitor's decision is cached in each process's memory, so a returning visitor reaching another instance would ask Xenia again. Share it with `tieredDecisionCache(shared)`: memory first, then your store, then Xenia. A store that is down or slower than 50 ms counts as a miss and never breaks the page. Next.js runs `proxy.ts` on Node.js, so any Redis works through your own client (the SDK adds no dependency):
929
+
930
+ ```ts
931
+ import { createClient } from "redis";
932
+ import { createXeniaEdge, tieredDecisionCache, type DecisionCache } from "@xenia-martech/sdk-js/edge";
933
+
934
+ const redis = createClient({ url: process.env.REDIS_URL });
935
+ const ready = redis.connect();
936
+
937
+ const shared: DecisionCache = {
938
+ get: async (key) => {
939
+ await ready;
940
+ const value = await redis.get(`xenia:${key}`);
941
+ return value ? JSON.parse(value) : undefined;
942
+ },
943
+ set: async (key, value, ttlMs) => {
944
+ await ready;
945
+ await redis.set(`xenia:${key}`, JSON.stringify(value), { PX: ttlMs });
946
+ },
947
+ };
948
+
949
+ const edge = createXeniaEdge({ baseUrl, token, manifestUrl, cache: tieredDecisionCache(shared) });
950
+ ```
951
+
952
+ On a runtime with no TCP sockets (edge functions, workers), implement the same `get` / `set` over any store it can reach by HTTP.
953
+
890
954
  **Consent.** `edgeConsent` reads the `xenia_consent` cookie the browser client's `consent()` writes:
891
955
 
892
956
  | Signal | Effect |
@@ -900,7 +964,7 @@ The page behind the rewrite reads the swaps back with `decodeVariant(token, secr
900
964
 
901
965
  **Debugging and QA.** `xeniaEdgeProxy({ debug, qa })` — turn both on only in development or on a QA deploy (e.g. `debug: process.env.NODE_ENV === "development"`):
902
966
 
903
- - `debug: true` writes what was decided for each of the last few pages to the `xenia_debug` cookie. Render `<XeniaDebugPanel />` from `/client/react` behind the same flag: a round Xenia launcher in the corner opens a small white card, like Next.js's dev tools menu, with the status, page, source and location, the decision per content key (click a key for the journey path it took), and the visitor profile. `position` picks the corner (default bottom-right, away from Next.js's own dev tools), and `defaultOpen` opens the card on page load (default closed; once the visitor opens or closes it, that sticks for the session).
967
+ - `debug: true` writes what was decided for each of the last few pages to the `xenia_debug` cookie. Render `<XeniaDebugPanel />` from `/client/react` behind the same flag: a round Xenia launcher in the corner opens a small white card, like Next.js's dev tools menu, with the status, page, source and location, the decision per content key (click a key for the journey path it took), and the visitor profile. `position` picks the corner (default bottom-right, away from Next.js's own dev tools), and `defaultOpen` opens the card on page load (default closed; once the visitor opens or closes it with the launcher, that sticks for the session — a click elsewhere on the page closes it for that page only). The card's pin button (or `keepOpen` to start pinned) keeps it open: clicks elsewhere and Escape leave it open, and only the Xenia launcher closes it.
904
968
  - Your own data: return `custom` from `debugInfo(info)` — `{ Moment: "thursday 12:00", Household: ["Emma: blue, shoes", "John: no taste"] }` — and the card shows each key as a row (a list one line per item). `renderExtra(page)` remains for anything else.
905
969
  - `qa: true` lets a tester place the visitor in a city (`?xn_geo=london`, from `QA_CITIES` or your own `qa: { cities }`), decide as of a day (`?xn_date=2026-12-24`), and forget both with `?xn_geo=off`; under `next dev`, `?xn_country=GB` too. The overrides last the visit.
906
970
 
@@ -384,6 +384,8 @@ interface EntryDecision {
384
384
  /** Array field replaced by `op: "items"`; absent = merge the replacement's array fields. */
385
385
  field?: string;
386
386
  ids?: string[];
387
+ /** Content type of `ids`, when known — CMSs like Contentstack can only fetch entries per type. */
388
+ contentType?: string;
387
389
  query?: EntryQuery;
388
390
  contentKey: string;
389
391
  bucket?: string;
@@ -785,6 +787,12 @@ interface XeniaMarker {
785
787
  * acts on markers that list its `contentKey`; a marker without keys is ignored.
786
788
  */
787
789
  keys?: string[];
790
+ /**
791
+ * Static replacements carried in the marker itself (CMSs without a reference field for them, e.g.
792
+ * Contentstack) and their content type. Fetched from the CMS in the round's fetch — no Xenia call.
793
+ */
794
+ ids?: string[];
795
+ ct?: string;
788
796
  }
789
797
  /** What the page resolver needs to know about a CMS's entries. `N` is the entry shape. */
790
798
  interface CmsAdapter<N extends object = Node> {
@@ -806,8 +814,11 @@ interface CmsAdapter<N extends object = Node> {
806
814
  clearMarker?(entry: N): void;
807
815
  /** Normalize a fetched payload before walking (e.g. resolve links). Defaults to identity. */
808
816
  prepare?(payload: unknown): unknown;
809
- /** Load entries by id in ONE call; order does not matter. */
810
- fetchEntries(ids: string[], locale?: string): Promise<N[]>;
817
+ /**
818
+ * Load entries by id in ONE round trip (parallel calls are fine); order does not matter. `types`
819
+ * maps an id to its content type when Xenia knows it — needed by CMSs that fetch per type.
820
+ */
821
+ fetchEntries(ids: string[], locale?: string, types?: Record<string, string>): Promise<N[]>;
811
822
  /** Run a content-type query in ONE call, in the query's order. */
812
823
  queryEntries(query: EntryQuery, locale?: string): Promise<N[]>;
813
824
  /**
@@ -384,6 +384,8 @@ interface EntryDecision {
384
384
  /** Array field replaced by `op: "items"`; absent = merge the replacement's array fields. */
385
385
  field?: string;
386
386
  ids?: string[];
387
+ /** Content type of `ids`, when known — CMSs like Contentstack can only fetch entries per type. */
388
+ contentType?: string;
387
389
  query?: EntryQuery;
388
390
  contentKey: string;
389
391
  bucket?: string;
@@ -785,6 +787,12 @@ interface XeniaMarker {
785
787
  * acts on markers that list its `contentKey`; a marker without keys is ignored.
786
788
  */
787
789
  keys?: string[];
790
+ /**
791
+ * Static replacements carried in the marker itself (CMSs without a reference field for them, e.g.
792
+ * Contentstack) and their content type. Fetched from the CMS in the round's fetch — no Xenia call.
793
+ */
794
+ ids?: string[];
795
+ ct?: string;
788
796
  }
789
797
  /** What the page resolver needs to know about a CMS's entries. `N` is the entry shape. */
790
798
  interface CmsAdapter<N extends object = Node> {
@@ -806,8 +814,11 @@ interface CmsAdapter<N extends object = Node> {
806
814
  clearMarker?(entry: N): void;
807
815
  /** Normalize a fetched payload before walking (e.g. resolve links). Defaults to identity. */
808
816
  prepare?(payload: unknown): unknown;
809
- /** Load entries by id in ONE call; order does not matter. */
810
- fetchEntries(ids: string[], locale?: string): Promise<N[]>;
817
+ /**
818
+ * Load entries by id in ONE round trip (parallel calls are fine); order does not matter. `types`
819
+ * maps an id to its content type when Xenia knows it — needed by CMSs that fetch per type.
820
+ */
821
+ fetchEntries(ids: string[], locale?: string, types?: Record<string, string>): Promise<N[]>;
811
822
  /** Run a content-type query in ONE call, in the query's order. */
812
823
  queryEntries(query: EntryQuery, locale?: string): Promise<N[]>;
813
824
  /**
@@ -1,4 +1,4 @@
1
- import { x as XeniaConfig, J as JsonValue, W as WebhookResponse, v as WidgetSubmitResponse, y as XeniaEvent, c as AnalyticsIngestResponse, I as IdentifyRequest, j as IdentifyResponse, d as ConsentState, p as ResolveOptions, q as ResolveResult, R as ResolveBatchItem, n as ResolveBatchResult, o as ResolveEntriesResult } from './types-DgVHS4Qw.cjs';
1
+ import { L as XeniaConfig, J as JsonValue, W as WebhookResponse, F as WidgetSubmitResponse, N as XeniaEvent, e as AnalyticsIngestResponse, I as IdentifyRequest, o as IdentifyResponse, h as ConsentState, w as ResolveOptions, x as ResolveResult, R as ResolveBatchItem, u as ResolveBatchResult, v as ResolveEntriesResult } from './personalize-content-D6xeBI7-.js';
2
2
 
3
3
  /**
4
4
  * Low-level, isomorphic Xenia API client. Wraps the raw endpoints; identity,
@@ -1,4 +1,4 @@
1
- import { x as XeniaConfig, J as JsonValue, W as WebhookResponse, v as WidgetSubmitResponse, y as XeniaEvent, c as AnalyticsIngestResponse, I as IdentifyRequest, j as IdentifyResponse, d as ConsentState, p as ResolveOptions, q as ResolveResult, R as ResolveBatchItem, n as ResolveBatchResult, o as ResolveEntriesResult } from './types-DgVHS4Qw.js';
1
+ import { L as XeniaConfig, J as JsonValue, W as WebhookResponse, F as WidgetSubmitResponse, N as XeniaEvent, e as AnalyticsIngestResponse, I as IdentifyRequest, o as IdentifyResponse, h as ConsentState, w as ResolveOptions, x as ResolveResult, R as ResolveBatchItem, u as ResolveBatchResult, v as ResolveEntriesResult } from './personalize-content-D6xeBI7-.cjs';
2
2
 
3
3
  /**
4
4
  * Low-level, isomorphic Xenia API client. Wraps the raw endpoints; identity,
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ // src/cms/marker.ts
4
+ var KINDS = /* @__PURE__ */ new Set(["static", "runtime"]);
5
+ var OPS = /* @__PURE__ */ new Set(["replace", "expand", "items"]);
6
+ var strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x !== "") : void 0;
7
+ function parseMarker(raw) {
8
+ let value = raw;
9
+ if (typeof value === "string") {
10
+ if (!value.trim()) return null;
11
+ try {
12
+ value = JSON.parse(value);
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ if (!value || typeof value !== "object") return null;
18
+ const m = value;
19
+ if (!KINDS.has(m.kind)) return null;
20
+ const ids = strings(m.ids);
21
+ return {
22
+ kind: m.kind,
23
+ op: OPS.has(m.op) ? m.op : "replace",
24
+ field: typeof m.field === "string" && m.field ? m.field : void 0,
25
+ limit: typeof m.limit === "number" ? m.limit : void 0,
26
+ keys: strings(m.keys),
27
+ ...ids?.length ? { ids } : {},
28
+ ...typeof m.ct === "string" && m.ct ? { ct: m.ct } : {}
29
+ };
30
+ }
31
+
32
+ exports.parseMarker = parseMarker;
33
+ //# sourceMappingURL=chunk-DPXINHQR.cjs.map
34
+ //# sourceMappingURL=chunk-DPXINHQR.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cms/marker.ts"],"names":[],"mappings":";;;AAEA,IAAM,wBAAQ,IAAI,GAAA,CAAI,CAAC,QAAA,EAAU,SAAS,CAAC,CAAA;AAC3C,IAAM,sBAAM,IAAI,GAAA,CAAI,CAAC,SAAA,EAAW,QAAA,EAAU,OAAO,CAAC,CAAA;AAElD,IAAM,UAAU,CAAC,CAAA,KACf,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA,GAAI,CAAA,CAAE,MAAA,CAAO,CAAC,MAAmB,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,KAAM,EAAE,CAAA,GAAI,MAAA;AAGhF,SAAS,YAAY,GAAA,EAAkC;AAC5D,EAAA,IAAI,KAAA,GAAQ,GAAA;AACZ,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,IAAI,CAAC,KAAA,CAAM,IAAA,EAAK,EAAG,OAAO,IAAA;AAC1B,IAAA,IAAI;AACF,MAAA,KAAA,GAAQ,IAAA,CAAK,MAAM,KAAK,CAAA;AAAA,IAC1B,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AAAA,EACF;AACA,EAAA,IAAI,CAAC,KAAA,IAAS,OAAO,KAAA,KAAU,UAAU,OAAO,IAAA;AAChD,EAAA,MAAM,CAAA,GAAI,KAAA;AACV,EAAA,IAAI,CAAC,KAAA,CAAM,GAAA,CAAI,CAAA,CAAE,IAAc,GAAG,OAAO,IAAA;AACzC,EAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,CAAA,CAAE,GAAG,CAAA;AACzB,EAAA,OAAO;AAAA,IACL,MAAM,CAAA,CAAE,IAAA;AAAA,IACR,IAAI,GAAA,CAAI,GAAA,CAAI,EAAE,EAAY,CAAA,GAAK,EAAE,EAAA,GAA2B,SAAA;AAAA,IAC5D,KAAA,EAAO,OAAO,CAAA,CAAE,KAAA,KAAU,YAAY,CAAA,CAAE,KAAA,GAAQ,EAAE,KAAA,GAAQ,MAAA;AAAA,IAC1D,OAAO,OAAO,CAAA,CAAE,KAAA,KAAU,QAAA,GAAW,EAAE,KAAA,GAAQ,MAAA;AAAA,IAC/C,IAAA,EAAM,OAAA,CAAQ,CAAA,CAAE,IAAI,CAAA;AAAA,IACpB,GAAI,GAAA,EAAK,MAAA,GAAS,EAAE,GAAA,KAAQ,EAAC;AAAA,IAC7B,GAAI,OAAO,CAAA,CAAE,EAAA,KAAO,QAAA,IAAY,CAAA,CAAE,EAAA,GAAK,EAAE,EAAA,EAAI,CAAA,CAAE,EAAA,EAAG,GAAI;AAAC,GACzD;AACF","file":"chunk-DPXINHQR.cjs","sourcesContent":["import type { XeniaMarker } from \"../core/personalize-content\";\n\nconst KINDS = new Set([\"static\", \"runtime\"]);\nconst OPS = new Set([\"replace\", \"expand\", \"items\"]);\n\nconst strings = (v: unknown): string[] | undefined =>\n Array.isArray(v) ? v.filter((x): x is string => typeof x === \"string\" && x !== \"\") : undefined;\n\n/** Parse a `xenia` marker field (object, or its JSON text). Unknown shapes → not personalized. */\nexport function parseMarker(raw: unknown): XeniaMarker | null {\n let value = raw;\n if (typeof value === \"string\") {\n if (!value.trim()) return null;\n try {\n value = JSON.parse(value);\n } catch {\n return null;\n }\n }\n if (!value || typeof value !== \"object\") return null;\n const m = value as Record<string, unknown>;\n if (!KINDS.has(m.kind as string)) return null;\n const ids = strings(m.ids);\n return {\n kind: m.kind as XeniaMarker[\"kind\"],\n op: OPS.has(m.op as string) ? (m.op as XeniaMarker[\"op\"]) : \"replace\",\n field: typeof m.field === \"string\" && m.field ? m.field : undefined,\n limit: typeof m.limit === \"number\" ? m.limit : undefined,\n keys: strings(m.keys),\n ...(ids?.length ? { ids } : {}),\n ...(typeof m.ct === \"string\" && m.ct ? { ct: m.ct } : {}),\n };\n}\n"]}