@recomnext/browser 0.5.0 → 0.5.1-dev.6e78c840

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -61,14 +61,57 @@ const cleanup = recomnext.trackImpressions(document.querySelector('.recs-contain
61
61
  once: true,
62
62
  });
63
63
 
64
- // Identity merge on login
65
- await recomnext.mergeIdentity('authenticated-user-id');
66
-
67
64
  // Cleanup
68
65
  cleanup(); // stop observing
69
66
  recomnext.destroy(); // flush pending impressions
70
67
  ```
71
68
 
69
+ ## Identity and sessions
70
+
71
+ Anonymous visitors get a stable `sess_…` id in **`localStorage`** (`recomnext_session_id`, shared across tabs). Legacy `sessionStorage` values migrate once on first read.
72
+
73
+ ### Recommended: `RecomnextIdentity` (widgets + SDK)
74
+
75
+ Configure once, then signal login/logout. The coordinator runs **one** idempotent merge per login, rotates anonymous storage on logout, and notifies mounted carousels/search clients to refetch.
76
+
77
+ ```typescript
78
+ import {
79
+ RecomnextBrowserClient,
80
+ RecomnextIdentity,
81
+ installRecomnextIdentityDomListener,
82
+ } from '@recomnext/browser';
83
+
84
+ RecomnextIdentity.configure({
85
+ baseUrl: '/api',
86
+ tenantId: 'my-tenant',
87
+ publicToken: 'rnx_pub_…', // when HMAC is enabled
88
+ // Optional host-owned anonymous id (e.g. sess_{device_uid}):
89
+ // getHostAnonymousUserId: () => `sess_${deviceUid}`,
90
+ // onHostLogout: () => rotateDeviceUid(), // required with getHostAnonymousUserId
91
+ // syncAuthAcrossTabs: true, // default — other tabs mirror login/logout
92
+ });
93
+ installRecomnextIdentityDomListener(); // HTML embeds: recomnext:identity event
94
+
95
+ await RecomnextIdentity.setAuthenticatedUserId('customer-42'); // merge first, then auth + widget refetch
96
+ await RecomnextIdentity.setAuthenticatedUserId(null); // logout + new sess_…
97
+ ```
98
+
99
+ Omit `userId` on `@recomnext/react-carousel` when using the coordinator; pass `userId` explicitly if you prefer manual control.
100
+
101
+ `mergeIdentity()` delegates to the coordinator when it is configured. Merge sends the same anonymous id used for ingest (`userId || sessionId` before login).
102
+
103
+ ### Manual path (no coordinator)
104
+
105
+ ```typescript
106
+ await recomnext.mergeIdentity('customer-42');
107
+ recomnext.setUserId('customer-42');
108
+
109
+ recomnext.setUserId('');
110
+ recomnext.clearSession(); // or clearSession('sess_custom')
111
+ ```
112
+
113
+ Re-exported helpers: `resolveAnonymousSessionId`, `readAnonymousSessionId`, `clearAnonymousSession`, `getAnonymousSessionId()` on the client.
114
+
72
115
  ## nextQL filters
73
116
 
74
117
  Recommendation `filter` / `boosters` are **opaque nextQL strings** — the SDK does not parse operators.
@@ -84,7 +127,8 @@ Use `contains` / `not_contains` **only** for array item attributes; use `in` / `
84
127
  ## Features
85
128
 
86
129
  - Zero dependencies in the published bundle (~3KB gzipped for recommendations; search methods add minimal size)
87
- - Automatic sessionId generation
130
+ - Cross-tab anonymous session in `localStorage`
131
+ - `RecomnextIdentity` coordinator for merge dedupe and widget auth sync
88
132
  - IntersectionObserver-based impression tracking
89
133
  - Batched impression flush (every 2s + sendBeacon on page unload)
90
134
  - Tree-shakeable ESM build
package/dist/client.d.ts CHANGED
@@ -48,6 +48,16 @@ export declare class RecomnextBrowserClient {
48
48
  setUserId(userId: string): void;
49
49
  setCountry(code: string): void;
50
50
  mergeIdentity(authenticatedUserId: string): Promise<unknown>;
51
+ /** Keep SDK fields aligned when RecomnextIdentity coordinator is active. */
52
+ private syncFromIdentityCoordinator;
53
+ /** Anonymous session id used before login (`sess_…`). Shared across tabs via localStorage. */
54
+ getAnonymousSessionId(): string;
55
+ /**
56
+ * Rotate anonymous session after logout so the next visitor on this browser
57
+ * does not inherit the previous anonymous bucket. Clears known userId separately
58
+ * via setUserId('') from the integrator.
59
+ */
60
+ clearSession(customId?: string): void;
51
61
  destroy(): void;
52
62
  }
53
63
  export type FireAndForgetPayload = {
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { RecomnextBrowserClient } from './client';
2
+ export { RECOMNEXT_SESSION_KEY, resolveAnonymousSessionId, readAnonymousSessionId, clearAnonymousSession, RecomnextIdentity, installRecomnextIdentityDomListener, RECOMNEXT_IDENTITY_DOM_EVENT, } from '@recomnext/shared-browser-auth';
3
+ export type { BrowserStorage, IdentityCoordinatorConfig, IdentityListener, IdentityState, } from '@recomnext/shared-browser-auth';
2
4
  export { ImpressionTracker } from './tracker';
3
5
  export { normalizeClientCountry } from './country';
4
6
  export type { FireAndForgetPayload } from './client';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrD,YAAY,EACV,WAAW,EACX,YAAY,EACZ,aAAa,EACb,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,4BAA4B,EAC5B,wBAAwB,EACxB,WAAW,EACX,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,mBAAmB,EACnB,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,0BAA0B,EAC1B,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EACL,qBAAqB,EACrB,yBAAyB,EACzB,sBAAsB,EACtB,qBAAqB,EACrB,iBAAiB,EACjB,mCAAmC,EACnC,4BAA4B,GAC7B,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,cAAc,EACd,yBAAyB,EACzB,gBAAgB,EAChB,aAAa,GACd,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrD,YAAY,EACV,WAAW,EACX,YAAY,EACZ,aAAa,EACb,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,4BAA4B,EAC5B,wBAAwB,EACxB,WAAW,EACX,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,gCAAgC,CAAC;AACxC,YAAY,EACV,mBAAmB,EACnB,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,0BAA0B,EAC1B,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,SAAS,CAAC"}
@@ -1,4 +1,4 @@
1
- function R(n){return v(n)}function v(n){if(n===null)return"null";switch(typeof n){case"boolean":return n?"true":"false";case"number":if(!Number.isFinite(n))throw new TypeError(`Cannot canonicalize non-finite number: ${String(n)}`);return JSON.stringify(n);case"string":return JSON.stringify(n);case"object":{if(Array.isArray(n))return"["+n.map(v).join(",")+"]";let e=Object.keys(n).sort(),t=[];for(let r of e){let i=n[r];typeof i>"u"||t.push(JSON.stringify(r)+":"+v(i))}return"{"+t.join(",")+"}"}case"undefined":throw new TypeError("Cannot canonicalize undefined at the root");default:throw new TypeError(`Cannot canonicalize value of type ${typeof n}`)}}var P=new Map;async function b(n){let e=n.method.toUpperCase(),t=$(n.path),r=n.now??Math.floor(Date.now()/1e3),i=e==="GET"||e==="DELETE"||e==="HEAD"||e==="OPTIONS"?"":await k(R(n.body??null)),s=new Map;for(let[f,h]of Object.entries(n.query??{}))h!=null&&s.set(f,String(h));s.delete("hmac_timestamp"),s.delete("frontend_timestamp"),s.delete("body_hash"),s.delete("hmac_sign"),s.delete("frontend_sign");let o=Array.from(s.entries()).sort(([f],[h])=>f<h?-1:f>h?1:0).map(([f,h])=>`${encodeURIComponent(f)}=${encodeURIComponent(h)}`),a=n.mode==="public"?"frontend_timestamp":"hmac_timestamp",l=n.mode==="public"?"frontend_sign":"hmac_sign",c=`${a}=${r}&body_hash=${i}`,u=o.length?`${o.join("&")}&${c}`:c,d=`${e}
2
- ${t}?${u}
3
- X-Tenant-Id:${n.tenantId}`,m=await A(n.token,d);return{signedPath:`${t}?${u}&${l}=${m}`,timestamp:r,bodyHash:i,signature:m,idempotencyKey:C()}}async function k(n){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(n));return E(new Uint8Array(e))}async function A(n,e){let t=await T(n),r=await crypto.subtle.sign("HMAC",t,new TextEncoder().encode(e));return E(new Uint8Array(r))}async function T(n){let e=P.get(n);return e||(e=crypto.subtle.importKey("raw",new TextEncoder().encode(n),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),P.set(n,e)),e}function w(n){return T(n)}function E(n){let e="";for(let t=0;t<n.length;t++)e+=n[t].toString(16).padStart(2,"0");return e}function $(n){let e=n.replace(/\/$/,"");return e.length>0?e:"/"}function C(){let n=globalThis.crypto;if(n?.randomUUID)return n.randomUUID();let e=new Uint8Array(16);n?.getRandomValues(e),e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t=E(e);return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}var I={maxAttempts:8,initialBackoffMs:1e3,maxBackoffMs:6e5,flushIntervalMs:2e3},g=class{constructor(e){if(this.items=[],this.timer=null,this.draining=!1,this.cfg={persist:e.persist??!1,persistKey:e.persistKey,maxAttempts:e.maxAttempts??I.maxAttempts,initialBackoffMs:e.initialBackoffMs??I.initialBackoffMs,maxBackoffMs:e.maxBackoffMs??I.maxBackoffMs,flushIntervalMs:e.flushIntervalMs??I.flushIntervalMs,onRetryDrop:e.onRetryDrop,signRequest:e.signRequest},this.cfg.persist){if(!this.cfg.persistKey)throw new Error("RetryQueue: persistKey required when persist=true");this.idb=F(),this.loadPersisted()}typeof window<"u"&&(this.timer=setInterval(()=>void this.drain(),this.cfg.flushIntervalMs),window.addEventListener("online",()=>void this.drain()),window.addEventListener("pagehide",()=>void this.drain()))}enqueue(e){let t={...e,id:e.id??q(),attempts:e.attempts??0,nextAttemptAt:e.nextAttemptAt??Date.now()};this.items.push(t),this.persist(),this.drain()}flush(){return this.drain()}destroy(){this.timer&&clearInterval(this.timer),this.timer=null}async drain(){if(!this.draining&&!(typeof navigator<"u"&&navigator.onLine===!1)){this.draining=!0;try{let e=Date.now(),t=this.items.filter(r=>(r.nextAttemptAt??0)<=e);for(let r of t)await this.attempt(r);await this.persist()}finally{this.draining=!1}}}async attempt(e){e.attempts=(e.attempts??0)+1;try{let{url:t,headers:r}=await this.cfg.signRequest(e),i=await fetch(t,{method:e.method,headers:{"Content-Type":"application/json",...r},body:JSON.stringify(e.payload),keepalive:!0});if(i.ok){this.removeById(e.id);return}if(i.status>=400&&i.status<500&&i.status!==429){this.removeById(e.id),this.cfg.onRetryDrop?.(e,new Error(`HTTP ${i.status}`));return}this.scheduleRetry(e,new Error(`HTTP ${i.status}`))}catch(t){this.scheduleRetry(e,t)}}scheduleRetry(e,t){if((e.attempts??0)>=this.cfg.maxAttempts){this.removeById(e.id),this.cfg.onRetryDrop?.(e,t);return}let r=Math.min(this.cfg.maxBackoffMs,this.cfg.initialBackoffMs*Math.pow(2,(e.attempts??1)-1)),i=.5+Math.random()*.5;e.nextAttemptAt=Date.now()+Math.floor(r*i)}removeById(e){this.items=this.items.filter(t=>t.id!==e)}async persist(){if(!(!this.cfg.persist||!this.idb||!this.cfg.persistKey))try{let e=await this.idb;if(!e)return;let t=e.transaction("queue","readwrite");t.objectStore("queue").put(JSON.parse(JSON.stringify(this.items)),this.cfg.persistKey),await U(t)}catch{}}async loadPersisted(){if(!(!this.cfg.persist||!this.idb||!this.cfg.persistKey))try{let e=await this.idb;if(!e)return;let i=e.transaction("queue","readonly").objectStore("queue").get(this.cfg.persistKey),s=await new Promise((o,a)=>{i.onsuccess=()=>o(i.result),i.onerror=()=>a(i.error)});Array.isArray(s)&&(this.items=s,this.drain())}catch{}}};function q(){let n=globalThis.crypto;return n?.randomUUID?n.randomUUID():`rnx-${Date.now()}-${Math.random().toString(36).slice(2,11)}`}function F(){return typeof indexedDB>"u"?Promise.resolve(null):new Promise(n=>{let e=indexedDB.open("recomnext-retry-queue",1);e.onupgradeneeded=()=>{let t=e.result;t.objectStoreNames.contains("queue")||t.createObjectStore("queue")},e.onsuccess=()=>n(e.result),e.onerror=()=>n(null),e.onblocked=()=>n(null)})}function U(n){return new Promise((e,t)=>{n.oncomplete=()=>e(),n.onerror=()=>t(n.error),n.onabort=()=>t(n.error)})}function y(n){if(n==null)return;let e=String(n).trim().toUpperCase();if(!(!e||e==="OTH"||!/^[A-Z]{2}$/.test(e)))return e}var p=class{constructor(e,t,r,i,s,o){this.queue=[];this.flushing=!1;this.observer=null;this.seen=new Set;this.timer=null;this.getUserId=r,this.getSessionId=i,this.retryQueue=s,this.getCountry=o??(()=>{}),typeof window<"u"&&(this.timer=setInterval(()=>this.flush(),2e3),window.addEventListener("pagehide",()=>this.flush(!0)))}observe(e,t={}){let r=t.itemSelector??"[data-item-id]",i=t.threshold??.5,s=t.once!==!1,o=t.scenario??"",a=t.country||this.getCountry()||void 0;return this.observer=new IntersectionObserver(c=>{for(let u of c){if(!u.isIntersecting)continue;let d=u.target,m=d.dataset.itemId;if(!m)continue;let S=`${o}:${m}`;s&&this.seen.has(S)||(this.seen.add(S),this.queue.push({itemId:m,scenario:o,country:a}),s&&this.observer?.unobserve(d))}},{root:null,threshold:i}),e.querySelectorAll(r).forEach(c=>this.observer.observe(c)),()=>{this.observer?.disconnect(),this.observer=null}}flush(e=!1){if(this.flushing||this.queue.length===0)return;this.flushing=!0;let t=this.queue.splice(0,this.queue.length),r=new Map;for(let{itemId:s,scenario:o,country:a}of t){let l=`${o}\0${a??""}`,c=r.get(l);c?c.itemIds.push(s):r.set(l,{itemIds:[s],country:a})}let i=Array.from(r.entries()).map(([s,{itemIds:o,country:a}])=>{let l=s.split("\0")[0];return{userId:this.getUserId()||void 0,sessionId:this.getSessionId()||void 0,itemIds:o,scenarioSlug:l||void 0,timestamp:new Date().toISOString(),country:a||void 0}});this.retryQueue.enqueue({path:"/ingestion/impressions",method:"POST",payload:{impressions:i}}),e&&this.retryQueue.flush(),this.flushing=!1}destroy(){this.flush(),this.observer?.disconnect(),this.timer&&clearInterval(this.timer)}};var O="recomnext_session_id",_=3,M=200,Q=2e3,B=new Set([429,500,502,503,504]),D="auth_replay_cache_unavailable",V=new Set(["/identity/merge"]),x=class{constructor(e){this.baseUrl=e.baseUrl.replace(/\/+$/,""),this.tenantId=e.tenantId,this.publicToken=e.publicToken,this.userId=e.userId??"",this.sessionId=e.sessionId??this.resolveSessionId(),this.country=y(e.country),this.publicToken&&w(this.publicToken),this.fireAndForgetQueue=new g({persist:e.persistRetries??!1,persistKey:`rnx:${this.tenantId}:fnf`,onRetryDrop:e.onRetryDrop,signRequest:t=>this.signFireAndForget(t)}),this.tracker=new p(this.baseUrl,this.tenantId,()=>this.userId,()=>this.sessionId,this.fireAndForgetQueue,()=>this.country)}resolveSessionId(){if(typeof sessionStorage>"u")return"";let e=sessionStorage.getItem(O);return e||(e=`sess_${Date.now()}_${Math.random().toString(36).slice(2,9)}`,sessionStorage.setItem(O,e)),e}effectiveUserId(){return this.userId||this.sessionId}resolveCountry(e){return y(e??this.country)}async request(e,t,r,i){let s=V.has(t)?0:_,o;for(let a=0;a<=s;a++){if(a>0){let d=Math.min(Q,M*Math.pow(2,a-1)),m=.5+Math.random()*.5;await L(Math.floor(d*m))}let{url:l,headers:c}=await this.buildSignedRequest(e,t,i,r),u;try{u=await fetch(l,{method:e,headers:{"Content-Type":"application/json",...c},body:r!==void 0?JSON.stringify(r):void 0})}catch(d){if(o=d,a>=s)throw d;continue}if(u.status===503){let d=await K(u.clone());if(d===D&&a<s){o=new Error(`recomnext: ${e} ${t} ${u.status} ${d}`);continue}}if(B.has(u.status)&&a<s){o=new Error(`recomnext: ${e} ${t} returned ${u.status}`);continue}if(!u.ok){let d=await u.text().catch(()=>"");throw new Error(`recomnext: ${e} ${t} returned ${u.status}: ${d}`)}return u.status===204?void 0:u.json()}throw o??new Error(`recomnext: ${e} ${t} retries exhausted`)}async signFireAndForget(e){let{url:t,headers:r}=await this.buildSignedRequest(e.method,e.path,void 0,e.payload);return{url:t,headers:r}}async buildSignedRequest(e,t,r,i){let s={"X-Tenant-Id":this.tenantId};if(!this.publicToken){let a=j(r);return{url:`${this.baseUrl}${t}${a}`,headers:s}}let o=await b({method:e,path:t,query:r,body:i??null,tenantId:this.tenantId,token:this.publicToken,mode:"public"});return{url:`${this.baseUrl}${o.signedPath}`,headers:{...s,"X-Idempotency-Key":o.idempotencyKey}}}qs(e){return e}includeQuery(e){return{returnProperties:e?.returnProperties,includedProperties:e?.includedProperties&&e.includedProperties.length>0?e.includedProperties.join(","):void 0}}overrideQuery(e){if(!e)return{};let t=r=>{if(r!==void 0)return r===null||r.length===0?"":r.join(",")};return{filter:e.filter===void 0?void 0:e.filter===null?"":e.filter,boosters:t(e.boosters),rulesSlugs:t(e.rulesSlugs),segmentationId:e.segmentationId===void 0?void 0:e.segmentationId===null?"":e.segmentationId,itemsPerSegment:e.itemsPerSegment===void 0?void 0:e.itemsPerSegment===null?"":e.itemsPerSegment,autoRelaxFilters:e.autoRelaxFilters,relaxEntireFilterOnEmpty:e.relaxEntireFilterOnEmpty}}overrideBody(e){if(!e)return{};let t={};return e.filter!==void 0&&(t.filter=e.filter),e.boosters!==void 0&&(t.boosters=e.boosters),e.rulesSlugs!==void 0&&(t.rulesSlugs=e.rulesSlugs),e.segmentationId!==void 0&&(t.segmentationId=e.segmentationId),e.itemsPerSegment!==void 0&&(t.itemsPerSegment=e.itemsPerSegment),e.autoRelaxFilters!==void 0&&(t.autoRelaxFilters=e.autoRelaxFilters),e.relaxEntireFilterOnEmpty!==void 0&&(t.relaxEntireFilterOnEmpty=e.relaxEntireFilterOnEmpty),t}getRecommendations(e){return this.request("GET",`/recommendations/user-to-item/${this.effectiveUserId()}`,void 0,this.qs({count:e?.count,scenario:e?.scenario,country:this.resolveCountry(e?.country),...this.includeQuery(e),...this.overrideQuery(e)}))}getRelatedItems(e,t){return this.request("GET",`/recommendations/item-to-item/${e}`,void 0,this.qs({count:t?.count,scenario:t?.scenario,country:this.resolveCountry(t?.country),...this.includeQuery(t),...this.overrideQuery(t)}))}getSimilarProducts(e,t){return this.request("GET",`/recommendations/similar-products/${e}`,void 0,this.qs({count:t?.count,scenario:t?.scenario,country:this.resolveCountry(t?.country),...this.includeQuery(t),...this.overrideQuery(t)}))}getRecentlyViewed(e){return this.request("GET",`/recommendations/recently-viewed/${this.effectiveUserId()}`,void 0,this.qs({count:e?.count,scenario:e?.scenario,country:this.resolveCountry(e?.country),...this.includeQuery(e),...this.overrideQuery(e)}))}getCartRecommendations(e,t){return this.request("POST","/recommendations/items-to-items",{itemIds:e,count:t?.count??10,returnProperties:t?.returnProperties,includedProperties:t?.includedProperties,country:this.resolveCountry(t?.country),...this.overrideBody(t)},this.qs({scenario:t?.scenario}))}recommend(e){return this.request("POST","/recommendations",{...e,userId:this.effectiveUserId(),country:this.resolveCountry(e.country)})}search(e,t){return this.request("GET","/recommendations/search",void 0,this.qs({query:e,count:t?.count,category:t?.category,priceRange:t?.priceRange,userId:t?.userId??(this.userId||void 0)}))}searchV2(e,t){return this.request("GET","/recommendations/search/v2",void 0,this.qs({query:e,count:t?.count,category:t?.category,priceRange:t?.priceRange,userId:t?.userId??(this.userId||void 0),locale:t?.locale,debugScores:t?.debugScores===!0?!0:void 0,algorithm:t?.algorithm}))}autocomplete(e,t){return this.request("GET","/recommendations/search/v2/autocomplete",void 0,this.qs({query:e,limit:t?.limit}))}getSearchV2Settings(){return this.request("GET","/recommendations/search/v2/settings")}listItems(e){return this.request("GET","/catalog/items",void 0,this.qs({page:e?.page,limit:e?.limit,category:e?.category,search:e?.search,availability:e?.availability}))}getCategories(){return this.request("GET","/catalog/items/categories")}getItem(e){return this.request("GET",`/catalog/items/${encodeURIComponent(e)}`)}trackView(e,t,r){this.enqueueInteraction("view",e,t,r)}trackCart(e,t,r){this.enqueueInteraction("cart",e,t,r)}trackPurchase(e,t,r){this.enqueueInteraction("purchase",e,t,r)}enqueueInteraction(e,t,r,i){let s=typeof r=="object"?r:void 0,o=typeof r=="string"?r:s?.scenarioSlug,a=this.resolveCountry(s?.country??i?.country);this.fireAndForgetQueue.enqueue({path:"/ingestion/interactions",method:"POST",payload:{interactions:[{userId:this.effectiveUserId(),itemId:t,type:e,scenarioSlug:o||void 0,timestamp:new Date().toISOString(),country:a}]}})}trackImpressions(e,t){return this.tracker.observe(e,t)}setUserId(e){this.userId=e}setCountry(e){this.country=y(e)}mergeIdentity(e){let t=this.sessionId;return this.userId=e,this.request("POST","/identity/merge",{userId:e,sessionIds:[t]})}destroy(){this.tracker.destroy(),this.fireAndForgetQueue.destroy()}};function j(n){if(!n)return"";let e=[];for(let[t,r]of Object.entries(n))r!=null&&e.push(`${encodeURIComponent(t)}=${encodeURIComponent(String(r))}`);return e.length?`?${e.join("&")}`:""}async function K(n){try{let e=await n.json();return typeof e.errorCode=="string"?e.errorCode:void 0}catch{return}}function L(n){return new Promise(e=>setTimeout(e,n))}export{p as ImpressionTracker,x as RecomnextBrowserClient,y as normalizeClientCountry};
1
+ function F(n){return k(n)}function k(n){if(n===null)return"null";switch(typeof n){case"boolean":return n?"true":"false";case"number":if(!Number.isFinite(n))throw new TypeError(`Cannot canonicalize non-finite number: ${String(n)}`);return JSON.stringify(n);case"string":return JSON.stringify(n);case"object":{if(Array.isArray(n))return"["+n.map(k).join(",")+"]";let e=Object.keys(n).sort(),t=[];for(let r of e){let i=n[r];typeof i>"u"||t.push(JSON.stringify(r)+":"+k(i))}return"{"+t.join(",")+"}"}case"undefined":throw new TypeError("Cannot canonicalize undefined at the root");default:throw new TypeError(`Cannot canonicalize value of type ${typeof n}`)}}var Q=new Map;async function E(n){let e=n.method.toUpperCase(),t=re(n.path),r=n.now??Math.floor(Date.now()/1e3),i=e==="GET"||e==="DELETE"||e==="HEAD"||e==="OPTIONS"?"":await te(F(n.body??null)),s=new Map;for(let[R,I]of Object.entries(n.query??{}))I!=null&&s.set(R,String(I));s.delete("hmac_timestamp"),s.delete("frontend_timestamp"),s.delete("body_hash"),s.delete("hmac_sign"),s.delete("frontend_sign");let o=Array.from(s.entries()).sort(([R],[I])=>R<I?-1:R>I?1:0).map(([R,I])=>`${encodeURIComponent(R)}=${encodeURIComponent(I)}`),a=n.mode==="public"?"frontend_timestamp":"hmac_timestamp",d=n.mode==="public"?"frontend_sign":"hmac_sign",f=`${a}=${r}&body_hash=${i}`,c=o.length?`${o.join("&")}&${f}`:f,l=`${e}
2
+ ${t}?${c}
3
+ X-Tenant-Id:${n.tenantId}`,p=await ne(n.token,l);return{signedPath:`${t}?${c}&${d}=${p}`,timestamp:r,bodyHash:i,signature:p,idempotencyKey:se()}}async function te(n){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(n));return M(new Uint8Array(e))}async function ne(n,e){let t=await H(n),r=await crypto.subtle.sign("HMAC",t,new TextEncoder().encode(e));return M(new Uint8Array(r))}async function H(n){let e=Q.get(n);return e||(e=crypto.subtle.importKey("raw",new TextEncoder().encode(n),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),Q.set(n,e)),e}function b(n){return H(n)}function M(n){let e="";for(let t=0;t<n.length;t++)e+=n[t].toString(16).padStart(2,"0");return e}function re(n){let e=n.replace(/\/$/,"");return e.length>0?e:"/"}function se(){let n=globalThis.crypto;if(n?.randomUUID)return n.randomUUID();let e=new Uint8Array(16);n?.getRandomValues(e),e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t=M(e);return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}var O={maxAttempts:8,initialBackoffMs:1e3,maxBackoffMs:6e5,flushIntervalMs:2e3},v=class{constructor(e){if(this.items=[],this.timer=null,this.draining=!1,this.cfg={persist:e.persist??!1,persistKey:e.persistKey,maxAttempts:e.maxAttempts??O.maxAttempts,initialBackoffMs:e.initialBackoffMs??O.initialBackoffMs,maxBackoffMs:e.maxBackoffMs??O.maxBackoffMs,flushIntervalMs:e.flushIntervalMs??O.flushIntervalMs,onRetryDrop:e.onRetryDrop,signRequest:e.signRequest},this.cfg.persist){if(!this.cfg.persistKey)throw new Error("RetryQueue: persistKey required when persist=true");this.idb=oe(),this.loadPersisted()}typeof window<"u"&&(this.timer=setInterval(()=>void this.drain(),this.cfg.flushIntervalMs),window.addEventListener("online",()=>void this.drain()),window.addEventListener("pagehide",()=>void this.drain()))}enqueue(e){let t={...e,id:e.id??ie(),attempts:e.attempts??0,nextAttemptAt:e.nextAttemptAt??Date.now()};this.items.push(t),this.persist(),this.drain()}flush(){return this.drain()}destroy(){this.timer&&clearInterval(this.timer),this.timer=null}async drain(){if(!this.draining&&!(typeof navigator<"u"&&navigator.onLine===!1)){this.draining=!0;try{let e=Date.now(),t=this.items.filter(r=>(r.nextAttemptAt??0)<=e);for(let r of t)await this.attempt(r);await this.persist()}finally{this.draining=!1}}}async attempt(e){e.attempts=(e.attempts??0)+1;try{let{url:t,headers:r}=await this.cfg.signRequest(e),i=await fetch(t,{method:e.method,headers:{"Content-Type":"application/json",...r},body:JSON.stringify(e.payload),keepalive:!0});if(i.ok){this.removeById(e.id);return}if(i.status>=400&&i.status<500&&i.status!==429){this.removeById(e.id),this.cfg.onRetryDrop?.(e,new Error(`HTTP ${i.status}`));return}this.scheduleRetry(e,new Error(`HTTP ${i.status}`))}catch(t){this.scheduleRetry(e,t)}}scheduleRetry(e,t){if((e.attempts??0)>=this.cfg.maxAttempts){this.removeById(e.id),this.cfg.onRetryDrop?.(e,t);return}let r=Math.min(this.cfg.maxBackoffMs,this.cfg.initialBackoffMs*Math.pow(2,(e.attempts??1)-1)),i=.5+Math.random()*.5;e.nextAttemptAt=Date.now()+Math.floor(r*i)}removeById(e){this.items=this.items.filter(t=>t.id!==e)}async persist(){if(!(!this.cfg.persist||!this.idb||!this.cfg.persistKey))try{let e=await this.idb;if(!e)return;let t=e.transaction("queue","readwrite");t.objectStore("queue").put(JSON.parse(JSON.stringify(this.items)),this.cfg.persistKey),await ae(t)}catch{}}async loadPersisted(){if(!(!this.cfg.persist||!this.idb||!this.cfg.persistKey))try{let e=await this.idb;if(!e)return;let i=e.transaction("queue","readonly").objectStore("queue").get(this.cfg.persistKey),s=await new Promise((o,a)=>{i.onsuccess=()=>o(i.result),i.onerror=()=>a(i.error)});Array.isArray(s)&&(this.items=s,this.drain())}catch{}}};function ie(){let n=globalThis.crypto;return n?.randomUUID?n.randomUUID():`rnx-${Date.now()}-${Math.random().toString(36).slice(2,11)}`}function oe(){return typeof indexedDB>"u"?Promise.resolve(null):new Promise(n=>{let e=indexedDB.open("recomnext-retry-queue",1);e.onupgradeneeded=()=>{let t=e.result;t.objectStoreNames.contains("queue")||t.createObjectStore("queue")},e.onsuccess=()=>n(e.result),e.onerror=()=>n(null),e.onblocked=()=>n(null)})}function ae(n){return new Promise((e,t)=>{n.oncomplete=()=>e(),n.onerror=()=>t(n.error),n.onabort=()=>t(n.error)})}var m="recomnext_session_id";function q(){return{localStorage:typeof globalThis<"u"&&typeof localStorage<"u"?localStorage:void 0,sessionStorage:typeof globalThis<"u"&&typeof sessionStorage<"u"?sessionStorage:void 0}}function U(){return`sess_${Date.now()}_${Math.random().toString(36).slice(2,9)}`}function S(n=q()){let{localStorage:e,sessionStorage:t}=n;if(!e)return"";let r=e.getItem(m);if(r)return r;if(t){let s=t.getItem(m);if(s)return e.setItem(m,s),t.removeItem(m),s}let i=U();return e.setItem(m,i),i}function h(n=q()){let{localStorage:e,sessionStorage:t}=n;if(e){let r=e.getItem(m);if(r)return r}return t?t.getItem(m)??"":""}function w(n,e=q()){let{localStorage:t,sessionStorage:r}=e,i=n!==void 0&&n!==""?n:U();return t&&t.setItem(m,i),r&&r.removeItem(m),i}var ue=new Set([429,500,502,503,504]),ce="auth_replay_cache_unavailable";function de(n){return new Promise(e=>setTimeout(e,n))}async function le(n){try{let e=await n.json();return typeof e.errorCode=="string"?e.errorCode:void 0}catch{return}}async function Y(n,e,t){let r;for(let i=0;i<=3;i++){if(i>0){let o=Math.min(2e3,200*Math.pow(2,i-1)),a=.5+Math.random()*.5;await de(Math.floor(o*a))}let s;try{s=await t()}catch(o){if(r=o,i>=3)throw o;continue}if(s.status===503){let o=await le(s.clone());if(o===ce&&i<3){r=new Error(`recomnext: ${n} ${e} ${s.status} ${o}`);continue}}if(ue.has(s.status)&&i<3){r=new Error(`recomnext: ${n} ${e} returned ${s.status}`);continue}if(!s.ok){let o=await s.text().catch(()=>"");throw new Error(`recomnext: ${n} ${e} returned ${s.status}: ${o}`)}return s}throw r??new Error(`recomnext: ${n} ${e} retries exhausted`)}var T="recomnext_authenticated_user_id",u=null,g=null,K=new Set,N=new Map,P=new Map,A=!1,X=!1;function J(n){return n.replace(/\/+$/,"")}function z(){let n=u?.getHostAnonymousUserId?.()??"";return typeof n=="string"?n.trim():""}function W(){if(g)return g;let n=z();if(n)return n;let e=h();return e||S()}function D(){return{authenticatedUserId:g,anonymousSessionId:h(),effectiveUserId:W()}}function L(){let n=D();for(let e of K)e(n)}function fe(n,e,t){return`${n}:${e}:${t}`}function me(n){return n===null?"logout":`login:${n}`}function B(n){A||!u||u.syncAuthAcrossTabs!==!1&&(typeof localStorage>"u"||(n===null?localStorage.removeItem(T):localStorage.setItem(T,n)))}function he(){if(typeof localStorage>"u")return null;let n=localStorage.getItem(T);return!n||n.trim()===""?null:n.trim()}async function G(n,e){if(!u||!e||e===n)return;let t=fe(u.tenantId,e,n),r=N.get(t);if(r){await r;return}let i=(async()=>{let s="/identity/merge",o={userId:n,sessionIds:[e]},a=J(u.baseUrl);await Y("POST",s,async()=>{if(u.publicToken){await b(u.publicToken);let d=await E({method:"POST",path:s,body:o,tenantId:u.tenantId,token:u.publicToken,mode:"public"});return fetch(`${a}${d.signedPath}`,{method:"POST",headers:{"Content-Type":"application/json","X-Tenant-Id":u.tenantId,"X-Idempotency-Key":d.idempotencyKey},body:JSON.stringify(o)})}return fetch(`${a}${s}`,{method:"POST",headers:{"Content-Type":"application/json","X-Tenant-Id":u.tenantId},body:JSON.stringify(o)})})})();N.set(t,i);try{await i}finally{N.delete(t)}}function Z(){if(!u)throw new Error("RecomnextIdentity.configure() must be called before use");return u}async function ge(n,e){let t=P.get(n);if(t){await t;return}let r=e();P.set(n,r);try{await r}finally{P.delete(n)}}async function V(n,e="local"){Z();let t=e==="peer-sync",r=e==="peer-sync",i=n==null?null:String(n).trim(),s=i===""?null:i;if(g===s)return;let o=g;if(s===null){!r&&u.getHostAnonymousUserId&&u.onHostLogout&&await u.onHostLogout(),w(),g=null,B(null),L();return}if(o!==null){let d=h()||z()||"";w(),!t&&d&&await G(s,d),g=s,B(s),L();return}let a=W();!t&&a&&await G(s,a),g=s,B(s),L()}function ye(){X||typeof window>"u"||(X=!0,window.addEventListener("storage",n=>{if(!u||u.syncAuthAcrossTabs===!1||n.key!==T||n.storageArea!==localStorage)return;let e=n.newValue===null||n.newValue===void 0||n.newValue.trim()===""?null:n.newValue.trim();e!==g&&(A=!0,V(e,"peer-sync").finally(()=>{A=!1}))}))}function pe(){if(!u||u.syncAuthAcrossTabs===!1)return;let n=he();n!==g&&(A=!0,V(n,"peer-sync").finally(()=>{A=!1}))}var y={configure(n){u={...n,baseUrl:J(n.baseUrl)},u.publicToken&&b(u.publicToken),ye(),pe()},isConfigured(){return u!==null},subscribe(n){return K.add(n),n(D()),()=>K.delete(n)},getState(){return u?D():{authenticatedUserId:null,anonymousSessionId:h(),effectiveUserId:h()||S()}},async setAuthenticatedUserId(n){Z();let e=n==null?null:String(n).trim(),t=e===""?null:e,r=me(t),i=P.get(r);if(g===t){i&&await i;return}await ge(r,()=>V(t))}},$="recomnext:identity";function ee(){if(typeof document>"u")return()=>{};let n=e=>{if(!y.isConfigured())return;let r=e.detail?.userId,i=r==null||String(r).trim()===""?null:String(r).trim();y.setAuthenticatedUserId(i)};return document.addEventListener($,n),()=>document.removeEventListener($,n)}function x(n){if(n==null)return;let e=String(n).trim().toUpperCase();if(!(!e||e==="OTH"||!/^[A-Z]{2}$/.test(e)))return e}var C=class{constructor(e,t,r,i,s,o){this.queue=[];this.flushing=!1;this.observer=null;this.seen=new Set;this.timer=null;this.getUserId=r,this.getSessionId=i,this.retryQueue=s,this.getCountry=o??(()=>{}),typeof window<"u"&&(this.timer=setInterval(()=>this.flush(),2e3),window.addEventListener("pagehide",()=>this.flush(!0)))}observe(e,t={}){let r=t.itemSelector??"[data-item-id]",i=t.threshold??.5,s=t.once!==!1,o=t.scenario??"",a=t.country||this.getCountry()||void 0;return this.observer=new IntersectionObserver(f=>{for(let c of f){if(!c.isIntersecting)continue;let l=c.target,p=l.dataset.itemId;if(!p)continue;let _=`${o}:${p}`;s&&this.seen.has(_)||(this.seen.add(_),this.queue.push({itemId:p,scenario:o,country:a}),s&&this.observer?.unobserve(l))}},{root:null,threshold:i}),e.querySelectorAll(r).forEach(f=>this.observer.observe(f)),()=>{this.observer?.disconnect(),this.observer=null}}flush(e=!1){if(this.flushing||this.queue.length===0)return;this.flushing=!0;let t=this.queue.splice(0,this.queue.length),r=new Map;for(let{itemId:s,scenario:o,country:a}of t){let d=`${o}\0${a??""}`,f=r.get(d);f?f.itemIds.push(s):r.set(d,{itemIds:[s],country:a})}let i=Array.from(r.entries()).map(([s,{itemIds:o,country:a}])=>{let d=s.split("\0")[0];return{userId:this.getUserId()||void 0,sessionId:this.getSessionId()||void 0,itemIds:o,scenarioSlug:d||void 0,timestamp:new Date().toISOString(),country:a||void 0}});this.retryQueue.enqueue({path:"/ingestion/impressions",method:"POST",payload:{impressions:i}}),e&&this.retryQueue.flush(),this.flushing=!1}destroy(){this.flush(),this.observer?.disconnect(),this.timer&&clearInterval(this.timer)}};var Ie=3,Se=200,we=2e3,Re=new Set([429,500,502,503,504]),be="auth_replay_cache_unavailable",j=class{constructor(e){this.baseUrl=e.baseUrl.replace(/\/+$/,""),this.tenantId=e.tenantId,this.publicToken=e.publicToken,this.userId=e.userId??"",this.sessionId=e.sessionId??this.resolveSessionId(),this.country=x(e.country),this.publicToken&&b(this.publicToken),this.fireAndForgetQueue=new v({persist:e.persistRetries??!1,persistKey:`rnx:${this.tenantId}:fnf`,onRetryDrop:e.onRetryDrop,signRequest:t=>this.signFireAndForget(t)}),this.tracker=new C(this.baseUrl,this.tenantId,()=>this.userId,()=>this.sessionId,this.fireAndForgetQueue,()=>this.country)}resolveSessionId(){return S()}effectiveUserId(){return this.userId||this.sessionId}resolveCountry(e){return x(e??this.country)}async request(e,t,r,i){let s=Ie,o;for(let a=0;a<=s;a++){if(a>0){let l=Math.min(we,Se*Math.pow(2,a-1)),p=.5+Math.random()*.5;await Te(Math.floor(l*p))}let{url:d,headers:f}=await this.buildSignedRequest(e,t,i,r),c;try{c=await fetch(d,{method:e,headers:{"Content-Type":"application/json",...f},body:r!==void 0?JSON.stringify(r):void 0})}catch(l){if(o=l,a>=s)throw l;continue}if(c.status===503){let l=await ve(c.clone());if(l===be&&a<s){o=new Error(`recomnext: ${e} ${t} ${c.status} ${l}`);continue}}if(Re.has(c.status)&&a<s){o=new Error(`recomnext: ${e} ${t} returned ${c.status}`);continue}if(!c.ok){let l=await c.text().catch(()=>"");throw new Error(`recomnext: ${e} ${t} returned ${c.status}: ${l}`)}return c.status===204?void 0:c.json()}throw o??new Error(`recomnext: ${e} ${t} retries exhausted`)}async signFireAndForget(e){let{url:t,headers:r}=await this.buildSignedRequest(e.method,e.path,void 0,e.payload);return{url:t,headers:r}}async buildSignedRequest(e,t,r,i){let s={"X-Tenant-Id":this.tenantId};if(!this.publicToken){let a=Ee(r);return{url:`${this.baseUrl}${t}${a}`,headers:s}}let o=await E({method:e,path:t,query:r,body:i??null,tenantId:this.tenantId,token:this.publicToken,mode:"public"});return{url:`${this.baseUrl}${o.signedPath}`,headers:{...s,"X-Idempotency-Key":o.idempotencyKey}}}qs(e){return e}includeQuery(e){return{returnProperties:e?.returnProperties,includedProperties:e?.includedProperties&&e.includedProperties.length>0?e.includedProperties.join(","):void 0}}overrideQuery(e){if(!e)return{};let t=r=>{if(r!==void 0)return r===null||r.length===0?"":r.join(",")};return{filter:e.filter===void 0?void 0:e.filter===null?"":e.filter,boosters:t(e.boosters),rulesSlugs:t(e.rulesSlugs),segmentationId:e.segmentationId===void 0?void 0:e.segmentationId===null?"":e.segmentationId,itemsPerSegment:e.itemsPerSegment===void 0?void 0:e.itemsPerSegment===null?"":e.itemsPerSegment,autoRelaxFilters:e.autoRelaxFilters,relaxEntireFilterOnEmpty:e.relaxEntireFilterOnEmpty}}overrideBody(e){if(!e)return{};let t={};return e.filter!==void 0&&(t.filter=e.filter),e.boosters!==void 0&&(t.boosters=e.boosters),e.rulesSlugs!==void 0&&(t.rulesSlugs=e.rulesSlugs),e.segmentationId!==void 0&&(t.segmentationId=e.segmentationId),e.itemsPerSegment!==void 0&&(t.itemsPerSegment=e.itemsPerSegment),e.autoRelaxFilters!==void 0&&(t.autoRelaxFilters=e.autoRelaxFilters),e.relaxEntireFilterOnEmpty!==void 0&&(t.relaxEntireFilterOnEmpty=e.relaxEntireFilterOnEmpty),t}getRecommendations(e){return this.request("GET",`/recommendations/user-to-item/${this.effectiveUserId()}`,void 0,this.qs({count:e?.count,scenario:e?.scenario,country:this.resolveCountry(e?.country),...this.includeQuery(e),...this.overrideQuery(e)}))}getRelatedItems(e,t){return this.request("GET",`/recommendations/item-to-item/${e}`,void 0,this.qs({count:t?.count,scenario:t?.scenario,country:this.resolveCountry(t?.country),...this.includeQuery(t),...this.overrideQuery(t)}))}getSimilarProducts(e,t){return this.request("GET",`/recommendations/similar-products/${e}`,void 0,this.qs({count:t?.count,scenario:t?.scenario,country:this.resolveCountry(t?.country),...this.includeQuery(t),...this.overrideQuery(t)}))}getRecentlyViewed(e){return this.request("GET",`/recommendations/recently-viewed/${this.effectiveUserId()}`,void 0,this.qs({count:e?.count,scenario:e?.scenario,country:this.resolveCountry(e?.country),...this.includeQuery(e),...this.overrideQuery(e)}))}getCartRecommendations(e,t){return this.request("POST","/recommendations/items-to-items",{itemIds:e,count:t?.count??10,returnProperties:t?.returnProperties,includedProperties:t?.includedProperties,country:this.resolveCountry(t?.country),...this.overrideBody(t)},this.qs({scenario:t?.scenario}))}recommend(e){return this.request("POST","/recommendations",{...e,userId:this.effectiveUserId(),country:this.resolveCountry(e.country)})}search(e,t){return this.request("GET","/recommendations/search",void 0,this.qs({query:e,count:t?.count,category:t?.category,priceRange:t?.priceRange,userId:t?.userId??(this.userId||void 0)}))}searchV2(e,t){return this.request("GET","/recommendations/search/v2",void 0,this.qs({query:e,count:t?.count,category:t?.category,priceRange:t?.priceRange,userId:t?.userId??(this.userId||void 0),locale:t?.locale,debugScores:t?.debugScores===!0?!0:void 0,algorithm:t?.algorithm}))}autocomplete(e,t){return this.request("GET","/recommendations/search/v2/autocomplete",void 0,this.qs({query:e,limit:t?.limit}))}getSearchV2Settings(){return this.request("GET","/recommendations/search/v2/settings")}listItems(e){return this.request("GET","/catalog/items",void 0,this.qs({page:e?.page,limit:e?.limit,category:e?.category,search:e?.search,availability:e?.availability}))}getCategories(){return this.request("GET","/catalog/items/categories")}getItem(e){return this.request("GET",`/catalog/items/${encodeURIComponent(e)}`)}trackView(e,t,r){this.enqueueInteraction("view",e,t,r)}trackCart(e,t,r){this.enqueueInteraction("cart",e,t,r)}trackPurchase(e,t,r){this.enqueueInteraction("purchase",e,t,r)}enqueueInteraction(e,t,r,i){let s=typeof r=="object"?r:void 0,o=typeof r=="string"?r:s?.scenarioSlug,a=this.resolveCountry(s?.country??i?.country);this.fireAndForgetQueue.enqueue({path:"/ingestion/interactions",method:"POST",payload:{interactions:[{userId:this.effectiveUserId(),itemId:t,type:e,scenarioSlug:o||void 0,timestamp:new Date().toISOString(),country:a}]}})}trackImpressions(e,t){return this.tracker.observe(e,t)}setUserId(e){this.userId=e}setCountry(e){this.country=x(e)}mergeIdentity(e){if(y.isConfigured())return y.setAuthenticatedUserId(e).then(()=>{this.userId=e,this.syncFromIdentityCoordinator()});let t=this.userId||this.sessionId;return this.userId=e,this.request("POST","/identity/merge",{userId:e,sessionIds:[t]})}syncFromIdentityCoordinator(){if(!y.isConfigured())return;let e=y.getState();this.userId=e.authenticatedUserId??"",this.sessionId=e.anonymousSessionId||h()||this.sessionId}getAnonymousSessionId(){return this.sessionId||h()}clearSession(e){this.sessionId=w(e)}destroy(){this.tracker.destroy(),this.fireAndForgetQueue.destroy()}};function Ee(n){if(!n)return"";let e=[];for(let[t,r]of Object.entries(n))r!=null&&e.push(`${encodeURIComponent(t)}=${encodeURIComponent(String(r))}`);return e.length?`?${e.join("&")}`:""}async function ve(n){try{let e=await n.json();return typeof e.errorCode=="string"?e.errorCode:void 0}catch{return}}function Te(n){return new Promise(e=>setTimeout(e,n))}export{C as ImpressionTracker,$ as RECOMNEXT_IDENTITY_DOM_EVENT,m as RECOMNEXT_SESSION_KEY,j as RecomnextBrowserClient,y as RecomnextIdentity,w as clearAnonymousSession,ee as installRecomnextIdentityDomListener,x as normalizeClientCountry,h as readAnonymousSessionId,S as resolveAnonymousSessionId};
4
4
  //# sourceMappingURL=recomnext-browser.esm.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../shared-browser-auth/src/canonical-json.ts", "../../shared-browser-auth/src/sign.ts", "../../shared-browser-auth/src/retry-queue.ts", "../src/country.ts", "../src/tracker.ts", "../src/client.ts"],
4
- "sourcesContent": ["// Canonical JSON serializer \u2014 byte-identical with the engine, the Node\n// server SDK, and the Python SDK. See repo-root\n// `tests/auth/canonical-vectors.json` for the conformance gate.\n\nexport function canonicalJson(value: unknown): string {\n return stringify(value);\n}\n\nfunction stringify(value: unknown): string {\n if (value === null) return 'null';\n switch (typeof value) {\n case 'boolean':\n return value ? 'true' : 'false';\n case 'number':\n if (!Number.isFinite(value)) {\n throw new TypeError(\n `Cannot canonicalize non-finite number: ${String(value)}`,\n );\n }\n return JSON.stringify(value);\n case 'string':\n return JSON.stringify(value);\n case 'object': {\n if (Array.isArray(value)) {\n return '[' + value.map(stringify).join(',') + ']';\n }\n const keys = Object.keys(value as Record<string, unknown>).sort();\n const parts: string[] = [];\n for (const k of keys) {\n const v = (value as Record<string, unknown>)[k];\n if (typeof v === 'undefined') continue;\n parts.push(JSON.stringify(k) + ':' + stringify(v));\n }\n return '{' + parts.join(',') + '}';\n }\n case 'undefined':\n throw new TypeError('Cannot canonicalize undefined at the root');\n default:\n throw new TypeError(\n `Cannot canonicalize value of type ${typeof value}`,\n );\n }\n}\n", "// WebCrypto-based HMAC-SHA256 signing for browser-side recomnext callers.\n// Same wire format as the server SDK (sdks/node/src/signing.ts) and the\n// engine guard (engine/src/auth/guards/hmac.guard.ts).\n//\n// Public vs private token contract:\n// - PUBLIC token: shipped in JS bundles. Wire uses `frontend_sign` +\n// `frontend_timestamp` query params. Engine rejects unless the request\n// origin is on the tenant's allow-list AND the route is on the\n// public-write whitelist (or is a GET).\n// - PRIVATE token: NEVER ship to a browser. WebCrypto signing of private\n// tokens is provided here for completeness (e.g. for Node-via-bundler\n// consumers), but the recomnext browser SDK refuses to accept a\n// privateToken in its config.\n\nimport { canonicalJson } from './canonical-json.js';\n\nexport type SignerMode = 'public' | 'private';\n\nexport interface SignInput {\n method: string;\n // Path WITHOUT querystring or trailing slash.\n path: string;\n // Caller-supplied query params; values coerced to strings.\n query?: Record<string, string | number | boolean | undefined>;\n // Parsed body \u2014 undefined for GET/DELETE/HEAD/OPTIONS.\n body?: unknown;\n tenantId: string;\n token: string;\n mode: SignerMode;\n // Test-only clock override.\n now?: number;\n}\n\nexport interface SignedRequest {\n signedPath: string;\n timestamp: number;\n bodyHash: string;\n signature: string;\n idempotencyKey: string;\n}\n\n// Cached CryptoKey objects keyed by raw token bytes. importKey() is async\n// and non-trivial; pages calling sign() in a hot loop (e.g. mouse-move\n// telemetry) would otherwise pay the cost on every event.\nconst KEY_CACHE = new Map<string, Promise<CryptoKey>>();\n\nexport async function sign(input: SignInput): Promise<SignedRequest> {\n const verb = input.method.toUpperCase();\n const path = effectivePath(input.path);\n const timestamp = input.now ?? Math.floor(Date.now() / 1000);\n\n const bodyHash =\n verb === 'GET' || verb === 'DELETE' || verb === 'HEAD' || verb === 'OPTIONS'\n ? ''\n : await sha256Hex(canonicalJson(input.body ?? null));\n\n // Preserve empty-string values (e.g. filter= / itemsPerSegment= clear\n // sentinels for request-time overrides). Only drop undefined/null omit.\n // Must stay lock-step with engine HmacGuard + node/python signers.\n const others = new Map<string, string>();\n for (const [k, v] of Object.entries(input.query ?? {})) {\n if (v === undefined || v === null) continue;\n others.set(k, String(v));\n }\n others.delete('hmac_timestamp');\n others.delete('frontend_timestamp');\n others.delete('body_hash');\n others.delete('hmac_sign');\n others.delete('frontend_sign');\n const sortedOthers = Array.from(others.entries())\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`);\n\n const tsParam = input.mode === 'public' ? 'frontend_timestamp' : 'hmac_timestamp';\n const sigParam = input.mode === 'public' ? 'frontend_sign' : 'hmac_sign';\n\n const trailing = `${tsParam}=${timestamp}&body_hash=${bodyHash}`;\n const queryForSigning = sortedOthers.length\n ? `${sortedOthers.join('&')}&${trailing}`\n : trailing;\n\n const signingString = `${verb}\\n${path}?${queryForSigning}\\nX-Tenant-Id:${input.tenantId}`;\n const signature = await hmacSha256Hex(input.token, signingString);\n const signedPath = `${path}?${queryForSigning}&${sigParam}=${signature}`;\n\n return {\n signedPath,\n timestamp,\n bodyHash,\n signature,\n idempotencyKey: makeIdempotencyKey(),\n };\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const bytes = await crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(input),\n );\n return toHex(new Uint8Array(bytes));\n}\n\nasync function hmacSha256Hex(token: string, payload: string): Promise<string> {\n const key = await getKey(token);\n const sig = await crypto.subtle.sign(\n 'HMAC',\n key,\n new TextEncoder().encode(payload),\n );\n return toHex(new Uint8Array(sig));\n}\n\nasync function getKey(token: string): Promise<CryptoKey> {\n let cached = KEY_CACHE.get(token);\n if (!cached) {\n cached = crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(token),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n KEY_CACHE.set(token, cached);\n }\n return cached;\n}\n\n// Preload the CryptoKey ahead of any sign() calls. Useful at SDK init time\n// so the first user interaction doesn't pay the importKey cost on the hot\n// path. Returns immediately on a second call for the same token.\nexport function preloadSignerKey(token: string): Promise<CryptoKey> {\n return getKey(token);\n}\n\nfunction toHex(bytes: Uint8Array): string {\n let out = '';\n for (let i = 0; i < bytes.length; i++) {\n out += bytes[i].toString(16).padStart(2, '0');\n }\n return out;\n}\n\nfunction effectivePath(path: string): string {\n const stripped = path.replace(/\\/$/, '');\n return stripped.length > 0 ? stripped : '/';\n}\n\nfunction makeIdempotencyKey(): string {\n const c = (globalThis as unknown as { crypto?: Crypto & { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n // Fallback for older WebView shells without randomUUID.\n const rand = new Uint8Array(16);\n c?.getRandomValues(rand);\n rand[6] = (rand[6] & 0x0f) | 0x40;\n rand[8] = (rand[8] & 0x3f) | 0x80;\n const hex = toHex(rand);\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n", "// Durable retry queue for browser fire-and-forget endpoints (impressions,\n// interactions). Per the auth-rollout plan:\n// - Synchronous retries (3 attempts, ~5s) for request-response calls\n// happen inline in the SDK; THIS file is exclusively for the\n// fire-and-forget endpoints (/ingestion/impressions,\n// /ingestion/interactions).\n// - `/identity/merge` is NEVER auto-retried (non-idempotent) \u2014 callers\n// must NOT enqueue merge requests via this class.\n// - Exponential backoff, capped at 10 minutes / 8 attempts. After the\n// cap, items are dropped and `onRetryDrop` is invoked for observability.\n// - Optional IndexedDB persistence via `persistRetries: true` so queued\n// items survive page reloads / device-sleep / network outages.\n//\n// Re-signing happens on EVERY attempt \u2014 the engine's replay cache rejects\n// the same signature twice, and the 30s signing window means a stale sig\n// is worse than no retry. The `signRequest` callback is invoked with the\n// item payload and must return a freshly-signed URL + idempotency key.\n\nexport interface QueuedItem<TPayload = unknown> {\n // Unique id used for dedupe / persistence. Auto-generated when not set.\n id?: string;\n // Logical path the item targets (used for routing on flush).\n path: string;\n // HTTP method. Currently every fire-and-forget endpoint is POST; the\n // field exists so the queue can also handle PUTs if needed later.\n method: string;\n // Payload sent as the JSON body. Must be canonical-JSON serializable.\n payload: TPayload;\n // Number of attempts so far (0 on first enqueue). The queue increments.\n attempts?: number;\n // Earliest wall-clock ms at which the next attempt should run. Updated\n // each time we back off.\n nextAttemptAt?: number;\n}\n\nexport interface RetryQueueConfig<TPayload = unknown> {\n // Persist queued items across page reloads. Disabled by default \u2014 the\n // demo / read-mostly integrations don't need it. Enabling pulls in an\n // IndexedDB-backed `idb-keyval`-style adapter (see indexed-db.ts).\n persist?: boolean;\n // Storage key for IndexedDB persistence (so multi-tenant pages don't\n // share queue state). Required when `persist: true`.\n persistKey?: string;\n // Maximum attempts before the item is dropped. Default 8.\n maxAttempts?: number;\n // Initial backoff in ms. Default 1000.\n initialBackoffMs?: number;\n // Backoff cap in ms. Default 600_000 (10m).\n maxBackoffMs?: number;\n // Periodic drain interval in ms. Default 2000.\n flushIntervalMs?: number;\n // Called for every item dropped after exhausting attempts. Used for\n // observability \u2014 log to Sentry / surface as a tracker event.\n onRetryDrop?: (item: QueuedItem<TPayload>, error: unknown) => void;\n // Signs each attempt's request fresh. Returns the absolute URL to POST\n // to plus any auth headers (X-Idempotency-Key, etc.). Must NOT cache\n // its return value \u2014 the queue calls this once per attempt.\n signRequest: (item: QueuedItem<TPayload>) => Promise<{\n url: string;\n headers: Record<string, string>;\n }>;\n}\n\nconst DEFAULTS = {\n maxAttempts: 8,\n initialBackoffMs: 1000,\n maxBackoffMs: 600_000,\n flushIntervalMs: 2000,\n};\n\nexport class RetryQueue<TPayload = unknown> {\n private readonly cfg: Required<\n Omit<RetryQueueConfig<TPayload>, 'persist' | 'persistKey' | 'onRetryDrop'>\n > & {\n persist: boolean;\n persistKey?: string;\n onRetryDrop?: RetryQueueConfig<TPayload>['onRetryDrop'];\n };\n private items: QueuedItem<TPayload>[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private draining = false;\n private idb?: Promise<IDBDatabase | null>;\n\n constructor(cfg: RetryQueueConfig<TPayload>) {\n this.cfg = {\n persist: cfg.persist ?? false,\n persistKey: cfg.persistKey,\n maxAttempts: cfg.maxAttempts ?? DEFAULTS.maxAttempts,\n initialBackoffMs: cfg.initialBackoffMs ?? DEFAULTS.initialBackoffMs,\n maxBackoffMs: cfg.maxBackoffMs ?? DEFAULTS.maxBackoffMs,\n flushIntervalMs: cfg.flushIntervalMs ?? DEFAULTS.flushIntervalMs,\n onRetryDrop: cfg.onRetryDrop,\n signRequest: cfg.signRequest,\n };\n\n if (this.cfg.persist) {\n if (!this.cfg.persistKey) {\n throw new Error('RetryQueue: persistKey required when persist=true');\n }\n this.idb = openIndexedDb();\n void this.loadPersisted();\n }\n\n if (typeof window !== 'undefined') {\n this.timer = setInterval(() => void this.drain(), this.cfg.flushIntervalMs);\n window.addEventListener('online', () => void this.drain());\n // Best-effort flush on page hide \u2014 sendBeacon is the documented\n // companion for items we never want to drop.\n window.addEventListener('pagehide', () => void this.drain());\n }\n }\n\n enqueue(item: QueuedItem<TPayload>): void {\n const enriched: QueuedItem<TPayload> = {\n ...item,\n id: item.id ?? makeId(),\n attempts: item.attempts ?? 0,\n nextAttemptAt: item.nextAttemptAt ?? Date.now(),\n };\n this.items.push(enriched);\n void this.persist();\n // Eager drain attempt \u2014 useful when the page is interactive and the\n // queued event should leave the page within a hundred ms or two.\n void this.drain();\n }\n\n // Manually trigger a drain cycle. Used by callers on critical pageviews\n // (e.g. checkout) where dropping the next backoff window matters.\n flush(): Promise<void> {\n return this.drain();\n }\n\n destroy(): void {\n if (this.timer) clearInterval(this.timer);\n this.timer = null;\n }\n\n private async drain(): Promise<void> {\n if (this.draining) return;\n if (typeof navigator !== 'undefined' && navigator.onLine === false) return;\n this.draining = true;\n try {\n const now = Date.now();\n const ready = this.items.filter((i) => (i.nextAttemptAt ?? 0) <= now);\n for (const item of ready) {\n await this.attempt(item);\n }\n await this.persist();\n } finally {\n this.draining = false;\n }\n }\n\n private async attempt(item: QueuedItem<TPayload>): Promise<void> {\n item.attempts = (item.attempts ?? 0) + 1;\n try {\n const { url, headers } = await this.cfg.signRequest(item);\n const res = await fetch(url, {\n method: item.method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n body: JSON.stringify(item.payload),\n keepalive: true,\n });\n if (res.ok) {\n this.removeById(item.id!);\n return;\n }\n // 4xx other than 429 is permanent \u2014 no amount of retry fixes a bad\n // payload. Drop immediately so we don't keep poisoning the queue.\n if (res.status >= 400 && res.status < 500 && res.status !== 429) {\n this.removeById(item.id!);\n this.cfg.onRetryDrop?.(item, new Error(`HTTP ${res.status}`));\n return;\n }\n this.scheduleRetry(item, new Error(`HTTP ${res.status}`));\n } catch (err) {\n this.scheduleRetry(item, err);\n }\n }\n\n private scheduleRetry(item: QueuedItem<TPayload>, err: unknown): void {\n if ((item.attempts ?? 0) >= this.cfg.maxAttempts) {\n this.removeById(item.id!);\n this.cfg.onRetryDrop?.(item, err);\n return;\n }\n const exp = Math.min(\n this.cfg.maxBackoffMs,\n this.cfg.initialBackoffMs * Math.pow(2, (item.attempts ?? 1) - 1),\n );\n // Decorrelated jitter \u2014 full backoff with a uniformly-distributed\n // multiplier in [0.5, 1.0) so multiple replicas don't sync into a\n // thundering herd when the backend recovers.\n const jitter = 0.5 + Math.random() * 0.5;\n item.nextAttemptAt = Date.now() + Math.floor(exp * jitter);\n }\n\n private removeById(id: string): void {\n this.items = this.items.filter((i) => i.id !== id);\n }\n\n // \u2500\u2500 IndexedDB persistence \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async persist(): Promise<void> {\n if (!this.cfg.persist || !this.idb || !this.cfg.persistKey) return;\n try {\n const db = await this.idb;\n if (!db) return;\n const tx = db.transaction('queue', 'readwrite');\n const store = tx.objectStore('queue');\n store.put(JSON.parse(JSON.stringify(this.items)), this.cfg.persistKey);\n await txDone(tx);\n } catch {\n // Persistence is best-effort \u2014 never throw out of an enqueue path.\n }\n }\n\n private async loadPersisted(): Promise<void> {\n if (!this.cfg.persist || !this.idb || !this.cfg.persistKey) return;\n try {\n const db = await this.idb;\n if (!db) return;\n const tx = db.transaction('queue', 'readonly');\n const store = tx.objectStore('queue');\n const req = store.get(this.cfg.persistKey);\n const data: unknown = await new Promise((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n if (Array.isArray(data)) {\n this.items = data as QueuedItem<TPayload>[];\n void this.drain();\n }\n } catch {\n // Lost-persistence is silent by design \u2014 don't break enqueue on\n // private-browsing / disabled-storage.\n }\n }\n}\n\nfunction makeId(): string {\n const c = (globalThis as unknown as { crypto?: Crypto & { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n return `rnx-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;\n}\n\nfunction openIndexedDb(): Promise<IDBDatabase | null> {\n if (typeof indexedDB === 'undefined') return Promise.resolve(null);\n return new Promise((resolve) => {\n const req = indexedDB.open('recomnext-retry-queue', 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains('queue')) db.createObjectStore('queue');\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => resolve(null);\n req.onblocked = () => resolve(null);\n });\n}\n\nfunction txDone(tx: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error);\n });\n}\n", "/**\n * Client-side country hygiene. Server allowlist is still the source of\n * truth \u2014 unknown 2-letter codes are dropped here only when they are not\n * ISO-2 shaped (OTH / empty / non-alpha). Valid-looking codes go on the\n * wire; the engine maps anything not in country.ts to global.\n */\nexport function normalizeClientCountry(\n raw?: string | null,\n): string | undefined {\n if (raw == null) return undefined;\n const code = String(raw).trim().toUpperCase();\n if (!code || code === 'OTH' || !/^[A-Z]{2}$/.test(code)) return undefined;\n return code;\n}\n", "import type { RetryQueue } from '@recomnext/shared-browser-auth';\nimport type { FireAndForgetPayload } from './client';\nimport type { ImpressionTrackerOptions } from './types';\n\nconst FLUSH_INTERVAL = 2000;\n\nexport class ImpressionTracker {\n private queue: { itemId: string; scenario: string; country?: string }[] = [];\n private flushing = false;\n private observer: IntersectionObserver | null = null;\n private seen = new Set<string>();\n private timer: ReturnType<typeof setInterval> | null = null;\n private readonly getUserId: () => string;\n private readonly getSessionId: () => string;\n private readonly getCountry: () => string | undefined;\n // The RetryQueue handles all signed POSTs to /ingestion/impressions\n // (single source of truth for fire-and-forget on this page). We still\n // batch impressions every 2s before enqueuing so we don't fan out N\n // signed requests for one viewport scan.\n private readonly retryQueue: RetryQueue<FireAndForgetPayload>;\n\n constructor(\n _baseUrl: string,\n _tenantId: string,\n getUserId: () => string,\n getSessionId: () => string,\n retryQueue: RetryQueue<FireAndForgetPayload>,\n getCountry?: () => string | undefined,\n ) {\n // baseUrl + tenantId are captured by the client during signRequest()\n // and no longer needed at the tracker level. Kept in the signature\n // for backwards compatibility with anyone constructing the tracker\n // directly (rare \u2014 almost everyone uses RecomnextBrowserClient).\n this.getUserId = getUserId;\n this.getSessionId = getSessionId;\n this.retryQueue = retryQueue;\n this.getCountry = getCountry ?? (() => undefined);\n if (typeof window !== 'undefined') {\n this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL);\n // sendBeacon is best-effort and CANNOT carry the X-Idempotency-Key\n // header (it's a Blob POST). On pagehide we therefore force the\n // queue's normal signed-fetch path to drain via `keepalive: true`,\n // which is now widely supported in all evergreen browsers.\n window.addEventListener('pagehide', () => this.flush(true));\n }\n }\n\n observe(container: Element, opts: ImpressionTrackerOptions = {}): () => void {\n const selector = opts.itemSelector ?? '[data-item-id]';\n const threshold = opts.threshold ?? 0.5;\n const once = opts.once !== false;\n const scenario = opts.scenario ?? '';\n const country = opts.country || this.getCountry() || undefined;\n\n this.observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n const el = entry.target as HTMLElement;\n const itemId = el.dataset.itemId;\n if (!itemId) continue;\n const key = `${scenario}:${itemId}`;\n if (once && this.seen.has(key)) continue;\n this.seen.add(key);\n this.queue.push({ itemId, scenario, country });\n if (once) this.observer?.unobserve(el);\n }\n },\n { root: null, threshold },\n );\n\n const elements = container.querySelectorAll(selector);\n elements.forEach((el) => this.observer!.observe(el));\n\n return () => {\n this.observer?.disconnect();\n this.observer = null;\n };\n }\n\n // `urgent` is set during pagehide. When true we kick the RetryQueue\n // immediately so it has a chance to land items inside the unload window\n // (keepalive: true on the inner fetch keeps the connection alive past\n // the navigation).\n private flush(urgent = false): void {\n if (this.flushing || this.queue.length === 0) return;\n this.flushing = true;\n const batch = this.queue.splice(0, this.queue.length);\n\n const grouped = new Map<string, { itemIds: string[]; country?: string }>();\n for (const { itemId, scenario, country } of batch) {\n const key = `${scenario}\\0${country ?? ''}`;\n const group = grouped.get(key);\n if (group) {\n group.itemIds.push(itemId);\n } else {\n grouped.set(key, { itemIds: [itemId], country });\n }\n }\n\n const impressions = Array.from(grouped.entries()).map(([key, { itemIds, country }]) => {\n const scenarioSlug = key.split('\\0')[0];\n return {\n userId: this.getUserId() || undefined,\n sessionId: this.getSessionId() || undefined,\n itemIds,\n scenarioSlug: scenarioSlug || undefined,\n timestamp: new Date().toISOString(),\n country: country || undefined,\n };\n });\n\n this.retryQueue.enqueue({\n path: '/ingestion/impressions',\n method: 'POST',\n payload: { impressions },\n });\n\n if (urgent) void this.retryQueue.flush();\n this.flushing = false;\n }\n\n destroy(): void {\n this.flush();\n this.observer?.disconnect();\n if (this.timer) clearInterval(this.timer);\n }\n}\n", "import {\n preloadSignerKey,\n RetryQueue,\n sign as signRequest,\n type QueuedItem,\n} from '@recomnext/shared-browser-auth';\n\nimport type {\n SearchOptions,\n SearchResponse,\n SearchV2Options,\n SearchV2Response,\n SearchV2AutocompleteResponse,\n SearchV2SettingsResponse,\n CatalogItem,\n CatalogListResponse,\n ListItemsOptions,\n} from '@recomnext/shared-search-types';\n\nimport { normalizeClientCountry } from './country';\nimport { ImpressionTracker } from './tracker';\nimport type {\n BrowserClientConfig,\n RecommendationResult,\n RecommendationOptions,\n RecommendOptions,\n ImpressionTrackerOptions,\n TrackEventOptions,\n} from './types';\n\nconst SESSION_KEY = 'recomnext_session_id';\n\n// Synchronous retry budget for request-response calls (recommendations,\n// catalog reads, identity/merge). Mirrors the Node + Python SDK behavior:\n// 3 attempts total, ~5s aggregate, re-signs on every attempt.\nconst SYNC_RETRIES = 3;\nconst SYNC_INITIAL_BACKOFF_MS = 200;\nconst SYNC_MAX_BACKOFF_MS = 2_000;\n\n// 5xx + 429 are auto-retried. We special-case 503 +\n// errorCode=auth_replay_cache_unavailable so the SDK retries even if an\n// operator tunes 503 out of the retry set.\nconst RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);\nconst AUTH_REPLAY_CACHE_UNAVAILABLE = 'auth_replay_cache_unavailable';\n\n// `/identity/merge` mutates the identity graph on every call. The plan\n// locks this to single-shot until the engine grows server-side\n// idempotency \u2014 never auto-retry, even on a transient 503.\nconst NEVER_RETRY_PATHS = new Set(['/identity/merge']);\n\nexport class RecomnextBrowserClient {\n private readonly baseUrl: string;\n private readonly tenantId: string;\n private readonly publicToken: string | undefined;\n private userId: string;\n private sessionId: string;\n private country: string | undefined;\n private readonly tracker: ImpressionTracker;\n // Lazy \u2014 only constructed once for fire-and-forget queues that the\n // tracker also reuses.\n readonly fireAndForgetQueue: RetryQueue<FireAndForgetPayload>;\n\n constructor(config: BrowserClientConfig) {\n this.baseUrl = config.baseUrl.replace(/\\/+$/, '');\n this.tenantId = config.tenantId;\n this.publicToken = config.publicToken;\n this.userId = config.userId ?? '';\n this.sessionId = config.sessionId ?? this.resolveSessionId();\n this.country = normalizeClientCountry(config.country);\n\n // Eagerly import the CryptoKey so the first signed event after page\n // load doesn't pay the importKey latency on the hot path.\n if (this.publicToken) void preloadSignerKey(this.publicToken);\n\n this.fireAndForgetQueue = new RetryQueue<FireAndForgetPayload>({\n persist: config.persistRetries ?? false,\n persistKey: `rnx:${this.tenantId}:fnf`,\n onRetryDrop: config.onRetryDrop,\n signRequest: (item) => this.signFireAndForget(item),\n });\n\n this.tracker = new ImpressionTracker(\n this.baseUrl,\n this.tenantId,\n () => this.userId,\n () => this.sessionId,\n this.fireAndForgetQueue,\n () => this.country,\n );\n }\n\n private resolveSessionId(): string {\n if (typeof sessionStorage === 'undefined') return '';\n let sid = sessionStorage.getItem(SESSION_KEY);\n if (!sid) {\n sid = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;\n sessionStorage.setItem(SESSION_KEY, sid);\n }\n return sid;\n }\n\n private effectiveUserId(): string {\n return this.userId || this.sessionId;\n }\n\n private resolveCountry(override?: string): string | undefined {\n return normalizeClientCountry(override ?? this.country);\n }\n\n private async request<T>(\n method: string,\n path: string,\n body?: unknown,\n query?: Record<string, string | number | boolean | undefined>,\n ): Promise<T> {\n const maxRetries = NEVER_RETRY_PATHS.has(path) ? 0 : SYNC_RETRIES;\n let lastErr: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n if (attempt > 0) {\n const exp = Math.min(\n SYNC_MAX_BACKOFF_MS,\n SYNC_INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1),\n );\n const jitter = 0.5 + Math.random() * 0.5;\n await sleep(Math.floor(exp * jitter));\n }\n\n // Always re-sign \u2014 the engine's replay cache rejects the same\n // signature twice, and the 30s signing window means a stale sig\n // would also fail.\n const { url, headers } = await this.buildSignedRequest(\n method,\n path,\n query,\n body,\n );\n\n let res: Response;\n try {\n res = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n } catch (err) {\n lastErr = err;\n if (attempt >= maxRetries) throw err;\n continue;\n }\n\n if (res.status === 503) {\n const code = await extractErrorCode(res.clone());\n if (code === AUTH_REPLAY_CACHE_UNAVAILABLE && attempt < maxRetries) {\n lastErr = new Error(`recomnext: ${method} ${path} ${res.status} ${code}`);\n continue;\n }\n }\n\n if (RETRYABLE_STATUS.has(res.status) && attempt < maxRetries) {\n lastErr = new Error(`recomnext: ${method} ${path} returned ${res.status}`);\n continue;\n }\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new Error(`recomnext: ${method} ${path} returned ${res.status}: ${text}`);\n }\n\n if (res.status === 204) return undefined as unknown as T;\n return res.json() as Promise<T>;\n }\n\n throw lastErr ?? new Error(`recomnext: ${method} ${path} retries exhausted`);\n }\n\n // Same shape as the RetryQueue's signRequest contract \u2014 used by the\n // queue *and* the impression tracker so both share a single signing\n // path.\n async signFireAndForget(item: QueuedItem<FireAndForgetPayload>) {\n const { url, headers } = await this.buildSignedRequest(\n item.method,\n item.path,\n undefined,\n item.payload,\n );\n return { url, headers };\n }\n\n private async buildSignedRequest(\n method: string,\n path: string,\n query: Record<string, string | number | boolean | undefined> | undefined,\n body: unknown,\n ): Promise<{ url: string; headers: Record<string, string> }> {\n const baseHeaders: Record<string, string> = {\n 'X-Tenant-Id': this.tenantId,\n };\n\n if (!this.publicToken) {\n // Legacy / no-auth mode. Engine accepts because\n // LEGACY_TENANT_AUTH=true. After Phase 7 cutover this path will be\n // rejected with 401.\n const qs = renderQs(query);\n return { url: `${this.baseUrl}${path}${qs}`, headers: baseHeaders };\n }\n\n const signed = await signRequest({\n method,\n path,\n query,\n body: body ?? null,\n tenantId: this.tenantId,\n token: this.publicToken,\n mode: 'public',\n });\n return {\n url: `${this.baseUrl}${signed.signedPath}`,\n headers: {\n ...baseHeaders,\n 'X-Idempotency-Key': signed.idempotencyKey,\n },\n };\n }\n\n private qs(\n params: Record<string, string | number | boolean | undefined>,\n ): Record<string, string | number | boolean | undefined> {\n return params;\n }\n\n private includeQuery(opts?: RecommendationOptions): {\n returnProperties?: boolean;\n includedProperties?: string;\n } {\n return {\n returnProperties: opts?.returnProperties,\n includedProperties:\n opts?.includedProperties && opts.includedProperties.length > 0\n ? opts.includedProperties.join(',')\n : undefined,\n };\n }\n\n /** Serialize override fields for GET query (comma-separated lists; '' clears). */\n private overrideQuery(opts?: RecommendationOptions): {\n filter?: string;\n boosters?: string;\n rulesSlugs?: string;\n segmentationId?: string;\n itemsPerSegment?: number | string;\n autoRelaxFilters?: boolean;\n relaxEntireFilterOnEmpty?: boolean;\n } {\n if (!opts) return {};\n const join = (v: string[] | null | undefined): string | undefined => {\n if (v === undefined) return undefined;\n if (v === null || v.length === 0) return '';\n return v.join(',');\n };\n return {\n filter:\n opts.filter === undefined\n ? undefined\n : opts.filter === null\n ? ''\n : opts.filter,\n boosters: join(opts.boosters),\n rulesSlugs: join(opts.rulesSlugs),\n segmentationId:\n opts.segmentationId === undefined\n ? undefined\n : opts.segmentationId === null\n ? ''\n : opts.segmentationId,\n // Present null \u2192 '' so the query param is sent; engine transform maps\n // '' \u2192 null \u2192 resolve to itemsPerSegment=1 (omit would keep scenario).\n itemsPerSegment:\n opts.itemsPerSegment === undefined\n ? undefined\n : opts.itemsPerSegment === null\n ? ''\n : opts.itemsPerSegment,\n autoRelaxFilters: opts.autoRelaxFilters,\n relaxEntireFilterOnEmpty: opts.relaxEntireFilterOnEmpty,\n };\n }\n\n private overrideBody(opts?: RecommendationOptions): {\n filter?: string | null;\n boosters?: string[] | null;\n rulesSlugs?: string[] | null;\n segmentationId?: string | null;\n itemsPerSegment?: number | null;\n autoRelaxFilters?: boolean;\n relaxEntireFilterOnEmpty?: boolean;\n } {\n if (!opts) return {};\n const body: {\n filter?: string | null;\n boosters?: string[] | null;\n rulesSlugs?: string[] | null;\n segmentationId?: string | null;\n itemsPerSegment?: number | null;\n autoRelaxFilters?: boolean;\n relaxEntireFilterOnEmpty?: boolean;\n } = {};\n if (opts.filter !== undefined) body.filter = opts.filter;\n if (opts.boosters !== undefined) body.boosters = opts.boosters;\n if (opts.rulesSlugs !== undefined) body.rulesSlugs = opts.rulesSlugs;\n if (opts.segmentationId !== undefined) {\n body.segmentationId = opts.segmentationId;\n }\n if (opts.itemsPerSegment !== undefined) {\n body.itemsPerSegment = opts.itemsPerSegment;\n }\n if (opts.autoRelaxFilters !== undefined) {\n body.autoRelaxFilters = opts.autoRelaxFilters;\n }\n if (opts.relaxEntireFilterOnEmpty !== undefined) {\n body.relaxEntireFilterOnEmpty = opts.relaxEntireFilterOnEmpty;\n }\n return body;\n }\n\n /* \u2500\u2500 Recommendations \u2500\u2500 */\n\n getRecommendations(opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/user-to-item/${this.effectiveUserId()}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getRelatedItems(itemId: string, opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/item-to-item/${itemId}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getSimilarProducts(itemId: string, opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/similar-products/${itemId}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getRecentlyViewed(opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/recently-viewed/${this.effectiveUserId()}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getCartRecommendations(itemIds: string[], opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'POST',\n '/recommendations/items-to-items',\n {\n itemIds,\n count: opts?.count ?? 10,\n returnProperties: opts?.returnProperties,\n includedProperties: opts?.includedProperties,\n country: this.resolveCountry(opts?.country),\n ...this.overrideBody(opts),\n },\n this.qs({ scenario: opts?.scenario }),\n );\n }\n\n recommend(opts: RecommendOptions): Promise<RecommendationResult> {\n return this.request('POST', '/recommendations', {\n ...opts,\n userId: this.effectiveUserId(),\n country: this.resolveCountry(opts.country),\n });\n }\n\n /* \u2500\u2500 Search \u2500\u2500 */\n\n search(query: string, opts?: SearchOptions): Promise<SearchResponse> {\n return this.request(\n 'GET',\n '/recommendations/search',\n undefined,\n this.qs({\n query,\n count: opts?.count,\n category: opts?.category,\n priceRange: opts?.priceRange,\n userId: opts?.userId ?? (this.userId || undefined),\n }),\n );\n }\n\n searchV2(query: string, opts?: SearchV2Options): Promise<SearchV2Response> {\n return this.request(\n 'GET',\n '/recommendations/search/v2',\n undefined,\n this.qs({\n query,\n count: opts?.count,\n category: opts?.category,\n priceRange: opts?.priceRange,\n userId: opts?.userId ?? (this.userId || undefined),\n locale: opts?.locale,\n debugScores: opts?.debugScores === true ? true : undefined,\n algorithm: opts?.algorithm,\n }),\n );\n }\n\n autocomplete(\n query: string,\n opts?: { limit?: number },\n ): Promise<SearchV2AutocompleteResponse> {\n return this.request(\n 'GET',\n '/recommendations/search/v2/autocomplete',\n undefined,\n this.qs({\n query,\n limit: opts?.limit,\n }),\n );\n }\n\n getSearchV2Settings(): Promise<SearchV2SettingsResponse> {\n return this.request('GET', '/recommendations/search/v2/settings');\n }\n\n /* \u2500\u2500 Catalog browse (public HMAC eligible) \u2500\u2500 */\n\n listItems(opts?: ListItemsOptions): Promise<CatalogListResponse> {\n return this.request(\n 'GET',\n '/catalog/items',\n undefined,\n this.qs({\n page: opts?.page,\n limit: opts?.limit,\n category: opts?.category,\n search: opts?.search,\n availability: opts?.availability,\n }),\n );\n }\n\n getCategories(): Promise<string[]> {\n return this.request('GET', '/catalog/items/categories');\n }\n\n getItem(externalId: string): Promise<CatalogItem> {\n return this.request('GET', `/catalog/items/${encodeURIComponent(externalId)}`);\n }\n\n /* \u2500\u2500 Event tracking \u2500\u2500 */\n\n trackView(\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n this.enqueueInteraction('view', itemId, scenarioSlugOrOpts, opts);\n }\n\n trackCart(\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n this.enqueueInteraction('cart', itemId, scenarioSlugOrOpts, opts);\n }\n\n trackPurchase(\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n this.enqueueInteraction('purchase', itemId, scenarioSlugOrOpts, opts);\n }\n\n private enqueueInteraction(\n type: 'view' | 'cart' | 'purchase',\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n const fromObject = typeof scenarioSlugOrOpts === 'object' ? scenarioSlugOrOpts : undefined;\n const scenarioSlug =\n typeof scenarioSlugOrOpts === 'string' ? scenarioSlugOrOpts : fromObject?.scenarioSlug;\n const country = this.resolveCountry(fromObject?.country ?? opts?.country);\n this.fireAndForgetQueue.enqueue({\n path: '/ingestion/interactions',\n method: 'POST',\n payload: {\n interactions: [{\n userId: this.effectiveUserId(),\n itemId,\n type,\n scenarioSlug: scenarioSlug || undefined,\n timestamp: new Date().toISOString(),\n country,\n }],\n },\n });\n }\n\n /* \u2500\u2500 Impression tracking \u2500\u2500 */\n\n trackImpressions(container: Element, opts?: ImpressionTrackerOptions): () => void {\n return this.tracker.observe(container, opts);\n }\n\n /* \u2500\u2500 Identity \u2500\u2500 */\n\n setUserId(userId: string): void {\n this.userId = userId;\n }\n\n setCountry(code: string): void {\n this.country = normalizeClientCountry(code);\n }\n\n mergeIdentity(authenticatedUserId: string): Promise<unknown> {\n const anonId = this.sessionId;\n this.userId = authenticatedUserId;\n // Note: never auto-retried (see NEVER_RETRY_PATHS) \u2014 callers should\n // handle 5xx/network failures themselves to avoid duplicate merges.\n return this.request('POST', '/identity/merge', {\n userId: authenticatedUserId,\n sessionIds: [anonId],\n });\n }\n\n /* \u2500\u2500 Lifecycle \u2500\u2500 */\n\n destroy(): void {\n this.tracker.destroy();\n this.fireAndForgetQueue.destroy();\n }\n}\n\n// Shape used by both the SDK's trackView/trackCart/trackPurchase calls\n// and the ImpressionTracker \u2014 they all funnel through the same queue.\nexport type FireAndForgetPayload = {\n interactions?: unknown[];\n impressions?: unknown[];\n};\n\nfunction renderQs(params?: Record<string, string | number | boolean | undefined>): string {\n if (!params) return '';\n const parts: string[] = [];\n for (const [k, v] of Object.entries(params)) {\n // Preserve empty string (GET clear sentinels). Drop only omit/null.\n if (v === undefined || v === null) continue;\n parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);\n }\n return parts.length ? `?${parts.join('&')}` : '';\n}\n\nasync function extractErrorCode(res: Response): Promise<string | undefined> {\n try {\n const body = (await res.json()) as { errorCode?: unknown };\n return typeof body.errorCode === 'string' ? body.errorCode : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n"],
5
- "mappings": "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",
6
- "names": ["canonicalJson", "value", "stringify", "keys", "parts", "k", "v", "KEY_CACHE", "sign", "input", "verb", "path", "effectivePath", "timestamp", "bodyHash", "sha256Hex", "canonicalJson", "others", "k", "v", "sortedOthers", "a", "b", "tsParam", "sigParam", "trailing", "queryForSigning", "signingString", "signature", "hmacSha256Hex", "makeIdempotencyKey", "bytes", "toHex", "token", "payload", "key", "getKey", "sig", "cached", "preloadSignerKey", "out", "i", "stripped", "c", "rand", "hex", "DEFAULTS", "RetryQueue", "cfg", "openIndexedDb", "item", "enriched", "makeId", "now", "ready", "i", "url", "headers", "res", "err", "exp", "jitter", "id", "db", "tx", "txDone", "req", "data", "resolve", "reject", "c", "normalizeClientCountry", "raw", "code", "ImpressionTracker", "_baseUrl", "_tenantId", "getUserId", "getSessionId", "retryQueue", "getCountry", "container", "opts", "selector", "threshold", "once", "scenario", "country", "entries", "entry", "el", "itemId", "key", "urgent", "batch", "grouped", "group", "impressions", "itemIds", "scenarioSlug", "SESSION_KEY", "SYNC_RETRIES", "SYNC_INITIAL_BACKOFF_MS", "SYNC_MAX_BACKOFF_MS", "RETRYABLE_STATUS", "AUTH_REPLAY_CACHE_UNAVAILABLE", "NEVER_RETRY_PATHS", "RecomnextBrowserClient", "config", "normalizeClientCountry", "preloadSignerKey", "RetryQueue", "item", "ImpressionTracker", "sid", "override", "method", "path", "body", "query", "maxRetries", "lastErr", "attempt", "exp", "jitter", "sleep", "url", "headers", "res", "err", "code", "extractErrorCode", "text", "baseHeaders", "qs", "renderQs", "signed", "sign", "params", "opts", "join", "v", "itemId", "itemIds", "externalId", "scenarioSlugOrOpts", "type", "fromObject", "scenarioSlug", "country", "container", "userId", "authenticatedUserId", "anonId", "parts", "k", "ms", "r"]
3
+ "sources": ["../../shared-browser-auth/src/canonical-json.ts", "../../shared-browser-auth/src/sign.ts", "../../shared-browser-auth/src/retry-queue.ts", "../../shared-browser-auth/src/session.ts", "../../shared-browser-auth/src/sync-retry-fetch.ts", "../../shared-browser-auth/src/identity-coordinator.ts", "../src/country.ts", "../src/tracker.ts", "../src/client.ts"],
4
+ "sourcesContent": ["// Canonical JSON serializer \u2014 byte-identical with the engine, the Node\n// server SDK, and the Python SDK. See repo-root\n// `tests/auth/canonical-vectors.json` for the conformance gate.\n\nexport function canonicalJson(value: unknown): string {\n return stringify(value);\n}\n\nfunction stringify(value: unknown): string {\n if (value === null) return 'null';\n switch (typeof value) {\n case 'boolean':\n return value ? 'true' : 'false';\n case 'number':\n if (!Number.isFinite(value)) {\n throw new TypeError(\n `Cannot canonicalize non-finite number: ${String(value)}`,\n );\n }\n return JSON.stringify(value);\n case 'string':\n return JSON.stringify(value);\n case 'object': {\n if (Array.isArray(value)) {\n return '[' + value.map(stringify).join(',') + ']';\n }\n const keys = Object.keys(value as Record<string, unknown>).sort();\n const parts: string[] = [];\n for (const k of keys) {\n const v = (value as Record<string, unknown>)[k];\n if (typeof v === 'undefined') continue;\n parts.push(JSON.stringify(k) + ':' + stringify(v));\n }\n return '{' + parts.join(',') + '}';\n }\n case 'undefined':\n throw new TypeError('Cannot canonicalize undefined at the root');\n default:\n throw new TypeError(\n `Cannot canonicalize value of type ${typeof value}`,\n );\n }\n}\n", "// WebCrypto-based HMAC-SHA256 signing for browser-side recomnext callers.\n// Same wire format as the server SDK (sdks/node/src/signing.ts) and the\n// engine guard (engine/src/auth/guards/hmac.guard.ts).\n//\n// Public vs private token contract:\n// - PUBLIC token: shipped in JS bundles. Wire uses `frontend_sign` +\n// `frontend_timestamp` query params. Engine rejects unless the request\n// origin is on the tenant's allow-list AND the route is on the\n// public-write whitelist (or is a GET).\n// - PRIVATE token: NEVER ship to a browser. WebCrypto signing of private\n// tokens is provided here for completeness (e.g. for Node-via-bundler\n// consumers), but the recomnext browser SDK refuses to accept a\n// privateToken in its config.\n\nimport { canonicalJson } from './canonical-json.js';\n\nexport type SignerMode = 'public' | 'private';\n\nexport interface SignInput {\n method: string;\n // Path WITHOUT querystring or trailing slash.\n path: string;\n // Caller-supplied query params; values coerced to strings.\n query?: Record<string, string | number | boolean | undefined>;\n // Parsed body \u2014 undefined for GET/DELETE/HEAD/OPTIONS.\n body?: unknown;\n tenantId: string;\n token: string;\n mode: SignerMode;\n // Test-only clock override.\n now?: number;\n}\n\nexport interface SignedRequest {\n signedPath: string;\n timestamp: number;\n bodyHash: string;\n signature: string;\n idempotencyKey: string;\n}\n\n// Cached CryptoKey objects keyed by raw token bytes. importKey() is async\n// and non-trivial; pages calling sign() in a hot loop (e.g. mouse-move\n// telemetry) would otherwise pay the cost on every event.\nconst KEY_CACHE = new Map<string, Promise<CryptoKey>>();\n\nexport async function sign(input: SignInput): Promise<SignedRequest> {\n const verb = input.method.toUpperCase();\n const path = effectivePath(input.path);\n const timestamp = input.now ?? Math.floor(Date.now() / 1000);\n\n const bodyHash =\n verb === 'GET' || verb === 'DELETE' || verb === 'HEAD' || verb === 'OPTIONS'\n ? ''\n : await sha256Hex(canonicalJson(input.body ?? null));\n\n // Preserve empty-string values (e.g. filter= / itemsPerSegment= clear\n // sentinels for request-time overrides). Only drop undefined/null omit.\n // Must stay lock-step with engine HmacGuard + node/python signers.\n const others = new Map<string, string>();\n for (const [k, v] of Object.entries(input.query ?? {})) {\n if (v === undefined || v === null) continue;\n others.set(k, String(v));\n }\n others.delete('hmac_timestamp');\n others.delete('frontend_timestamp');\n others.delete('body_hash');\n others.delete('hmac_sign');\n others.delete('frontend_sign');\n const sortedOthers = Array.from(others.entries())\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`);\n\n const tsParam = input.mode === 'public' ? 'frontend_timestamp' : 'hmac_timestamp';\n const sigParam = input.mode === 'public' ? 'frontend_sign' : 'hmac_sign';\n\n const trailing = `${tsParam}=${timestamp}&body_hash=${bodyHash}`;\n const queryForSigning = sortedOthers.length\n ? `${sortedOthers.join('&')}&${trailing}`\n : trailing;\n\n const signingString = `${verb}\\n${path}?${queryForSigning}\\nX-Tenant-Id:${input.tenantId}`;\n const signature = await hmacSha256Hex(input.token, signingString);\n const signedPath = `${path}?${queryForSigning}&${sigParam}=${signature}`;\n\n return {\n signedPath,\n timestamp,\n bodyHash,\n signature,\n idempotencyKey: makeIdempotencyKey(),\n };\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const bytes = await crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(input),\n );\n return toHex(new Uint8Array(bytes));\n}\n\nasync function hmacSha256Hex(token: string, payload: string): Promise<string> {\n const key = await getKey(token);\n const sig = await crypto.subtle.sign(\n 'HMAC',\n key,\n new TextEncoder().encode(payload),\n );\n return toHex(new Uint8Array(sig));\n}\n\nasync function getKey(token: string): Promise<CryptoKey> {\n let cached = KEY_CACHE.get(token);\n if (!cached) {\n cached = crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(token),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n KEY_CACHE.set(token, cached);\n }\n return cached;\n}\n\n// Preload the CryptoKey ahead of any sign() calls. Useful at SDK init time\n// so the first user interaction doesn't pay the importKey cost on the hot\n// path. Returns immediately on a second call for the same token.\nexport function preloadSignerKey(token: string): Promise<CryptoKey> {\n return getKey(token);\n}\n\nfunction toHex(bytes: Uint8Array): string {\n let out = '';\n for (let i = 0; i < bytes.length; i++) {\n out += bytes[i].toString(16).padStart(2, '0');\n }\n return out;\n}\n\nfunction effectivePath(path: string): string {\n const stripped = path.replace(/\\/$/, '');\n return stripped.length > 0 ? stripped : '/';\n}\n\nfunction makeIdempotencyKey(): string {\n const c = (globalThis as unknown as { crypto?: Crypto & { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n // Fallback for older WebView shells without randomUUID.\n const rand = new Uint8Array(16);\n c?.getRandomValues(rand);\n rand[6] = (rand[6] & 0x0f) | 0x40;\n rand[8] = (rand[8] & 0x3f) | 0x80;\n const hex = toHex(rand);\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n", "// Durable retry queue for browser fire-and-forget endpoints (impressions,\n// interactions). Per the auth-rollout plan:\n// - Synchronous retries (3 attempts, ~5s) for request-response calls\n// happen inline in the SDK; THIS file is exclusively for the\n// fire-and-forget endpoints (/ingestion/impressions,\n// /ingestion/interactions).\n// - `/identity/merge` is not queued here; synchronous merge retries use\n// `fetchWithSyncRetries` / `RecomnextBrowserClient.request()` instead.\n// - Exponential backoff, capped at 10 minutes / 8 attempts. After the\n// cap, items are dropped and `onRetryDrop` is invoked for observability.\n// - Optional IndexedDB persistence via `persistRetries: true` so queued\n// items survive page reloads / device-sleep / network outages.\n//\n// Re-signing happens on EVERY attempt \u2014 the engine's replay cache rejects\n// the same signature twice, and the 30s signing window means a stale sig\n// is worse than no retry. The `signRequest` callback is invoked with the\n// item payload and must return a freshly-signed URL + idempotency key.\n\nexport interface QueuedItem<TPayload = unknown> {\n // Unique id used for dedupe / persistence. Auto-generated when not set.\n id?: string;\n // Logical path the item targets (used for routing on flush).\n path: string;\n // HTTP method. Currently every fire-and-forget endpoint is POST; the\n // field exists so the queue can also handle PUTs if needed later.\n method: string;\n // Payload sent as the JSON body. Must be canonical-JSON serializable.\n payload: TPayload;\n // Number of attempts so far (0 on first enqueue). The queue increments.\n attempts?: number;\n // Earliest wall-clock ms at which the next attempt should run. Updated\n // each time we back off.\n nextAttemptAt?: number;\n}\n\nexport interface RetryQueueConfig<TPayload = unknown> {\n // Persist queued items across page reloads. Disabled by default \u2014 the\n // demo / read-mostly integrations don't need it. Enabling pulls in an\n // IndexedDB-backed `idb-keyval`-style adapter (see indexed-db.ts).\n persist?: boolean;\n // Storage key for IndexedDB persistence (so multi-tenant pages don't\n // share queue state). Required when `persist: true`.\n persistKey?: string;\n // Maximum attempts before the item is dropped. Default 8.\n maxAttempts?: number;\n // Initial backoff in ms. Default 1000.\n initialBackoffMs?: number;\n // Backoff cap in ms. Default 600_000 (10m).\n maxBackoffMs?: number;\n // Periodic drain interval in ms. Default 2000.\n flushIntervalMs?: number;\n // Called for every item dropped after exhausting attempts. Used for\n // observability \u2014 log to Sentry / surface as a tracker event.\n onRetryDrop?: (item: QueuedItem<TPayload>, error: unknown) => void;\n // Signs each attempt's request fresh. Returns the absolute URL to POST\n // to plus any auth headers (X-Idempotency-Key, etc.). Must NOT cache\n // its return value \u2014 the queue calls this once per attempt.\n signRequest: (item: QueuedItem<TPayload>) => Promise<{\n url: string;\n headers: Record<string, string>;\n }>;\n}\n\nconst DEFAULTS = {\n maxAttempts: 8,\n initialBackoffMs: 1000,\n maxBackoffMs: 600_000,\n flushIntervalMs: 2000,\n};\n\nexport class RetryQueue<TPayload = unknown> {\n private readonly cfg: Required<\n Omit<RetryQueueConfig<TPayload>, 'persist' | 'persistKey' | 'onRetryDrop'>\n > & {\n persist: boolean;\n persistKey?: string;\n onRetryDrop?: RetryQueueConfig<TPayload>['onRetryDrop'];\n };\n private items: QueuedItem<TPayload>[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private draining = false;\n private idb?: Promise<IDBDatabase | null>;\n\n constructor(cfg: RetryQueueConfig<TPayload>) {\n this.cfg = {\n persist: cfg.persist ?? false,\n persistKey: cfg.persistKey,\n maxAttempts: cfg.maxAttempts ?? DEFAULTS.maxAttempts,\n initialBackoffMs: cfg.initialBackoffMs ?? DEFAULTS.initialBackoffMs,\n maxBackoffMs: cfg.maxBackoffMs ?? DEFAULTS.maxBackoffMs,\n flushIntervalMs: cfg.flushIntervalMs ?? DEFAULTS.flushIntervalMs,\n onRetryDrop: cfg.onRetryDrop,\n signRequest: cfg.signRequest,\n };\n\n if (this.cfg.persist) {\n if (!this.cfg.persistKey) {\n throw new Error('RetryQueue: persistKey required when persist=true');\n }\n this.idb = openIndexedDb();\n void this.loadPersisted();\n }\n\n if (typeof window !== 'undefined') {\n this.timer = setInterval(() => void this.drain(), this.cfg.flushIntervalMs);\n window.addEventListener('online', () => void this.drain());\n // Best-effort flush on page hide \u2014 sendBeacon is the documented\n // companion for items we never want to drop.\n window.addEventListener('pagehide', () => void this.drain());\n }\n }\n\n enqueue(item: QueuedItem<TPayload>): void {\n const enriched: QueuedItem<TPayload> = {\n ...item,\n id: item.id ?? makeId(),\n attempts: item.attempts ?? 0,\n nextAttemptAt: item.nextAttemptAt ?? Date.now(),\n };\n this.items.push(enriched);\n void this.persist();\n // Eager drain attempt \u2014 useful when the page is interactive and the\n // queued event should leave the page within a hundred ms or two.\n void this.drain();\n }\n\n // Manually trigger a drain cycle. Used by callers on critical pageviews\n // (e.g. checkout) where dropping the next backoff window matters.\n flush(): Promise<void> {\n return this.drain();\n }\n\n destroy(): void {\n if (this.timer) clearInterval(this.timer);\n this.timer = null;\n }\n\n private async drain(): Promise<void> {\n if (this.draining) return;\n if (typeof navigator !== 'undefined' && navigator.onLine === false) return;\n this.draining = true;\n try {\n const now = Date.now();\n const ready = this.items.filter((i) => (i.nextAttemptAt ?? 0) <= now);\n for (const item of ready) {\n await this.attempt(item);\n }\n await this.persist();\n } finally {\n this.draining = false;\n }\n }\n\n private async attempt(item: QueuedItem<TPayload>): Promise<void> {\n item.attempts = (item.attempts ?? 0) + 1;\n try {\n const { url, headers } = await this.cfg.signRequest(item);\n const res = await fetch(url, {\n method: item.method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n body: JSON.stringify(item.payload),\n keepalive: true,\n });\n if (res.ok) {\n this.removeById(item.id!);\n return;\n }\n // 4xx other than 429 is permanent \u2014 no amount of retry fixes a bad\n // payload. Drop immediately so we don't keep poisoning the queue.\n if (res.status >= 400 && res.status < 500 && res.status !== 429) {\n this.removeById(item.id!);\n this.cfg.onRetryDrop?.(item, new Error(`HTTP ${res.status}`));\n return;\n }\n this.scheduleRetry(item, new Error(`HTTP ${res.status}`));\n } catch (err) {\n this.scheduleRetry(item, err);\n }\n }\n\n private scheduleRetry(item: QueuedItem<TPayload>, err: unknown): void {\n if ((item.attempts ?? 0) >= this.cfg.maxAttempts) {\n this.removeById(item.id!);\n this.cfg.onRetryDrop?.(item, err);\n return;\n }\n const exp = Math.min(\n this.cfg.maxBackoffMs,\n this.cfg.initialBackoffMs * Math.pow(2, (item.attempts ?? 1) - 1),\n );\n // Decorrelated jitter \u2014 full backoff with a uniformly-distributed\n // multiplier in [0.5, 1.0) so multiple replicas don't sync into a\n // thundering herd when the backend recovers.\n const jitter = 0.5 + Math.random() * 0.5;\n item.nextAttemptAt = Date.now() + Math.floor(exp * jitter);\n }\n\n private removeById(id: string): void {\n this.items = this.items.filter((i) => i.id !== id);\n }\n\n // \u2500\u2500 IndexedDB persistence \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async persist(): Promise<void> {\n if (!this.cfg.persist || !this.idb || !this.cfg.persistKey) return;\n try {\n const db = await this.idb;\n if (!db) return;\n const tx = db.transaction('queue', 'readwrite');\n const store = tx.objectStore('queue');\n store.put(JSON.parse(JSON.stringify(this.items)), this.cfg.persistKey);\n await txDone(tx);\n } catch {\n // Persistence is best-effort \u2014 never throw out of an enqueue path.\n }\n }\n\n private async loadPersisted(): Promise<void> {\n if (!this.cfg.persist || !this.idb || !this.cfg.persistKey) return;\n try {\n const db = await this.idb;\n if (!db) return;\n const tx = db.transaction('queue', 'readonly');\n const store = tx.objectStore('queue');\n const req = store.get(this.cfg.persistKey);\n const data: unknown = await new Promise((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n if (Array.isArray(data)) {\n this.items = data as QueuedItem<TPayload>[];\n void this.drain();\n }\n } catch {\n // Lost-persistence is silent by design \u2014 don't break enqueue on\n // private-browsing / disabled-storage.\n }\n }\n}\n\nfunction makeId(): string {\n const c = (globalThis as unknown as { crypto?: Crypto & { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n return `rnx-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;\n}\n\nfunction openIndexedDb(): Promise<IDBDatabase | null> {\n if (typeof indexedDB === 'undefined') return Promise.resolve(null);\n return new Promise((resolve) => {\n const req = indexedDB.open('recomnext-retry-queue', 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains('queue')) db.createObjectStore('queue');\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => resolve(null);\n req.onblocked = () => resolve(null);\n });\n}\n\nfunction txDone(tx: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error);\n });\n}\n", "/** localStorage key for the anonymous RecomNext session id (shared across tabs). */\nexport const RECOMNEXT_SESSION_KEY = 'recomnext_session_id';\n\nexport type BrowserStorage = {\n localStorage: Storage | undefined;\n sessionStorage: Storage | undefined;\n};\n\nfunction defaultStorage(): BrowserStorage {\n return {\n localStorage:\n typeof globalThis !== 'undefined' && typeof localStorage !== 'undefined'\n ? localStorage\n : undefined,\n sessionStorage:\n typeof globalThis !== 'undefined' && typeof sessionStorage !== 'undefined'\n ? sessionStorage\n : undefined,\n };\n}\n\nexport function generateAnonymousSessionId(): string {\n return `sess_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;\n}\n\n/**\n * Resolve or create the anonymous session id. Stored in localStorage so all\n * tabs on the origin share one bucket. One-release migration: if local is\n * empty, copy from sessionStorage then remove the legacy key.\n */\nexport function resolveAnonymousSessionId(\n storage: BrowserStorage = defaultStorage(),\n): string {\n const { localStorage: ls, sessionStorage: ss } = storage;\n if (!ls) return '';\n\n const fromLocal = ls.getItem(RECOMNEXT_SESSION_KEY);\n if (fromLocal) return fromLocal;\n\n if (ss) {\n const legacy = ss.getItem(RECOMNEXT_SESSION_KEY);\n if (legacy) {\n ls.setItem(RECOMNEXT_SESSION_KEY, legacy);\n ss.removeItem(RECOMNEXT_SESSION_KEY);\n return legacy;\n }\n }\n\n const sid = generateAnonymousSessionId();\n ls.setItem(RECOMNEXT_SESSION_KEY, sid);\n return sid;\n}\n\n/** Read without creating a new id (includes legacy sessionStorage). */\nexport function readAnonymousSessionId(\n storage: BrowserStorage = defaultStorage(),\n): string {\n const { localStorage: ls, sessionStorage: ss } = storage;\n if (ls) {\n const fromLocal = ls.getItem(RECOMNEXT_SESSION_KEY);\n if (fromLocal) return fromLocal;\n }\n if (ss) return ss.getItem(RECOMNEXT_SESSION_KEY) ?? '';\n return '';\n}\n\n/**\n * Rotate anonymous session (logout hygiene). Removes legacy sessionStorage key,\n * writes the next id to localStorage, and returns that id.\n */\nexport function clearAnonymousSession(\n customId?: string,\n storage: BrowserStorage = defaultStorage(),\n): string {\n const { localStorage: ls, sessionStorage: ss } = storage;\n const next =\n customId !== undefined && customId !== ''\n ? customId\n : generateAnonymousSessionId();\n\n if (ls) ls.setItem(RECOMNEXT_SESSION_KEY, next);\n if (ss) ss.removeItem(RECOMNEXT_SESSION_KEY);\n return next;\n}\n", "const SYNC_RETRIES = 3;\nconst SYNC_INITIAL_BACKOFF_MS = 200;\nconst SYNC_MAX_BACKOFF_MS = 2_000;\n\nconst RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);\nconst AUTH_REPLAY_CACHE_UNAVAILABLE = 'auth_replay_cache_unavailable';\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\nasync function extractErrorCode(res: Response): Promise<string | undefined> {\n try {\n const body = (await res.json()) as { errorCode?: unknown };\n return typeof body.errorCode === 'string' ? body.errorCode : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Mirrors {@link RecomnextBrowserClient} synchronous retry budget for identity/merge.\n * Caller must re-sign on each attempt via {@link performFetch}.\n */\nexport async function fetchWithSyncRetries(\n method: string,\n path: string,\n performFetch: () => Promise<Response>,\n): Promise<Response> {\n let lastErr: unknown;\n\n for (let attempt = 0; attempt <= SYNC_RETRIES; attempt++) {\n if (attempt > 0) {\n const exp = Math.min(\n SYNC_MAX_BACKOFF_MS,\n SYNC_INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1),\n );\n const jitter = 0.5 + Math.random() * 0.5;\n await sleep(Math.floor(exp * jitter));\n }\n\n let res: Response;\n try {\n res = await performFetch();\n } catch (err) {\n lastErr = err;\n if (attempt >= SYNC_RETRIES) throw err;\n continue;\n }\n\n if (res.status === 503) {\n const code = await extractErrorCode(res.clone());\n if (code === AUTH_REPLAY_CACHE_UNAVAILABLE && attempt < SYNC_RETRIES) {\n lastErr = new Error(`recomnext: ${method} ${path} ${res.status} ${code}`);\n continue;\n }\n }\n\n if (RETRYABLE_STATUS.has(res.status) && attempt < SYNC_RETRIES) {\n lastErr = new Error(`recomnext: ${method} ${path} returned ${res.status}`);\n continue;\n }\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new Error(`recomnext: ${method} ${path} returned ${res.status}: ${text}`);\n }\n\n return res;\n }\n\n throw lastErr ?? new Error(`recomnext: ${method} ${path} retries exhausted`);\n}\n", "import { sign, preloadSignerKey } from './sign.js';\nimport { fetchWithSyncRetries } from './sync-retry-fetch.js';\nimport {\n clearAnonymousSession,\n readAnonymousSessionId,\n resolveAnonymousSessionId,\n} from './session.js';\n\nexport type IdentityListener = (state: IdentityState) => void;\n\nexport interface IdentityState {\n authenticatedUserId: string | null;\n anonymousSessionId: string;\n effectiveUserId: string;\n}\n\nexport interface IdentityCoordinatorConfig {\n tenantId: string;\n baseUrl: string;\n publicToken?: string;\n /** Host-owned anonymous id (e.g. Driffle `sess_{device_uid}`) when not logged in. */\n getHostAnonymousUserId?: () => string;\n /**\n * When {@link getHostAnonymousUserId} is set, call on logout to rotate the host id\n * (e.g. new `device_uid` cookie). The merge ledger is one-shot per anonymous id.\n */\n onHostLogout?: () => void | Promise<void>;\n /** Mirror authenticated user id to localStorage for other tabs (default true). */\n syncAuthAcrossTabs?: boolean;\n}\n\n/** localStorage key for cross-tab authenticated user sync. */\nexport const RECOMNEXT_AUTH_SYNC_KEY = 'recomnext_authenticated_user_id';\n\nlet config: IdentityCoordinatorConfig | null = null;\nlet authenticatedUserId: string | null = null;\nconst listeners = new Set<IdentityListener>();\nconst mergeInFlight = new Map<string, Promise<void>>();\nconst authTransitionInFlight = new Map<string, Promise<void>>();\nlet suppressAuthSyncWrite = false;\nlet storageListenerInstalled = false;\n\nfunction normalizeBaseUrl(baseUrl: string): string {\n return baseUrl.replace(/\\/+$/, '');\n}\n\nfunction hostAnonymousId(): string {\n const id = config?.getHostAnonymousUserId?.() ?? '';\n return typeof id === 'string' ? id.trim() : '';\n}\n\nfunction resolveEffectiveUserId(): string {\n if (authenticatedUserId) {\n return authenticatedUserId;\n }\n const host = hostAnonymousId();\n if (host) return host;\n const stored = readAnonymousSessionId();\n if (stored) return stored;\n return resolveAnonymousSessionId();\n}\n\nfunction snapshotState(): IdentityState {\n return {\n authenticatedUserId,\n anonymousSessionId: readAnonymousSessionId(),\n effectiveUserId: resolveEffectiveUserId(),\n };\n}\n\nfunction notify(): void {\n const state = snapshotState();\n for (const listener of listeners) {\n listener(state);\n }\n}\n\nfunction mergeKey(tenantId: string, anonId: string, knownUserId: string): string {\n return `${tenantId}:${anonId}:${knownUserId}`;\n}\n\nfunction authTransitionKey(nextId: string | null): string {\n return nextId === null ? 'logout' : `login:${nextId}`;\n}\n\nfunction writeAuthSync(userId: string | null): void {\n if (suppressAuthSyncWrite || !config) return;\n if (config.syncAuthAcrossTabs === false) return;\n if (typeof localStorage === 'undefined') return;\n\n if (userId === null) {\n localStorage.removeItem(RECOMNEXT_AUTH_SYNC_KEY);\n } else {\n localStorage.setItem(RECOMNEXT_AUTH_SYNC_KEY, userId);\n }\n}\n\nfunction readAuthSync(): string | null {\n if (typeof localStorage === 'undefined') return null;\n const raw = localStorage.getItem(RECOMNEXT_AUTH_SYNC_KEY);\n if (!raw || raw.trim() === '') return null;\n return raw.trim();\n}\n\nasync function postMerge(knownUserId: string, anonId: string): Promise<void> {\n if (!config) return;\n if (!anonId || anonId === knownUserId) return;\n\n const key = mergeKey(config.tenantId, anonId, knownUserId);\n const existing = mergeInFlight.get(key);\n if (existing) {\n await existing;\n return;\n }\n\n const job = (async () => {\n const path = '/identity/merge';\n const body = { userId: knownUserId, sessionIds: [anonId] };\n const baseUrl = normalizeBaseUrl(config!.baseUrl);\n\n await fetchWithSyncRetries('POST', path, async () => {\n if (config!.publicToken) {\n await preloadSignerKey(config!.publicToken);\n const signed = await sign({\n method: 'POST',\n path,\n body,\n tenantId: config!.tenantId,\n token: config!.publicToken,\n mode: 'public',\n });\n return fetch(`${baseUrl}${signed.signedPath}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Tenant-Id': config!.tenantId,\n 'X-Idempotency-Key': signed.idempotencyKey,\n },\n body: JSON.stringify(body),\n });\n }\n\n return fetch(`${baseUrl}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Tenant-Id': config!.tenantId,\n },\n body: JSON.stringify(body),\n });\n });\n })();\n\n mergeInFlight.set(key, job);\n try {\n await job;\n } finally {\n mergeInFlight.delete(key);\n }\n}\n\nfunction assertConfigured(): IdentityCoordinatorConfig {\n if (!config) {\n throw new Error('RecomnextIdentity.configure() must be called before use');\n }\n return config;\n}\n\nasync function runAuthTransition(\n key: string,\n fn: () => Promise<void>,\n): Promise<void> {\n const existing = authTransitionInFlight.get(key);\n if (existing) {\n await existing;\n return;\n }\n\n const job = fn();\n authTransitionInFlight.set(key, job);\n try {\n await job;\n } finally {\n authTransitionInFlight.delete(key);\n }\n}\n\ntype AuthApplySource = 'local' | 'peer-sync';\n\nasync function applyAuthenticatedUserId(\n nextId: string | null,\n source: AuthApplySource = 'local',\n): Promise<void> {\n assertConfigured();\n const skipMerge = source === 'peer-sync';\n const skipHostLogout = source === 'peer-sync';\n\n const normalized =\n nextId === null || nextId === undefined ? null : String(nextId).trim();\n const targetId = normalized === '' ? null : normalized;\n\n if (authenticatedUserId === targetId) {\n return;\n }\n\n const prevAuth = authenticatedUserId;\n\n if (targetId === null) {\n if (!skipHostLogout && config!.getHostAnonymousUserId && config!.onHostLogout) {\n await config!.onHostLogout();\n }\n clearAnonymousSession();\n authenticatedUserId = null;\n writeAuthSync(null);\n notify();\n return;\n }\n\n if (prevAuth !== null) {\n const anonBeforeClear =\n readAnonymousSessionId() || hostAnonymousId() || '';\n clearAnonymousSession();\n if (!skipMerge && anonBeforeClear) {\n await postMerge(targetId, anonBeforeClear);\n }\n authenticatedUserId = targetId;\n writeAuthSync(targetId);\n notify();\n return;\n }\n\n const anonForMerge = resolveEffectiveUserId();\n if (!skipMerge && anonForMerge) {\n await postMerge(targetId, anonForMerge);\n }\n authenticatedUserId = targetId;\n writeAuthSync(targetId);\n notify();\n}\n\nfunction installStorageListener(): void {\n if (storageListenerInstalled || typeof window === 'undefined') return;\n storageListenerInstalled = true;\n\n window.addEventListener('storage', (event) => {\n if (!config || config.syncAuthAcrossTabs === false) return;\n if (event.key !== RECOMNEXT_AUTH_SYNC_KEY) return;\n if (event.storageArea !== localStorage) return;\n\n const peerUserId =\n event.newValue === null || event.newValue === undefined\n ? null\n : event.newValue.trim() === ''\n ? null\n : event.newValue.trim();\n\n if (peerUserId === authenticatedUserId) return;\n\n suppressAuthSyncWrite = true;\n void applyAuthenticatedUserId(peerUserId, 'peer-sync').finally(() => {\n suppressAuthSyncWrite = false;\n });\n });\n}\n\nfunction hydrateAuthFromStorage(): void {\n if (!config || config.syncAuthAcrossTabs === false) return;\n const synced = readAuthSync();\n if (synced !== authenticatedUserId) {\n suppressAuthSyncWrite = true;\n void applyAuthenticatedUserId(synced, 'peer-sync').finally(() => {\n suppressAuthSyncWrite = false;\n });\n }\n}\n\n/** @internal Test reset */\nexport function resetIdentityCoordinatorForTests(): void {\n config = null;\n authenticatedUserId = null;\n listeners.clear();\n mergeInFlight.clear();\n authTransitionInFlight.clear();\n suppressAuthSyncWrite = false;\n storageListenerInstalled = false;\n}\n\nexport const RecomnextIdentity = {\n configure(coordinatorConfig: IdentityCoordinatorConfig): void {\n config = {\n ...coordinatorConfig,\n baseUrl: normalizeBaseUrl(coordinatorConfig.baseUrl),\n };\n if (config.publicToken) {\n void preloadSignerKey(config.publicToken);\n }\n installStorageListener();\n hydrateAuthFromStorage();\n },\n\n isConfigured(): boolean {\n return config !== null;\n },\n\n subscribe(listener: IdentityListener): () => void {\n listeners.add(listener);\n listener(snapshotState());\n return () => listeners.delete(listener);\n },\n\n getState(): IdentityState {\n if (!config) {\n return {\n authenticatedUserId: null,\n anonymousSessionId: readAnonymousSessionId(),\n effectiveUserId:\n readAnonymousSessionId() || resolveAnonymousSessionId(),\n };\n }\n return snapshotState();\n },\n\n async setAuthenticatedUserId(next: string | null): Promise<void> {\n assertConfigured();\n\n const normalized =\n next === null || next === undefined ? null : String(next).trim();\n const nextId = normalized === '' ? null : normalized;\n\n const transitionKey = authTransitionKey(nextId);\n const pending = authTransitionInFlight.get(transitionKey);\n if (authenticatedUserId === nextId) {\n if (pending) await pending;\n return;\n }\n\n await runAuthTransition(transitionKey, () =>\n applyAuthenticatedUserId(nextId),\n );\n },\n};\n\nexport const RECOMNEXT_IDENTITY_DOM_EVENT = 'recomnext:identity';\n\nexport function installRecomnextIdentityDomListener(): () => void {\n if (typeof document === 'undefined') {\n return () => undefined;\n }\n\n const handler = (event: Event) => {\n if (!RecomnextIdentity.isConfigured()) return;\n const detail = (event as CustomEvent<{ userId?: string | null }>).detail;\n const raw = detail?.userId;\n const userId =\n raw === null || raw === undefined\n ? null\n : String(raw).trim() === ''\n ? null\n : String(raw).trim();\n void RecomnextIdentity.setAuthenticatedUserId(userId);\n };\n\n document.addEventListener(RECOMNEXT_IDENTITY_DOM_EVENT, handler);\n return () => document.removeEventListener(RECOMNEXT_IDENTITY_DOM_EVENT, handler);\n}\n", "/**\n * Client-side country hygiene. Server allowlist is still the source of\n * truth \u2014 unknown 2-letter codes are dropped here only when they are not\n * ISO-2 shaped (OTH / empty / non-alpha). Valid-looking codes go on the\n * wire; the engine maps anything not in country.ts to global.\n */\nexport function normalizeClientCountry(\n raw?: string | null,\n): string | undefined {\n if (raw == null) return undefined;\n const code = String(raw).trim().toUpperCase();\n if (!code || code === 'OTH' || !/^[A-Z]{2}$/.test(code)) return undefined;\n return code;\n}\n", "import type { RetryQueue } from '@recomnext/shared-browser-auth';\nimport type { FireAndForgetPayload } from './client';\nimport type { ImpressionTrackerOptions } from './types';\n\nconst FLUSH_INTERVAL = 2000;\n\nexport class ImpressionTracker {\n private queue: { itemId: string; scenario: string; country?: string }[] = [];\n private flushing = false;\n private observer: IntersectionObserver | null = null;\n private seen = new Set<string>();\n private timer: ReturnType<typeof setInterval> | null = null;\n private readonly getUserId: () => string;\n private readonly getSessionId: () => string;\n private readonly getCountry: () => string | undefined;\n // The RetryQueue handles all signed POSTs to /ingestion/impressions\n // (single source of truth for fire-and-forget on this page). We still\n // batch impressions every 2s before enqueuing so we don't fan out N\n // signed requests for one viewport scan.\n private readonly retryQueue: RetryQueue<FireAndForgetPayload>;\n\n constructor(\n _baseUrl: string,\n _tenantId: string,\n getUserId: () => string,\n getSessionId: () => string,\n retryQueue: RetryQueue<FireAndForgetPayload>,\n getCountry?: () => string | undefined,\n ) {\n // baseUrl + tenantId are captured by the client during signRequest()\n // and no longer needed at the tracker level. Kept in the signature\n // for backwards compatibility with anyone constructing the tracker\n // directly (rare \u2014 almost everyone uses RecomnextBrowserClient).\n this.getUserId = getUserId;\n this.getSessionId = getSessionId;\n this.retryQueue = retryQueue;\n this.getCountry = getCountry ?? (() => undefined);\n if (typeof window !== 'undefined') {\n this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL);\n // sendBeacon is best-effort and CANNOT carry the X-Idempotency-Key\n // header (it's a Blob POST). On pagehide we therefore force the\n // queue's normal signed-fetch path to drain via `keepalive: true`,\n // which is now widely supported in all evergreen browsers.\n window.addEventListener('pagehide', () => this.flush(true));\n }\n }\n\n observe(container: Element, opts: ImpressionTrackerOptions = {}): () => void {\n const selector = opts.itemSelector ?? '[data-item-id]';\n const threshold = opts.threshold ?? 0.5;\n const once = opts.once !== false;\n const scenario = opts.scenario ?? '';\n const country = opts.country || this.getCountry() || undefined;\n\n this.observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n const el = entry.target as HTMLElement;\n const itemId = el.dataset.itemId;\n if (!itemId) continue;\n const key = `${scenario}:${itemId}`;\n if (once && this.seen.has(key)) continue;\n this.seen.add(key);\n this.queue.push({ itemId, scenario, country });\n if (once) this.observer?.unobserve(el);\n }\n },\n { root: null, threshold },\n );\n\n const elements = container.querySelectorAll(selector);\n elements.forEach((el) => this.observer!.observe(el));\n\n return () => {\n this.observer?.disconnect();\n this.observer = null;\n };\n }\n\n // `urgent` is set during pagehide. When true we kick the RetryQueue\n // immediately so it has a chance to land items inside the unload window\n // (keepalive: true on the inner fetch keeps the connection alive past\n // the navigation).\n private flush(urgent = false): void {\n if (this.flushing || this.queue.length === 0) return;\n this.flushing = true;\n const batch = this.queue.splice(0, this.queue.length);\n\n const grouped = new Map<string, { itemIds: string[]; country?: string }>();\n for (const { itemId, scenario, country } of batch) {\n const key = `${scenario}\\0${country ?? ''}`;\n const group = grouped.get(key);\n if (group) {\n group.itemIds.push(itemId);\n } else {\n grouped.set(key, { itemIds: [itemId], country });\n }\n }\n\n const impressions = Array.from(grouped.entries()).map(([key, { itemIds, country }]) => {\n const scenarioSlug = key.split('\\0')[0];\n return {\n userId: this.getUserId() || undefined,\n sessionId: this.getSessionId() || undefined,\n itemIds,\n scenarioSlug: scenarioSlug || undefined,\n timestamp: new Date().toISOString(),\n country: country || undefined,\n };\n });\n\n this.retryQueue.enqueue({\n path: '/ingestion/impressions',\n method: 'POST',\n payload: { impressions },\n });\n\n if (urgent) void this.retryQueue.flush();\n this.flushing = false;\n }\n\n destroy(): void {\n this.flush();\n this.observer?.disconnect();\n if (this.timer) clearInterval(this.timer);\n }\n}\n", "import {\n preloadSignerKey,\n RetryQueue,\n sign as signRequest,\n resolveAnonymousSessionId,\n clearAnonymousSession,\n readAnonymousSessionId,\n RecomnextIdentity,\n type QueuedItem,\n} from '@recomnext/shared-browser-auth';\n\nimport type {\n SearchOptions,\n SearchResponse,\n SearchV2Options,\n SearchV2Response,\n SearchV2AutocompleteResponse,\n SearchV2SettingsResponse,\n CatalogItem,\n CatalogListResponse,\n ListItemsOptions,\n} from '@recomnext/shared-search-types';\n\nimport { normalizeClientCountry } from './country';\nimport { ImpressionTracker } from './tracker';\nimport type {\n BrowserClientConfig,\n RecommendationResult,\n RecommendationOptions,\n RecommendOptions,\n ImpressionTrackerOptions,\n TrackEventOptions,\n} from './types';\n\n// Synchronous retry budget for request-response calls (recommendations,\n// catalog reads, identity/merge). Mirrors the Node + Python SDK behavior:\n// 3 attempts total, ~5s aggregate, re-signs on every attempt.\nconst SYNC_RETRIES = 3;\nconst SYNC_INITIAL_BACKOFF_MS = 200;\nconst SYNC_MAX_BACKOFF_MS = 2_000;\n\n// 5xx + 429 are auto-retried. We special-case 503 +\n// errorCode=auth_replay_cache_unavailable so the SDK retries even if an\n// operator tunes 503 out of the retry set.\nconst RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);\nconst AUTH_REPLAY_CACHE_UNAVAILABLE = 'auth_replay_cache_unavailable';\n\nexport class RecomnextBrowserClient {\n private readonly baseUrl: string;\n private readonly tenantId: string;\n private readonly publicToken: string | undefined;\n private userId: string;\n private sessionId: string;\n private country: string | undefined;\n private readonly tracker: ImpressionTracker;\n // Lazy \u2014 only constructed once for fire-and-forget queues that the\n // tracker also reuses.\n readonly fireAndForgetQueue: RetryQueue<FireAndForgetPayload>;\n\n constructor(config: BrowserClientConfig) {\n this.baseUrl = config.baseUrl.replace(/\\/+$/, '');\n this.tenantId = config.tenantId;\n this.publicToken = config.publicToken;\n this.userId = config.userId ?? '';\n this.sessionId = config.sessionId ?? this.resolveSessionId();\n this.country = normalizeClientCountry(config.country);\n\n // Eagerly import the CryptoKey so the first signed event after page\n // load doesn't pay the importKey latency on the hot path.\n if (this.publicToken) void preloadSignerKey(this.publicToken);\n\n this.fireAndForgetQueue = new RetryQueue<FireAndForgetPayload>({\n persist: config.persistRetries ?? false,\n persistKey: `rnx:${this.tenantId}:fnf`,\n onRetryDrop: config.onRetryDrop,\n signRequest: (item) => this.signFireAndForget(item),\n });\n\n this.tracker = new ImpressionTracker(\n this.baseUrl,\n this.tenantId,\n () => this.userId,\n () => this.sessionId,\n this.fireAndForgetQueue,\n () => this.country,\n );\n }\n\n private resolveSessionId(): string {\n return resolveAnonymousSessionId();\n }\n\n private effectiveUserId(): string {\n return this.userId || this.sessionId;\n }\n\n private resolveCountry(override?: string): string | undefined {\n return normalizeClientCountry(override ?? this.country);\n }\n\n private async request<T>(\n method: string,\n path: string,\n body?: unknown,\n query?: Record<string, string | number | boolean | undefined>,\n ): Promise<T> {\n const maxRetries = SYNC_RETRIES;\n let lastErr: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n if (attempt > 0) {\n const exp = Math.min(\n SYNC_MAX_BACKOFF_MS,\n SYNC_INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1),\n );\n const jitter = 0.5 + Math.random() * 0.5;\n await sleep(Math.floor(exp * jitter));\n }\n\n // Always re-sign \u2014 the engine's replay cache rejects the same\n // signature twice, and the 30s signing window means a stale sig\n // would also fail.\n const { url, headers } = await this.buildSignedRequest(\n method,\n path,\n query,\n body,\n );\n\n let res: Response;\n try {\n res = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n } catch (err) {\n lastErr = err;\n if (attempt >= maxRetries) throw err;\n continue;\n }\n\n if (res.status === 503) {\n const code = await extractErrorCode(res.clone());\n if (code === AUTH_REPLAY_CACHE_UNAVAILABLE && attempt < maxRetries) {\n lastErr = new Error(`recomnext: ${method} ${path} ${res.status} ${code}`);\n continue;\n }\n }\n\n if (RETRYABLE_STATUS.has(res.status) && attempt < maxRetries) {\n lastErr = new Error(`recomnext: ${method} ${path} returned ${res.status}`);\n continue;\n }\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new Error(`recomnext: ${method} ${path} returned ${res.status}: ${text}`);\n }\n\n if (res.status === 204) return undefined as unknown as T;\n return res.json() as Promise<T>;\n }\n\n throw lastErr ?? new Error(`recomnext: ${method} ${path} retries exhausted`);\n }\n\n // Same shape as the RetryQueue's signRequest contract \u2014 used by the\n // queue *and* the impression tracker so both share a single signing\n // path.\n async signFireAndForget(item: QueuedItem<FireAndForgetPayload>) {\n const { url, headers } = await this.buildSignedRequest(\n item.method,\n item.path,\n undefined,\n item.payload,\n );\n return { url, headers };\n }\n\n private async buildSignedRequest(\n method: string,\n path: string,\n query: Record<string, string | number | boolean | undefined> | undefined,\n body: unknown,\n ): Promise<{ url: string; headers: Record<string, string> }> {\n const baseHeaders: Record<string, string> = {\n 'X-Tenant-Id': this.tenantId,\n };\n\n if (!this.publicToken) {\n // Legacy / no-auth mode. Engine accepts because\n // LEGACY_TENANT_AUTH=true. After Phase 7 cutover this path will be\n // rejected with 401.\n const qs = renderQs(query);\n return { url: `${this.baseUrl}${path}${qs}`, headers: baseHeaders };\n }\n\n const signed = await signRequest({\n method,\n path,\n query,\n body: body ?? null,\n tenantId: this.tenantId,\n token: this.publicToken,\n mode: 'public',\n });\n return {\n url: `${this.baseUrl}${signed.signedPath}`,\n headers: {\n ...baseHeaders,\n 'X-Idempotency-Key': signed.idempotencyKey,\n },\n };\n }\n\n private qs(\n params: Record<string, string | number | boolean | undefined>,\n ): Record<string, string | number | boolean | undefined> {\n return params;\n }\n\n private includeQuery(opts?: RecommendationOptions): {\n returnProperties?: boolean;\n includedProperties?: string;\n } {\n return {\n returnProperties: opts?.returnProperties,\n includedProperties:\n opts?.includedProperties && opts.includedProperties.length > 0\n ? opts.includedProperties.join(',')\n : undefined,\n };\n }\n\n /** Serialize override fields for GET query (comma-separated lists; '' clears). */\n private overrideQuery(opts?: RecommendationOptions): {\n filter?: string;\n boosters?: string;\n rulesSlugs?: string;\n segmentationId?: string;\n itemsPerSegment?: number | string;\n autoRelaxFilters?: boolean;\n relaxEntireFilterOnEmpty?: boolean;\n } {\n if (!opts) return {};\n const join = (v: string[] | null | undefined): string | undefined => {\n if (v === undefined) return undefined;\n if (v === null || v.length === 0) return '';\n return v.join(',');\n };\n return {\n filter:\n opts.filter === undefined\n ? undefined\n : opts.filter === null\n ? ''\n : opts.filter,\n boosters: join(opts.boosters),\n rulesSlugs: join(opts.rulesSlugs),\n segmentationId:\n opts.segmentationId === undefined\n ? undefined\n : opts.segmentationId === null\n ? ''\n : opts.segmentationId,\n // Present null \u2192 '' so the query param is sent; engine transform maps\n // '' \u2192 null \u2192 resolve to itemsPerSegment=1 (omit would keep scenario).\n itemsPerSegment:\n opts.itemsPerSegment === undefined\n ? undefined\n : opts.itemsPerSegment === null\n ? ''\n : opts.itemsPerSegment,\n autoRelaxFilters: opts.autoRelaxFilters,\n relaxEntireFilterOnEmpty: opts.relaxEntireFilterOnEmpty,\n };\n }\n\n private overrideBody(opts?: RecommendationOptions): {\n filter?: string | null;\n boosters?: string[] | null;\n rulesSlugs?: string[] | null;\n segmentationId?: string | null;\n itemsPerSegment?: number | null;\n autoRelaxFilters?: boolean;\n relaxEntireFilterOnEmpty?: boolean;\n } {\n if (!opts) return {};\n const body: {\n filter?: string | null;\n boosters?: string[] | null;\n rulesSlugs?: string[] | null;\n segmentationId?: string | null;\n itemsPerSegment?: number | null;\n autoRelaxFilters?: boolean;\n relaxEntireFilterOnEmpty?: boolean;\n } = {};\n if (opts.filter !== undefined) body.filter = opts.filter;\n if (opts.boosters !== undefined) body.boosters = opts.boosters;\n if (opts.rulesSlugs !== undefined) body.rulesSlugs = opts.rulesSlugs;\n if (opts.segmentationId !== undefined) {\n body.segmentationId = opts.segmentationId;\n }\n if (opts.itemsPerSegment !== undefined) {\n body.itemsPerSegment = opts.itemsPerSegment;\n }\n if (opts.autoRelaxFilters !== undefined) {\n body.autoRelaxFilters = opts.autoRelaxFilters;\n }\n if (opts.relaxEntireFilterOnEmpty !== undefined) {\n body.relaxEntireFilterOnEmpty = opts.relaxEntireFilterOnEmpty;\n }\n return body;\n }\n\n /* \u2500\u2500 Recommendations \u2500\u2500 */\n\n getRecommendations(opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/user-to-item/${this.effectiveUserId()}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getRelatedItems(itemId: string, opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/item-to-item/${itemId}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getSimilarProducts(itemId: string, opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/similar-products/${itemId}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getRecentlyViewed(opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/recently-viewed/${this.effectiveUserId()}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getCartRecommendations(itemIds: string[], opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'POST',\n '/recommendations/items-to-items',\n {\n itemIds,\n count: opts?.count ?? 10,\n returnProperties: opts?.returnProperties,\n includedProperties: opts?.includedProperties,\n country: this.resolveCountry(opts?.country),\n ...this.overrideBody(opts),\n },\n this.qs({ scenario: opts?.scenario }),\n );\n }\n\n recommend(opts: RecommendOptions): Promise<RecommendationResult> {\n return this.request('POST', '/recommendations', {\n ...opts,\n userId: this.effectiveUserId(),\n country: this.resolveCountry(opts.country),\n });\n }\n\n /* \u2500\u2500 Search \u2500\u2500 */\n\n search(query: string, opts?: SearchOptions): Promise<SearchResponse> {\n return this.request(\n 'GET',\n '/recommendations/search',\n undefined,\n this.qs({\n query,\n count: opts?.count,\n category: opts?.category,\n priceRange: opts?.priceRange,\n userId: opts?.userId ?? (this.userId || undefined),\n }),\n );\n }\n\n searchV2(query: string, opts?: SearchV2Options): Promise<SearchV2Response> {\n return this.request(\n 'GET',\n '/recommendations/search/v2',\n undefined,\n this.qs({\n query,\n count: opts?.count,\n category: opts?.category,\n priceRange: opts?.priceRange,\n userId: opts?.userId ?? (this.userId || undefined),\n locale: opts?.locale,\n debugScores: opts?.debugScores === true ? true : undefined,\n algorithm: opts?.algorithm,\n }),\n );\n }\n\n autocomplete(\n query: string,\n opts?: { limit?: number },\n ): Promise<SearchV2AutocompleteResponse> {\n return this.request(\n 'GET',\n '/recommendations/search/v2/autocomplete',\n undefined,\n this.qs({\n query,\n limit: opts?.limit,\n }),\n );\n }\n\n getSearchV2Settings(): Promise<SearchV2SettingsResponse> {\n return this.request('GET', '/recommendations/search/v2/settings');\n }\n\n /* \u2500\u2500 Catalog browse (public HMAC eligible) \u2500\u2500 */\n\n listItems(opts?: ListItemsOptions): Promise<CatalogListResponse> {\n return this.request(\n 'GET',\n '/catalog/items',\n undefined,\n this.qs({\n page: opts?.page,\n limit: opts?.limit,\n category: opts?.category,\n search: opts?.search,\n availability: opts?.availability,\n }),\n );\n }\n\n getCategories(): Promise<string[]> {\n return this.request('GET', '/catalog/items/categories');\n }\n\n getItem(externalId: string): Promise<CatalogItem> {\n return this.request('GET', `/catalog/items/${encodeURIComponent(externalId)}`);\n }\n\n /* \u2500\u2500 Event tracking \u2500\u2500 */\n\n trackView(\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n this.enqueueInteraction('view', itemId, scenarioSlugOrOpts, opts);\n }\n\n trackCart(\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n this.enqueueInteraction('cart', itemId, scenarioSlugOrOpts, opts);\n }\n\n trackPurchase(\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n this.enqueueInteraction('purchase', itemId, scenarioSlugOrOpts, opts);\n }\n\n private enqueueInteraction(\n type: 'view' | 'cart' | 'purchase',\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n const fromObject = typeof scenarioSlugOrOpts === 'object' ? scenarioSlugOrOpts : undefined;\n const scenarioSlug =\n typeof scenarioSlugOrOpts === 'string' ? scenarioSlugOrOpts : fromObject?.scenarioSlug;\n const country = this.resolveCountry(fromObject?.country ?? opts?.country);\n this.fireAndForgetQueue.enqueue({\n path: '/ingestion/interactions',\n method: 'POST',\n payload: {\n interactions: [{\n userId: this.effectiveUserId(),\n itemId,\n type,\n scenarioSlug: scenarioSlug || undefined,\n timestamp: new Date().toISOString(),\n country,\n }],\n },\n });\n }\n\n /* \u2500\u2500 Impression tracking \u2500\u2500 */\n\n trackImpressions(container: Element, opts?: ImpressionTrackerOptions): () => void {\n return this.tracker.observe(container, opts);\n }\n\n /* \u2500\u2500 Identity \u2500\u2500 */\n\n setUserId(userId: string): void {\n this.userId = userId;\n }\n\n setCountry(code: string): void {\n this.country = normalizeClientCountry(code);\n }\n\n mergeIdentity(authenticatedUserId: string): Promise<unknown> {\n if (RecomnextIdentity.isConfigured()) {\n return RecomnextIdentity.setAuthenticatedUserId(authenticatedUserId).then(\n () => {\n this.userId = authenticatedUserId;\n this.syncFromIdentityCoordinator();\n },\n );\n }\n const anonId = this.userId || this.sessionId;\n this.userId = authenticatedUserId;\n return this.request('POST', '/identity/merge', {\n userId: authenticatedUserId,\n sessionIds: [anonId],\n });\n }\n\n /** Keep SDK fields aligned when RecomnextIdentity coordinator is active. */\n private syncFromIdentityCoordinator(): void {\n if (!RecomnextIdentity.isConfigured()) return;\n const state = RecomnextIdentity.getState();\n this.userId = state.authenticatedUserId ?? '';\n this.sessionId =\n state.anonymousSessionId || readAnonymousSessionId() || this.sessionId;\n }\n\n /** Anonymous session id used before login (`sess_\u2026`). Shared across tabs via localStorage. */\n getAnonymousSessionId(): string {\n return this.sessionId || readAnonymousSessionId();\n }\n\n /**\n * Rotate anonymous session after logout so the next visitor on this browser\n * does not inherit the previous anonymous bucket. Clears known userId separately\n * via setUserId('') from the integrator.\n */\n clearSession(customId?: string): void {\n this.sessionId = clearAnonymousSession(customId);\n }\n\n /* \u2500\u2500 Lifecycle \u2500\u2500 */\n\n destroy(): void {\n this.tracker.destroy();\n this.fireAndForgetQueue.destroy();\n }\n}\n\n// Shape used by both the SDK's trackView/trackCart/trackPurchase calls\n// and the ImpressionTracker \u2014 they all funnel through the same queue.\nexport type FireAndForgetPayload = {\n interactions?: unknown[];\n impressions?: unknown[];\n};\n\nfunction renderQs(params?: Record<string, string | number | boolean | undefined>): string {\n if (!params) return '';\n const parts: string[] = [];\n for (const [k, v] of Object.entries(params)) {\n // Preserve empty string (GET clear sentinels). Drop only omit/null.\n if (v === undefined || v === null) continue;\n parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);\n }\n return parts.length ? `?${parts.join('&')}` : '';\n}\n\nasync function extractErrorCode(res: Response): Promise<string | undefined> {\n try {\n const body = (await res.json()) as { errorCode?: unknown };\n return typeof body.errorCode === 'string' ? body.errorCode : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["canonicalJson", "value", "stringify", "keys", "parts", "k", "v", "KEY_CACHE", "sign", "input", "verb", "path", "effectivePath", "timestamp", "bodyHash", "sha256Hex", "canonicalJson", "others", "k", "v", "sortedOthers", "a", "b", "tsParam", "sigParam", "trailing", "queryForSigning", "signingString", "signature", "hmacSha256Hex", "makeIdempotencyKey", "bytes", "toHex", "token", "payload", "key", "getKey", "sig", "cached", "preloadSignerKey", "out", "i", "stripped", "c", "rand", "hex", "DEFAULTS", "RetryQueue", "cfg", "openIndexedDb", "item", "enriched", "makeId", "now", "ready", "i", "url", "headers", "res", "err", "exp", "jitter", "id", "db", "tx", "txDone", "req", "data", "resolve", "reject", "c", "RECOMNEXT_SESSION_KEY", "defaultStorage", "generateAnonymousSessionId", "resolveAnonymousSessionId", "storage", "ls", "ss", "fromLocal", "legacy", "sid", "readAnonymousSessionId", "clearAnonymousSession", "customId", "next", "RETRYABLE_STATUS", "AUTH_REPLAY_CACHE_UNAVAILABLE", "sleep", "ms", "r", "extractErrorCode", "res", "body", "fetchWithSyncRetries", "method", "path", "performFetch", "lastErr", "attempt", "exp", "jitter", "err", "code", "text", "RECOMNEXT_AUTH_SYNC_KEY", "config", "authenticatedUserId", "listeners", "mergeInFlight", "authTransitionInFlight", "suppressAuthSyncWrite", "storageListenerInstalled", "normalizeBaseUrl", "baseUrl", "hostAnonymousId", "id", "resolveEffectiveUserId", "host", "stored", "readAnonymousSessionId", "resolveAnonymousSessionId", "snapshotState", "notify", "state", "listener", "mergeKey", "tenantId", "anonId", "knownUserId", "authTransitionKey", "nextId", "writeAuthSync", "userId", "readAuthSync", "raw", "postMerge", "key", "existing", "job", "path", "body", "fetchWithSyncRetries", "preloadSignerKey", "signed", "sign", "assertConfigured", "runAuthTransition", "fn", "applyAuthenticatedUserId", "source", "skipMerge", "skipHostLogout", "normalized", "targetId", "prevAuth", "clearAnonymousSession", "anonBeforeClear", "anonForMerge", "installStorageListener", "event", "peerUserId", "hydrateAuthFromStorage", "synced", "RecomnextIdentity", "coordinatorConfig", "config", "normalizeBaseUrl", "preloadSignerKey", "installStorageListener", "hydrateAuthFromStorage", "listener", "listeners", "snapshotState", "readAnonymousSessionId", "resolveAnonymousSessionId", "next", "assertConfigured", "normalized", "nextId", "transitionKey", "authTransitionKey", "pending", "authTransitionInFlight", "authenticatedUserId", "runAuthTransition", "applyAuthenticatedUserId", "RECOMNEXT_IDENTITY_DOM_EVENT", "installRecomnextIdentityDomListener", "handler", "event", "raw", "userId", "normalizeClientCountry", "raw", "code", "ImpressionTracker", "_baseUrl", "_tenantId", "getUserId", "getSessionId", "retryQueue", "getCountry", "container", "opts", "selector", "threshold", "once", "scenario", "country", "entries", "entry", "el", "itemId", "key", "urgent", "batch", "grouped", "group", "impressions", "itemIds", "scenarioSlug", "SYNC_RETRIES", "SYNC_INITIAL_BACKOFF_MS", "SYNC_MAX_BACKOFF_MS", "RETRYABLE_STATUS", "AUTH_REPLAY_CACHE_UNAVAILABLE", "RecomnextBrowserClient", "config", "normalizeClientCountry", "preloadSignerKey", "RetryQueue", "item", "ImpressionTracker", "resolveAnonymousSessionId", "override", "method", "path", "body", "query", "maxRetries", "lastErr", "attempt", "exp", "jitter", "sleep", "url", "headers", "res", "err", "code", "extractErrorCode", "text", "baseHeaders", "qs", "renderQs", "signed", "sign", "params", "opts", "join", "v", "itemId", "itemIds", "externalId", "scenarioSlugOrOpts", "type", "fromObject", "scenarioSlug", "country", "container", "userId", "authenticatedUserId", "RecomnextIdentity", "anonId", "state", "readAnonymousSessionId", "customId", "clearAnonymousSession", "parts", "k", "ms", "r"]
7
7
  }
@@ -1,4 +1,4 @@
1
- "use strict";var RecomnextBrowser=(()=>{var R=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var $=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var q=(n,e)=>{for(var t in e)R(n,t,{get:e[t],enumerable:!0})},F=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of $(e))!C.call(n,i)&&i!==t&&R(n,i,{get:()=>e[i],enumerable:!(r=A(e,i))||r.enumerable});return n};var U=n=>F(R({},"__esModule",{value:!0}),n);var Z={};q(Z,{ImpressionTracker:()=>y,RecomnextBrowserClient:()=>S,normalizeClientCountry:()=>g});function w(n){return b(n)}function b(n){if(n===null)return"null";switch(typeof n){case"boolean":return n?"true":"false";case"number":if(!Number.isFinite(n))throw new TypeError(`Cannot canonicalize non-finite number: ${String(n)}`);return JSON.stringify(n);case"string":return JSON.stringify(n);case"object":{if(Array.isArray(n))return"["+n.map(b).join(",")+"]";let e=Object.keys(n).sort(),t=[];for(let r of e){let i=n[r];typeof i>"u"||t.push(JSON.stringify(r)+":"+b(i))}return"{"+t.join(",")+"}"}case"undefined":throw new TypeError("Cannot canonicalize undefined at the root");default:throw new TypeError(`Cannot canonicalize value of type ${typeof n}`)}}var T=new Map;async function E(n){let e=n.method.toUpperCase(),t=Q(n.path),r=n.now??Math.floor(Date.now()/1e3),i=e==="GET"||e==="DELETE"||e==="HEAD"||e==="OPTIONS"?"":await _(w(n.body??null)),s=new Map;for(let[f,h]of Object.entries(n.query??{}))h!=null&&s.set(f,String(h));s.delete("hmac_timestamp"),s.delete("frontend_timestamp"),s.delete("body_hash"),s.delete("hmac_sign"),s.delete("frontend_sign");let o=Array.from(s.entries()).sort(([f],[h])=>f<h?-1:f>h?1:0).map(([f,h])=>`${encodeURIComponent(f)}=${encodeURIComponent(h)}`),a=n.mode==="public"?"frontend_timestamp":"hmac_timestamp",l=n.mode==="public"?"frontend_sign":"hmac_sign",c=`${a}=${r}&body_hash=${i}`,u=o.length?`${o.join("&")}&${c}`:c,d=`${e}
2
- ${t}?${u}
3
- X-Tenant-Id:${n.tenantId}`,m=await M(n.token,d);return{signedPath:`${t}?${u}&${l}=${m}`,timestamp:r,bodyHash:i,signature:m,idempotencyKey:B()}}async function _(n){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(n));return P(new Uint8Array(e))}async function M(n,e){let t=await O(n),r=await crypto.subtle.sign("HMAC",t,new TextEncoder().encode(e));return P(new Uint8Array(r))}async function O(n){let e=T.get(n);return e||(e=crypto.subtle.importKey("raw",new TextEncoder().encode(n),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),T.set(n,e)),e}function x(n){return O(n)}function P(n){let e="";for(let t=0;t<n.length;t++)e+=n[t].toString(16).padStart(2,"0");return e}function Q(n){let e=n.replace(/\/$/,"");return e.length>0?e:"/"}function B(){let n=globalThis.crypto;if(n?.randomUUID)return n.randomUUID();let e=new Uint8Array(16);n?.getRandomValues(e),e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t=P(e);return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}var I={maxAttempts:8,initialBackoffMs:1e3,maxBackoffMs:6e5,flushIntervalMs:2e3},p=class{constructor(e){if(this.items=[],this.timer=null,this.draining=!1,this.cfg={persist:e.persist??!1,persistKey:e.persistKey,maxAttempts:e.maxAttempts??I.maxAttempts,initialBackoffMs:e.initialBackoffMs??I.initialBackoffMs,maxBackoffMs:e.maxBackoffMs??I.maxBackoffMs,flushIntervalMs:e.flushIntervalMs??I.flushIntervalMs,onRetryDrop:e.onRetryDrop,signRequest:e.signRequest},this.cfg.persist){if(!this.cfg.persistKey)throw new Error("RetryQueue: persistKey required when persist=true");this.idb=V(),this.loadPersisted()}typeof window<"u"&&(this.timer=setInterval(()=>void this.drain(),this.cfg.flushIntervalMs),window.addEventListener("online",()=>void this.drain()),window.addEventListener("pagehide",()=>void this.drain()))}enqueue(e){let t={...e,id:e.id??D(),attempts:e.attempts??0,nextAttemptAt:e.nextAttemptAt??Date.now()};this.items.push(t),this.persist(),this.drain()}flush(){return this.drain()}destroy(){this.timer&&clearInterval(this.timer),this.timer=null}async drain(){if(!this.draining&&!(typeof navigator<"u"&&navigator.onLine===!1)){this.draining=!0;try{let e=Date.now(),t=this.items.filter(r=>(r.nextAttemptAt??0)<=e);for(let r of t)await this.attempt(r);await this.persist()}finally{this.draining=!1}}}async attempt(e){e.attempts=(e.attempts??0)+1;try{let{url:t,headers:r}=await this.cfg.signRequest(e),i=await fetch(t,{method:e.method,headers:{"Content-Type":"application/json",...r},body:JSON.stringify(e.payload),keepalive:!0});if(i.ok){this.removeById(e.id);return}if(i.status>=400&&i.status<500&&i.status!==429){this.removeById(e.id),this.cfg.onRetryDrop?.(e,new Error(`HTTP ${i.status}`));return}this.scheduleRetry(e,new Error(`HTTP ${i.status}`))}catch(t){this.scheduleRetry(e,t)}}scheduleRetry(e,t){if((e.attempts??0)>=this.cfg.maxAttempts){this.removeById(e.id),this.cfg.onRetryDrop?.(e,t);return}let r=Math.min(this.cfg.maxBackoffMs,this.cfg.initialBackoffMs*Math.pow(2,(e.attempts??1)-1)),i=.5+Math.random()*.5;e.nextAttemptAt=Date.now()+Math.floor(r*i)}removeById(e){this.items=this.items.filter(t=>t.id!==e)}async persist(){if(!(!this.cfg.persist||!this.idb||!this.cfg.persistKey))try{let e=await this.idb;if(!e)return;let t=e.transaction("queue","readwrite");t.objectStore("queue").put(JSON.parse(JSON.stringify(this.items)),this.cfg.persistKey),await j(t)}catch{}}async loadPersisted(){if(!(!this.cfg.persist||!this.idb||!this.cfg.persistKey))try{let e=await this.idb;if(!e)return;let i=e.transaction("queue","readonly").objectStore("queue").get(this.cfg.persistKey),s=await new Promise((o,a)=>{i.onsuccess=()=>o(i.result),i.onerror=()=>a(i.error)});Array.isArray(s)&&(this.items=s,this.drain())}catch{}}};function D(){let n=globalThis.crypto;return n?.randomUUID?n.randomUUID():`rnx-${Date.now()}-${Math.random().toString(36).slice(2,11)}`}function V(){return typeof indexedDB>"u"?Promise.resolve(null):new Promise(n=>{let e=indexedDB.open("recomnext-retry-queue",1);e.onupgradeneeded=()=>{let t=e.result;t.objectStoreNames.contains("queue")||t.createObjectStore("queue")},e.onsuccess=()=>n(e.result),e.onerror=()=>n(null),e.onblocked=()=>n(null)})}function j(n){return new Promise((e,t)=>{n.oncomplete=()=>e(),n.onerror=()=>t(n.error),n.onabort=()=>t(n.error)})}function g(n){if(n==null)return;let e=String(n).trim().toUpperCase();if(!(!e||e==="OTH"||!/^[A-Z]{2}$/.test(e)))return e}var y=class{constructor(e,t,r,i,s,o){this.queue=[];this.flushing=!1;this.observer=null;this.seen=new Set;this.timer=null;this.getUserId=r,this.getSessionId=i,this.retryQueue=s,this.getCountry=o??(()=>{}),typeof window<"u"&&(this.timer=setInterval(()=>this.flush(),2e3),window.addEventListener("pagehide",()=>this.flush(!0)))}observe(e,t={}){let r=t.itemSelector??"[data-item-id]",i=t.threshold??.5,s=t.once!==!1,o=t.scenario??"",a=t.country||this.getCountry()||void 0;return this.observer=new IntersectionObserver(c=>{for(let u of c){if(!u.isIntersecting)continue;let d=u.target,m=d.dataset.itemId;if(!m)continue;let v=`${o}:${m}`;s&&this.seen.has(v)||(this.seen.add(v),this.queue.push({itemId:m,scenario:o,country:a}),s&&this.observer?.unobserve(d))}},{root:null,threshold:i}),e.querySelectorAll(r).forEach(c=>this.observer.observe(c)),()=>{this.observer?.disconnect(),this.observer=null}}flush(e=!1){if(this.flushing||this.queue.length===0)return;this.flushing=!0;let t=this.queue.splice(0,this.queue.length),r=new Map;for(let{itemId:s,scenario:o,country:a}of t){let l=`${o}\0${a??""}`,c=r.get(l);c?c.itemIds.push(s):r.set(l,{itemIds:[s],country:a})}let i=Array.from(r.entries()).map(([s,{itemIds:o,country:a}])=>{let l=s.split("\0")[0];return{userId:this.getUserId()||void 0,sessionId:this.getSessionId()||void 0,itemIds:o,scenarioSlug:l||void 0,timestamp:new Date().toISOString(),country:a||void 0}});this.retryQueue.enqueue({path:"/ingestion/impressions",method:"POST",payload:{impressions:i}}),e&&this.retryQueue.flush(),this.flushing=!1}destroy(){this.flush(),this.observer?.disconnect(),this.timer&&clearInterval(this.timer)}};var k="recomnext_session_id",K=3,L=200,H=2e3,N=new Set([429,500,502,503,504]),G="auth_replay_cache_unavailable",J=new Set(["/identity/merge"]),S=class{constructor(e){this.baseUrl=e.baseUrl.replace(/\/+$/,""),this.tenantId=e.tenantId,this.publicToken=e.publicToken,this.userId=e.userId??"",this.sessionId=e.sessionId??this.resolveSessionId(),this.country=g(e.country),this.publicToken&&x(this.publicToken),this.fireAndForgetQueue=new p({persist:e.persistRetries??!1,persistKey:`rnx:${this.tenantId}:fnf`,onRetryDrop:e.onRetryDrop,signRequest:t=>this.signFireAndForget(t)}),this.tracker=new y(this.baseUrl,this.tenantId,()=>this.userId,()=>this.sessionId,this.fireAndForgetQueue,()=>this.country)}resolveSessionId(){if(typeof sessionStorage>"u")return"";let e=sessionStorage.getItem(k);return e||(e=`sess_${Date.now()}_${Math.random().toString(36).slice(2,9)}`,sessionStorage.setItem(k,e)),e}effectiveUserId(){return this.userId||this.sessionId}resolveCountry(e){return g(e??this.country)}async request(e,t,r,i){let s=J.has(t)?0:K,o;for(let a=0;a<=s;a++){if(a>0){let d=Math.min(H,L*Math.pow(2,a-1)),m=.5+Math.random()*.5;await X(Math.floor(d*m))}let{url:l,headers:c}=await this.buildSignedRequest(e,t,i,r),u;try{u=await fetch(l,{method:e,headers:{"Content-Type":"application/json",...c},body:r!==void 0?JSON.stringify(r):void 0})}catch(d){if(o=d,a>=s)throw d;continue}if(u.status===503){let d=await z(u.clone());if(d===G&&a<s){o=new Error(`recomnext: ${e} ${t} ${u.status} ${d}`);continue}}if(N.has(u.status)&&a<s){o=new Error(`recomnext: ${e} ${t} returned ${u.status}`);continue}if(!u.ok){let d=await u.text().catch(()=>"");throw new Error(`recomnext: ${e} ${t} returned ${u.status}: ${d}`)}return u.status===204?void 0:u.json()}throw o??new Error(`recomnext: ${e} ${t} retries exhausted`)}async signFireAndForget(e){let{url:t,headers:r}=await this.buildSignedRequest(e.method,e.path,void 0,e.payload);return{url:t,headers:r}}async buildSignedRequest(e,t,r,i){let s={"X-Tenant-Id":this.tenantId};if(!this.publicToken){let a=Y(r);return{url:`${this.baseUrl}${t}${a}`,headers:s}}let o=await E({method:e,path:t,query:r,body:i??null,tenantId:this.tenantId,token:this.publicToken,mode:"public"});return{url:`${this.baseUrl}${o.signedPath}`,headers:{...s,"X-Idempotency-Key":o.idempotencyKey}}}qs(e){return e}includeQuery(e){return{returnProperties:e?.returnProperties,includedProperties:e?.includedProperties&&e.includedProperties.length>0?e.includedProperties.join(","):void 0}}overrideQuery(e){if(!e)return{};let t=r=>{if(r!==void 0)return r===null||r.length===0?"":r.join(",")};return{filter:e.filter===void 0?void 0:e.filter===null?"":e.filter,boosters:t(e.boosters),rulesSlugs:t(e.rulesSlugs),segmentationId:e.segmentationId===void 0?void 0:e.segmentationId===null?"":e.segmentationId,itemsPerSegment:e.itemsPerSegment===void 0?void 0:e.itemsPerSegment===null?"":e.itemsPerSegment,autoRelaxFilters:e.autoRelaxFilters,relaxEntireFilterOnEmpty:e.relaxEntireFilterOnEmpty}}overrideBody(e){if(!e)return{};let t={};return e.filter!==void 0&&(t.filter=e.filter),e.boosters!==void 0&&(t.boosters=e.boosters),e.rulesSlugs!==void 0&&(t.rulesSlugs=e.rulesSlugs),e.segmentationId!==void 0&&(t.segmentationId=e.segmentationId),e.itemsPerSegment!==void 0&&(t.itemsPerSegment=e.itemsPerSegment),e.autoRelaxFilters!==void 0&&(t.autoRelaxFilters=e.autoRelaxFilters),e.relaxEntireFilterOnEmpty!==void 0&&(t.relaxEntireFilterOnEmpty=e.relaxEntireFilterOnEmpty),t}getRecommendations(e){return this.request("GET",`/recommendations/user-to-item/${this.effectiveUserId()}`,void 0,this.qs({count:e?.count,scenario:e?.scenario,country:this.resolveCountry(e?.country),...this.includeQuery(e),...this.overrideQuery(e)}))}getRelatedItems(e,t){return this.request("GET",`/recommendations/item-to-item/${e}`,void 0,this.qs({count:t?.count,scenario:t?.scenario,country:this.resolveCountry(t?.country),...this.includeQuery(t),...this.overrideQuery(t)}))}getSimilarProducts(e,t){return this.request("GET",`/recommendations/similar-products/${e}`,void 0,this.qs({count:t?.count,scenario:t?.scenario,country:this.resolveCountry(t?.country),...this.includeQuery(t),...this.overrideQuery(t)}))}getRecentlyViewed(e){return this.request("GET",`/recommendations/recently-viewed/${this.effectiveUserId()}`,void 0,this.qs({count:e?.count,scenario:e?.scenario,country:this.resolveCountry(e?.country),...this.includeQuery(e),...this.overrideQuery(e)}))}getCartRecommendations(e,t){return this.request("POST","/recommendations/items-to-items",{itemIds:e,count:t?.count??10,returnProperties:t?.returnProperties,includedProperties:t?.includedProperties,country:this.resolveCountry(t?.country),...this.overrideBody(t)},this.qs({scenario:t?.scenario}))}recommend(e){return this.request("POST","/recommendations",{...e,userId:this.effectiveUserId(),country:this.resolveCountry(e.country)})}search(e,t){return this.request("GET","/recommendations/search",void 0,this.qs({query:e,count:t?.count,category:t?.category,priceRange:t?.priceRange,userId:t?.userId??(this.userId||void 0)}))}searchV2(e,t){return this.request("GET","/recommendations/search/v2",void 0,this.qs({query:e,count:t?.count,category:t?.category,priceRange:t?.priceRange,userId:t?.userId??(this.userId||void 0),locale:t?.locale,debugScores:t?.debugScores===!0?!0:void 0,algorithm:t?.algorithm}))}autocomplete(e,t){return this.request("GET","/recommendations/search/v2/autocomplete",void 0,this.qs({query:e,limit:t?.limit}))}getSearchV2Settings(){return this.request("GET","/recommendations/search/v2/settings")}listItems(e){return this.request("GET","/catalog/items",void 0,this.qs({page:e?.page,limit:e?.limit,category:e?.category,search:e?.search,availability:e?.availability}))}getCategories(){return this.request("GET","/catalog/items/categories")}getItem(e){return this.request("GET",`/catalog/items/${encodeURIComponent(e)}`)}trackView(e,t,r){this.enqueueInteraction("view",e,t,r)}trackCart(e,t,r){this.enqueueInteraction("cart",e,t,r)}trackPurchase(e,t,r){this.enqueueInteraction("purchase",e,t,r)}enqueueInteraction(e,t,r,i){let s=typeof r=="object"?r:void 0,o=typeof r=="string"?r:s?.scenarioSlug,a=this.resolveCountry(s?.country??i?.country);this.fireAndForgetQueue.enqueue({path:"/ingestion/interactions",method:"POST",payload:{interactions:[{userId:this.effectiveUserId(),itemId:t,type:e,scenarioSlug:o||void 0,timestamp:new Date().toISOString(),country:a}]}})}trackImpressions(e,t){return this.tracker.observe(e,t)}setUserId(e){this.userId=e}setCountry(e){this.country=g(e)}mergeIdentity(e){let t=this.sessionId;return this.userId=e,this.request("POST","/identity/merge",{userId:e,sessionIds:[t]})}destroy(){this.tracker.destroy(),this.fireAndForgetQueue.destroy()}};function Y(n){if(!n)return"";let e=[];for(let[t,r]of Object.entries(n))r!=null&&e.push(`${encodeURIComponent(t)}=${encodeURIComponent(String(r))}`);return e.length?`?${e.join("&")}`:""}async function z(n){try{let e=await n.json();return typeof e.errorCode=="string"?e.errorCode:void 0}catch{return}}function X(n){return new Promise(e=>setTimeout(e,n))}return U(Z);})();
1
+ "use strict";var RecomnextBrowser=(()=>{var F=Object.defineProperty;var ne=Object.getOwnPropertyDescriptor;var re=Object.getOwnPropertyNames;var se=Object.prototype.hasOwnProperty;var ie=(n,e)=>{for(var t in e)F(n,t,{get:e[t],enumerable:!0})},oe=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of re(e))!se.call(n,s)&&s!==t&&F(n,s,{get:()=>e[s],enumerable:!(r=ne(e,s))||r.enumerable});return n};var ae=n=>oe(F({},"__esModule",{value:!0}),n);var ke={};ie(ke,{ImpressionTracker:()=>v,RECOMNEXT_IDENTITY_DOM_EVENT:()=>O,RECOMNEXT_SESSION_KEY:()=>f,RecomnextBrowserClient:()=>_,RecomnextIdentity:()=>y,clearAnonymousSession:()=>w,installRecomnextIdentityDomListener:()=>H,normalizeClientCountry:()=>E,readAnonymousSessionId:()=>m,resolveAnonymousSessionId:()=>S});function q(n){return M(n)}function M(n){if(n===null)return"null";switch(typeof n){case"boolean":return n?"true":"false";case"number":if(!Number.isFinite(n))throw new TypeError(`Cannot canonicalize non-finite number: ${String(n)}`);return JSON.stringify(n);case"string":return JSON.stringify(n);case"object":{if(Array.isArray(n))return"["+n.map(M).join(",")+"]";let e=Object.keys(n).sort(),t=[];for(let r of e){let s=n[r];typeof s>"u"||t.push(JSON.stringify(r)+":"+M(s))}return"{"+t.join(",")+"}"}case"undefined":throw new TypeError("Cannot canonicalize undefined at the root");default:throw new TypeError(`Cannot canonicalize value of type ${typeof n}`)}}var Y=new Map;async function T(n){let e=n.method.toUpperCase(),t=de(n.path),r=n.now??Math.floor(Date.now()/1e3),s=e==="GET"||e==="DELETE"||e==="HEAD"||e==="OPTIONS"?"":await ue(q(n.body??null)),i=new Map;for(let[R,I]of Object.entries(n.query??{}))I!=null&&i.set(R,String(I));i.delete("hmac_timestamp"),i.delete("frontend_timestamp"),i.delete("body_hash"),i.delete("hmac_sign"),i.delete("frontend_sign");let o=Array.from(i.entries()).sort(([R],[I])=>R<I?-1:R>I?1:0).map(([R,I])=>`${encodeURIComponent(R)}=${encodeURIComponent(I)}`),a=n.mode==="public"?"frontend_timestamp":"hmac_timestamp",d=n.mode==="public"?"frontend_sign":"hmac_sign",h=`${a}=${r}&body_hash=${s}`,c=o.length?`${o.join("&")}&${h}`:h,l=`${e}
2
+ ${t}?${c}
3
+ X-Tenant-Id:${n.tenantId}`,p=await ce(n.token,l);return{signedPath:`${t}?${c}&${d}=${p}`,timestamp:r,bodyHash:s,signature:p,idempotencyKey:le()}}async function ue(n){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(n));return U(new Uint8Array(e))}async function ce(n,e){let t=await X(n),r=await crypto.subtle.sign("HMAC",t,new TextEncoder().encode(e));return U(new Uint8Array(r))}async function X(n){let e=Y.get(n);return e||(e=crypto.subtle.importKey("raw",new TextEncoder().encode(n),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),Y.set(n,e)),e}function b(n){return X(n)}function U(n){let e="";for(let t=0;t<n.length;t++)e+=n[t].toString(16).padStart(2,"0");return e}function de(n){let e=n.replace(/\/$/,"");return e.length>0?e:"/"}function le(){let n=globalThis.crypto;if(n?.randomUUID)return n.randomUUID();let e=new Uint8Array(16);n?.getRandomValues(e),e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t=U(e);return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}var P={maxAttempts:8,initialBackoffMs:1e3,maxBackoffMs:6e5,flushIntervalMs:2e3},A=class{constructor(e){if(this.items=[],this.timer=null,this.draining=!1,this.cfg={persist:e.persist??!1,persistKey:e.persistKey,maxAttempts:e.maxAttempts??P.maxAttempts,initialBackoffMs:e.initialBackoffMs??P.initialBackoffMs,maxBackoffMs:e.maxBackoffMs??P.maxBackoffMs,flushIntervalMs:e.flushIntervalMs??P.flushIntervalMs,onRetryDrop:e.onRetryDrop,signRequest:e.signRequest},this.cfg.persist){if(!this.cfg.persistKey)throw new Error("RetryQueue: persistKey required when persist=true");this.idb=me(),this.loadPersisted()}typeof window<"u"&&(this.timer=setInterval(()=>void this.drain(),this.cfg.flushIntervalMs),window.addEventListener("online",()=>void this.drain()),window.addEventListener("pagehide",()=>void this.drain()))}enqueue(e){let t={...e,id:e.id??fe(),attempts:e.attempts??0,nextAttemptAt:e.nextAttemptAt??Date.now()};this.items.push(t),this.persist(),this.drain()}flush(){return this.drain()}destroy(){this.timer&&clearInterval(this.timer),this.timer=null}async drain(){if(!this.draining&&!(typeof navigator<"u"&&navigator.onLine===!1)){this.draining=!0;try{let e=Date.now(),t=this.items.filter(r=>(r.nextAttemptAt??0)<=e);for(let r of t)await this.attempt(r);await this.persist()}finally{this.draining=!1}}}async attempt(e){e.attempts=(e.attempts??0)+1;try{let{url:t,headers:r}=await this.cfg.signRequest(e),s=await fetch(t,{method:e.method,headers:{"Content-Type":"application/json",...r},body:JSON.stringify(e.payload),keepalive:!0});if(s.ok){this.removeById(e.id);return}if(s.status>=400&&s.status<500&&s.status!==429){this.removeById(e.id),this.cfg.onRetryDrop?.(e,new Error(`HTTP ${s.status}`));return}this.scheduleRetry(e,new Error(`HTTP ${s.status}`))}catch(t){this.scheduleRetry(e,t)}}scheduleRetry(e,t){if((e.attempts??0)>=this.cfg.maxAttempts){this.removeById(e.id),this.cfg.onRetryDrop?.(e,t);return}let r=Math.min(this.cfg.maxBackoffMs,this.cfg.initialBackoffMs*Math.pow(2,(e.attempts??1)-1)),s=.5+Math.random()*.5;e.nextAttemptAt=Date.now()+Math.floor(r*s)}removeById(e){this.items=this.items.filter(t=>t.id!==e)}async persist(){if(!(!this.cfg.persist||!this.idb||!this.cfg.persistKey))try{let e=await this.idb;if(!e)return;let t=e.transaction("queue","readwrite");t.objectStore("queue").put(JSON.parse(JSON.stringify(this.items)),this.cfg.persistKey),await he(t)}catch{}}async loadPersisted(){if(!(!this.cfg.persist||!this.idb||!this.cfg.persistKey))try{let e=await this.idb;if(!e)return;let s=e.transaction("queue","readonly").objectStore("queue").get(this.cfg.persistKey),i=await new Promise((o,a)=>{s.onsuccess=()=>o(s.result),s.onerror=()=>a(s.error)});Array.isArray(i)&&(this.items=i,this.drain())}catch{}}};function fe(){let n=globalThis.crypto;return n?.randomUUID?n.randomUUID():`rnx-${Date.now()}-${Math.random().toString(36).slice(2,11)}`}function me(){return typeof indexedDB>"u"?Promise.resolve(null):new Promise(n=>{let e=indexedDB.open("recomnext-retry-queue",1);e.onupgradeneeded=()=>{let t=e.result;t.objectStoreNames.contains("queue")||t.createObjectStore("queue")},e.onsuccess=()=>n(e.result),e.onerror=()=>n(null),e.onblocked=()=>n(null)})}function he(n){return new Promise((e,t)=>{n.oncomplete=()=>e(),n.onerror=()=>t(n.error),n.onabort=()=>t(n.error)})}var f="recomnext_session_id";function N(){return{localStorage:typeof globalThis<"u"&&typeof localStorage<"u"?localStorage:void 0,sessionStorage:typeof globalThis<"u"&&typeof sessionStorage<"u"?sessionStorage:void 0}}function L(){return`sess_${Date.now()}_${Math.random().toString(36).slice(2,9)}`}function S(n=N()){let{localStorage:e,sessionStorage:t}=n;if(!e)return"";let r=e.getItem(f);if(r)return r;if(t){let i=t.getItem(f);if(i)return e.setItem(f,i),t.removeItem(f),i}let s=L();return e.setItem(f,s),s}function m(n=N()){let{localStorage:e,sessionStorage:t}=n;if(e){let r=e.getItem(f);if(r)return r}return t?t.getItem(f)??"":""}function w(n,e=N()){let{localStorage:t,sessionStorage:r}=e,s=n!==void 0&&n!==""?n:L();return t&&t.setItem(f,s),r&&r.removeItem(f),s}var ge=new Set([429,500,502,503,504]),ye="auth_replay_cache_unavailable";function pe(n){return new Promise(e=>setTimeout(e,n))}async function Ie(n){try{let e=await n.json();return typeof e.errorCode=="string"?e.errorCode:void 0}catch{return}}async function G(n,e,t){let r;for(let s=0;s<=3;s++){if(s>0){let o=Math.min(2e3,200*Math.pow(2,s-1)),a=.5+Math.random()*.5;await pe(Math.floor(o*a))}let i;try{i=await t()}catch(o){if(r=o,s>=3)throw o;continue}if(i.status===503){let o=await Ie(i.clone());if(o===ye&&s<3){r=new Error(`recomnext: ${n} ${e} ${i.status} ${o}`);continue}}if(ge.has(i.status)&&s<3){r=new Error(`recomnext: ${n} ${e} returned ${i.status}`);continue}if(!i.ok){let o=await i.text().catch(()=>"");throw new Error(`recomnext: ${n} ${e} returned ${i.status}: ${o}`)}return i}throw r??new Error(`recomnext: ${n} ${e} retries exhausted`)}var x="recomnext_authenticated_user_id",u=null,g=null,V=new Set,B=new Map,$=new Map,C=!1,J=!1;function W(n){return n.replace(/\/+$/,"")}function Z(){let n=u?.getHostAnonymousUserId?.()??"";return typeof n=="string"?n.trim():""}function ee(){if(g)return g;let n=Z();if(n)return n;let e=m();return e||S()}function j(){return{authenticatedUserId:g,anonymousSessionId:m(),effectiveUserId:ee()}}function K(){let n=j();for(let e of V)e(n)}function Se(n,e,t){return`${n}:${e}:${t}`}function we(n){return n===null?"logout":`login:${n}`}function D(n){C||!u||u.syncAuthAcrossTabs!==!1&&(typeof localStorage>"u"||(n===null?localStorage.removeItem(x):localStorage.setItem(x,n)))}function Re(){if(typeof localStorage>"u")return null;let n=localStorage.getItem(x);return!n||n.trim()===""?null:n.trim()}async function z(n,e){if(!u||!e||e===n)return;let t=Se(u.tenantId,e,n),r=B.get(t);if(r){await r;return}let s=(async()=>{let i="/identity/merge",o={userId:n,sessionIds:[e]},a=W(u.baseUrl);await G("POST",i,async()=>{if(u.publicToken){await b(u.publicToken);let d=await T({method:"POST",path:i,body:o,tenantId:u.tenantId,token:u.publicToken,mode:"public"});return fetch(`${a}${d.signedPath}`,{method:"POST",headers:{"Content-Type":"application/json","X-Tenant-Id":u.tenantId,"X-Idempotency-Key":d.idempotencyKey},body:JSON.stringify(o)})}return fetch(`${a}${i}`,{method:"POST",headers:{"Content-Type":"application/json","X-Tenant-Id":u.tenantId},body:JSON.stringify(o)})})})();B.set(t,s);try{await s}finally{B.delete(t)}}function te(){if(!u)throw new Error("RecomnextIdentity.configure() must be called before use");return u}async function be(n,e){let t=$.get(n);if(t){await t;return}let r=e();$.set(n,r);try{await r}finally{$.delete(n)}}async function Q(n,e="local"){te();let t=e==="peer-sync",r=e==="peer-sync",s=n==null?null:String(n).trim(),i=s===""?null:s;if(g===i)return;let o=g;if(i===null){!r&&u.getHostAnonymousUserId&&u.onHostLogout&&await u.onHostLogout(),w(),g=null,D(null),K();return}if(o!==null){let d=m()||Z()||"";w(),!t&&d&&await z(i,d),g=i,D(i),K();return}let a=ee();!t&&a&&await z(i,a),g=i,D(i),K()}function Ee(){J||typeof window>"u"||(J=!0,window.addEventListener("storage",n=>{if(!u||u.syncAuthAcrossTabs===!1||n.key!==x||n.storageArea!==localStorage)return;let e=n.newValue===null||n.newValue===void 0||n.newValue.trim()===""?null:n.newValue.trim();e!==g&&(C=!0,Q(e,"peer-sync").finally(()=>{C=!1}))}))}function ve(){if(!u||u.syncAuthAcrossTabs===!1)return;let n=Re();n!==g&&(C=!0,Q(n,"peer-sync").finally(()=>{C=!1}))}var y={configure(n){u={...n,baseUrl:W(n.baseUrl)},u.publicToken&&b(u.publicToken),Ee(),ve()},isConfigured(){return u!==null},subscribe(n){return V.add(n),n(j()),()=>V.delete(n)},getState(){return u?j():{authenticatedUserId:null,anonymousSessionId:m(),effectiveUserId:m()||S()}},async setAuthenticatedUserId(n){te();let e=n==null?null:String(n).trim(),t=e===""?null:e,r=we(t),s=$.get(r);if(g===t){s&&await s;return}await be(r,()=>Q(t))}},O="recomnext:identity";function H(){if(typeof document>"u")return()=>{};let n=e=>{if(!y.isConfigured())return;let r=e.detail?.userId,s=r==null||String(r).trim()===""?null:String(r).trim();y.setAuthenticatedUserId(s)};return document.addEventListener(O,n),()=>document.removeEventListener(O,n)}function E(n){if(n==null)return;let e=String(n).trim().toUpperCase();if(!(!e||e==="OTH"||!/^[A-Z]{2}$/.test(e)))return e}var v=class{constructor(e,t,r,s,i,o){this.queue=[];this.flushing=!1;this.observer=null;this.seen=new Set;this.timer=null;this.getUserId=r,this.getSessionId=s,this.retryQueue=i,this.getCountry=o??(()=>{}),typeof window<"u"&&(this.timer=setInterval(()=>this.flush(),2e3),window.addEventListener("pagehide",()=>this.flush(!0)))}observe(e,t={}){let r=t.itemSelector??"[data-item-id]",s=t.threshold??.5,i=t.once!==!1,o=t.scenario??"",a=t.country||this.getCountry()||void 0;return this.observer=new IntersectionObserver(h=>{for(let c of h){if(!c.isIntersecting)continue;let l=c.target,p=l.dataset.itemId;if(!p)continue;let k=`${o}:${p}`;i&&this.seen.has(k)||(this.seen.add(k),this.queue.push({itemId:p,scenario:o,country:a}),i&&this.observer?.unobserve(l))}},{root:null,threshold:s}),e.querySelectorAll(r).forEach(h=>this.observer.observe(h)),()=>{this.observer?.disconnect(),this.observer=null}}flush(e=!1){if(this.flushing||this.queue.length===0)return;this.flushing=!0;let t=this.queue.splice(0,this.queue.length),r=new Map;for(let{itemId:i,scenario:o,country:a}of t){let d=`${o}\0${a??""}`,h=r.get(d);h?h.itemIds.push(i):r.set(d,{itemIds:[i],country:a})}let s=Array.from(r.entries()).map(([i,{itemIds:o,country:a}])=>{let d=i.split("\0")[0];return{userId:this.getUserId()||void 0,sessionId:this.getSessionId()||void 0,itemIds:o,scenarioSlug:d||void 0,timestamp:new Date().toISOString(),country:a||void 0}});this.retryQueue.enqueue({path:"/ingestion/impressions",method:"POST",payload:{impressions:s}}),e&&this.retryQueue.flush(),this.flushing=!1}destroy(){this.flush(),this.observer?.disconnect(),this.timer&&clearInterval(this.timer)}};var Te=3,Ae=200,xe=2e3,Ce=new Set([429,500,502,503,504]),Oe="auth_replay_cache_unavailable",_=class{constructor(e){this.baseUrl=e.baseUrl.replace(/\/+$/,""),this.tenantId=e.tenantId,this.publicToken=e.publicToken,this.userId=e.userId??"",this.sessionId=e.sessionId??this.resolveSessionId(),this.country=E(e.country),this.publicToken&&b(this.publicToken),this.fireAndForgetQueue=new A({persist:e.persistRetries??!1,persistKey:`rnx:${this.tenantId}:fnf`,onRetryDrop:e.onRetryDrop,signRequest:t=>this.signFireAndForget(t)}),this.tracker=new v(this.baseUrl,this.tenantId,()=>this.userId,()=>this.sessionId,this.fireAndForgetQueue,()=>this.country)}resolveSessionId(){return S()}effectiveUserId(){return this.userId||this.sessionId}resolveCountry(e){return E(e??this.country)}async request(e,t,r,s){let i=Te,o;for(let a=0;a<=i;a++){if(a>0){let l=Math.min(xe,Ae*Math.pow(2,a-1)),p=.5+Math.random()*.5;await _e(Math.floor(l*p))}let{url:d,headers:h}=await this.buildSignedRequest(e,t,s,r),c;try{c=await fetch(d,{method:e,headers:{"Content-Type":"application/json",...h},body:r!==void 0?JSON.stringify(r):void 0})}catch(l){if(o=l,a>=i)throw l;continue}if(c.status===503){let l=await $e(c.clone());if(l===Oe&&a<i){o=new Error(`recomnext: ${e} ${t} ${c.status} ${l}`);continue}}if(Ce.has(c.status)&&a<i){o=new Error(`recomnext: ${e} ${t} returned ${c.status}`);continue}if(!c.ok){let l=await c.text().catch(()=>"");throw new Error(`recomnext: ${e} ${t} returned ${c.status}: ${l}`)}return c.status===204?void 0:c.json()}throw o??new Error(`recomnext: ${e} ${t} retries exhausted`)}async signFireAndForget(e){let{url:t,headers:r}=await this.buildSignedRequest(e.method,e.path,void 0,e.payload);return{url:t,headers:r}}async buildSignedRequest(e,t,r,s){let i={"X-Tenant-Id":this.tenantId};if(!this.publicToken){let a=Pe(r);return{url:`${this.baseUrl}${t}${a}`,headers:i}}let o=await T({method:e,path:t,query:r,body:s??null,tenantId:this.tenantId,token:this.publicToken,mode:"public"});return{url:`${this.baseUrl}${o.signedPath}`,headers:{...i,"X-Idempotency-Key":o.idempotencyKey}}}qs(e){return e}includeQuery(e){return{returnProperties:e?.returnProperties,includedProperties:e?.includedProperties&&e.includedProperties.length>0?e.includedProperties.join(","):void 0}}overrideQuery(e){if(!e)return{};let t=r=>{if(r!==void 0)return r===null||r.length===0?"":r.join(",")};return{filter:e.filter===void 0?void 0:e.filter===null?"":e.filter,boosters:t(e.boosters),rulesSlugs:t(e.rulesSlugs),segmentationId:e.segmentationId===void 0?void 0:e.segmentationId===null?"":e.segmentationId,itemsPerSegment:e.itemsPerSegment===void 0?void 0:e.itemsPerSegment===null?"":e.itemsPerSegment,autoRelaxFilters:e.autoRelaxFilters,relaxEntireFilterOnEmpty:e.relaxEntireFilterOnEmpty}}overrideBody(e){if(!e)return{};let t={};return e.filter!==void 0&&(t.filter=e.filter),e.boosters!==void 0&&(t.boosters=e.boosters),e.rulesSlugs!==void 0&&(t.rulesSlugs=e.rulesSlugs),e.segmentationId!==void 0&&(t.segmentationId=e.segmentationId),e.itemsPerSegment!==void 0&&(t.itemsPerSegment=e.itemsPerSegment),e.autoRelaxFilters!==void 0&&(t.autoRelaxFilters=e.autoRelaxFilters),e.relaxEntireFilterOnEmpty!==void 0&&(t.relaxEntireFilterOnEmpty=e.relaxEntireFilterOnEmpty),t}getRecommendations(e){return this.request("GET",`/recommendations/user-to-item/${this.effectiveUserId()}`,void 0,this.qs({count:e?.count,scenario:e?.scenario,country:this.resolveCountry(e?.country),...this.includeQuery(e),...this.overrideQuery(e)}))}getRelatedItems(e,t){return this.request("GET",`/recommendations/item-to-item/${e}`,void 0,this.qs({count:t?.count,scenario:t?.scenario,country:this.resolveCountry(t?.country),...this.includeQuery(t),...this.overrideQuery(t)}))}getSimilarProducts(e,t){return this.request("GET",`/recommendations/similar-products/${e}`,void 0,this.qs({count:t?.count,scenario:t?.scenario,country:this.resolveCountry(t?.country),...this.includeQuery(t),...this.overrideQuery(t)}))}getRecentlyViewed(e){return this.request("GET",`/recommendations/recently-viewed/${this.effectiveUserId()}`,void 0,this.qs({count:e?.count,scenario:e?.scenario,country:this.resolveCountry(e?.country),...this.includeQuery(e),...this.overrideQuery(e)}))}getCartRecommendations(e,t){return this.request("POST","/recommendations/items-to-items",{itemIds:e,count:t?.count??10,returnProperties:t?.returnProperties,includedProperties:t?.includedProperties,country:this.resolveCountry(t?.country),...this.overrideBody(t)},this.qs({scenario:t?.scenario}))}recommend(e){return this.request("POST","/recommendations",{...e,userId:this.effectiveUserId(),country:this.resolveCountry(e.country)})}search(e,t){return this.request("GET","/recommendations/search",void 0,this.qs({query:e,count:t?.count,category:t?.category,priceRange:t?.priceRange,userId:t?.userId??(this.userId||void 0)}))}searchV2(e,t){return this.request("GET","/recommendations/search/v2",void 0,this.qs({query:e,count:t?.count,category:t?.category,priceRange:t?.priceRange,userId:t?.userId??(this.userId||void 0),locale:t?.locale,debugScores:t?.debugScores===!0?!0:void 0,algorithm:t?.algorithm}))}autocomplete(e,t){return this.request("GET","/recommendations/search/v2/autocomplete",void 0,this.qs({query:e,limit:t?.limit}))}getSearchV2Settings(){return this.request("GET","/recommendations/search/v2/settings")}listItems(e){return this.request("GET","/catalog/items",void 0,this.qs({page:e?.page,limit:e?.limit,category:e?.category,search:e?.search,availability:e?.availability}))}getCategories(){return this.request("GET","/catalog/items/categories")}getItem(e){return this.request("GET",`/catalog/items/${encodeURIComponent(e)}`)}trackView(e,t,r){this.enqueueInteraction("view",e,t,r)}trackCart(e,t,r){this.enqueueInteraction("cart",e,t,r)}trackPurchase(e,t,r){this.enqueueInteraction("purchase",e,t,r)}enqueueInteraction(e,t,r,s){let i=typeof r=="object"?r:void 0,o=typeof r=="string"?r:i?.scenarioSlug,a=this.resolveCountry(i?.country??s?.country);this.fireAndForgetQueue.enqueue({path:"/ingestion/interactions",method:"POST",payload:{interactions:[{userId:this.effectiveUserId(),itemId:t,type:e,scenarioSlug:o||void 0,timestamp:new Date().toISOString(),country:a}]}})}trackImpressions(e,t){return this.tracker.observe(e,t)}setUserId(e){this.userId=e}setCountry(e){this.country=E(e)}mergeIdentity(e){if(y.isConfigured())return y.setAuthenticatedUserId(e).then(()=>{this.userId=e,this.syncFromIdentityCoordinator()});let t=this.userId||this.sessionId;return this.userId=e,this.request("POST","/identity/merge",{userId:e,sessionIds:[t]})}syncFromIdentityCoordinator(){if(!y.isConfigured())return;let e=y.getState();this.userId=e.authenticatedUserId??"",this.sessionId=e.anonymousSessionId||m()||this.sessionId}getAnonymousSessionId(){return this.sessionId||m()}clearSession(e){this.sessionId=w(e)}destroy(){this.tracker.destroy(),this.fireAndForgetQueue.destroy()}};function Pe(n){if(!n)return"";let e=[];for(let[t,r]of Object.entries(n))r!=null&&e.push(`${encodeURIComponent(t)}=${encodeURIComponent(String(r))}`);return e.length?`?${e.join("&")}`:""}async function $e(n){try{let e=await n.json();return typeof e.errorCode=="string"?e.errorCode:void 0}catch{return}}function _e(n){return new Promise(e=>setTimeout(e,n))}return ae(ke);})();
4
4
  //# sourceMappingURL=recomnext-browser.umd.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/index.ts", "../../shared-browser-auth/src/canonical-json.ts", "../../shared-browser-auth/src/sign.ts", "../../shared-browser-auth/src/retry-queue.ts", "../src/country.ts", "../src/tracker.ts", "../src/client.ts"],
4
- "sourcesContent": ["export { RecomnextBrowserClient } from './client';\nexport { ImpressionTracker } from './tracker';\nexport { normalizeClientCountry } from './country';\nexport type { FireAndForgetPayload } from './client';\nexport type {\n FacetBucket,\n SearchFacets,\n SearchOptions,\n SearchResponse,\n SearchV2QueryConstraint,\n SearchV2ScoreBreakdown,\n SearchV2ItemScore,\n SearchV2Options,\n SearchV2Response,\n SearchV2AutocompleteResponse,\n SearchV2SettingsResponse,\n CatalogItem,\n CatalogListResponse,\n ListItemsOptions,\n} from '@recomnext/shared-search-types';\nexport type {\n BrowserClientConfig,\n RecommendedItem,\n RecommendationResult,\n FilterRelaxation,\n ProjectionOptions,\n RecommendationOptions,\n RecommendOptions,\n PostProcessOverrideOptions,\n InteractionType,\n InteractionPayload,\n ImpressionPayload,\n ImpressionTrackerOptions,\n TrackEventOptions,\n} from './types';\n", "// Canonical JSON serializer \u2014 byte-identical with the engine, the Node\n// server SDK, and the Python SDK. See repo-root\n// `tests/auth/canonical-vectors.json` for the conformance gate.\n\nexport function canonicalJson(value: unknown): string {\n return stringify(value);\n}\n\nfunction stringify(value: unknown): string {\n if (value === null) return 'null';\n switch (typeof value) {\n case 'boolean':\n return value ? 'true' : 'false';\n case 'number':\n if (!Number.isFinite(value)) {\n throw new TypeError(\n `Cannot canonicalize non-finite number: ${String(value)}`,\n );\n }\n return JSON.stringify(value);\n case 'string':\n return JSON.stringify(value);\n case 'object': {\n if (Array.isArray(value)) {\n return '[' + value.map(stringify).join(',') + ']';\n }\n const keys = Object.keys(value as Record<string, unknown>).sort();\n const parts: string[] = [];\n for (const k of keys) {\n const v = (value as Record<string, unknown>)[k];\n if (typeof v === 'undefined') continue;\n parts.push(JSON.stringify(k) + ':' + stringify(v));\n }\n return '{' + parts.join(',') + '}';\n }\n case 'undefined':\n throw new TypeError('Cannot canonicalize undefined at the root');\n default:\n throw new TypeError(\n `Cannot canonicalize value of type ${typeof value}`,\n );\n }\n}\n", "// WebCrypto-based HMAC-SHA256 signing for browser-side recomnext callers.\n// Same wire format as the server SDK (sdks/node/src/signing.ts) and the\n// engine guard (engine/src/auth/guards/hmac.guard.ts).\n//\n// Public vs private token contract:\n// - PUBLIC token: shipped in JS bundles. Wire uses `frontend_sign` +\n// `frontend_timestamp` query params. Engine rejects unless the request\n// origin is on the tenant's allow-list AND the route is on the\n// public-write whitelist (or is a GET).\n// - PRIVATE token: NEVER ship to a browser. WebCrypto signing of private\n// tokens is provided here for completeness (e.g. for Node-via-bundler\n// consumers), but the recomnext browser SDK refuses to accept a\n// privateToken in its config.\n\nimport { canonicalJson } from './canonical-json.js';\n\nexport type SignerMode = 'public' | 'private';\n\nexport interface SignInput {\n method: string;\n // Path WITHOUT querystring or trailing slash.\n path: string;\n // Caller-supplied query params; values coerced to strings.\n query?: Record<string, string | number | boolean | undefined>;\n // Parsed body \u2014 undefined for GET/DELETE/HEAD/OPTIONS.\n body?: unknown;\n tenantId: string;\n token: string;\n mode: SignerMode;\n // Test-only clock override.\n now?: number;\n}\n\nexport interface SignedRequest {\n signedPath: string;\n timestamp: number;\n bodyHash: string;\n signature: string;\n idempotencyKey: string;\n}\n\n// Cached CryptoKey objects keyed by raw token bytes. importKey() is async\n// and non-trivial; pages calling sign() in a hot loop (e.g. mouse-move\n// telemetry) would otherwise pay the cost on every event.\nconst KEY_CACHE = new Map<string, Promise<CryptoKey>>();\n\nexport async function sign(input: SignInput): Promise<SignedRequest> {\n const verb = input.method.toUpperCase();\n const path = effectivePath(input.path);\n const timestamp = input.now ?? Math.floor(Date.now() / 1000);\n\n const bodyHash =\n verb === 'GET' || verb === 'DELETE' || verb === 'HEAD' || verb === 'OPTIONS'\n ? ''\n : await sha256Hex(canonicalJson(input.body ?? null));\n\n // Preserve empty-string values (e.g. filter= / itemsPerSegment= clear\n // sentinels for request-time overrides). Only drop undefined/null omit.\n // Must stay lock-step with engine HmacGuard + node/python signers.\n const others = new Map<string, string>();\n for (const [k, v] of Object.entries(input.query ?? {})) {\n if (v === undefined || v === null) continue;\n others.set(k, String(v));\n }\n others.delete('hmac_timestamp');\n others.delete('frontend_timestamp');\n others.delete('body_hash');\n others.delete('hmac_sign');\n others.delete('frontend_sign');\n const sortedOthers = Array.from(others.entries())\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`);\n\n const tsParam = input.mode === 'public' ? 'frontend_timestamp' : 'hmac_timestamp';\n const sigParam = input.mode === 'public' ? 'frontend_sign' : 'hmac_sign';\n\n const trailing = `${tsParam}=${timestamp}&body_hash=${bodyHash}`;\n const queryForSigning = sortedOthers.length\n ? `${sortedOthers.join('&')}&${trailing}`\n : trailing;\n\n const signingString = `${verb}\\n${path}?${queryForSigning}\\nX-Tenant-Id:${input.tenantId}`;\n const signature = await hmacSha256Hex(input.token, signingString);\n const signedPath = `${path}?${queryForSigning}&${sigParam}=${signature}`;\n\n return {\n signedPath,\n timestamp,\n bodyHash,\n signature,\n idempotencyKey: makeIdempotencyKey(),\n };\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const bytes = await crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(input),\n );\n return toHex(new Uint8Array(bytes));\n}\n\nasync function hmacSha256Hex(token: string, payload: string): Promise<string> {\n const key = await getKey(token);\n const sig = await crypto.subtle.sign(\n 'HMAC',\n key,\n new TextEncoder().encode(payload),\n );\n return toHex(new Uint8Array(sig));\n}\n\nasync function getKey(token: string): Promise<CryptoKey> {\n let cached = KEY_CACHE.get(token);\n if (!cached) {\n cached = crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(token),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n KEY_CACHE.set(token, cached);\n }\n return cached;\n}\n\n// Preload the CryptoKey ahead of any sign() calls. Useful at SDK init time\n// so the first user interaction doesn't pay the importKey cost on the hot\n// path. Returns immediately on a second call for the same token.\nexport function preloadSignerKey(token: string): Promise<CryptoKey> {\n return getKey(token);\n}\n\nfunction toHex(bytes: Uint8Array): string {\n let out = '';\n for (let i = 0; i < bytes.length; i++) {\n out += bytes[i].toString(16).padStart(2, '0');\n }\n return out;\n}\n\nfunction effectivePath(path: string): string {\n const stripped = path.replace(/\\/$/, '');\n return stripped.length > 0 ? stripped : '/';\n}\n\nfunction makeIdempotencyKey(): string {\n const c = (globalThis as unknown as { crypto?: Crypto & { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n // Fallback for older WebView shells without randomUUID.\n const rand = new Uint8Array(16);\n c?.getRandomValues(rand);\n rand[6] = (rand[6] & 0x0f) | 0x40;\n rand[8] = (rand[8] & 0x3f) | 0x80;\n const hex = toHex(rand);\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n", "// Durable retry queue for browser fire-and-forget endpoints (impressions,\n// interactions). Per the auth-rollout plan:\n// - Synchronous retries (3 attempts, ~5s) for request-response calls\n// happen inline in the SDK; THIS file is exclusively for the\n// fire-and-forget endpoints (/ingestion/impressions,\n// /ingestion/interactions).\n// - `/identity/merge` is NEVER auto-retried (non-idempotent) \u2014 callers\n// must NOT enqueue merge requests via this class.\n// - Exponential backoff, capped at 10 minutes / 8 attempts. After the\n// cap, items are dropped and `onRetryDrop` is invoked for observability.\n// - Optional IndexedDB persistence via `persistRetries: true` so queued\n// items survive page reloads / device-sleep / network outages.\n//\n// Re-signing happens on EVERY attempt \u2014 the engine's replay cache rejects\n// the same signature twice, and the 30s signing window means a stale sig\n// is worse than no retry. The `signRequest` callback is invoked with the\n// item payload and must return a freshly-signed URL + idempotency key.\n\nexport interface QueuedItem<TPayload = unknown> {\n // Unique id used for dedupe / persistence. Auto-generated when not set.\n id?: string;\n // Logical path the item targets (used for routing on flush).\n path: string;\n // HTTP method. Currently every fire-and-forget endpoint is POST; the\n // field exists so the queue can also handle PUTs if needed later.\n method: string;\n // Payload sent as the JSON body. Must be canonical-JSON serializable.\n payload: TPayload;\n // Number of attempts so far (0 on first enqueue). The queue increments.\n attempts?: number;\n // Earliest wall-clock ms at which the next attempt should run. Updated\n // each time we back off.\n nextAttemptAt?: number;\n}\n\nexport interface RetryQueueConfig<TPayload = unknown> {\n // Persist queued items across page reloads. Disabled by default \u2014 the\n // demo / read-mostly integrations don't need it. Enabling pulls in an\n // IndexedDB-backed `idb-keyval`-style adapter (see indexed-db.ts).\n persist?: boolean;\n // Storage key for IndexedDB persistence (so multi-tenant pages don't\n // share queue state). Required when `persist: true`.\n persistKey?: string;\n // Maximum attempts before the item is dropped. Default 8.\n maxAttempts?: number;\n // Initial backoff in ms. Default 1000.\n initialBackoffMs?: number;\n // Backoff cap in ms. Default 600_000 (10m).\n maxBackoffMs?: number;\n // Periodic drain interval in ms. Default 2000.\n flushIntervalMs?: number;\n // Called for every item dropped after exhausting attempts. Used for\n // observability \u2014 log to Sentry / surface as a tracker event.\n onRetryDrop?: (item: QueuedItem<TPayload>, error: unknown) => void;\n // Signs each attempt's request fresh. Returns the absolute URL to POST\n // to plus any auth headers (X-Idempotency-Key, etc.). Must NOT cache\n // its return value \u2014 the queue calls this once per attempt.\n signRequest: (item: QueuedItem<TPayload>) => Promise<{\n url: string;\n headers: Record<string, string>;\n }>;\n}\n\nconst DEFAULTS = {\n maxAttempts: 8,\n initialBackoffMs: 1000,\n maxBackoffMs: 600_000,\n flushIntervalMs: 2000,\n};\n\nexport class RetryQueue<TPayload = unknown> {\n private readonly cfg: Required<\n Omit<RetryQueueConfig<TPayload>, 'persist' | 'persistKey' | 'onRetryDrop'>\n > & {\n persist: boolean;\n persistKey?: string;\n onRetryDrop?: RetryQueueConfig<TPayload>['onRetryDrop'];\n };\n private items: QueuedItem<TPayload>[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private draining = false;\n private idb?: Promise<IDBDatabase | null>;\n\n constructor(cfg: RetryQueueConfig<TPayload>) {\n this.cfg = {\n persist: cfg.persist ?? false,\n persistKey: cfg.persistKey,\n maxAttempts: cfg.maxAttempts ?? DEFAULTS.maxAttempts,\n initialBackoffMs: cfg.initialBackoffMs ?? DEFAULTS.initialBackoffMs,\n maxBackoffMs: cfg.maxBackoffMs ?? DEFAULTS.maxBackoffMs,\n flushIntervalMs: cfg.flushIntervalMs ?? DEFAULTS.flushIntervalMs,\n onRetryDrop: cfg.onRetryDrop,\n signRequest: cfg.signRequest,\n };\n\n if (this.cfg.persist) {\n if (!this.cfg.persistKey) {\n throw new Error('RetryQueue: persistKey required when persist=true');\n }\n this.idb = openIndexedDb();\n void this.loadPersisted();\n }\n\n if (typeof window !== 'undefined') {\n this.timer = setInterval(() => void this.drain(), this.cfg.flushIntervalMs);\n window.addEventListener('online', () => void this.drain());\n // Best-effort flush on page hide \u2014 sendBeacon is the documented\n // companion for items we never want to drop.\n window.addEventListener('pagehide', () => void this.drain());\n }\n }\n\n enqueue(item: QueuedItem<TPayload>): void {\n const enriched: QueuedItem<TPayload> = {\n ...item,\n id: item.id ?? makeId(),\n attempts: item.attempts ?? 0,\n nextAttemptAt: item.nextAttemptAt ?? Date.now(),\n };\n this.items.push(enriched);\n void this.persist();\n // Eager drain attempt \u2014 useful when the page is interactive and the\n // queued event should leave the page within a hundred ms or two.\n void this.drain();\n }\n\n // Manually trigger a drain cycle. Used by callers on critical pageviews\n // (e.g. checkout) where dropping the next backoff window matters.\n flush(): Promise<void> {\n return this.drain();\n }\n\n destroy(): void {\n if (this.timer) clearInterval(this.timer);\n this.timer = null;\n }\n\n private async drain(): Promise<void> {\n if (this.draining) return;\n if (typeof navigator !== 'undefined' && navigator.onLine === false) return;\n this.draining = true;\n try {\n const now = Date.now();\n const ready = this.items.filter((i) => (i.nextAttemptAt ?? 0) <= now);\n for (const item of ready) {\n await this.attempt(item);\n }\n await this.persist();\n } finally {\n this.draining = false;\n }\n }\n\n private async attempt(item: QueuedItem<TPayload>): Promise<void> {\n item.attempts = (item.attempts ?? 0) + 1;\n try {\n const { url, headers } = await this.cfg.signRequest(item);\n const res = await fetch(url, {\n method: item.method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n body: JSON.stringify(item.payload),\n keepalive: true,\n });\n if (res.ok) {\n this.removeById(item.id!);\n return;\n }\n // 4xx other than 429 is permanent \u2014 no amount of retry fixes a bad\n // payload. Drop immediately so we don't keep poisoning the queue.\n if (res.status >= 400 && res.status < 500 && res.status !== 429) {\n this.removeById(item.id!);\n this.cfg.onRetryDrop?.(item, new Error(`HTTP ${res.status}`));\n return;\n }\n this.scheduleRetry(item, new Error(`HTTP ${res.status}`));\n } catch (err) {\n this.scheduleRetry(item, err);\n }\n }\n\n private scheduleRetry(item: QueuedItem<TPayload>, err: unknown): void {\n if ((item.attempts ?? 0) >= this.cfg.maxAttempts) {\n this.removeById(item.id!);\n this.cfg.onRetryDrop?.(item, err);\n return;\n }\n const exp = Math.min(\n this.cfg.maxBackoffMs,\n this.cfg.initialBackoffMs * Math.pow(2, (item.attempts ?? 1) - 1),\n );\n // Decorrelated jitter \u2014 full backoff with a uniformly-distributed\n // multiplier in [0.5, 1.0) so multiple replicas don't sync into a\n // thundering herd when the backend recovers.\n const jitter = 0.5 + Math.random() * 0.5;\n item.nextAttemptAt = Date.now() + Math.floor(exp * jitter);\n }\n\n private removeById(id: string): void {\n this.items = this.items.filter((i) => i.id !== id);\n }\n\n // \u2500\u2500 IndexedDB persistence \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async persist(): Promise<void> {\n if (!this.cfg.persist || !this.idb || !this.cfg.persistKey) return;\n try {\n const db = await this.idb;\n if (!db) return;\n const tx = db.transaction('queue', 'readwrite');\n const store = tx.objectStore('queue');\n store.put(JSON.parse(JSON.stringify(this.items)), this.cfg.persistKey);\n await txDone(tx);\n } catch {\n // Persistence is best-effort \u2014 never throw out of an enqueue path.\n }\n }\n\n private async loadPersisted(): Promise<void> {\n if (!this.cfg.persist || !this.idb || !this.cfg.persistKey) return;\n try {\n const db = await this.idb;\n if (!db) return;\n const tx = db.transaction('queue', 'readonly');\n const store = tx.objectStore('queue');\n const req = store.get(this.cfg.persistKey);\n const data: unknown = await new Promise((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n if (Array.isArray(data)) {\n this.items = data as QueuedItem<TPayload>[];\n void this.drain();\n }\n } catch {\n // Lost-persistence is silent by design \u2014 don't break enqueue on\n // private-browsing / disabled-storage.\n }\n }\n}\n\nfunction makeId(): string {\n const c = (globalThis as unknown as { crypto?: Crypto & { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n return `rnx-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;\n}\n\nfunction openIndexedDb(): Promise<IDBDatabase | null> {\n if (typeof indexedDB === 'undefined') return Promise.resolve(null);\n return new Promise((resolve) => {\n const req = indexedDB.open('recomnext-retry-queue', 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains('queue')) db.createObjectStore('queue');\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => resolve(null);\n req.onblocked = () => resolve(null);\n });\n}\n\nfunction txDone(tx: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error);\n });\n}\n", "/**\n * Client-side country hygiene. Server allowlist is still the source of\n * truth \u2014 unknown 2-letter codes are dropped here only when they are not\n * ISO-2 shaped (OTH / empty / non-alpha). Valid-looking codes go on the\n * wire; the engine maps anything not in country.ts to global.\n */\nexport function normalizeClientCountry(\n raw?: string | null,\n): string | undefined {\n if (raw == null) return undefined;\n const code = String(raw).trim().toUpperCase();\n if (!code || code === 'OTH' || !/^[A-Z]{2}$/.test(code)) return undefined;\n return code;\n}\n", "import type { RetryQueue } from '@recomnext/shared-browser-auth';\nimport type { FireAndForgetPayload } from './client';\nimport type { ImpressionTrackerOptions } from './types';\n\nconst FLUSH_INTERVAL = 2000;\n\nexport class ImpressionTracker {\n private queue: { itemId: string; scenario: string; country?: string }[] = [];\n private flushing = false;\n private observer: IntersectionObserver | null = null;\n private seen = new Set<string>();\n private timer: ReturnType<typeof setInterval> | null = null;\n private readonly getUserId: () => string;\n private readonly getSessionId: () => string;\n private readonly getCountry: () => string | undefined;\n // The RetryQueue handles all signed POSTs to /ingestion/impressions\n // (single source of truth for fire-and-forget on this page). We still\n // batch impressions every 2s before enqueuing so we don't fan out N\n // signed requests for one viewport scan.\n private readonly retryQueue: RetryQueue<FireAndForgetPayload>;\n\n constructor(\n _baseUrl: string,\n _tenantId: string,\n getUserId: () => string,\n getSessionId: () => string,\n retryQueue: RetryQueue<FireAndForgetPayload>,\n getCountry?: () => string | undefined,\n ) {\n // baseUrl + tenantId are captured by the client during signRequest()\n // and no longer needed at the tracker level. Kept in the signature\n // for backwards compatibility with anyone constructing the tracker\n // directly (rare \u2014 almost everyone uses RecomnextBrowserClient).\n this.getUserId = getUserId;\n this.getSessionId = getSessionId;\n this.retryQueue = retryQueue;\n this.getCountry = getCountry ?? (() => undefined);\n if (typeof window !== 'undefined') {\n this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL);\n // sendBeacon is best-effort and CANNOT carry the X-Idempotency-Key\n // header (it's a Blob POST). On pagehide we therefore force the\n // queue's normal signed-fetch path to drain via `keepalive: true`,\n // which is now widely supported in all evergreen browsers.\n window.addEventListener('pagehide', () => this.flush(true));\n }\n }\n\n observe(container: Element, opts: ImpressionTrackerOptions = {}): () => void {\n const selector = opts.itemSelector ?? '[data-item-id]';\n const threshold = opts.threshold ?? 0.5;\n const once = opts.once !== false;\n const scenario = opts.scenario ?? '';\n const country = opts.country || this.getCountry() || undefined;\n\n this.observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n const el = entry.target as HTMLElement;\n const itemId = el.dataset.itemId;\n if (!itemId) continue;\n const key = `${scenario}:${itemId}`;\n if (once && this.seen.has(key)) continue;\n this.seen.add(key);\n this.queue.push({ itemId, scenario, country });\n if (once) this.observer?.unobserve(el);\n }\n },\n { root: null, threshold },\n );\n\n const elements = container.querySelectorAll(selector);\n elements.forEach((el) => this.observer!.observe(el));\n\n return () => {\n this.observer?.disconnect();\n this.observer = null;\n };\n }\n\n // `urgent` is set during pagehide. When true we kick the RetryQueue\n // immediately so it has a chance to land items inside the unload window\n // (keepalive: true on the inner fetch keeps the connection alive past\n // the navigation).\n private flush(urgent = false): void {\n if (this.flushing || this.queue.length === 0) return;\n this.flushing = true;\n const batch = this.queue.splice(0, this.queue.length);\n\n const grouped = new Map<string, { itemIds: string[]; country?: string }>();\n for (const { itemId, scenario, country } of batch) {\n const key = `${scenario}\\0${country ?? ''}`;\n const group = grouped.get(key);\n if (group) {\n group.itemIds.push(itemId);\n } else {\n grouped.set(key, { itemIds: [itemId], country });\n }\n }\n\n const impressions = Array.from(grouped.entries()).map(([key, { itemIds, country }]) => {\n const scenarioSlug = key.split('\\0')[0];\n return {\n userId: this.getUserId() || undefined,\n sessionId: this.getSessionId() || undefined,\n itemIds,\n scenarioSlug: scenarioSlug || undefined,\n timestamp: new Date().toISOString(),\n country: country || undefined,\n };\n });\n\n this.retryQueue.enqueue({\n path: '/ingestion/impressions',\n method: 'POST',\n payload: { impressions },\n });\n\n if (urgent) void this.retryQueue.flush();\n this.flushing = false;\n }\n\n destroy(): void {\n this.flush();\n this.observer?.disconnect();\n if (this.timer) clearInterval(this.timer);\n }\n}\n", "import {\n preloadSignerKey,\n RetryQueue,\n sign as signRequest,\n type QueuedItem,\n} from '@recomnext/shared-browser-auth';\n\nimport type {\n SearchOptions,\n SearchResponse,\n SearchV2Options,\n SearchV2Response,\n SearchV2AutocompleteResponse,\n SearchV2SettingsResponse,\n CatalogItem,\n CatalogListResponse,\n ListItemsOptions,\n} from '@recomnext/shared-search-types';\n\nimport { normalizeClientCountry } from './country';\nimport { ImpressionTracker } from './tracker';\nimport type {\n BrowserClientConfig,\n RecommendationResult,\n RecommendationOptions,\n RecommendOptions,\n ImpressionTrackerOptions,\n TrackEventOptions,\n} from './types';\n\nconst SESSION_KEY = 'recomnext_session_id';\n\n// Synchronous retry budget for request-response calls (recommendations,\n// catalog reads, identity/merge). Mirrors the Node + Python SDK behavior:\n// 3 attempts total, ~5s aggregate, re-signs on every attempt.\nconst SYNC_RETRIES = 3;\nconst SYNC_INITIAL_BACKOFF_MS = 200;\nconst SYNC_MAX_BACKOFF_MS = 2_000;\n\n// 5xx + 429 are auto-retried. We special-case 503 +\n// errorCode=auth_replay_cache_unavailable so the SDK retries even if an\n// operator tunes 503 out of the retry set.\nconst RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);\nconst AUTH_REPLAY_CACHE_UNAVAILABLE = 'auth_replay_cache_unavailable';\n\n// `/identity/merge` mutates the identity graph on every call. The plan\n// locks this to single-shot until the engine grows server-side\n// idempotency \u2014 never auto-retry, even on a transient 503.\nconst NEVER_RETRY_PATHS = new Set(['/identity/merge']);\n\nexport class RecomnextBrowserClient {\n private readonly baseUrl: string;\n private readonly tenantId: string;\n private readonly publicToken: string | undefined;\n private userId: string;\n private sessionId: string;\n private country: string | undefined;\n private readonly tracker: ImpressionTracker;\n // Lazy \u2014 only constructed once for fire-and-forget queues that the\n // tracker also reuses.\n readonly fireAndForgetQueue: RetryQueue<FireAndForgetPayload>;\n\n constructor(config: BrowserClientConfig) {\n this.baseUrl = config.baseUrl.replace(/\\/+$/, '');\n this.tenantId = config.tenantId;\n this.publicToken = config.publicToken;\n this.userId = config.userId ?? '';\n this.sessionId = config.sessionId ?? this.resolveSessionId();\n this.country = normalizeClientCountry(config.country);\n\n // Eagerly import the CryptoKey so the first signed event after page\n // load doesn't pay the importKey latency on the hot path.\n if (this.publicToken) void preloadSignerKey(this.publicToken);\n\n this.fireAndForgetQueue = new RetryQueue<FireAndForgetPayload>({\n persist: config.persistRetries ?? false,\n persistKey: `rnx:${this.tenantId}:fnf`,\n onRetryDrop: config.onRetryDrop,\n signRequest: (item) => this.signFireAndForget(item),\n });\n\n this.tracker = new ImpressionTracker(\n this.baseUrl,\n this.tenantId,\n () => this.userId,\n () => this.sessionId,\n this.fireAndForgetQueue,\n () => this.country,\n );\n }\n\n private resolveSessionId(): string {\n if (typeof sessionStorage === 'undefined') return '';\n let sid = sessionStorage.getItem(SESSION_KEY);\n if (!sid) {\n sid = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;\n sessionStorage.setItem(SESSION_KEY, sid);\n }\n return sid;\n }\n\n private effectiveUserId(): string {\n return this.userId || this.sessionId;\n }\n\n private resolveCountry(override?: string): string | undefined {\n return normalizeClientCountry(override ?? this.country);\n }\n\n private async request<T>(\n method: string,\n path: string,\n body?: unknown,\n query?: Record<string, string | number | boolean | undefined>,\n ): Promise<T> {\n const maxRetries = NEVER_RETRY_PATHS.has(path) ? 0 : SYNC_RETRIES;\n let lastErr: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n if (attempt > 0) {\n const exp = Math.min(\n SYNC_MAX_BACKOFF_MS,\n SYNC_INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1),\n );\n const jitter = 0.5 + Math.random() * 0.5;\n await sleep(Math.floor(exp * jitter));\n }\n\n // Always re-sign \u2014 the engine's replay cache rejects the same\n // signature twice, and the 30s signing window means a stale sig\n // would also fail.\n const { url, headers } = await this.buildSignedRequest(\n method,\n path,\n query,\n body,\n );\n\n let res: Response;\n try {\n res = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n } catch (err) {\n lastErr = err;\n if (attempt >= maxRetries) throw err;\n continue;\n }\n\n if (res.status === 503) {\n const code = await extractErrorCode(res.clone());\n if (code === AUTH_REPLAY_CACHE_UNAVAILABLE && attempt < maxRetries) {\n lastErr = new Error(`recomnext: ${method} ${path} ${res.status} ${code}`);\n continue;\n }\n }\n\n if (RETRYABLE_STATUS.has(res.status) && attempt < maxRetries) {\n lastErr = new Error(`recomnext: ${method} ${path} returned ${res.status}`);\n continue;\n }\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new Error(`recomnext: ${method} ${path} returned ${res.status}: ${text}`);\n }\n\n if (res.status === 204) return undefined as unknown as T;\n return res.json() as Promise<T>;\n }\n\n throw lastErr ?? new Error(`recomnext: ${method} ${path} retries exhausted`);\n }\n\n // Same shape as the RetryQueue's signRequest contract \u2014 used by the\n // queue *and* the impression tracker so both share a single signing\n // path.\n async signFireAndForget(item: QueuedItem<FireAndForgetPayload>) {\n const { url, headers } = await this.buildSignedRequest(\n item.method,\n item.path,\n undefined,\n item.payload,\n );\n return { url, headers };\n }\n\n private async buildSignedRequest(\n method: string,\n path: string,\n query: Record<string, string | number | boolean | undefined> | undefined,\n body: unknown,\n ): Promise<{ url: string; headers: Record<string, string> }> {\n const baseHeaders: Record<string, string> = {\n 'X-Tenant-Id': this.tenantId,\n };\n\n if (!this.publicToken) {\n // Legacy / no-auth mode. Engine accepts because\n // LEGACY_TENANT_AUTH=true. After Phase 7 cutover this path will be\n // rejected with 401.\n const qs = renderQs(query);\n return { url: `${this.baseUrl}${path}${qs}`, headers: baseHeaders };\n }\n\n const signed = await signRequest({\n method,\n path,\n query,\n body: body ?? null,\n tenantId: this.tenantId,\n token: this.publicToken,\n mode: 'public',\n });\n return {\n url: `${this.baseUrl}${signed.signedPath}`,\n headers: {\n ...baseHeaders,\n 'X-Idempotency-Key': signed.idempotencyKey,\n },\n };\n }\n\n private qs(\n params: Record<string, string | number | boolean | undefined>,\n ): Record<string, string | number | boolean | undefined> {\n return params;\n }\n\n private includeQuery(opts?: RecommendationOptions): {\n returnProperties?: boolean;\n includedProperties?: string;\n } {\n return {\n returnProperties: opts?.returnProperties,\n includedProperties:\n opts?.includedProperties && opts.includedProperties.length > 0\n ? opts.includedProperties.join(',')\n : undefined,\n };\n }\n\n /** Serialize override fields for GET query (comma-separated lists; '' clears). */\n private overrideQuery(opts?: RecommendationOptions): {\n filter?: string;\n boosters?: string;\n rulesSlugs?: string;\n segmentationId?: string;\n itemsPerSegment?: number | string;\n autoRelaxFilters?: boolean;\n relaxEntireFilterOnEmpty?: boolean;\n } {\n if (!opts) return {};\n const join = (v: string[] | null | undefined): string | undefined => {\n if (v === undefined) return undefined;\n if (v === null || v.length === 0) return '';\n return v.join(',');\n };\n return {\n filter:\n opts.filter === undefined\n ? undefined\n : opts.filter === null\n ? ''\n : opts.filter,\n boosters: join(opts.boosters),\n rulesSlugs: join(opts.rulesSlugs),\n segmentationId:\n opts.segmentationId === undefined\n ? undefined\n : opts.segmentationId === null\n ? ''\n : opts.segmentationId,\n // Present null \u2192 '' so the query param is sent; engine transform maps\n // '' \u2192 null \u2192 resolve to itemsPerSegment=1 (omit would keep scenario).\n itemsPerSegment:\n opts.itemsPerSegment === undefined\n ? undefined\n : opts.itemsPerSegment === null\n ? ''\n : opts.itemsPerSegment,\n autoRelaxFilters: opts.autoRelaxFilters,\n relaxEntireFilterOnEmpty: opts.relaxEntireFilterOnEmpty,\n };\n }\n\n private overrideBody(opts?: RecommendationOptions): {\n filter?: string | null;\n boosters?: string[] | null;\n rulesSlugs?: string[] | null;\n segmentationId?: string | null;\n itemsPerSegment?: number | null;\n autoRelaxFilters?: boolean;\n relaxEntireFilterOnEmpty?: boolean;\n } {\n if (!opts) return {};\n const body: {\n filter?: string | null;\n boosters?: string[] | null;\n rulesSlugs?: string[] | null;\n segmentationId?: string | null;\n itemsPerSegment?: number | null;\n autoRelaxFilters?: boolean;\n relaxEntireFilterOnEmpty?: boolean;\n } = {};\n if (opts.filter !== undefined) body.filter = opts.filter;\n if (opts.boosters !== undefined) body.boosters = opts.boosters;\n if (opts.rulesSlugs !== undefined) body.rulesSlugs = opts.rulesSlugs;\n if (opts.segmentationId !== undefined) {\n body.segmentationId = opts.segmentationId;\n }\n if (opts.itemsPerSegment !== undefined) {\n body.itemsPerSegment = opts.itemsPerSegment;\n }\n if (opts.autoRelaxFilters !== undefined) {\n body.autoRelaxFilters = opts.autoRelaxFilters;\n }\n if (opts.relaxEntireFilterOnEmpty !== undefined) {\n body.relaxEntireFilterOnEmpty = opts.relaxEntireFilterOnEmpty;\n }\n return body;\n }\n\n /* \u2500\u2500 Recommendations \u2500\u2500 */\n\n getRecommendations(opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/user-to-item/${this.effectiveUserId()}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getRelatedItems(itemId: string, opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/item-to-item/${itemId}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getSimilarProducts(itemId: string, opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/similar-products/${itemId}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getRecentlyViewed(opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/recently-viewed/${this.effectiveUserId()}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getCartRecommendations(itemIds: string[], opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'POST',\n '/recommendations/items-to-items',\n {\n itemIds,\n count: opts?.count ?? 10,\n returnProperties: opts?.returnProperties,\n includedProperties: opts?.includedProperties,\n country: this.resolveCountry(opts?.country),\n ...this.overrideBody(opts),\n },\n this.qs({ scenario: opts?.scenario }),\n );\n }\n\n recommend(opts: RecommendOptions): Promise<RecommendationResult> {\n return this.request('POST', '/recommendations', {\n ...opts,\n userId: this.effectiveUserId(),\n country: this.resolveCountry(opts.country),\n });\n }\n\n /* \u2500\u2500 Search \u2500\u2500 */\n\n search(query: string, opts?: SearchOptions): Promise<SearchResponse> {\n return this.request(\n 'GET',\n '/recommendations/search',\n undefined,\n this.qs({\n query,\n count: opts?.count,\n category: opts?.category,\n priceRange: opts?.priceRange,\n userId: opts?.userId ?? (this.userId || undefined),\n }),\n );\n }\n\n searchV2(query: string, opts?: SearchV2Options): Promise<SearchV2Response> {\n return this.request(\n 'GET',\n '/recommendations/search/v2',\n undefined,\n this.qs({\n query,\n count: opts?.count,\n category: opts?.category,\n priceRange: opts?.priceRange,\n userId: opts?.userId ?? (this.userId || undefined),\n locale: opts?.locale,\n debugScores: opts?.debugScores === true ? true : undefined,\n algorithm: opts?.algorithm,\n }),\n );\n }\n\n autocomplete(\n query: string,\n opts?: { limit?: number },\n ): Promise<SearchV2AutocompleteResponse> {\n return this.request(\n 'GET',\n '/recommendations/search/v2/autocomplete',\n undefined,\n this.qs({\n query,\n limit: opts?.limit,\n }),\n );\n }\n\n getSearchV2Settings(): Promise<SearchV2SettingsResponse> {\n return this.request('GET', '/recommendations/search/v2/settings');\n }\n\n /* \u2500\u2500 Catalog browse (public HMAC eligible) \u2500\u2500 */\n\n listItems(opts?: ListItemsOptions): Promise<CatalogListResponse> {\n return this.request(\n 'GET',\n '/catalog/items',\n undefined,\n this.qs({\n page: opts?.page,\n limit: opts?.limit,\n category: opts?.category,\n search: opts?.search,\n availability: opts?.availability,\n }),\n );\n }\n\n getCategories(): Promise<string[]> {\n return this.request('GET', '/catalog/items/categories');\n }\n\n getItem(externalId: string): Promise<CatalogItem> {\n return this.request('GET', `/catalog/items/${encodeURIComponent(externalId)}`);\n }\n\n /* \u2500\u2500 Event tracking \u2500\u2500 */\n\n trackView(\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n this.enqueueInteraction('view', itemId, scenarioSlugOrOpts, opts);\n }\n\n trackCart(\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n this.enqueueInteraction('cart', itemId, scenarioSlugOrOpts, opts);\n }\n\n trackPurchase(\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n this.enqueueInteraction('purchase', itemId, scenarioSlugOrOpts, opts);\n }\n\n private enqueueInteraction(\n type: 'view' | 'cart' | 'purchase',\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n const fromObject = typeof scenarioSlugOrOpts === 'object' ? scenarioSlugOrOpts : undefined;\n const scenarioSlug =\n typeof scenarioSlugOrOpts === 'string' ? scenarioSlugOrOpts : fromObject?.scenarioSlug;\n const country = this.resolveCountry(fromObject?.country ?? opts?.country);\n this.fireAndForgetQueue.enqueue({\n path: '/ingestion/interactions',\n method: 'POST',\n payload: {\n interactions: [{\n userId: this.effectiveUserId(),\n itemId,\n type,\n scenarioSlug: scenarioSlug || undefined,\n timestamp: new Date().toISOString(),\n country,\n }],\n },\n });\n }\n\n /* \u2500\u2500 Impression tracking \u2500\u2500 */\n\n trackImpressions(container: Element, opts?: ImpressionTrackerOptions): () => void {\n return this.tracker.observe(container, opts);\n }\n\n /* \u2500\u2500 Identity \u2500\u2500 */\n\n setUserId(userId: string): void {\n this.userId = userId;\n }\n\n setCountry(code: string): void {\n this.country = normalizeClientCountry(code);\n }\n\n mergeIdentity(authenticatedUserId: string): Promise<unknown> {\n const anonId = this.sessionId;\n this.userId = authenticatedUserId;\n // Note: never auto-retried (see NEVER_RETRY_PATHS) \u2014 callers should\n // handle 5xx/network failures themselves to avoid duplicate merges.\n return this.request('POST', '/identity/merge', {\n userId: authenticatedUserId,\n sessionIds: [anonId],\n });\n }\n\n /* \u2500\u2500 Lifecycle \u2500\u2500 */\n\n destroy(): void {\n this.tracker.destroy();\n this.fireAndForgetQueue.destroy();\n }\n}\n\n// Shape used by both the SDK's trackView/trackCart/trackPurchase calls\n// and the ImpressionTracker \u2014 they all funnel through the same queue.\nexport type FireAndForgetPayload = {\n interactions?: unknown[];\n impressions?: unknown[];\n};\n\nfunction renderQs(params?: Record<string, string | number | boolean | undefined>): string {\n if (!params) return '';\n const parts: string[] = [];\n for (const [k, v] of Object.entries(params)) {\n // Preserve empty string (GET clear sentinels). Drop only omit/null.\n if (v === undefined || v === null) continue;\n parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);\n }\n return parts.length ? `?${parts.join('&')}` : '';\n}\n\nasync function extractErrorCode(res: Response): Promise<string | undefined> {\n try {\n const body = (await res.json()) as { errorCode?: unknown };\n return typeof body.errorCode === 'string' ? body.errorCode : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n"],
5
- "mappings": "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",
6
- "names": ["index_exports", "__export", "ImpressionTracker", "RecomnextBrowserClient", "normalizeClientCountry", "canonicalJson", "value", "stringify", "keys", "parts", "k", "v", "KEY_CACHE", "sign", "input", "verb", "path", "effectivePath", "timestamp", "bodyHash", "sha256Hex", "canonicalJson", "others", "k", "v", "sortedOthers", "a", "b", "tsParam", "sigParam", "trailing", "queryForSigning", "signingString", "signature", "hmacSha256Hex", "makeIdempotencyKey", "bytes", "toHex", "token", "payload", "key", "getKey", "sig", "cached", "preloadSignerKey", "out", "i", "stripped", "c", "rand", "hex", "DEFAULTS", "RetryQueue", "cfg", "openIndexedDb", "item", "enriched", "makeId", "now", "ready", "i", "url", "headers", "res", "err", "exp", "jitter", "id", "db", "tx", "txDone", "req", "data", "resolve", "reject", "c", "normalizeClientCountry", "raw", "code", "ImpressionTracker", "_baseUrl", "_tenantId", "getUserId", "getSessionId", "retryQueue", "getCountry", "container", "opts", "selector", "threshold", "once", "scenario", "country", "entries", "entry", "el", "itemId", "key", "urgent", "batch", "grouped", "group", "impressions", "itemIds", "scenarioSlug", "SESSION_KEY", "SYNC_RETRIES", "SYNC_INITIAL_BACKOFF_MS", "SYNC_MAX_BACKOFF_MS", "RETRYABLE_STATUS", "AUTH_REPLAY_CACHE_UNAVAILABLE", "NEVER_RETRY_PATHS", "RecomnextBrowserClient", "config", "normalizeClientCountry", "preloadSignerKey", "RetryQueue", "item", "ImpressionTracker", "sid", "override", "method", "path", "body", "query", "maxRetries", "lastErr", "attempt", "exp", "jitter", "sleep", "url", "headers", "res", "err", "code", "extractErrorCode", "text", "baseHeaders", "qs", "renderQs", "signed", "sign", "params", "opts", "join", "v", "itemId", "itemIds", "externalId", "scenarioSlugOrOpts", "type", "fromObject", "scenarioSlug", "country", "container", "userId", "authenticatedUserId", "anonId", "parts", "k", "ms", "r"]
3
+ "sources": ["../src/index.ts", "../../shared-browser-auth/src/canonical-json.ts", "../../shared-browser-auth/src/sign.ts", "../../shared-browser-auth/src/retry-queue.ts", "../../shared-browser-auth/src/session.ts", "../../shared-browser-auth/src/sync-retry-fetch.ts", "../../shared-browser-auth/src/identity-coordinator.ts", "../src/country.ts", "../src/tracker.ts", "../src/client.ts"],
4
+ "sourcesContent": ["export { RecomnextBrowserClient } from './client';\nexport {\n RECOMNEXT_SESSION_KEY,\n resolveAnonymousSessionId,\n readAnonymousSessionId,\n clearAnonymousSession,\n RecomnextIdentity,\n installRecomnextIdentityDomListener,\n RECOMNEXT_IDENTITY_DOM_EVENT,\n} from '@recomnext/shared-browser-auth';\nexport type {\n BrowserStorage,\n IdentityCoordinatorConfig,\n IdentityListener,\n IdentityState,\n} from '@recomnext/shared-browser-auth';\nexport { ImpressionTracker } from './tracker';\nexport { normalizeClientCountry } from './country';\nexport type { FireAndForgetPayload } from './client';\nexport type {\n FacetBucket,\n SearchFacets,\n SearchOptions,\n SearchResponse,\n SearchV2QueryConstraint,\n SearchV2ScoreBreakdown,\n SearchV2ItemScore,\n SearchV2Options,\n SearchV2Response,\n SearchV2AutocompleteResponse,\n SearchV2SettingsResponse,\n CatalogItem,\n CatalogListResponse,\n ListItemsOptions,\n} from '@recomnext/shared-search-types';\nexport type {\n BrowserClientConfig,\n RecommendedItem,\n RecommendationResult,\n FilterRelaxation,\n ProjectionOptions,\n RecommendationOptions,\n RecommendOptions,\n PostProcessOverrideOptions,\n InteractionType,\n InteractionPayload,\n ImpressionPayload,\n ImpressionTrackerOptions,\n TrackEventOptions,\n} from './types';\n", "// Canonical JSON serializer \u2014 byte-identical with the engine, the Node\n// server SDK, and the Python SDK. See repo-root\n// `tests/auth/canonical-vectors.json` for the conformance gate.\n\nexport function canonicalJson(value: unknown): string {\n return stringify(value);\n}\n\nfunction stringify(value: unknown): string {\n if (value === null) return 'null';\n switch (typeof value) {\n case 'boolean':\n return value ? 'true' : 'false';\n case 'number':\n if (!Number.isFinite(value)) {\n throw new TypeError(\n `Cannot canonicalize non-finite number: ${String(value)}`,\n );\n }\n return JSON.stringify(value);\n case 'string':\n return JSON.stringify(value);\n case 'object': {\n if (Array.isArray(value)) {\n return '[' + value.map(stringify).join(',') + ']';\n }\n const keys = Object.keys(value as Record<string, unknown>).sort();\n const parts: string[] = [];\n for (const k of keys) {\n const v = (value as Record<string, unknown>)[k];\n if (typeof v === 'undefined') continue;\n parts.push(JSON.stringify(k) + ':' + stringify(v));\n }\n return '{' + parts.join(',') + '}';\n }\n case 'undefined':\n throw new TypeError('Cannot canonicalize undefined at the root');\n default:\n throw new TypeError(\n `Cannot canonicalize value of type ${typeof value}`,\n );\n }\n}\n", "// WebCrypto-based HMAC-SHA256 signing for browser-side recomnext callers.\n// Same wire format as the server SDK (sdks/node/src/signing.ts) and the\n// engine guard (engine/src/auth/guards/hmac.guard.ts).\n//\n// Public vs private token contract:\n// - PUBLIC token: shipped in JS bundles. Wire uses `frontend_sign` +\n// `frontend_timestamp` query params. Engine rejects unless the request\n// origin is on the tenant's allow-list AND the route is on the\n// public-write whitelist (or is a GET).\n// - PRIVATE token: NEVER ship to a browser. WebCrypto signing of private\n// tokens is provided here for completeness (e.g. for Node-via-bundler\n// consumers), but the recomnext browser SDK refuses to accept a\n// privateToken in its config.\n\nimport { canonicalJson } from './canonical-json.js';\n\nexport type SignerMode = 'public' | 'private';\n\nexport interface SignInput {\n method: string;\n // Path WITHOUT querystring or trailing slash.\n path: string;\n // Caller-supplied query params; values coerced to strings.\n query?: Record<string, string | number | boolean | undefined>;\n // Parsed body \u2014 undefined for GET/DELETE/HEAD/OPTIONS.\n body?: unknown;\n tenantId: string;\n token: string;\n mode: SignerMode;\n // Test-only clock override.\n now?: number;\n}\n\nexport interface SignedRequest {\n signedPath: string;\n timestamp: number;\n bodyHash: string;\n signature: string;\n idempotencyKey: string;\n}\n\n// Cached CryptoKey objects keyed by raw token bytes. importKey() is async\n// and non-trivial; pages calling sign() in a hot loop (e.g. mouse-move\n// telemetry) would otherwise pay the cost on every event.\nconst KEY_CACHE = new Map<string, Promise<CryptoKey>>();\n\nexport async function sign(input: SignInput): Promise<SignedRequest> {\n const verb = input.method.toUpperCase();\n const path = effectivePath(input.path);\n const timestamp = input.now ?? Math.floor(Date.now() / 1000);\n\n const bodyHash =\n verb === 'GET' || verb === 'DELETE' || verb === 'HEAD' || verb === 'OPTIONS'\n ? ''\n : await sha256Hex(canonicalJson(input.body ?? null));\n\n // Preserve empty-string values (e.g. filter= / itemsPerSegment= clear\n // sentinels for request-time overrides). Only drop undefined/null omit.\n // Must stay lock-step with engine HmacGuard + node/python signers.\n const others = new Map<string, string>();\n for (const [k, v] of Object.entries(input.query ?? {})) {\n if (v === undefined || v === null) continue;\n others.set(k, String(v));\n }\n others.delete('hmac_timestamp');\n others.delete('frontend_timestamp');\n others.delete('body_hash');\n others.delete('hmac_sign');\n others.delete('frontend_sign');\n const sortedOthers = Array.from(others.entries())\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`);\n\n const tsParam = input.mode === 'public' ? 'frontend_timestamp' : 'hmac_timestamp';\n const sigParam = input.mode === 'public' ? 'frontend_sign' : 'hmac_sign';\n\n const trailing = `${tsParam}=${timestamp}&body_hash=${bodyHash}`;\n const queryForSigning = sortedOthers.length\n ? `${sortedOthers.join('&')}&${trailing}`\n : trailing;\n\n const signingString = `${verb}\\n${path}?${queryForSigning}\\nX-Tenant-Id:${input.tenantId}`;\n const signature = await hmacSha256Hex(input.token, signingString);\n const signedPath = `${path}?${queryForSigning}&${sigParam}=${signature}`;\n\n return {\n signedPath,\n timestamp,\n bodyHash,\n signature,\n idempotencyKey: makeIdempotencyKey(),\n };\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const bytes = await crypto.subtle.digest(\n 'SHA-256',\n new TextEncoder().encode(input),\n );\n return toHex(new Uint8Array(bytes));\n}\n\nasync function hmacSha256Hex(token: string, payload: string): Promise<string> {\n const key = await getKey(token);\n const sig = await crypto.subtle.sign(\n 'HMAC',\n key,\n new TextEncoder().encode(payload),\n );\n return toHex(new Uint8Array(sig));\n}\n\nasync function getKey(token: string): Promise<CryptoKey> {\n let cached = KEY_CACHE.get(token);\n if (!cached) {\n cached = crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(token),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n KEY_CACHE.set(token, cached);\n }\n return cached;\n}\n\n// Preload the CryptoKey ahead of any sign() calls. Useful at SDK init time\n// so the first user interaction doesn't pay the importKey cost on the hot\n// path. Returns immediately on a second call for the same token.\nexport function preloadSignerKey(token: string): Promise<CryptoKey> {\n return getKey(token);\n}\n\nfunction toHex(bytes: Uint8Array): string {\n let out = '';\n for (let i = 0; i < bytes.length; i++) {\n out += bytes[i].toString(16).padStart(2, '0');\n }\n return out;\n}\n\nfunction effectivePath(path: string): string {\n const stripped = path.replace(/\\/$/, '');\n return stripped.length > 0 ? stripped : '/';\n}\n\nfunction makeIdempotencyKey(): string {\n const c = (globalThis as unknown as { crypto?: Crypto & { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n // Fallback for older WebView shells without randomUUID.\n const rand = new Uint8Array(16);\n c?.getRandomValues(rand);\n rand[6] = (rand[6] & 0x0f) | 0x40;\n rand[8] = (rand[8] & 0x3f) | 0x80;\n const hex = toHex(rand);\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n", "// Durable retry queue for browser fire-and-forget endpoints (impressions,\n// interactions). Per the auth-rollout plan:\n// - Synchronous retries (3 attempts, ~5s) for request-response calls\n// happen inline in the SDK; THIS file is exclusively for the\n// fire-and-forget endpoints (/ingestion/impressions,\n// /ingestion/interactions).\n// - `/identity/merge` is not queued here; synchronous merge retries use\n// `fetchWithSyncRetries` / `RecomnextBrowserClient.request()` instead.\n// - Exponential backoff, capped at 10 minutes / 8 attempts. After the\n// cap, items are dropped and `onRetryDrop` is invoked for observability.\n// - Optional IndexedDB persistence via `persistRetries: true` so queued\n// items survive page reloads / device-sleep / network outages.\n//\n// Re-signing happens on EVERY attempt \u2014 the engine's replay cache rejects\n// the same signature twice, and the 30s signing window means a stale sig\n// is worse than no retry. The `signRequest` callback is invoked with the\n// item payload and must return a freshly-signed URL + idempotency key.\n\nexport interface QueuedItem<TPayload = unknown> {\n // Unique id used for dedupe / persistence. Auto-generated when not set.\n id?: string;\n // Logical path the item targets (used for routing on flush).\n path: string;\n // HTTP method. Currently every fire-and-forget endpoint is POST; the\n // field exists so the queue can also handle PUTs if needed later.\n method: string;\n // Payload sent as the JSON body. Must be canonical-JSON serializable.\n payload: TPayload;\n // Number of attempts so far (0 on first enqueue). The queue increments.\n attempts?: number;\n // Earliest wall-clock ms at which the next attempt should run. Updated\n // each time we back off.\n nextAttemptAt?: number;\n}\n\nexport interface RetryQueueConfig<TPayload = unknown> {\n // Persist queued items across page reloads. Disabled by default \u2014 the\n // demo / read-mostly integrations don't need it. Enabling pulls in an\n // IndexedDB-backed `idb-keyval`-style adapter (see indexed-db.ts).\n persist?: boolean;\n // Storage key for IndexedDB persistence (so multi-tenant pages don't\n // share queue state). Required when `persist: true`.\n persistKey?: string;\n // Maximum attempts before the item is dropped. Default 8.\n maxAttempts?: number;\n // Initial backoff in ms. Default 1000.\n initialBackoffMs?: number;\n // Backoff cap in ms. Default 600_000 (10m).\n maxBackoffMs?: number;\n // Periodic drain interval in ms. Default 2000.\n flushIntervalMs?: number;\n // Called for every item dropped after exhausting attempts. Used for\n // observability \u2014 log to Sentry / surface as a tracker event.\n onRetryDrop?: (item: QueuedItem<TPayload>, error: unknown) => void;\n // Signs each attempt's request fresh. Returns the absolute URL to POST\n // to plus any auth headers (X-Idempotency-Key, etc.). Must NOT cache\n // its return value \u2014 the queue calls this once per attempt.\n signRequest: (item: QueuedItem<TPayload>) => Promise<{\n url: string;\n headers: Record<string, string>;\n }>;\n}\n\nconst DEFAULTS = {\n maxAttempts: 8,\n initialBackoffMs: 1000,\n maxBackoffMs: 600_000,\n flushIntervalMs: 2000,\n};\n\nexport class RetryQueue<TPayload = unknown> {\n private readonly cfg: Required<\n Omit<RetryQueueConfig<TPayload>, 'persist' | 'persistKey' | 'onRetryDrop'>\n > & {\n persist: boolean;\n persistKey?: string;\n onRetryDrop?: RetryQueueConfig<TPayload>['onRetryDrop'];\n };\n private items: QueuedItem<TPayload>[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private draining = false;\n private idb?: Promise<IDBDatabase | null>;\n\n constructor(cfg: RetryQueueConfig<TPayload>) {\n this.cfg = {\n persist: cfg.persist ?? false,\n persistKey: cfg.persistKey,\n maxAttempts: cfg.maxAttempts ?? DEFAULTS.maxAttempts,\n initialBackoffMs: cfg.initialBackoffMs ?? DEFAULTS.initialBackoffMs,\n maxBackoffMs: cfg.maxBackoffMs ?? DEFAULTS.maxBackoffMs,\n flushIntervalMs: cfg.flushIntervalMs ?? DEFAULTS.flushIntervalMs,\n onRetryDrop: cfg.onRetryDrop,\n signRequest: cfg.signRequest,\n };\n\n if (this.cfg.persist) {\n if (!this.cfg.persistKey) {\n throw new Error('RetryQueue: persistKey required when persist=true');\n }\n this.idb = openIndexedDb();\n void this.loadPersisted();\n }\n\n if (typeof window !== 'undefined') {\n this.timer = setInterval(() => void this.drain(), this.cfg.flushIntervalMs);\n window.addEventListener('online', () => void this.drain());\n // Best-effort flush on page hide \u2014 sendBeacon is the documented\n // companion for items we never want to drop.\n window.addEventListener('pagehide', () => void this.drain());\n }\n }\n\n enqueue(item: QueuedItem<TPayload>): void {\n const enriched: QueuedItem<TPayload> = {\n ...item,\n id: item.id ?? makeId(),\n attempts: item.attempts ?? 0,\n nextAttemptAt: item.nextAttemptAt ?? Date.now(),\n };\n this.items.push(enriched);\n void this.persist();\n // Eager drain attempt \u2014 useful when the page is interactive and the\n // queued event should leave the page within a hundred ms or two.\n void this.drain();\n }\n\n // Manually trigger a drain cycle. Used by callers on critical pageviews\n // (e.g. checkout) where dropping the next backoff window matters.\n flush(): Promise<void> {\n return this.drain();\n }\n\n destroy(): void {\n if (this.timer) clearInterval(this.timer);\n this.timer = null;\n }\n\n private async drain(): Promise<void> {\n if (this.draining) return;\n if (typeof navigator !== 'undefined' && navigator.onLine === false) return;\n this.draining = true;\n try {\n const now = Date.now();\n const ready = this.items.filter((i) => (i.nextAttemptAt ?? 0) <= now);\n for (const item of ready) {\n await this.attempt(item);\n }\n await this.persist();\n } finally {\n this.draining = false;\n }\n }\n\n private async attempt(item: QueuedItem<TPayload>): Promise<void> {\n item.attempts = (item.attempts ?? 0) + 1;\n try {\n const { url, headers } = await this.cfg.signRequest(item);\n const res = await fetch(url, {\n method: item.method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n body: JSON.stringify(item.payload),\n keepalive: true,\n });\n if (res.ok) {\n this.removeById(item.id!);\n return;\n }\n // 4xx other than 429 is permanent \u2014 no amount of retry fixes a bad\n // payload. Drop immediately so we don't keep poisoning the queue.\n if (res.status >= 400 && res.status < 500 && res.status !== 429) {\n this.removeById(item.id!);\n this.cfg.onRetryDrop?.(item, new Error(`HTTP ${res.status}`));\n return;\n }\n this.scheduleRetry(item, new Error(`HTTP ${res.status}`));\n } catch (err) {\n this.scheduleRetry(item, err);\n }\n }\n\n private scheduleRetry(item: QueuedItem<TPayload>, err: unknown): void {\n if ((item.attempts ?? 0) >= this.cfg.maxAttempts) {\n this.removeById(item.id!);\n this.cfg.onRetryDrop?.(item, err);\n return;\n }\n const exp = Math.min(\n this.cfg.maxBackoffMs,\n this.cfg.initialBackoffMs * Math.pow(2, (item.attempts ?? 1) - 1),\n );\n // Decorrelated jitter \u2014 full backoff with a uniformly-distributed\n // multiplier in [0.5, 1.0) so multiple replicas don't sync into a\n // thundering herd when the backend recovers.\n const jitter = 0.5 + Math.random() * 0.5;\n item.nextAttemptAt = Date.now() + Math.floor(exp * jitter);\n }\n\n private removeById(id: string): void {\n this.items = this.items.filter((i) => i.id !== id);\n }\n\n // \u2500\u2500 IndexedDB persistence \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n private async persist(): Promise<void> {\n if (!this.cfg.persist || !this.idb || !this.cfg.persistKey) return;\n try {\n const db = await this.idb;\n if (!db) return;\n const tx = db.transaction('queue', 'readwrite');\n const store = tx.objectStore('queue');\n store.put(JSON.parse(JSON.stringify(this.items)), this.cfg.persistKey);\n await txDone(tx);\n } catch {\n // Persistence is best-effort \u2014 never throw out of an enqueue path.\n }\n }\n\n private async loadPersisted(): Promise<void> {\n if (!this.cfg.persist || !this.idb || !this.cfg.persistKey) return;\n try {\n const db = await this.idb;\n if (!db) return;\n const tx = db.transaction('queue', 'readonly');\n const store = tx.objectStore('queue');\n const req = store.get(this.cfg.persistKey);\n const data: unknown = await new Promise((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n if (Array.isArray(data)) {\n this.items = data as QueuedItem<TPayload>[];\n void this.drain();\n }\n } catch {\n // Lost-persistence is silent by design \u2014 don't break enqueue on\n // private-browsing / disabled-storage.\n }\n }\n}\n\nfunction makeId(): string {\n const c = (globalThis as unknown as { crypto?: Crypto & { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) return c.randomUUID();\n return `rnx-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;\n}\n\nfunction openIndexedDb(): Promise<IDBDatabase | null> {\n if (typeof indexedDB === 'undefined') return Promise.resolve(null);\n return new Promise((resolve) => {\n const req = indexedDB.open('recomnext-retry-queue', 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains('queue')) db.createObjectStore('queue');\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => resolve(null);\n req.onblocked = () => resolve(null);\n });\n}\n\nfunction txDone(tx: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error);\n });\n}\n", "/** localStorage key for the anonymous RecomNext session id (shared across tabs). */\nexport const RECOMNEXT_SESSION_KEY = 'recomnext_session_id';\n\nexport type BrowserStorage = {\n localStorage: Storage | undefined;\n sessionStorage: Storage | undefined;\n};\n\nfunction defaultStorage(): BrowserStorage {\n return {\n localStorage:\n typeof globalThis !== 'undefined' && typeof localStorage !== 'undefined'\n ? localStorage\n : undefined,\n sessionStorage:\n typeof globalThis !== 'undefined' && typeof sessionStorage !== 'undefined'\n ? sessionStorage\n : undefined,\n };\n}\n\nexport function generateAnonymousSessionId(): string {\n return `sess_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;\n}\n\n/**\n * Resolve or create the anonymous session id. Stored in localStorage so all\n * tabs on the origin share one bucket. One-release migration: if local is\n * empty, copy from sessionStorage then remove the legacy key.\n */\nexport function resolveAnonymousSessionId(\n storage: BrowserStorage = defaultStorage(),\n): string {\n const { localStorage: ls, sessionStorage: ss } = storage;\n if (!ls) return '';\n\n const fromLocal = ls.getItem(RECOMNEXT_SESSION_KEY);\n if (fromLocal) return fromLocal;\n\n if (ss) {\n const legacy = ss.getItem(RECOMNEXT_SESSION_KEY);\n if (legacy) {\n ls.setItem(RECOMNEXT_SESSION_KEY, legacy);\n ss.removeItem(RECOMNEXT_SESSION_KEY);\n return legacy;\n }\n }\n\n const sid = generateAnonymousSessionId();\n ls.setItem(RECOMNEXT_SESSION_KEY, sid);\n return sid;\n}\n\n/** Read without creating a new id (includes legacy sessionStorage). */\nexport function readAnonymousSessionId(\n storage: BrowserStorage = defaultStorage(),\n): string {\n const { localStorage: ls, sessionStorage: ss } = storage;\n if (ls) {\n const fromLocal = ls.getItem(RECOMNEXT_SESSION_KEY);\n if (fromLocal) return fromLocal;\n }\n if (ss) return ss.getItem(RECOMNEXT_SESSION_KEY) ?? '';\n return '';\n}\n\n/**\n * Rotate anonymous session (logout hygiene). Removes legacy sessionStorage key,\n * writes the next id to localStorage, and returns that id.\n */\nexport function clearAnonymousSession(\n customId?: string,\n storage: BrowserStorage = defaultStorage(),\n): string {\n const { localStorage: ls, sessionStorage: ss } = storage;\n const next =\n customId !== undefined && customId !== ''\n ? customId\n : generateAnonymousSessionId();\n\n if (ls) ls.setItem(RECOMNEXT_SESSION_KEY, next);\n if (ss) ss.removeItem(RECOMNEXT_SESSION_KEY);\n return next;\n}\n", "const SYNC_RETRIES = 3;\nconst SYNC_INITIAL_BACKOFF_MS = 200;\nconst SYNC_MAX_BACKOFF_MS = 2_000;\n\nconst RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);\nconst AUTH_REPLAY_CACHE_UNAVAILABLE = 'auth_replay_cache_unavailable';\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n\nasync function extractErrorCode(res: Response): Promise<string | undefined> {\n try {\n const body = (await res.json()) as { errorCode?: unknown };\n return typeof body.errorCode === 'string' ? body.errorCode : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Mirrors {@link RecomnextBrowserClient} synchronous retry budget for identity/merge.\n * Caller must re-sign on each attempt via {@link performFetch}.\n */\nexport async function fetchWithSyncRetries(\n method: string,\n path: string,\n performFetch: () => Promise<Response>,\n): Promise<Response> {\n let lastErr: unknown;\n\n for (let attempt = 0; attempt <= SYNC_RETRIES; attempt++) {\n if (attempt > 0) {\n const exp = Math.min(\n SYNC_MAX_BACKOFF_MS,\n SYNC_INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1),\n );\n const jitter = 0.5 + Math.random() * 0.5;\n await sleep(Math.floor(exp * jitter));\n }\n\n let res: Response;\n try {\n res = await performFetch();\n } catch (err) {\n lastErr = err;\n if (attempt >= SYNC_RETRIES) throw err;\n continue;\n }\n\n if (res.status === 503) {\n const code = await extractErrorCode(res.clone());\n if (code === AUTH_REPLAY_CACHE_UNAVAILABLE && attempt < SYNC_RETRIES) {\n lastErr = new Error(`recomnext: ${method} ${path} ${res.status} ${code}`);\n continue;\n }\n }\n\n if (RETRYABLE_STATUS.has(res.status) && attempt < SYNC_RETRIES) {\n lastErr = new Error(`recomnext: ${method} ${path} returned ${res.status}`);\n continue;\n }\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new Error(`recomnext: ${method} ${path} returned ${res.status}: ${text}`);\n }\n\n return res;\n }\n\n throw lastErr ?? new Error(`recomnext: ${method} ${path} retries exhausted`);\n}\n", "import { sign, preloadSignerKey } from './sign.js';\nimport { fetchWithSyncRetries } from './sync-retry-fetch.js';\nimport {\n clearAnonymousSession,\n readAnonymousSessionId,\n resolveAnonymousSessionId,\n} from './session.js';\n\nexport type IdentityListener = (state: IdentityState) => void;\n\nexport interface IdentityState {\n authenticatedUserId: string | null;\n anonymousSessionId: string;\n effectiveUserId: string;\n}\n\nexport interface IdentityCoordinatorConfig {\n tenantId: string;\n baseUrl: string;\n publicToken?: string;\n /** Host-owned anonymous id (e.g. Driffle `sess_{device_uid}`) when not logged in. */\n getHostAnonymousUserId?: () => string;\n /**\n * When {@link getHostAnonymousUserId} is set, call on logout to rotate the host id\n * (e.g. new `device_uid` cookie). The merge ledger is one-shot per anonymous id.\n */\n onHostLogout?: () => void | Promise<void>;\n /** Mirror authenticated user id to localStorage for other tabs (default true). */\n syncAuthAcrossTabs?: boolean;\n}\n\n/** localStorage key for cross-tab authenticated user sync. */\nexport const RECOMNEXT_AUTH_SYNC_KEY = 'recomnext_authenticated_user_id';\n\nlet config: IdentityCoordinatorConfig | null = null;\nlet authenticatedUserId: string | null = null;\nconst listeners = new Set<IdentityListener>();\nconst mergeInFlight = new Map<string, Promise<void>>();\nconst authTransitionInFlight = new Map<string, Promise<void>>();\nlet suppressAuthSyncWrite = false;\nlet storageListenerInstalled = false;\n\nfunction normalizeBaseUrl(baseUrl: string): string {\n return baseUrl.replace(/\\/+$/, '');\n}\n\nfunction hostAnonymousId(): string {\n const id = config?.getHostAnonymousUserId?.() ?? '';\n return typeof id === 'string' ? id.trim() : '';\n}\n\nfunction resolveEffectiveUserId(): string {\n if (authenticatedUserId) {\n return authenticatedUserId;\n }\n const host = hostAnonymousId();\n if (host) return host;\n const stored = readAnonymousSessionId();\n if (stored) return stored;\n return resolveAnonymousSessionId();\n}\n\nfunction snapshotState(): IdentityState {\n return {\n authenticatedUserId,\n anonymousSessionId: readAnonymousSessionId(),\n effectiveUserId: resolveEffectiveUserId(),\n };\n}\n\nfunction notify(): void {\n const state = snapshotState();\n for (const listener of listeners) {\n listener(state);\n }\n}\n\nfunction mergeKey(tenantId: string, anonId: string, knownUserId: string): string {\n return `${tenantId}:${anonId}:${knownUserId}`;\n}\n\nfunction authTransitionKey(nextId: string | null): string {\n return nextId === null ? 'logout' : `login:${nextId}`;\n}\n\nfunction writeAuthSync(userId: string | null): void {\n if (suppressAuthSyncWrite || !config) return;\n if (config.syncAuthAcrossTabs === false) return;\n if (typeof localStorage === 'undefined') return;\n\n if (userId === null) {\n localStorage.removeItem(RECOMNEXT_AUTH_SYNC_KEY);\n } else {\n localStorage.setItem(RECOMNEXT_AUTH_SYNC_KEY, userId);\n }\n}\n\nfunction readAuthSync(): string | null {\n if (typeof localStorage === 'undefined') return null;\n const raw = localStorage.getItem(RECOMNEXT_AUTH_SYNC_KEY);\n if (!raw || raw.trim() === '') return null;\n return raw.trim();\n}\n\nasync function postMerge(knownUserId: string, anonId: string): Promise<void> {\n if (!config) return;\n if (!anonId || anonId === knownUserId) return;\n\n const key = mergeKey(config.tenantId, anonId, knownUserId);\n const existing = mergeInFlight.get(key);\n if (existing) {\n await existing;\n return;\n }\n\n const job = (async () => {\n const path = '/identity/merge';\n const body = { userId: knownUserId, sessionIds: [anonId] };\n const baseUrl = normalizeBaseUrl(config!.baseUrl);\n\n await fetchWithSyncRetries('POST', path, async () => {\n if (config!.publicToken) {\n await preloadSignerKey(config!.publicToken);\n const signed = await sign({\n method: 'POST',\n path,\n body,\n tenantId: config!.tenantId,\n token: config!.publicToken,\n mode: 'public',\n });\n return fetch(`${baseUrl}${signed.signedPath}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Tenant-Id': config!.tenantId,\n 'X-Idempotency-Key': signed.idempotencyKey,\n },\n body: JSON.stringify(body),\n });\n }\n\n return fetch(`${baseUrl}${path}`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Tenant-Id': config!.tenantId,\n },\n body: JSON.stringify(body),\n });\n });\n })();\n\n mergeInFlight.set(key, job);\n try {\n await job;\n } finally {\n mergeInFlight.delete(key);\n }\n}\n\nfunction assertConfigured(): IdentityCoordinatorConfig {\n if (!config) {\n throw new Error('RecomnextIdentity.configure() must be called before use');\n }\n return config;\n}\n\nasync function runAuthTransition(\n key: string,\n fn: () => Promise<void>,\n): Promise<void> {\n const existing = authTransitionInFlight.get(key);\n if (existing) {\n await existing;\n return;\n }\n\n const job = fn();\n authTransitionInFlight.set(key, job);\n try {\n await job;\n } finally {\n authTransitionInFlight.delete(key);\n }\n}\n\ntype AuthApplySource = 'local' | 'peer-sync';\n\nasync function applyAuthenticatedUserId(\n nextId: string | null,\n source: AuthApplySource = 'local',\n): Promise<void> {\n assertConfigured();\n const skipMerge = source === 'peer-sync';\n const skipHostLogout = source === 'peer-sync';\n\n const normalized =\n nextId === null || nextId === undefined ? null : String(nextId).trim();\n const targetId = normalized === '' ? null : normalized;\n\n if (authenticatedUserId === targetId) {\n return;\n }\n\n const prevAuth = authenticatedUserId;\n\n if (targetId === null) {\n if (!skipHostLogout && config!.getHostAnonymousUserId && config!.onHostLogout) {\n await config!.onHostLogout();\n }\n clearAnonymousSession();\n authenticatedUserId = null;\n writeAuthSync(null);\n notify();\n return;\n }\n\n if (prevAuth !== null) {\n const anonBeforeClear =\n readAnonymousSessionId() || hostAnonymousId() || '';\n clearAnonymousSession();\n if (!skipMerge && anonBeforeClear) {\n await postMerge(targetId, anonBeforeClear);\n }\n authenticatedUserId = targetId;\n writeAuthSync(targetId);\n notify();\n return;\n }\n\n const anonForMerge = resolveEffectiveUserId();\n if (!skipMerge && anonForMerge) {\n await postMerge(targetId, anonForMerge);\n }\n authenticatedUserId = targetId;\n writeAuthSync(targetId);\n notify();\n}\n\nfunction installStorageListener(): void {\n if (storageListenerInstalled || typeof window === 'undefined') return;\n storageListenerInstalled = true;\n\n window.addEventListener('storage', (event) => {\n if (!config || config.syncAuthAcrossTabs === false) return;\n if (event.key !== RECOMNEXT_AUTH_SYNC_KEY) return;\n if (event.storageArea !== localStorage) return;\n\n const peerUserId =\n event.newValue === null || event.newValue === undefined\n ? null\n : event.newValue.trim() === ''\n ? null\n : event.newValue.trim();\n\n if (peerUserId === authenticatedUserId) return;\n\n suppressAuthSyncWrite = true;\n void applyAuthenticatedUserId(peerUserId, 'peer-sync').finally(() => {\n suppressAuthSyncWrite = false;\n });\n });\n}\n\nfunction hydrateAuthFromStorage(): void {\n if (!config || config.syncAuthAcrossTabs === false) return;\n const synced = readAuthSync();\n if (synced !== authenticatedUserId) {\n suppressAuthSyncWrite = true;\n void applyAuthenticatedUserId(synced, 'peer-sync').finally(() => {\n suppressAuthSyncWrite = false;\n });\n }\n}\n\n/** @internal Test reset */\nexport function resetIdentityCoordinatorForTests(): void {\n config = null;\n authenticatedUserId = null;\n listeners.clear();\n mergeInFlight.clear();\n authTransitionInFlight.clear();\n suppressAuthSyncWrite = false;\n storageListenerInstalled = false;\n}\n\nexport const RecomnextIdentity = {\n configure(coordinatorConfig: IdentityCoordinatorConfig): void {\n config = {\n ...coordinatorConfig,\n baseUrl: normalizeBaseUrl(coordinatorConfig.baseUrl),\n };\n if (config.publicToken) {\n void preloadSignerKey(config.publicToken);\n }\n installStorageListener();\n hydrateAuthFromStorage();\n },\n\n isConfigured(): boolean {\n return config !== null;\n },\n\n subscribe(listener: IdentityListener): () => void {\n listeners.add(listener);\n listener(snapshotState());\n return () => listeners.delete(listener);\n },\n\n getState(): IdentityState {\n if (!config) {\n return {\n authenticatedUserId: null,\n anonymousSessionId: readAnonymousSessionId(),\n effectiveUserId:\n readAnonymousSessionId() || resolveAnonymousSessionId(),\n };\n }\n return snapshotState();\n },\n\n async setAuthenticatedUserId(next: string | null): Promise<void> {\n assertConfigured();\n\n const normalized =\n next === null || next === undefined ? null : String(next).trim();\n const nextId = normalized === '' ? null : normalized;\n\n const transitionKey = authTransitionKey(nextId);\n const pending = authTransitionInFlight.get(transitionKey);\n if (authenticatedUserId === nextId) {\n if (pending) await pending;\n return;\n }\n\n await runAuthTransition(transitionKey, () =>\n applyAuthenticatedUserId(nextId),\n );\n },\n};\n\nexport const RECOMNEXT_IDENTITY_DOM_EVENT = 'recomnext:identity';\n\nexport function installRecomnextIdentityDomListener(): () => void {\n if (typeof document === 'undefined') {\n return () => undefined;\n }\n\n const handler = (event: Event) => {\n if (!RecomnextIdentity.isConfigured()) return;\n const detail = (event as CustomEvent<{ userId?: string | null }>).detail;\n const raw = detail?.userId;\n const userId =\n raw === null || raw === undefined\n ? null\n : String(raw).trim() === ''\n ? null\n : String(raw).trim();\n void RecomnextIdentity.setAuthenticatedUserId(userId);\n };\n\n document.addEventListener(RECOMNEXT_IDENTITY_DOM_EVENT, handler);\n return () => document.removeEventListener(RECOMNEXT_IDENTITY_DOM_EVENT, handler);\n}\n", "/**\n * Client-side country hygiene. Server allowlist is still the source of\n * truth \u2014 unknown 2-letter codes are dropped here only when they are not\n * ISO-2 shaped (OTH / empty / non-alpha). Valid-looking codes go on the\n * wire; the engine maps anything not in country.ts to global.\n */\nexport function normalizeClientCountry(\n raw?: string | null,\n): string | undefined {\n if (raw == null) return undefined;\n const code = String(raw).trim().toUpperCase();\n if (!code || code === 'OTH' || !/^[A-Z]{2}$/.test(code)) return undefined;\n return code;\n}\n", "import type { RetryQueue } from '@recomnext/shared-browser-auth';\nimport type { FireAndForgetPayload } from './client';\nimport type { ImpressionTrackerOptions } from './types';\n\nconst FLUSH_INTERVAL = 2000;\n\nexport class ImpressionTracker {\n private queue: { itemId: string; scenario: string; country?: string }[] = [];\n private flushing = false;\n private observer: IntersectionObserver | null = null;\n private seen = new Set<string>();\n private timer: ReturnType<typeof setInterval> | null = null;\n private readonly getUserId: () => string;\n private readonly getSessionId: () => string;\n private readonly getCountry: () => string | undefined;\n // The RetryQueue handles all signed POSTs to /ingestion/impressions\n // (single source of truth for fire-and-forget on this page). We still\n // batch impressions every 2s before enqueuing so we don't fan out N\n // signed requests for one viewport scan.\n private readonly retryQueue: RetryQueue<FireAndForgetPayload>;\n\n constructor(\n _baseUrl: string,\n _tenantId: string,\n getUserId: () => string,\n getSessionId: () => string,\n retryQueue: RetryQueue<FireAndForgetPayload>,\n getCountry?: () => string | undefined,\n ) {\n // baseUrl + tenantId are captured by the client during signRequest()\n // and no longer needed at the tracker level. Kept in the signature\n // for backwards compatibility with anyone constructing the tracker\n // directly (rare \u2014 almost everyone uses RecomnextBrowserClient).\n this.getUserId = getUserId;\n this.getSessionId = getSessionId;\n this.retryQueue = retryQueue;\n this.getCountry = getCountry ?? (() => undefined);\n if (typeof window !== 'undefined') {\n this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL);\n // sendBeacon is best-effort and CANNOT carry the X-Idempotency-Key\n // header (it's a Blob POST). On pagehide we therefore force the\n // queue's normal signed-fetch path to drain via `keepalive: true`,\n // which is now widely supported in all evergreen browsers.\n window.addEventListener('pagehide', () => this.flush(true));\n }\n }\n\n observe(container: Element, opts: ImpressionTrackerOptions = {}): () => void {\n const selector = opts.itemSelector ?? '[data-item-id]';\n const threshold = opts.threshold ?? 0.5;\n const once = opts.once !== false;\n const scenario = opts.scenario ?? '';\n const country = opts.country || this.getCountry() || undefined;\n\n this.observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n const el = entry.target as HTMLElement;\n const itemId = el.dataset.itemId;\n if (!itemId) continue;\n const key = `${scenario}:${itemId}`;\n if (once && this.seen.has(key)) continue;\n this.seen.add(key);\n this.queue.push({ itemId, scenario, country });\n if (once) this.observer?.unobserve(el);\n }\n },\n { root: null, threshold },\n );\n\n const elements = container.querySelectorAll(selector);\n elements.forEach((el) => this.observer!.observe(el));\n\n return () => {\n this.observer?.disconnect();\n this.observer = null;\n };\n }\n\n // `urgent` is set during pagehide. When true we kick the RetryQueue\n // immediately so it has a chance to land items inside the unload window\n // (keepalive: true on the inner fetch keeps the connection alive past\n // the navigation).\n private flush(urgent = false): void {\n if (this.flushing || this.queue.length === 0) return;\n this.flushing = true;\n const batch = this.queue.splice(0, this.queue.length);\n\n const grouped = new Map<string, { itemIds: string[]; country?: string }>();\n for (const { itemId, scenario, country } of batch) {\n const key = `${scenario}\\0${country ?? ''}`;\n const group = grouped.get(key);\n if (group) {\n group.itemIds.push(itemId);\n } else {\n grouped.set(key, { itemIds: [itemId], country });\n }\n }\n\n const impressions = Array.from(grouped.entries()).map(([key, { itemIds, country }]) => {\n const scenarioSlug = key.split('\\0')[0];\n return {\n userId: this.getUserId() || undefined,\n sessionId: this.getSessionId() || undefined,\n itemIds,\n scenarioSlug: scenarioSlug || undefined,\n timestamp: new Date().toISOString(),\n country: country || undefined,\n };\n });\n\n this.retryQueue.enqueue({\n path: '/ingestion/impressions',\n method: 'POST',\n payload: { impressions },\n });\n\n if (urgent) void this.retryQueue.flush();\n this.flushing = false;\n }\n\n destroy(): void {\n this.flush();\n this.observer?.disconnect();\n if (this.timer) clearInterval(this.timer);\n }\n}\n", "import {\n preloadSignerKey,\n RetryQueue,\n sign as signRequest,\n resolveAnonymousSessionId,\n clearAnonymousSession,\n readAnonymousSessionId,\n RecomnextIdentity,\n type QueuedItem,\n} from '@recomnext/shared-browser-auth';\n\nimport type {\n SearchOptions,\n SearchResponse,\n SearchV2Options,\n SearchV2Response,\n SearchV2AutocompleteResponse,\n SearchV2SettingsResponse,\n CatalogItem,\n CatalogListResponse,\n ListItemsOptions,\n} from '@recomnext/shared-search-types';\n\nimport { normalizeClientCountry } from './country';\nimport { ImpressionTracker } from './tracker';\nimport type {\n BrowserClientConfig,\n RecommendationResult,\n RecommendationOptions,\n RecommendOptions,\n ImpressionTrackerOptions,\n TrackEventOptions,\n} from './types';\n\n// Synchronous retry budget for request-response calls (recommendations,\n// catalog reads, identity/merge). Mirrors the Node + Python SDK behavior:\n// 3 attempts total, ~5s aggregate, re-signs on every attempt.\nconst SYNC_RETRIES = 3;\nconst SYNC_INITIAL_BACKOFF_MS = 200;\nconst SYNC_MAX_BACKOFF_MS = 2_000;\n\n// 5xx + 429 are auto-retried. We special-case 503 +\n// errorCode=auth_replay_cache_unavailable so the SDK retries even if an\n// operator tunes 503 out of the retry set.\nconst RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);\nconst AUTH_REPLAY_CACHE_UNAVAILABLE = 'auth_replay_cache_unavailable';\n\nexport class RecomnextBrowserClient {\n private readonly baseUrl: string;\n private readonly tenantId: string;\n private readonly publicToken: string | undefined;\n private userId: string;\n private sessionId: string;\n private country: string | undefined;\n private readonly tracker: ImpressionTracker;\n // Lazy \u2014 only constructed once for fire-and-forget queues that the\n // tracker also reuses.\n readonly fireAndForgetQueue: RetryQueue<FireAndForgetPayload>;\n\n constructor(config: BrowserClientConfig) {\n this.baseUrl = config.baseUrl.replace(/\\/+$/, '');\n this.tenantId = config.tenantId;\n this.publicToken = config.publicToken;\n this.userId = config.userId ?? '';\n this.sessionId = config.sessionId ?? this.resolveSessionId();\n this.country = normalizeClientCountry(config.country);\n\n // Eagerly import the CryptoKey so the first signed event after page\n // load doesn't pay the importKey latency on the hot path.\n if (this.publicToken) void preloadSignerKey(this.publicToken);\n\n this.fireAndForgetQueue = new RetryQueue<FireAndForgetPayload>({\n persist: config.persistRetries ?? false,\n persistKey: `rnx:${this.tenantId}:fnf`,\n onRetryDrop: config.onRetryDrop,\n signRequest: (item) => this.signFireAndForget(item),\n });\n\n this.tracker = new ImpressionTracker(\n this.baseUrl,\n this.tenantId,\n () => this.userId,\n () => this.sessionId,\n this.fireAndForgetQueue,\n () => this.country,\n );\n }\n\n private resolveSessionId(): string {\n return resolveAnonymousSessionId();\n }\n\n private effectiveUserId(): string {\n return this.userId || this.sessionId;\n }\n\n private resolveCountry(override?: string): string | undefined {\n return normalizeClientCountry(override ?? this.country);\n }\n\n private async request<T>(\n method: string,\n path: string,\n body?: unknown,\n query?: Record<string, string | number | boolean | undefined>,\n ): Promise<T> {\n const maxRetries = SYNC_RETRIES;\n let lastErr: unknown;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n if (attempt > 0) {\n const exp = Math.min(\n SYNC_MAX_BACKOFF_MS,\n SYNC_INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1),\n );\n const jitter = 0.5 + Math.random() * 0.5;\n await sleep(Math.floor(exp * jitter));\n }\n\n // Always re-sign \u2014 the engine's replay cache rejects the same\n // signature twice, and the 30s signing window means a stale sig\n // would also fail.\n const { url, headers } = await this.buildSignedRequest(\n method,\n path,\n query,\n body,\n );\n\n let res: Response;\n try {\n res = await fetch(url, {\n method,\n headers: {\n 'Content-Type': 'application/json',\n ...headers,\n },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n });\n } catch (err) {\n lastErr = err;\n if (attempt >= maxRetries) throw err;\n continue;\n }\n\n if (res.status === 503) {\n const code = await extractErrorCode(res.clone());\n if (code === AUTH_REPLAY_CACHE_UNAVAILABLE && attempt < maxRetries) {\n lastErr = new Error(`recomnext: ${method} ${path} ${res.status} ${code}`);\n continue;\n }\n }\n\n if (RETRYABLE_STATUS.has(res.status) && attempt < maxRetries) {\n lastErr = new Error(`recomnext: ${method} ${path} returned ${res.status}`);\n continue;\n }\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw new Error(`recomnext: ${method} ${path} returned ${res.status}: ${text}`);\n }\n\n if (res.status === 204) return undefined as unknown as T;\n return res.json() as Promise<T>;\n }\n\n throw lastErr ?? new Error(`recomnext: ${method} ${path} retries exhausted`);\n }\n\n // Same shape as the RetryQueue's signRequest contract \u2014 used by the\n // queue *and* the impression tracker so both share a single signing\n // path.\n async signFireAndForget(item: QueuedItem<FireAndForgetPayload>) {\n const { url, headers } = await this.buildSignedRequest(\n item.method,\n item.path,\n undefined,\n item.payload,\n );\n return { url, headers };\n }\n\n private async buildSignedRequest(\n method: string,\n path: string,\n query: Record<string, string | number | boolean | undefined> | undefined,\n body: unknown,\n ): Promise<{ url: string; headers: Record<string, string> }> {\n const baseHeaders: Record<string, string> = {\n 'X-Tenant-Id': this.tenantId,\n };\n\n if (!this.publicToken) {\n // Legacy / no-auth mode. Engine accepts because\n // LEGACY_TENANT_AUTH=true. After Phase 7 cutover this path will be\n // rejected with 401.\n const qs = renderQs(query);\n return { url: `${this.baseUrl}${path}${qs}`, headers: baseHeaders };\n }\n\n const signed = await signRequest({\n method,\n path,\n query,\n body: body ?? null,\n tenantId: this.tenantId,\n token: this.publicToken,\n mode: 'public',\n });\n return {\n url: `${this.baseUrl}${signed.signedPath}`,\n headers: {\n ...baseHeaders,\n 'X-Idempotency-Key': signed.idempotencyKey,\n },\n };\n }\n\n private qs(\n params: Record<string, string | number | boolean | undefined>,\n ): Record<string, string | number | boolean | undefined> {\n return params;\n }\n\n private includeQuery(opts?: RecommendationOptions): {\n returnProperties?: boolean;\n includedProperties?: string;\n } {\n return {\n returnProperties: opts?.returnProperties,\n includedProperties:\n opts?.includedProperties && opts.includedProperties.length > 0\n ? opts.includedProperties.join(',')\n : undefined,\n };\n }\n\n /** Serialize override fields for GET query (comma-separated lists; '' clears). */\n private overrideQuery(opts?: RecommendationOptions): {\n filter?: string;\n boosters?: string;\n rulesSlugs?: string;\n segmentationId?: string;\n itemsPerSegment?: number | string;\n autoRelaxFilters?: boolean;\n relaxEntireFilterOnEmpty?: boolean;\n } {\n if (!opts) return {};\n const join = (v: string[] | null | undefined): string | undefined => {\n if (v === undefined) return undefined;\n if (v === null || v.length === 0) return '';\n return v.join(',');\n };\n return {\n filter:\n opts.filter === undefined\n ? undefined\n : opts.filter === null\n ? ''\n : opts.filter,\n boosters: join(opts.boosters),\n rulesSlugs: join(opts.rulesSlugs),\n segmentationId:\n opts.segmentationId === undefined\n ? undefined\n : opts.segmentationId === null\n ? ''\n : opts.segmentationId,\n // Present null \u2192 '' so the query param is sent; engine transform maps\n // '' \u2192 null \u2192 resolve to itemsPerSegment=1 (omit would keep scenario).\n itemsPerSegment:\n opts.itemsPerSegment === undefined\n ? undefined\n : opts.itemsPerSegment === null\n ? ''\n : opts.itemsPerSegment,\n autoRelaxFilters: opts.autoRelaxFilters,\n relaxEntireFilterOnEmpty: opts.relaxEntireFilterOnEmpty,\n };\n }\n\n private overrideBody(opts?: RecommendationOptions): {\n filter?: string | null;\n boosters?: string[] | null;\n rulesSlugs?: string[] | null;\n segmentationId?: string | null;\n itemsPerSegment?: number | null;\n autoRelaxFilters?: boolean;\n relaxEntireFilterOnEmpty?: boolean;\n } {\n if (!opts) return {};\n const body: {\n filter?: string | null;\n boosters?: string[] | null;\n rulesSlugs?: string[] | null;\n segmentationId?: string | null;\n itemsPerSegment?: number | null;\n autoRelaxFilters?: boolean;\n relaxEntireFilterOnEmpty?: boolean;\n } = {};\n if (opts.filter !== undefined) body.filter = opts.filter;\n if (opts.boosters !== undefined) body.boosters = opts.boosters;\n if (opts.rulesSlugs !== undefined) body.rulesSlugs = opts.rulesSlugs;\n if (opts.segmentationId !== undefined) {\n body.segmentationId = opts.segmentationId;\n }\n if (opts.itemsPerSegment !== undefined) {\n body.itemsPerSegment = opts.itemsPerSegment;\n }\n if (opts.autoRelaxFilters !== undefined) {\n body.autoRelaxFilters = opts.autoRelaxFilters;\n }\n if (opts.relaxEntireFilterOnEmpty !== undefined) {\n body.relaxEntireFilterOnEmpty = opts.relaxEntireFilterOnEmpty;\n }\n return body;\n }\n\n /* \u2500\u2500 Recommendations \u2500\u2500 */\n\n getRecommendations(opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/user-to-item/${this.effectiveUserId()}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getRelatedItems(itemId: string, opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/item-to-item/${itemId}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getSimilarProducts(itemId: string, opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/similar-products/${itemId}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getRecentlyViewed(opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'GET',\n `/recommendations/recently-viewed/${this.effectiveUserId()}`,\n undefined,\n this.qs({\n count: opts?.count,\n scenario: opts?.scenario,\n country: this.resolveCountry(opts?.country),\n ...this.includeQuery(opts),\n ...this.overrideQuery(opts),\n }),\n );\n }\n\n getCartRecommendations(itemIds: string[], opts?: RecommendationOptions): Promise<RecommendationResult> {\n return this.request(\n 'POST',\n '/recommendations/items-to-items',\n {\n itemIds,\n count: opts?.count ?? 10,\n returnProperties: opts?.returnProperties,\n includedProperties: opts?.includedProperties,\n country: this.resolveCountry(opts?.country),\n ...this.overrideBody(opts),\n },\n this.qs({ scenario: opts?.scenario }),\n );\n }\n\n recommend(opts: RecommendOptions): Promise<RecommendationResult> {\n return this.request('POST', '/recommendations', {\n ...opts,\n userId: this.effectiveUserId(),\n country: this.resolveCountry(opts.country),\n });\n }\n\n /* \u2500\u2500 Search \u2500\u2500 */\n\n search(query: string, opts?: SearchOptions): Promise<SearchResponse> {\n return this.request(\n 'GET',\n '/recommendations/search',\n undefined,\n this.qs({\n query,\n count: opts?.count,\n category: opts?.category,\n priceRange: opts?.priceRange,\n userId: opts?.userId ?? (this.userId || undefined),\n }),\n );\n }\n\n searchV2(query: string, opts?: SearchV2Options): Promise<SearchV2Response> {\n return this.request(\n 'GET',\n '/recommendations/search/v2',\n undefined,\n this.qs({\n query,\n count: opts?.count,\n category: opts?.category,\n priceRange: opts?.priceRange,\n userId: opts?.userId ?? (this.userId || undefined),\n locale: opts?.locale,\n debugScores: opts?.debugScores === true ? true : undefined,\n algorithm: opts?.algorithm,\n }),\n );\n }\n\n autocomplete(\n query: string,\n opts?: { limit?: number },\n ): Promise<SearchV2AutocompleteResponse> {\n return this.request(\n 'GET',\n '/recommendations/search/v2/autocomplete',\n undefined,\n this.qs({\n query,\n limit: opts?.limit,\n }),\n );\n }\n\n getSearchV2Settings(): Promise<SearchV2SettingsResponse> {\n return this.request('GET', '/recommendations/search/v2/settings');\n }\n\n /* \u2500\u2500 Catalog browse (public HMAC eligible) \u2500\u2500 */\n\n listItems(opts?: ListItemsOptions): Promise<CatalogListResponse> {\n return this.request(\n 'GET',\n '/catalog/items',\n undefined,\n this.qs({\n page: opts?.page,\n limit: opts?.limit,\n category: opts?.category,\n search: opts?.search,\n availability: opts?.availability,\n }),\n );\n }\n\n getCategories(): Promise<string[]> {\n return this.request('GET', '/catalog/items/categories');\n }\n\n getItem(externalId: string): Promise<CatalogItem> {\n return this.request('GET', `/catalog/items/${encodeURIComponent(externalId)}`);\n }\n\n /* \u2500\u2500 Event tracking \u2500\u2500 */\n\n trackView(\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n this.enqueueInteraction('view', itemId, scenarioSlugOrOpts, opts);\n }\n\n trackCart(\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n this.enqueueInteraction('cart', itemId, scenarioSlugOrOpts, opts);\n }\n\n trackPurchase(\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n this.enqueueInteraction('purchase', itemId, scenarioSlugOrOpts, opts);\n }\n\n private enqueueInteraction(\n type: 'view' | 'cart' | 'purchase',\n itemId: string,\n scenarioSlugOrOpts?: string | TrackEventOptions,\n opts?: TrackEventOptions,\n ): void {\n const fromObject = typeof scenarioSlugOrOpts === 'object' ? scenarioSlugOrOpts : undefined;\n const scenarioSlug =\n typeof scenarioSlugOrOpts === 'string' ? scenarioSlugOrOpts : fromObject?.scenarioSlug;\n const country = this.resolveCountry(fromObject?.country ?? opts?.country);\n this.fireAndForgetQueue.enqueue({\n path: '/ingestion/interactions',\n method: 'POST',\n payload: {\n interactions: [{\n userId: this.effectiveUserId(),\n itemId,\n type,\n scenarioSlug: scenarioSlug || undefined,\n timestamp: new Date().toISOString(),\n country,\n }],\n },\n });\n }\n\n /* \u2500\u2500 Impression tracking \u2500\u2500 */\n\n trackImpressions(container: Element, opts?: ImpressionTrackerOptions): () => void {\n return this.tracker.observe(container, opts);\n }\n\n /* \u2500\u2500 Identity \u2500\u2500 */\n\n setUserId(userId: string): void {\n this.userId = userId;\n }\n\n setCountry(code: string): void {\n this.country = normalizeClientCountry(code);\n }\n\n mergeIdentity(authenticatedUserId: string): Promise<unknown> {\n if (RecomnextIdentity.isConfigured()) {\n return RecomnextIdentity.setAuthenticatedUserId(authenticatedUserId).then(\n () => {\n this.userId = authenticatedUserId;\n this.syncFromIdentityCoordinator();\n },\n );\n }\n const anonId = this.userId || this.sessionId;\n this.userId = authenticatedUserId;\n return this.request('POST', '/identity/merge', {\n userId: authenticatedUserId,\n sessionIds: [anonId],\n });\n }\n\n /** Keep SDK fields aligned when RecomnextIdentity coordinator is active. */\n private syncFromIdentityCoordinator(): void {\n if (!RecomnextIdentity.isConfigured()) return;\n const state = RecomnextIdentity.getState();\n this.userId = state.authenticatedUserId ?? '';\n this.sessionId =\n state.anonymousSessionId || readAnonymousSessionId() || this.sessionId;\n }\n\n /** Anonymous session id used before login (`sess_\u2026`). Shared across tabs via localStorage. */\n getAnonymousSessionId(): string {\n return this.sessionId || readAnonymousSessionId();\n }\n\n /**\n * Rotate anonymous session after logout so the next visitor on this browser\n * does not inherit the previous anonymous bucket. Clears known userId separately\n * via setUserId('') from the integrator.\n */\n clearSession(customId?: string): void {\n this.sessionId = clearAnonymousSession(customId);\n }\n\n /* \u2500\u2500 Lifecycle \u2500\u2500 */\n\n destroy(): void {\n this.tracker.destroy();\n this.fireAndForgetQueue.destroy();\n }\n}\n\n// Shape used by both the SDK's trackView/trackCart/trackPurchase calls\n// and the ImpressionTracker \u2014 they all funnel through the same queue.\nexport type FireAndForgetPayload = {\n interactions?: unknown[];\n impressions?: unknown[];\n};\n\nfunction renderQs(params?: Record<string, string | number | boolean | undefined>): string {\n if (!params) return '';\n const parts: string[] = [];\n for (const [k, v] of Object.entries(params)) {\n // Preserve empty string (GET clear sentinels). Drop only omit/null.\n if (v === undefined || v === null) continue;\n parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);\n }\n return parts.length ? `?${parts.join('&')}` : '';\n}\n\nasync function extractErrorCode(res: Response): Promise<string | undefined> {\n try {\n const body = (await res.json()) as { errorCode?: unknown };\n return typeof body.errorCode === 'string' ? body.errorCode : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["index_exports", "__export", "ImpressionTracker", "RECOMNEXT_IDENTITY_DOM_EVENT", "RECOMNEXT_SESSION_KEY", "RecomnextBrowserClient", "RecomnextIdentity", "clearAnonymousSession", "installRecomnextIdentityDomListener", "normalizeClientCountry", "readAnonymousSessionId", "resolveAnonymousSessionId", "canonicalJson", "value", "stringify", "keys", "parts", "k", "v", "KEY_CACHE", "sign", "input", "verb", "path", "effectivePath", "timestamp", "bodyHash", "sha256Hex", "canonicalJson", "others", "k", "v", "sortedOthers", "a", "b", "tsParam", "sigParam", "trailing", "queryForSigning", "signingString", "signature", "hmacSha256Hex", "makeIdempotencyKey", "bytes", "toHex", "token", "payload", "key", "getKey", "sig", "cached", "preloadSignerKey", "out", "i", "stripped", "c", "rand", "hex", "DEFAULTS", "RetryQueue", "cfg", "openIndexedDb", "item", "enriched", "makeId", "now", "ready", "i", "url", "headers", "res", "err", "exp", "jitter", "id", "db", "tx", "txDone", "req", "data", "resolve", "reject", "c", "RECOMNEXT_SESSION_KEY", "defaultStorage", "generateAnonymousSessionId", "resolveAnonymousSessionId", "storage", "ls", "ss", "fromLocal", "legacy", "sid", "readAnonymousSessionId", "clearAnonymousSession", "customId", "next", "RETRYABLE_STATUS", "AUTH_REPLAY_CACHE_UNAVAILABLE", "sleep", "ms", "r", "extractErrorCode", "res", "body", "fetchWithSyncRetries", "method", "path", "performFetch", "lastErr", "attempt", "exp", "jitter", "err", "code", "text", "RECOMNEXT_AUTH_SYNC_KEY", "config", "authenticatedUserId", "listeners", "mergeInFlight", "authTransitionInFlight", "suppressAuthSyncWrite", "storageListenerInstalled", "normalizeBaseUrl", "baseUrl", "hostAnonymousId", "id", "resolveEffectiveUserId", "host", "stored", "readAnonymousSessionId", "resolveAnonymousSessionId", "snapshotState", "notify", "state", "listener", "mergeKey", "tenantId", "anonId", "knownUserId", "authTransitionKey", "nextId", "writeAuthSync", "userId", "readAuthSync", "raw", "postMerge", "key", "existing", "job", "path", "body", "fetchWithSyncRetries", "preloadSignerKey", "signed", "sign", "assertConfigured", "runAuthTransition", "fn", "applyAuthenticatedUserId", "source", "skipMerge", "skipHostLogout", "normalized", "targetId", "prevAuth", "clearAnonymousSession", "anonBeforeClear", "anonForMerge", "installStorageListener", "event", "peerUserId", "hydrateAuthFromStorage", "synced", "RecomnextIdentity", "coordinatorConfig", "config", "normalizeBaseUrl", "preloadSignerKey", "installStorageListener", "hydrateAuthFromStorage", "listener", "listeners", "snapshotState", "readAnonymousSessionId", "resolveAnonymousSessionId", "next", "assertConfigured", "normalized", "nextId", "transitionKey", "authTransitionKey", "pending", "authTransitionInFlight", "authenticatedUserId", "runAuthTransition", "applyAuthenticatedUserId", "RECOMNEXT_IDENTITY_DOM_EVENT", "installRecomnextIdentityDomListener", "handler", "event", "raw", "userId", "normalizeClientCountry", "raw", "code", "ImpressionTracker", "_baseUrl", "_tenantId", "getUserId", "getSessionId", "retryQueue", "getCountry", "container", "opts", "selector", "threshold", "once", "scenario", "country", "entries", "entry", "el", "itemId", "key", "urgent", "batch", "grouped", "group", "impressions", "itemIds", "scenarioSlug", "SYNC_RETRIES", "SYNC_INITIAL_BACKOFF_MS", "SYNC_MAX_BACKOFF_MS", "RETRYABLE_STATUS", "AUTH_REPLAY_CACHE_UNAVAILABLE", "RecomnextBrowserClient", "config", "normalizeClientCountry", "preloadSignerKey", "RetryQueue", "item", "ImpressionTracker", "resolveAnonymousSessionId", "override", "method", "path", "body", "query", "maxRetries", "lastErr", "attempt", "exp", "jitter", "sleep", "url", "headers", "res", "err", "code", "extractErrorCode", "text", "baseHeaders", "qs", "renderQs", "signed", "sign", "params", "opts", "join", "v", "itemId", "itemIds", "externalId", "scenarioSlugOrOpts", "type", "fromObject", "scenarioSlug", "country", "container", "userId", "authenticatedUserId", "RecomnextIdentity", "anonId", "state", "readAnonymousSessionId", "customId", "clearAnonymousSession", "parts", "k", "ms", "r"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recomnext/browser",
3
- "version": "0.5.0",
3
+ "version": "0.5.1-dev.6e78c840",
4
4
  "description": "Lightweight browser SDK for recomnext — recommendations, event tracking, and impression tracking",
5
5
  "type": "module",
6
6
  "main": "dist/recomnext-browser.umd.js",