@xenia-martech/sdk-js 1.2.0-dev.2 → 1.2.0-dev.4

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 (82) hide show
  1. package/README.md +112 -3
  2. package/dist/{actions-BcdlokGO.d.cts → actions-C18Kv-4w.d.cts} +50 -2
  3. package/dist/{actions-BcdlokGO.d.ts → actions-C18Kv-4w.d.ts} +50 -2
  4. package/dist/{api-CktVQ6Gl.d.ts → api-0IycTYJR.d.cts} +4 -2
  5. package/dist/{api-BEPW0Uan.d.cts → api-COXWLL5L.d.ts} +4 -2
  6. package/dist/{chunk-3PYPPIUM.cjs → chunk-2DR5AVPX.cjs} +11 -46
  7. package/dist/chunk-2DR5AVPX.cjs.map +1 -0
  8. package/dist/chunk-BQ5PJCZ7.js +179 -0
  9. package/dist/chunk-BQ5PJCZ7.js.map +1 -0
  10. package/dist/{chunk-DWFVDNU5.js → chunk-GWRDZIVA.js} +5 -37
  11. package/dist/chunk-GWRDZIVA.js.map +1 -0
  12. package/dist/{chunk-7NGJC7CF.js → chunk-LW5AEJ6K.js} +20 -183
  13. package/dist/chunk-LW5AEJ6K.js.map +1 -0
  14. package/dist/chunk-N5M4PI7C.cjs +192 -0
  15. package/dist/chunk-N5M4PI7C.cjs.map +1 -0
  16. package/dist/chunk-OMMIRSPK.js +336 -0
  17. package/dist/chunk-OMMIRSPK.js.map +1 -0
  18. package/dist/chunk-QEYONKWY.js +42 -0
  19. package/dist/chunk-QEYONKWY.js.map +1 -0
  20. package/dist/{chunk-IBNIRT7J.cjs → chunk-RWE2OCQL.cjs} +20 -194
  21. package/dist/chunk-RWE2OCQL.cjs.map +1 -0
  22. package/dist/chunk-WHGK55JP.cjs +46 -0
  23. package/dist/chunk-WHGK55JP.cjs.map +1 -0
  24. package/dist/chunk-YLMMTKQL.cjs +358 -0
  25. package/dist/chunk-YLMMTKQL.cjs.map +1 -0
  26. package/dist/client/index.cjs +320 -31
  27. package/dist/client/index.cjs.map +1 -1
  28. package/dist/client/index.d.cts +15 -4
  29. package/dist/client/index.d.ts +15 -4
  30. package/dist/client/index.js +320 -31
  31. package/dist/client/index.js.map +1 -1
  32. package/dist/client/react/index.cjs +731 -34
  33. package/dist/client/react/index.cjs.map +1 -1
  34. package/dist/client/react/index.d.cts +32 -3
  35. package/dist/client/react/index.d.ts +32 -3
  36. package/dist/client/react/index.js +732 -37
  37. package/dist/client/react/index.js.map +1 -1
  38. package/dist/cms/contentful.cjs +2 -1
  39. package/dist/cms/contentful.cjs.map +1 -1
  40. package/dist/cms/contentful.d.cts +2 -1
  41. package/dist/cms/contentful.d.ts +2 -1
  42. package/dist/cms/contentful.js +2 -1
  43. package/dist/cms/contentful.js.map +1 -1
  44. package/dist/consent-Cemn91j3.d.ts +248 -0
  45. package/dist/consent-DgWdjsSy.d.cts +248 -0
  46. package/dist/{cookie-names-CLqtmGBM.d.ts → cookie-names-BvUaW3uo.d.ts} +1 -1
  47. package/dist/{cookie-names-BACctagz.d.cts → cookie-names-CnL65Oly.d.cts} +1 -1
  48. package/dist/edge/index.cjs +280 -0
  49. package/dist/edge/index.cjs.map +1 -0
  50. package/dist/edge/index.d.cts +61 -0
  51. package/dist/edge/index.d.ts +61 -0
  52. package/dist/edge/index.js +191 -0
  53. package/dist/edge/index.js.map +1 -0
  54. package/dist/index.cjs +41 -40
  55. package/dist/index.d.cts +5 -4
  56. package/dist/index.d.ts +5 -4
  57. package/dist/index.js +2 -1
  58. package/dist/next/index.cjs +320 -15
  59. package/dist/next/index.cjs.map +1 -1
  60. package/dist/next/index.d.cts +180 -6
  61. package/dist/next/index.d.ts +180 -6
  62. package/dist/next/index.js +309 -9
  63. package/dist/next/index.js.map +1 -1
  64. package/dist/personalize-content-c2DDAV6g.d.ts +140 -0
  65. package/dist/personalize-content-ghZPKlf1.d.cts +140 -0
  66. package/dist/server/index.cjs +34 -32
  67. package/dist/server/index.cjs.map +1 -1
  68. package/dist/server/index.d.cts +6 -5
  69. package/dist/server/index.d.ts +6 -5
  70. package/dist/server/index.js +5 -3
  71. package/dist/server/index.js.map +1 -1
  72. package/dist/{personalize-content-Clc1oMYY.d.ts → types-DgVHS4Qw.d.cts} +7 -127
  73. package/dist/{personalize-content-Clc1oMYY.d.cts → types-DgVHS4Qw.d.ts} +7 -127
  74. package/dist/xenia-sdk.iife.js +3 -3
  75. package/dist/xenia-sdk.iife.js.map +1 -1
  76. package/dist/{xenia-server-o_wDOcei.d.cts → xenia-server-C3O-cWQT.d.cts} +3 -2
  77. package/dist/{xenia-server-BWhWD0La.d.ts → xenia-server-DV8Sjmoo.d.ts} +3 -2
  78. package/package.json +14 -1
  79. package/dist/chunk-3PYPPIUM.cjs.map +0 -1
  80. package/dist/chunk-7NGJC7CF.js.map +0 -1
  81. package/dist/chunk-DWFVDNU5.js.map +0 -1
  82. package/dist/chunk-IBNIRT7J.cjs.map +0 -1
@@ -0,0 +1,248 @@
1
+ import { P as PersonalizationSwap } from './types-DgVHS4Qw.cjs';
2
+
3
+ /** Wire types of Xenia's `/api/personalization/edge` endpoints (see the backend's personalization-edge-api.md). */
4
+
5
+ /** `edge`: decidable from request data alone; `server`: needs a lookup, always decided by `/decide`. */
6
+ type EdgeMode = "edge" | "server" | "off";
7
+ interface ManifestKey {
8
+ contentKey: string;
9
+ mode: EdgeMode;
10
+ pages: string[];
11
+ }
12
+ /** Which content keys target which pages — the proxy's "call Xenia?" switch. */
13
+ interface Manifest {
14
+ version: string;
15
+ generatedAt: string;
16
+ keys: ManifestKey[];
17
+ }
18
+ /** A storefront's own, public copy of the manifest: just the targeted page patterns. */
19
+ interface PublicManifest {
20
+ version: string;
21
+ pages: string[];
22
+ }
23
+ /** What `/decide` decided for one content key. */
24
+ interface EdgeDecision {
25
+ contentKey: string;
26
+ mode: EdgeMode;
27
+ personalized: boolean;
28
+ /** `personalized` | `control`; set when Xenia recorded an exposure. */
29
+ bucket: string | null;
30
+ variant: string | null;
31
+ baseEntryId: string | null;
32
+ swaps: PersonalizationSwap[];
33
+ trace: Record<string, string>;
34
+ reason: string | null;
35
+ }
36
+ /** The visitor profile Xenia decided with (`profile.*` journey facts), when profiles are on. */
37
+ type ProfileFacts = Record<string, unknown>;
38
+ interface DecideResult {
39
+ version: string;
40
+ decisions: EdgeDecision[];
41
+ profileVersion?: number;
42
+ profile?: ProfileFacts | null;
43
+ }
44
+ /**
45
+ * A page the visitor viewed: path, when (epoch ms) and, once they've left it, the seconds spent
46
+ * there. Sent with `/decide` so profile rules see views whose analytics events haven't landed yet.
47
+ */
48
+ interface TrailView {
49
+ path: string;
50
+ at: number;
51
+ seconds?: number;
52
+ }
53
+ /** Request data journey rules can read (`utmCampaign`, `device`, `query.ref`, `geo.country`, …). */
54
+ type EdgeFacts = Record<string, string | number | boolean | Record<string, string>>;
55
+ /** Where a decision came from — surfaced in `Server-Timing`. */
56
+ type DecisionSource = "cache" | "xenia" | "timeout" | "error" | "no-manifest";
57
+ /** The visitor's location in Xenia's `ViewerGeo` shape — the only keys `/decide` accepts in `facts.geo`. */
58
+ type ViewerGeo = Partial<Record<"country" | "countryName" | "region" | "regionName" | "city" | "postalCode" | "timeZone" | "latitude" | "longitude" | "source", string>>;
59
+
60
+ /** What a visitor's cached decision holds: the decisions and the profile they were made with. */
61
+ type CachedDecision = {
62
+ decisions: EdgeDecision[];
63
+ profile?: ProfileFacts | null;
64
+ };
65
+ type DecisionCache = {
66
+ get(key: string): CachedDecision | undefined;
67
+ set(key: string, value: CachedDecision, ttlMs: number): void;
68
+ };
69
+ type XeniaEdgeConfig = {
70
+ /** Xenia engine base URL, e.g. `https://engine.xeniamartech.com/xenia`. */
71
+ baseUrl: string;
72
+ /** A server-type SDK token. Never expose it to the browser. */
73
+ token: string;
74
+ /** Hard deadline for one `/decide` call. Default 150 ms. */
75
+ budgetMs?: number;
76
+ /**
77
+ * Where to read the manifest. Default: Xenia's `/edge/manifest` (with the token). A storefront
78
+ * can serve its own CDN-cached copy instead (e.g. a Next.js ISR route) — then no token is sent.
79
+ */
80
+ manifestUrl?: string;
81
+ /**
82
+ * A manifest baked in at build time. An instance starts with it in memory, so a cold start never
83
+ * waits: the first request uses it and triggers a background refresh. Empty version = none.
84
+ */
85
+ initialManifest?: Manifest | PublicManifest;
86
+ /** How long a cold start waits for the first manifest (only without `initialManifest`). Default 300 ms. */
87
+ manifestWaitMs?: number;
88
+ /** Manifest refresh interval. Default 30 s. */
89
+ manifestTtlMs?: number;
90
+ /** How long a visitor keeps a decision. Default 30 min (a session). */
91
+ decisionTtlMs?: number;
92
+ /**
93
+ * How long a provisional decision is kept (see `provisional`): Jev's answer, or the visitor's first
94
+ * signals, usually land seconds later, so the next view should ask again. Default 15 s.
95
+ */
96
+ provisionalDecisionTtlMs?: number;
97
+ cache?: DecisionCache;
98
+ fetch?: typeof fetch;
99
+ onError?: (error: unknown, context: string) => void;
100
+ };
101
+ type DecideInput = {
102
+ host: string | null;
103
+ path: string;
104
+ visitorId: string;
105
+ facts: EdgeFacts;
106
+ /** Facts that identify this visitor's context for caching (e.g. landing UTM, device). */
107
+ cacheFacts?: EdgeFacts;
108
+ /** The visitor's recent page views, most recent first (see `TrailView`). */
109
+ trail?: TrailView[];
110
+ /** This call's time budget, instead of the client's (a decision that calls a back office). */
111
+ budgetMs?: number;
112
+ /** How long to keep this call's answer, from what it said, instead of the client's. */
113
+ ttlMs?: (answer: CachedDecision) => number;
114
+ };
115
+ type DecideOutcome = {
116
+ decisions: EdgeDecision[];
117
+ /** The profile Xenia decided with (also on cache hits), when profiles are on. */
118
+ profile?: ProfileFacts | null;
119
+ source: DecisionSource;
120
+ /** Time spent in Xenia, 0 on a cache hit. */
121
+ ms: number;
122
+ };
123
+ /** Bounded in-memory LRU with per-entry expiry. */
124
+ declare function memoryDecisionCache(maxEntries?: number): DecisionCache;
125
+ declare function createXeniaEdge(config: XeniaEdgeConfig): {
126
+ match: (host: string | null, path: string) => Promise<ManifestKey[] | null>;
127
+ decide: (keys: ManifestKey[], input: DecideInput) => Promise<DecideOutcome>;
128
+ refreshManifest: () => Promise<void>;
129
+ };
130
+ type XeniaEdge = ReturnType<typeof createXeniaEdge>;
131
+
132
+ /**
133
+ * The proxy hands a decision to the page as a signed token in the rewritten URL
134
+ * (`/xv/<token>/women`). The token is just the swaps — identical for every visitor in the same
135
+ * variant — so each variant renders once and is then served from the ISR/CDN cache like any static
136
+ * page. A site may carry its own data along (`extra`), e.g. what the page fills itself from.
137
+ *
138
+ * Signed (HMAC-SHA256) so nobody can craft a URL that renders arbitrary entries. Web Crypto only, so
139
+ * it runs in any runtime.
140
+ */
141
+
142
+ /**
143
+ * One swap: the base entry's field, the entry shown in its place, and the content key whose
144
+ * decision it is — so the page can report impressions and clicks per journey and entry.
145
+ */
146
+ type VariantSwap = [identifier: string, replacementEntryId: string, contentKey?: string];
147
+ /** Swaps for one base entry. */
148
+ interface SwapGroup {
149
+ base: string;
150
+ swaps: VariantSwap[];
151
+ }
152
+ /** The swap groups of the personalized decisions, in a stable order (stable cache keys). */
153
+ declare function swapGroups(decisions: EdgeDecision[]): SwapGroup[];
154
+ /**
155
+ * The token for a variant. An `extra` that would push it past the payload limit is left out — the
156
+ * page still renders its swaps — rather than making a token no page accepts.
157
+ */
158
+ declare function encodeVariant(groups: SwapGroup[], secret: string, extra?: unknown): Promise<string>;
159
+ /** The variant in a token, or `null` if it is malformed or not signed with `secret`. */
160
+ declare function decodeVariant<E = unknown>(token: string, secret: string): Promise<{
161
+ groups: SwapGroup[];
162
+ extra?: E;
163
+ } | null>;
164
+
165
+ /**
166
+ * Facts about a request that journey rules read — where the visitor is, on what device, where they
167
+ * came from, their local time — derived from a standard `Request` in any runtime.
168
+ */
169
+
170
+ /** The location headers `viewerGeo` can read, when trusted. */
171
+ type GeoHeader = "x-xn-geo" | "x-nf-geo" | "x-country" | "x-vercel-ip-country" | "cloudfront-viewer-country" | "cf-ipcountry";
172
+ /**
173
+ * The visitor's location as the hosting edge resolved it, and which source supplied it (`via`).
174
+ * Geo must come from the storefront's edge, never Xenia's: `/decide` is server to server. Tried in
175
+ * order — whichever the host provides:
176
+ * 1. Netlify's per-request `Netlify.context.geo` (can't be spoofed by the client, unlike a header)
177
+ * 2. `x-xn-geo` (URI-encoded JSON) from the site's own edge function
178
+ * 3. a `request.geo` some runtimes attach
179
+ * 4. Netlify's `x-nf-geo` header (base64 or URI-encoded JSON)
180
+ * 5. `x-country` (Netlify), `x-vercel-ip-*`, `cloudfront-viewer-*`, `cf-ipcountry`
181
+ * 6. `?xn_country=GB` — only with `devOverride`, for testing geo rules without an edge
182
+ *
183
+ * Headers (2, 4 and 5) are read only when named in `trust`: a visitor can send any header, and only
184
+ * the site knows which ones its own host or edge function sets (and overwrites). Name exactly those,
185
+ * e.g. `["x-vercel-ip-country"]` on Vercel, `["cf-ipcountry"]` behind Cloudflare,
186
+ * `["x-nf-geo", "x-country"]` on Netlify. 1 and 3 come from the runtime and are always used.
187
+ */
188
+ declare function viewerGeo(request: Request, opts?: {
189
+ devOverride?: boolean;
190
+ trust?: readonly GeoHeader[];
191
+ }): {
192
+ geo?: ViewerGeo;
193
+ via: string;
194
+ };
195
+ declare function deviceOf(headers: Headers): "mobile" | "tablet" | "desktop";
196
+ /** Crawlers and link-preview fetchers: never decided, never recorded. */
197
+ declare function isBot(userAgent: string | null | undefined): boolean;
198
+ /** Link prefetches pass through a proxy too; they aren't page views. */
199
+ declare function isPrefetch(headers: Headers): boolean;
200
+ /**
201
+ * Whether this is the browser loading a page (a full page load), by `Sec-Fetch-Dest`. Requests
202
+ * without it (old browsers, curl) count as page loads.
203
+ */
204
+ declare function isPageLoad(headers: Headers): boolean;
205
+ /**
206
+ * This session's landing context — first-touch attribution, so a campaign carries across pages:
207
+ * the UTMs (≤200 chars each) and an external referrer (≤500), refreshed when the visitor arrives
208
+ * from a new campaign. `saved` is the value kept from earlier (a cookie); `changed` says to save it.
209
+ */
210
+ declare function landingContext(url: URL, referrer: string | null | undefined, saved: string | undefined): {
211
+ ctx: Record<string, string>;
212
+ changed: boolean;
213
+ };
214
+ /** Meteorological seasons: Dec–Feb winter, Mar–May spring, Jun–Aug summer, Sep–Nov fall. */
215
+ declare function seasonOf(month: number, southernHemisphere?: boolean): string;
216
+ interface LocalTime {
217
+ local_weekday: string;
218
+ local_month: number;
219
+ local_hour: number;
220
+ is_weekend: boolean;
221
+ season: string;
222
+ }
223
+ /** Weekday, month, hour and season where the visitor is (their time zone; UTC when unknown). */
224
+ declare function localTime(geo: ViewerGeo | undefined, now: Date): LocalTime;
225
+
226
+ interface EdgeConsent {
227
+ /** Off: don't ask Xenia, serve base content. */
228
+ personalization: boolean;
229
+ /** Off: decide, but keep no trail or click cookies and send no trail. */
230
+ analytics: boolean;
231
+ }
232
+ /**
233
+ * What the edge flow may do for this request, from the consent cookie the browser client's
234
+ * `consent()` writes and the visitor's privacy signals:
235
+ * - no consent cookie (or an unreadable one) → no choice made: everything on, as before;
236
+ * - an explicit choice → followed (JSON `{analytics, personalization}`, or the scopes form
237
+ * `analytics,personalization` where an unlisted scope is refused);
238
+ * - `Sec-GPC: 1` / `DNT: 1` → analytics off, like the browser client's `respectDnt` (default on).
239
+ *
240
+ * Unlike `getConsent()`, an absent cookie isn't "all denied": the edge can't tell a visitor who
241
+ * refused from one who was never asked, and refusing both would change every first page view.
242
+ */
243
+ declare function edgeConsent(headers: Headers, opts?: {
244
+ prefix?: string;
245
+ respectDnt?: boolean;
246
+ }): EdgeConsent;
247
+
248
+ export { type CachedDecision as C, type DecideInput as D, type EdgeConsent as E, type GeoHeader as G, type LocalTime as L, type Manifest as M, type PublicManifest as P, type SwapGroup as S, type TrailView as T, type VariantSwap as V, type XeniaEdge as X, type DecideOutcome as a, type DecideResult as b, type DecisionCache as c, type DecisionSource as d, type EdgeDecision as e, type EdgeFacts as f, type EdgeMode as g, type ManifestKey as h, type ProfileFacts as i, type ViewerGeo as j, type XeniaEdgeConfig as k, createXeniaEdge as l, decodeVariant as m, deviceOf as n, edgeConsent as o, encodeVariant as p, isBot as q, isPageLoad as r, isPrefetch as s, landingContext as t, localTime as u, memoryDecisionCache as v, seasonOf as w, swapGroups as x, viewerGeo as y };
@@ -1,4 +1,4 @@
1
- import { y as RetryConfig, e as CircuitBreakerConfig, H as HttpMethod, J as JsonValue, K as XeniaAction, f as ConsentState } from './personalize-content-Clc1oMYY.js';
1
+ import { r as RetryConfig, C as CircuitBreakerConfig, H as HttpMethod, J as JsonValue, X as XeniaAction, d as ConsentState } from './types-DgVHS4Qw.js';
2
2
 
3
3
  /** Returns true when running in a browser environment (SSR-safe). */
4
4
  declare function isBrowser(): boolean;
@@ -1,4 +1,4 @@
1
- import { y as RetryConfig, e as CircuitBreakerConfig, H as HttpMethod, J as JsonValue, K as XeniaAction, f as ConsentState } from './personalize-content-Clc1oMYY.cjs';
1
+ import { r as RetryConfig, C as CircuitBreakerConfig, H as HttpMethod, J as JsonValue, X as XeniaAction, d as ConsentState } from './types-DgVHS4Qw.cjs';
2
2
 
3
3
  /** Returns true when running in a browser environment (SSR-safe). */
4
4
  declare function isBrowser(): boolean;
@@ -0,0 +1,280 @@
1
+ 'use strict';
2
+
3
+ var chunkYLMMTKQL_cjs = require('../chunk-YLMMTKQL.cjs');
4
+ require('../chunk-WHGK55JP.cjs');
5
+ require('../chunk-N5M4PI7C.cjs');
6
+
7
+ // src/edge/matcher.ts
8
+ function normalizePath(path) {
9
+ if (!path || !path.trim()) return "/";
10
+ let p = path.trim().toLowerCase();
11
+ const cut = Math.min(
12
+ ...["?", "#"].map((c) => p.includes(c) ? p.indexOf(c) : p.length)
13
+ );
14
+ p = p.slice(0, cut);
15
+ if (!p.startsWith("/")) p = "/" + p;
16
+ while (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
17
+ return p;
18
+ }
19
+ var normalizeHost = (host) => (host ?? "").trim().toLowerCase().replace(/:\d+$/, "");
20
+ function matches(pattern, host, path) {
21
+ if (!pattern) return false;
22
+ let patternPath = pattern;
23
+ if (!pattern.startsWith("/")) {
24
+ const slash = pattern.indexOf("/");
25
+ const patternHost = slash < 0 ? pattern : pattern.slice(0, slash);
26
+ if (patternHost !== host) return false;
27
+ patternPath = slash < 0 ? "/" : pattern.slice(slash);
28
+ }
29
+ if (patternPath === "/*") return true;
30
+ if (patternPath.endsWith("/*")) {
31
+ const prefix = patternPath.slice(0, -2);
32
+ return path === prefix || path.startsWith(prefix + "/");
33
+ }
34
+ return path === patternPath;
35
+ }
36
+ function matchesAny(patterns, host, path) {
37
+ if (!patterns?.length) return false;
38
+ const h = normalizeHost(host);
39
+ const p = normalizePath(path);
40
+ return patterns.some((pattern) => matches(pattern, h, p));
41
+ }
42
+
43
+ // src/edge/client.ts
44
+ function memoryDecisionCache(maxEntries = 1e4) {
45
+ const entries = /* @__PURE__ */ new Map();
46
+ return {
47
+ get(key) {
48
+ const hit = entries.get(key);
49
+ if (!hit) return void 0;
50
+ entries.delete(key);
51
+ if (hit.expires < Date.now()) return void 0;
52
+ entries.set(key, hit);
53
+ return hit.value;
54
+ },
55
+ set(key, value, ttlMs) {
56
+ entries.delete(key);
57
+ entries.set(key, { value, expires: Date.now() + ttlMs });
58
+ while (entries.size > maxEntries) {
59
+ entries.delete(entries.keys().next().value);
60
+ }
61
+ }
62
+ };
63
+ }
64
+ var stableJson = (facts) => JSON.stringify(
65
+ Object.keys(facts ?? {}).sort().map((k) => [k, facts[k]])
66
+ );
67
+ function normalizeManifest(m) {
68
+ if ("keys" in m && Array.isArray(m.keys)) return m;
69
+ const pages = m.pages ?? [];
70
+ return {
71
+ version: m.version,
72
+ generatedAt: "",
73
+ keys: pages.map((p) => ({ contentKey: "", mode: "server", pages: [p] }))
74
+ };
75
+ }
76
+ function provisional(profile) {
77
+ const answers = profile?.answers;
78
+ if (!answers || typeof answers !== "object") return false;
79
+ const steps = Object.values(answers);
80
+ return steps.some((a) => a?.source === "signals" && a.jev === true) || steps.length > 0 && steps.every((a) => !a?.choice);
81
+ }
82
+ function createXeniaEdge(config) {
83
+ const baseUrl = config.baseUrl.replace(/\/+$/, "");
84
+ const budgetMs = config.budgetMs ?? 150;
85
+ const manifestWaitMs = config.manifestWaitMs ?? 300;
86
+ const manifestTtlMs = config.manifestTtlMs ?? 3e4;
87
+ const decisionTtlMs = config.decisionTtlMs ?? 30 * 6e4;
88
+ const provisionalDecisionTtlMs = config.provisionalDecisionTtlMs ?? 15e3;
89
+ const cache = config.cache ?? memoryDecisionCache();
90
+ const doFetch = config.fetch ?? fetch;
91
+ const onError = config.onError ?? (() => {
92
+ });
93
+ const auth = { Authorization: `Bearer ${config.token}` };
94
+ let manifest = config.initialManifest?.version ? normalizeManifest(config.initialManifest) : null;
95
+ let manifestAt = 0;
96
+ let refreshing = null;
97
+ const inflight = /* @__PURE__ */ new Map();
98
+ function refreshManifest() {
99
+ refreshing ??= (async () => {
100
+ try {
101
+ const res = await doFetch(config.manifestUrl ?? `${baseUrl}/api/personalization/edge/manifest`, {
102
+ headers: config.manifestUrl ? {} : { ...auth, ...manifest ? { "If-None-Match": `"${manifest.version}"` } : {} },
103
+ signal: AbortSignal.timeout(5e3),
104
+ cache: "no-store"
105
+ });
106
+ if (res.status === 304) {
107
+ manifestAt = Date.now();
108
+ } else if (res.ok) {
109
+ manifest = normalizeManifest(await res.json());
110
+ manifestAt = Date.now();
111
+ } else {
112
+ onError(new Error(`manifest ${res.status}`), "manifest");
113
+ manifestAt = Date.now();
114
+ }
115
+ } catch (e) {
116
+ onError(e, "manifest");
117
+ manifestAt = Date.now();
118
+ } finally {
119
+ refreshing = null;
120
+ }
121
+ })();
122
+ return refreshing;
123
+ }
124
+ async function currentManifest() {
125
+ if (Date.now() - manifestAt > manifestTtlMs) {
126
+ const pending = refreshManifest();
127
+ if (!manifest) {
128
+ await Promise.race([pending, new Promise((r) => setTimeout(r, manifestWaitMs))]);
129
+ }
130
+ }
131
+ return manifest;
132
+ }
133
+ async function match(host, path) {
134
+ const m = await currentManifest();
135
+ if (!m) return null;
136
+ return m.keys.filter((k) => k.mode !== "off" && matchesAny(k.pages, host, path));
137
+ }
138
+ async function decide(keys, input) {
139
+ const version = manifest?.version ?? "";
140
+ const contentKeys = keys.map((k) => k.contentKey).filter(Boolean).sort();
141
+ const scope = contentKeys.length ? contentKeys.join(",") : keys.flatMap((k) => k.pages).sort().join(",");
142
+ const cacheKey = [version, input.visitorId, scope, stableJson(input.cacheFacts)].join("|");
143
+ const cached = cache.get(cacheKey);
144
+ if (cached) return { decisions: cached.decisions, profile: cached.profile, source: "cache", ms: 0 };
145
+ const pending = inflight.get(cacheKey);
146
+ if (pending) return pending;
147
+ const call = (async () => {
148
+ const started = Date.now();
149
+ try {
150
+ const res = await doFetch(`${baseUrl}/api/personalization/edge/decide`, {
151
+ method: "POST",
152
+ headers: { ...auth, "Content-Type": "application/json" },
153
+ body: JSON.stringify({
154
+ contentKeys,
155
+ host: input.host,
156
+ path: input.path,
157
+ visitorId: input.visitorId,
158
+ facts: input.facts,
159
+ ...input.trail?.length ? { trail: input.trail } : {}
160
+ }),
161
+ signal: AbortSignal.timeout(input.budgetMs ?? budgetMs),
162
+ cache: "no-store"
163
+ });
164
+ if (!res.ok) {
165
+ onError(new Error(`decide ${res.status}`), "decide");
166
+ return { decisions: [], source: "error", ms: Date.now() - started };
167
+ }
168
+ const result = await res.json();
169
+ if (!Array.isArray(result?.decisions)) throw new Error("decide: reply has no decisions list");
170
+ const decisions = result.decisions.filter((d) => d && Array.isArray(d.swaps));
171
+ const answer = { decisions, profile: result.profile };
172
+ cache.set(cacheKey, answer, input.ttlMs?.(answer) ?? (provisional(result.profile) ? provisionalDecisionTtlMs : decisionTtlMs));
173
+ if (manifest && result.version && result.version !== manifest.version) {
174
+ void refreshManifest();
175
+ }
176
+ return { decisions, profile: result.profile, source: "xenia", ms: Date.now() - started };
177
+ } catch (e) {
178
+ const timedOut = e instanceof DOMException && e.name === "TimeoutError";
179
+ if (!timedOut) onError(e, "decide");
180
+ return { decisions: [], source: timedOut ? "timeout" : "error", ms: Date.now() - started };
181
+ } finally {
182
+ inflight.delete(cacheKey);
183
+ }
184
+ })();
185
+ inflight.set(cacheKey, call);
186
+ return call;
187
+ }
188
+ return { match, decide, refreshManifest };
189
+ }
190
+
191
+ Object.defineProperty(exports, "MAX_TRAIL", {
192
+ enumerable: true,
193
+ get: function () { return chunkYLMMTKQL_cjs.MAX_TRAIL; }
194
+ });
195
+ Object.defineProperty(exports, "TRAIL_MAX_AGE_S", {
196
+ enumerable: true,
197
+ get: function () { return chunkYLMMTKQL_cjs.TRAIL_MAX_AGE_S; }
198
+ });
199
+ Object.defineProperty(exports, "decodeVariant", {
200
+ enumerable: true,
201
+ get: function () { return chunkYLMMTKQL_cjs.decodeVariant; }
202
+ });
203
+ Object.defineProperty(exports, "deviceOf", {
204
+ enumerable: true,
205
+ get: function () { return chunkYLMMTKQL_cjs.deviceOf; }
206
+ });
207
+ Object.defineProperty(exports, "edgeConsent", {
208
+ enumerable: true,
209
+ get: function () { return chunkYLMMTKQL_cjs.edgeConsent; }
210
+ });
211
+ Object.defineProperty(exports, "encodeVariant", {
212
+ enumerable: true,
213
+ get: function () { return chunkYLMMTKQL_cjs.encodeVariant; }
214
+ });
215
+ Object.defineProperty(exports, "fetchManifest", {
216
+ enumerable: true,
217
+ get: function () { return chunkYLMMTKQL_cjs.fetchManifest; }
218
+ });
219
+ Object.defineProperty(exports, "isBot", {
220
+ enumerable: true,
221
+ get: function () { return chunkYLMMTKQL_cjs.isBot; }
222
+ });
223
+ Object.defineProperty(exports, "isPageLoad", {
224
+ enumerable: true,
225
+ get: function () { return chunkYLMMTKQL_cjs.isPageLoad; }
226
+ });
227
+ Object.defineProperty(exports, "isPrefetch", {
228
+ enumerable: true,
229
+ get: function () { return chunkYLMMTKQL_cjs.isPrefetch; }
230
+ });
231
+ Object.defineProperty(exports, "landingContext", {
232
+ enumerable: true,
233
+ get: function () { return chunkYLMMTKQL_cjs.landingContext; }
234
+ });
235
+ Object.defineProperty(exports, "localTime", {
236
+ enumerable: true,
237
+ get: function () { return chunkYLMMTKQL_cjs.localTime; }
238
+ });
239
+ Object.defineProperty(exports, "readTrail", {
240
+ enumerable: true,
241
+ get: function () { return chunkYLMMTKQL_cjs.readTrail; }
242
+ });
243
+ Object.defineProperty(exports, "seasonOf", {
244
+ enumerable: true,
245
+ get: function () { return chunkYLMMTKQL_cjs.seasonOf; }
246
+ });
247
+ Object.defineProperty(exports, "swapGroups", {
248
+ enumerable: true,
249
+ get: function () { return chunkYLMMTKQL_cjs.swapGroups; }
250
+ });
251
+ Object.defineProperty(exports, "trailCookieName", {
252
+ enumerable: true,
253
+ get: function () { return chunkYLMMTKQL_cjs.trailCookieName; }
254
+ });
255
+ Object.defineProperty(exports, "trailCookieValue", {
256
+ enumerable: true,
257
+ get: function () { return chunkYLMMTKQL_cjs.trailCookieValue; }
258
+ });
259
+ Object.defineProperty(exports, "trimManifest", {
260
+ enumerable: true,
261
+ get: function () { return chunkYLMMTKQL_cjs.trimManifest; }
262
+ });
263
+ Object.defineProperty(exports, "verifyWebhook", {
264
+ enumerable: true,
265
+ get: function () { return chunkYLMMTKQL_cjs.verifyWebhook; }
266
+ });
267
+ Object.defineProperty(exports, "viewerGeo", {
268
+ enumerable: true,
269
+ get: function () { return chunkYLMMTKQL_cjs.viewerGeo; }
270
+ });
271
+ Object.defineProperty(exports, "withView", {
272
+ enumerable: true,
273
+ get: function () { return chunkYLMMTKQL_cjs.withView; }
274
+ });
275
+ exports.createXeniaEdge = createXeniaEdge;
276
+ exports.matchesAny = matchesAny;
277
+ exports.memoryDecisionCache = memoryDecisionCache;
278
+ exports.normalizePath = normalizePath;
279
+ //# sourceMappingURL=index.cjs.map
280
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/edge/matcher.ts","../../src/edge/client.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Page matching for the Xenia manifest — a port of the backend's\n * `EdgePageMatcher`, which is the reference:\n *\n * - `/women` exactly that path (case, trailing slash, query ignored)\n * - `/women/*` `/women` and everything below it, not `/womenswear`\n * - `/*` every path\n * - `uk.example.com/women` that path on that host only; hostless patterns match any host\n */\n\nexport function normalizePath(path: string | null | undefined): string {\n if (!path || !path.trim()) return \"/\";\n let p = path.trim().toLowerCase();\n const cut = Math.min(\n ...[\"?\", \"#\"].map((c) => (p.includes(c) ? p.indexOf(c) : p.length)),\n );\n p = p.slice(0, cut);\n if (!p.startsWith(\"/\")) p = \"/\" + p;\n while (p.length > 1 && p.endsWith(\"/\")) p = p.slice(0, -1);\n return p;\n}\n\nconst normalizeHost = (host: string | null | undefined) =>\n (host ?? \"\").trim().toLowerCase().replace(/:\\d+$/, \"\");\n\nfunction matches(pattern: string, host: string, path: string): boolean {\n if (!pattern) return false;\n let patternPath = pattern;\n if (!pattern.startsWith(\"/\")) {\n const slash = pattern.indexOf(\"/\");\n const patternHost = slash < 0 ? pattern : pattern.slice(0, slash);\n if (patternHost !== host) return false;\n patternPath = slash < 0 ? \"/\" : pattern.slice(slash);\n }\n if (patternPath === \"/*\") return true;\n if (patternPath.endsWith(\"/*\")) {\n const prefix = patternPath.slice(0, -2);\n return path === prefix || path.startsWith(prefix + \"/\");\n }\n return path === patternPath;\n}\n\nexport function matchesAny(\n patterns: readonly string[] | null | undefined,\n host: string | null | undefined,\n path: string | null | undefined,\n): boolean {\n if (!patterns?.length) return false;\n const h = normalizeHost(host);\n const p = normalizePath(path);\n return patterns.some((pattern) => matches(pattern, h, p));\n}\n","/**\n * Proxy-side Xenia client for the hybrid personalization flow:\n *\n * 1. `match()` — synchronous, in-memory: does any journey target this page?\n * Answered from the manifest, refreshed in the background every\n * `manifestTtlMs`. Pages no journey targets never call Xenia.\n * 2. `decide()` — for targeted pages only: a per-visitor decision cache hit\n * costs nothing; a miss calls Xenia's `/decide` with a hard `budgetMs`\n * deadline. Timeouts and errors resolve to \"no decision\" (base content),\n * never an exception — Xenia can't slow down or break a page.\n *\n * Runtime-agnostic (only `fetch`, `AbortSignal`, `Map`). The decision cache is\n * per process; swap in `cache` for a shared store (Redis, edge KV).\n */\nimport { matchesAny } from \"./matcher\";\nimport type {\n DecideResult,\n DecisionSource,\n EdgeDecision,\n EdgeFacts,\n Manifest,\n ManifestKey,\n ProfileFacts,\n PublicManifest,\n TrailView,\n} from \"./types\";\n\n/** What a visitor's cached decision holds: the decisions and the profile they were made with. */\nexport type CachedDecision = { decisions: EdgeDecision[]; profile?: ProfileFacts | null };\n\nexport type DecisionCache = {\n get(key: string): CachedDecision | undefined;\n set(key: string, value: CachedDecision, ttlMs: number): void;\n};\n\nexport type XeniaEdgeConfig = {\n /** Xenia engine base URL, e.g. `https://engine.xeniamartech.com/xenia`. */\n baseUrl: string;\n /** A server-type SDK token. Never expose it to the browser. */\n token: string;\n /** Hard deadline for one `/decide` call. Default 150 ms. */\n budgetMs?: number;\n /**\n * Where to read the manifest. Default: Xenia's `/edge/manifest` (with the token). A storefront\n * can serve its own CDN-cached copy instead (e.g. a Next.js ISR route) — then no token is sent.\n */\n manifestUrl?: string;\n /**\n * A manifest baked in at build time. An instance starts with it in memory, so a cold start never\n * waits: the first request uses it and triggers a background refresh. Empty version = none.\n */\n initialManifest?: Manifest | PublicManifest;\n /** How long a cold start waits for the first manifest (only without `initialManifest`). Default 300 ms. */\n manifestWaitMs?: number;\n /** Manifest refresh interval. Default 30 s. */\n manifestTtlMs?: number;\n /** How long a visitor keeps a decision. Default 30 min (a session). */\n decisionTtlMs?: number;\n /**\n * How long a provisional decision is kept (see `provisional`): Jev's answer, or the visitor's first\n * signals, usually land seconds later, so the next view should ask again. Default 15 s.\n */\n provisionalDecisionTtlMs?: number;\n cache?: DecisionCache;\n fetch?: typeof fetch;\n onError?: (error: unknown, context: string) => void;\n};\n\nexport type DecideInput = {\n host: string | null;\n path: string;\n visitorId: string;\n facts: EdgeFacts;\n /** Facts that identify this visitor's context for caching (e.g. landing UTM, device). */\n cacheFacts?: EdgeFacts;\n /** The visitor's recent page views, most recent first (see `TrailView`). */\n trail?: TrailView[];\n /** This call's time budget, instead of the client's (a decision that calls a back office). */\n budgetMs?: number;\n /** How long to keep this call's answer, from what it said, instead of the client's. */\n ttlMs?: (answer: CachedDecision) => number;\n};\n\nexport type DecideOutcome = {\n decisions: EdgeDecision[];\n /** The profile Xenia decided with (also on cache hits), when profiles are on. */\n profile?: ProfileFacts | null;\n source: DecisionSource;\n /** Time spent in Xenia, 0 on a cache hit. */\n ms: number;\n};\n\n/** Bounded in-memory LRU with per-entry expiry. */\nexport function memoryDecisionCache(maxEntries = 10_000): DecisionCache {\n const entries = new Map<string, { value: CachedDecision; expires: number }>();\n return {\n get(key) {\n const hit = entries.get(key);\n if (!hit) return undefined;\n entries.delete(key);\n if (hit.expires < Date.now()) return undefined;\n entries.set(key, hit); // most recently used last\n return hit.value;\n },\n set(key, value, ttlMs) {\n entries.delete(key);\n entries.set(key, { value, expires: Date.now() + ttlMs });\n while (entries.size > maxEntries) {\n entries.delete(entries.keys().next().value as string);\n }\n },\n };\n}\n\nconst stableJson = (facts: EdgeFacts | undefined) =>\n JSON.stringify(\n Object.keys(facts ?? {})\n .sort()\n .map((k) => [k, facts![k]]),\n );\n\n/**\n * Xenia's manifest, or a storefront's trimmed public copy (`{version, pages}`). The trimmed form\n * becomes one key-less entry per page pattern: the proxy then decides by path, and Xenia picks the\n * content keys itself.\n */\nfunction normalizeManifest(m: Manifest | PublicManifest): Manifest {\n if (\"keys\" in m && Array.isArray(m.keys)) return m;\n const pages = (m as PublicManifest).pages ?? [];\n return {\n version: m.version,\n generatedAt: \"\",\n keys: pages.map((p) => ({ contentKey: \"\", mode: \"server\" as const, pages: [p] })),\n };\n}\n\n/**\n * A provisional decision, to ask again soon:\n * - a Visitor Intent step that refines with Jev answered from its signals because Jev hasn't yet;\n * - Visitor Intent steps with no answer at all: the visitor's first signals may land any moment —\n * and for a visitor who is the same in every browser from another browser,\n * which this browser's cache facts (trail, clicks) never see.\n * Steps answered from signals alone change with the visitor's own views, which the cache facts do see.\n */\nfunction provisional(profile: ProfileFacts | null | undefined): boolean {\n const answers = profile?.answers;\n if (!answers || typeof answers !== \"object\") return false;\n const steps = Object.values(answers as Record<string, { choice?: string | null; source?: string; jev?: boolean } | null>);\n return steps.some((a) => a?.source === \"signals\" && a.jev === true) || (steps.length > 0 && steps.every((a) => !a?.choice));\n}\n\nexport function createXeniaEdge(config: XeniaEdgeConfig) {\n const baseUrl = config.baseUrl.replace(/\\/+$/, \"\");\n const budgetMs = config.budgetMs ?? 150;\n const manifestWaitMs = config.manifestWaitMs ?? 300;\n const manifestTtlMs = config.manifestTtlMs ?? 30_000;\n const decisionTtlMs = config.decisionTtlMs ?? 30 * 60_000;\n const provisionalDecisionTtlMs = config.provisionalDecisionTtlMs ?? 15_000;\n const cache = config.cache ?? memoryDecisionCache();\n const doFetch = config.fetch ?? fetch;\n const onError = config.onError ?? (() => {});\n const auth = { Authorization: `Bearer ${config.token}` };\n\n // A bundled manifest is used right away; manifestAt 0 makes the first request refresh it in the\n // background (currentManifest only blocks when there is no manifest at all).\n let manifest: Manifest | null = config.initialManifest?.version\n ? normalizeManifest(config.initialManifest)\n : null;\n let manifestAt = 0;\n let refreshing: Promise<void> | null = null;\n const inflight = new Map<string, Promise<DecideOutcome>>();\n\n /** Fetch the manifest (304-aware); keeps the last good one on failure. */\n function refreshManifest(): Promise<void> {\n refreshing ??= (async () => {\n try {\n const res = await doFetch(config.manifestUrl ?? `${baseUrl}/api/personalization/edge/manifest`, {\n headers: config.manifestUrl\n ? {}\n : { ...auth, ...(manifest ? { \"If-None-Match\": `\"${manifest.version}\"` } : {}) },\n signal: AbortSignal.timeout(5_000),\n cache: \"no-store\",\n });\n if (res.status === 304) {\n manifestAt = Date.now();\n } else if (res.ok) {\n manifest = normalizeManifest((await res.json()) as Manifest | PublicManifest);\n manifestAt = Date.now();\n } else {\n onError(new Error(`manifest ${res.status}`), \"manifest\");\n manifestAt = Date.now(); // back off for a TTL before retrying\n }\n } catch (e) {\n onError(e, \"manifest\");\n manifestAt = Date.now();\n } finally {\n refreshing = null;\n }\n })();\n return refreshing;\n }\n\n /** The current manifest; stale-while-revalidate, a bounded wait on cold start. */\n async function currentManifest(): Promise<Manifest | null> {\n if (Date.now() - manifestAt > manifestTtlMs) {\n const pending = refreshManifest();\n if (!manifest) {\n await Promise.race([pending, new Promise((r) => setTimeout(r, manifestWaitMs))]);\n }\n }\n return manifest;\n }\n\n /**\n * The content keys targeting this page, or `null` when no manifest is\n * available yet. An empty array means \"don't call Xenia\".\n */\n async function match(host: string | null, path: string): Promise<ManifestKey[] | null> {\n const m = await currentManifest();\n if (!m) return null;\n return m.keys.filter((k) => k.mode !== \"off\" && matchesAny(k.pages, host, path));\n }\n\n async function decide(keys: ManifestKey[], input: DecideInput): Promise<DecideOutcome> {\n const version = manifest?.version ?? \"\";\n // Known keys are sent explicitly; with a trimmed manifest there are none, and Xenia selects\n // the keys whose pages match the path. Either way the cache is shared by every page that\n // matches the same keys / patterns (e.g. all of /women/*).\n const contentKeys = keys.map((k) => k.contentKey).filter(Boolean).sort();\n const scope = contentKeys.length ? contentKeys.join(\",\") : keys.flatMap((k) => k.pages).sort().join(\",\");\n const cacheKey = [version, input.visitorId, scope, stableJson(input.cacheFacts)].join(\"|\");\n\n const cached = cache.get(cacheKey);\n if (cached) return { decisions: cached.decisions, profile: cached.profile, source: \"cache\", ms: 0 };\n\n // Concurrent requests from one visitor (HTML + RSC prefetches) share a call.\n const pending = inflight.get(cacheKey);\n if (pending) return pending;\n\n const call = (async (): Promise<DecideOutcome> => {\n const started = Date.now();\n try {\n const res = await doFetch(`${baseUrl}/api/personalization/edge/decide`, {\n method: \"POST\",\n headers: { ...auth, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n contentKeys,\n host: input.host,\n path: input.path,\n visitorId: input.visitorId,\n facts: input.facts,\n ...(input.trail?.length ? { trail: input.trail } : {}),\n }),\n signal: AbortSignal.timeout(input.budgetMs ?? budgetMs),\n cache: \"no-store\",\n });\n if (!res.ok) {\n onError(new Error(`decide ${res.status}`), \"decide\");\n return { decisions: [], source: \"error\", ms: Date.now() - started };\n }\n const result = (await res.json()) as DecideResult | null;\n // A reply in the wrong shape is an error, never cached: it would break every view for the TTL.\n if (!Array.isArray(result?.decisions)) throw new Error(\"decide: reply has no decisions list\");\n const decisions = result.decisions.filter((d) => d && Array.isArray(d.swaps));\n const answer = { decisions, profile: result.profile };\n cache.set(cacheKey, answer, input.ttlMs?.(answer) ?? (provisional(result.profile) ? provisionalDecisionTtlMs : decisionTtlMs));\n if (manifest && result.version && result.version !== manifest.version) {\n void refreshManifest(); // journeys changed — pick up the new manifest\n }\n return { decisions, profile: result.profile, source: \"xenia\", ms: Date.now() - started };\n } catch (e) {\n const timedOut = e instanceof DOMException && e.name === \"TimeoutError\";\n if (!timedOut) onError(e, \"decide\");\n return { decisions: [], source: timedOut ? \"timeout\" : \"error\", ms: Date.now() - started };\n } finally {\n inflight.delete(cacheKey);\n }\n })();\n inflight.set(cacheKey, call);\n return call;\n }\n\n return { match, decide, refreshManifest };\n}\n\nexport type XeniaEdge = ReturnType<typeof createXeniaEdge>;\n"]}
@@ -0,0 +1,61 @@
1
+ import { M as Manifest, P as PublicManifest, T as TrailView } from '../consent-DgWdjsSy.cjs';
2
+ export { C as CachedDecision, D as DecideInput, a as DecideOutcome, b as DecideResult, c as DecisionCache, d as DecisionSource, E as EdgeConsent, e as EdgeDecision, f as EdgeFacts, g as EdgeMode, G as GeoHeader, L as LocalTime, h as ManifestKey, i as ProfileFacts, S as SwapGroup, V as VariantSwap, j as ViewerGeo, X as XeniaEdge, k as XeniaEdgeConfig, l as createXeniaEdge, m as decodeVariant, n as deviceOf, o as edgeConsent, p as encodeVariant, q as isBot, r as isPageLoad, s as isPrefetch, t as landingContext, u as localTime, v as memoryDecisionCache, w as seasonOf, x as swapGroups, y as viewerGeo } from '../consent-DgWdjsSy.cjs';
3
+ import '../types-DgVHS4Qw.cjs';
4
+
5
+ /**
6
+ * Page matching for the Xenia manifest — a port of the backend's
7
+ * `EdgePageMatcher`, which is the reference:
8
+ *
9
+ * - `/women` exactly that path (case, trailing slash, query ignored)
10
+ * - `/women/*` `/women` and everything below it, not `/womenswear`
11
+ * - `/*` every path
12
+ * - `uk.example.com/women` that path on that host only; hostless patterns match any host
13
+ */
14
+ declare function normalizePath(path: string | null | undefined): string;
15
+ declare function matchesAny(patterns: readonly string[] | null | undefined, host: string | null | undefined, path: string | null | undefined): boolean;
16
+
17
+ interface FetchManifestOptions {
18
+ /** Xenia engine base URL. Default the hosted engine. */
19
+ baseUrl?: string;
20
+ /** A server-type SDK token. */
21
+ token: string;
22
+ /** Default 5000 ms. */
23
+ timeoutMs?: number;
24
+ fetch?: typeof fetch;
25
+ /** Extra fetch options, e.g. Next.js `{ next: { revalidate } }`. */
26
+ init?: RequestInit;
27
+ }
28
+ /** Xenia's manifest (`GET /api/personalization/edge/manifest`). Throws on a non-2xx answer. */
29
+ declare function fetchManifest(opts: FetchManifestOptions): Promise<Manifest>;
30
+ /**
31
+ * The manifest as a storefront may serve it publicly (e.g. from a CDN): the version and the page
32
+ * patterns any live journey targets — no content keys, modes or rules. A proxy reading it decides by
33
+ * path, and Xenia picks the content keys itself.
34
+ */
35
+ declare function trimManifest(m: Manifest): PublicManifest;
36
+
37
+ /**
38
+ * Whether a storefront webhook from Xenia is genuine and fresh:
39
+ * `X-Xenia-Signature: t=<unix s>,v1=<hex HMAC-SHA256(secret, "<t>.<body>")>`. Requests older than
40
+ * `toleranceS` are refused, so a captured request can't be replayed. Pass the raw body text.
41
+ */
42
+ declare function verifyWebhook(input: {
43
+ headers: Headers;
44
+ body: string;
45
+ }, secret: string, toleranceS?: number): Promise<boolean>;
46
+
47
+ declare const MAX_TRAIL = 8;
48
+ /** Cookie lifetime: a visit ends after 30 idle minutes, like the SDK's session. */
49
+ declare const TRAIL_MAX_AGE_S: number;
50
+ declare function trailCookieName(prefix?: string): string;
51
+ /** The trail in a cookie value (raw or URI-encoded); anything malformed reads as no trail. */
52
+ declare function readTrail(value: string | undefined | null): TrailView[];
53
+ /**
54
+ * The trail with this view first: a revisit moves to the front and keeps its first time (its old
55
+ * time on page is dropped — this view's isn't known yet). The same array when `path` is already first.
56
+ */
57
+ declare function withView(trail: TrailView[], path: string, now?: number): TrailView[];
58
+ /** The cookie value for a trail (not URI-encoded; cookie setters encode it). */
59
+ declare function trailCookieValue(trail: TrailView[]): string;
60
+
61
+ export { type FetchManifestOptions, MAX_TRAIL, Manifest, PublicManifest, TRAIL_MAX_AGE_S, TrailView, fetchManifest, matchesAny, normalizePath, readTrail, trailCookieName, trailCookieValue, trimManifest, verifyWebhook, withView };