@recomnext/browser 0.1.0

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/LICENSE.md ADDED
@@ -0,0 +1,161 @@
1
+ # Recomnext Public SDK License Agreement
2
+
3
+ **Version 1.0 — Effective 26 May 2026**
4
+
5
+ This License Agreement ("**Agreement**") is a binding legal contract between you (either an individual or the legal entity you represent, "**Licensee**" or "**You**") and **Remittx Private Limited**, a company incorporated under the laws of India and having its registered office at 1st Floor, Gopala Krishna Complex, Residency Road, Bengaluru, Karnataka 560025, India ("**Licensor**", "**We**", or "**Recomnext**"). It governs Your access to and use of the **Recomnext** software development kits, client libraries, browser widgets, code samples, type definitions, documentation, and any updates, patches, or modifications thereto that are published by Licensor on the npm registry (including without limitation the packages `@recomnext/browser`, `@recomnext/node`, `@recomnext/carousel`, and `@recomnext/react-carousel`) or any other public distribution channel (collectively, the "**SDK**").
6
+
7
+ **BY DOWNLOADING, INSTALLING, COPYING, ACCESSING, OR OTHERWISE USING THE SDK, YOU ACKNOWLEDGE THAT YOU HAVE READ, UNDERSTOOD, AND AGREE TO BE BOUND BY THIS AGREEMENT. IF YOU DO NOT AGREE, YOU MUST NOT INSTALL OR USE THE SDK AND MUST IMMEDIATELY DELETE ALL COPIES IN YOUR POSSESSION.**
8
+
9
+ ---
10
+
11
+ ## 1. Definitions
12
+
13
+ **1.1** "**Recomnext Service**" means the proprietary recommendations software-as-a-service platform operated by Licensor, including its backend systems, APIs, models, dashboards, and any successor or replacement services.
14
+
15
+ **1.2** "**Authorized Subscription**" means a current, paid, and in-good-standing subscription, account, or written commercial agreement with Licensor entitling You to access the Recomnext Service.
16
+
17
+ **1.3** "**API Key**" means a credential issued by Licensor to a specific Licensee under an Authorized Subscription.
18
+
19
+ **1.4** "**Application**" means a software product, service, website, or other system developed by You into which the SDK is integrated, provided such system communicates exclusively with the Recomnext Service.
20
+
21
+ **1.5** "**Configuration**" means setting values for the parameters, options, and public interfaces expressly documented by Licensor as user-configurable. Configuration does not include any change to the source, compiled, or transpiled code of the SDK.
22
+
23
+ **1.6** "**Proprietary Notices**" means any copyright, trademark, patent, license, or other proprietary or attribution notices included in or on the SDK.
24
+
25
+ ---
26
+
27
+ ## 2. License Grant
28
+
29
+ Subject to Your continued, strict compliance with this Agreement, Licensor grants You a **limited, non-exclusive, non-transferable, non-sublicensable, royalty-free, revocable** license, during the term of this Agreement, to:
30
+
31
+ **(a)** install and use the SDK, in its unmodified form as published by Licensor, solely to integrate Your Application with, and consume, the Recomnext Service; and
32
+
33
+ **(b)** apply Configuration to the SDK as documented by Licensor.
34
+
35
+ No other rights are granted, by implication, estoppel, exhaustion, or otherwise. All rights not expressly granted are reserved by Licensor.
36
+
37
+ ---
38
+
39
+ ## 3. Conditions and Restrictions
40
+
41
+ **3.1 Subscription Requirement.** The SDK is operational only when used in connection with an Authorized Subscription and a valid API Key. You acknowledge that any use of the SDK without a valid API Key and an Authorized Subscription is unauthorized and constitutes a material breach of this Agreement.
42
+
43
+ **3.2 No Modification.** You may apply Configuration as defined in Section 1.5, and nothing more. You shall not, and shall not permit any third party to, modify, adapt, translate, port, alter, fix, enhance, patch, monkey-patch, hot-patch, transpile, recompile, fork, or create derivative works of the SDK, in whole or in part. The right to fork, branch, or otherwise change the SDK's source or compiled code is **not granted**, including for purposes of bug fixes, compatibility shims, or internal use.
44
+
45
+ **3.3 No Reverse Engineering.** You shall not, and shall not permit any third party to, reverse engineer, decompile, disassemble, decrypt, deobfuscate, or otherwise attempt to derive or reconstruct the source code, underlying ideas, algorithms, model weights, file formats, programming interfaces, protocols, or trade secrets of the SDK or the Recomnext Service, except solely to the minimum extent that such activity is expressly permitted by mandatory applicable law notwithstanding this restriction, and only after giving Licensor at least thirty (30) days' prior written notice and a reasonable opportunity to provide the relevant interoperability information itself.
46
+
47
+ **3.4 No Competing Use.** You shall not use the SDK, the Recomnext Service, any output thereof, or any information, data, telemetry, or know-how derived from either to design, develop, train, evaluate, benchmark, market, or operate any product or service that competes, in whole or in part, with the Recomnext Service, including any recommendations, personalization, ranking, search-relevance, or similar machine-learning service offering.
48
+
49
+ **3.5 No Redistribution.** You shall not sublicense, sell, rent, lease, lend, distribute, republish, repackage, mirror, proxy, host, or otherwise make the SDK available to any third party, including by republishing the SDK or any portion thereof to the npm registry or to any other public, private, internal, or air-gapped package registry, repository, content delivery network, or distribution channel. Bundling the SDK as a dependency of Your Application solely for end-user execution of that Application against the Recomnext Service is permitted.
50
+
51
+ **3.6 No Use With Non-Recomnext Backends.** You shall not configure, redirect, alter, or operate the SDK to communicate with, authenticate against, route through, or interoperate with any backend, API, service, emulator, proxy, clone, fork, reimplementation, or replacement that is not the official Recomnext Service operated by Licensor. Any attempt to point, adapt, or otherwise use the SDK against a third-party, self-hosted, open-source, or unauthorized substitute for the Recomnext Service is strictly prohibited and constitutes a material breach.
52
+
53
+ **3.7 No Circumvention.** You shall not circumvent, disable, or interfere with any security, rate-limiting, telemetry, license-verification, or access-control mechanism of the SDK or the Recomnext Service.
54
+
55
+ **3.8 Preservation of Notices.** You shall not remove, obscure, or alter any Proprietary Notices in or on the SDK, its documentation, or any output of the SDK that includes such notices.
56
+
57
+ **3.9 Compliance With Law.** You shall use the SDK only in compliance with all applicable laws, including without limitation data protection, privacy, consumer protection, export control, sanctions, and intellectual property laws.
58
+
59
+ ---
60
+
61
+ ## 4. Ownership and Intellectual Property
62
+
63
+ **4.1 Reservation of Rights.** The SDK and the Recomnext Service are licensed, not sold. Licensor and its licensors retain all right, title, and interest in and to the SDK, the Recomnext Service, and all related intellectual property, including all copyrights, patents, trademarks, trade secrets, models, training data, know-how, and other proprietary rights. Nothing in this Agreement transfers ownership of any of the foregoing to You.
64
+
65
+ **4.2 Trademarks.** "**Recomnext**" and the associated logos and product names are trademarks of Licensor. No trademark license is granted under this Agreement, except the limited right to reference "Recomnext" by name, in plain text and without alteration of any logo, solely to factually indicate that Your Application integrates with the Recomnext Service.
66
+
67
+ **4.3 Feedback.** If You provide Licensor with any suggestions, comments, ideas, improvements, error reports, bug reports, or other feedback regarding the SDK or the Recomnext Service ("**Feedback**"), You hereby grant Licensor a perpetual, irrevocable, worldwide, royalty-free, fully paid-up, sublicensable, and transferable license to use, reproduce, modify, distribute, and commercialize such Feedback for any purpose, without obligation, accounting, or attribution to You.
68
+
69
+ ---
70
+
71
+ ## 5. Audit Rights
72
+
73
+ Upon not less than fifteen (15) days' prior written notice, and not more frequently than once in any twelve (12) month period (except where Licensor reasonably suspects breach, in which case no frequency limit applies), Licensor or an independent auditor designated by Licensor may, at Licensor's expense, audit Your use of the SDK to verify compliance with this Agreement. You shall cooperate reasonably and in good faith with such audit and shall provide access to relevant records, package manifests, build artifacts, source repositories, and deployment environments strictly necessary for the audit. If an audit reveals material non-compliance, You shall (a) reimburse Licensor for the reasonable costs of the audit, (b) promptly remediate the non-compliance, and (c) pay any fees that would have been due had Your use been properly licensed, all without prejudice to any other remedy available to Licensor.
74
+
75
+ ---
76
+
77
+ ## 6. Term and Termination
78
+
79
+ **6.1 Term.** This Agreement begins on the date You first download, install, or use the SDK and continues until terminated as provided in this Section 6.
80
+
81
+ **6.2 Termination for Breach.** This Agreement and all rights granted hereunder terminate **automatically and without notice** if You breach any provision of this Agreement, including without limitation Sections 3.2 (No Modification), 3.3 (No Reverse Engineering), 3.4 (No Competing Use), 3.5 (No Redistribution), 3.6 (No Use With Non-Recomnext Backends), or 3.7 (No Circumvention).
82
+
83
+ **6.3 Termination on Loss of Subscription.** This Agreement terminates automatically upon the expiration, suspension, cancellation, or non-renewal of Your Authorized Subscription, except that Licensor may, at its sole discretion, permit continued installation of the SDK in non-functional form pending resumption of an Authorized Subscription.
84
+
85
+ **6.4 Termination for Convenience by Licensor.** Licensor may terminate this Agreement, or discontinue the SDK or the Recomnext Service in whole or in part, at any time upon thirty (30) days' written or electronic notice. Licensor shall have no liability for any such termination or discontinuation.
86
+
87
+ **6.5 Effect of Termination.** Upon termination, all rights granted to You under this Agreement immediately cease. You shall, within seven (7) days of termination: (a) cease all use of the SDK; (b) remove the SDK from all systems, package manifests, lockfiles, build artifacts, container images, and deployments under Your control; and (c) destroy or permanently delete all copies of the SDK in Your possession or control. Upon Licensor's request, You shall certify such destruction in writing, signed by an authorized officer.
88
+
89
+ **6.6 Survival.** Sections 1, 3.3, 3.4, 4, 5, 6.5, 6.6, 7, 8, 9, 10, and 11 survive termination of this Agreement.
90
+
91
+ ---
92
+
93
+ ## 7. Disclaimer of Warranties
94
+
95
+ THE SDK IS PROVIDED **"AS IS" AND "AS AVAILABLE"**, WITH ALL FAULTS AND WITHOUT WARRANTY OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR, ITS AFFILIATES, AND ITS LICENSORS EXPRESSLY DISCLAIM ALL WARRANTIES, REPRESENTATIONS, AND CONDITIONS, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING WITHOUT LIMITATION ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, QUIET ENJOYMENT, ACCURACY, OR THAT THE SDK WILL BE UNINTERRUPTED, ERROR-FREE, SECURE, OR PRODUCE ANY PARTICULAR RESULT. NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED FROM LICENSOR OR THROUGH THE SDK SHALL CREATE ANY WARRANTY NOT EXPRESSLY STATED HEREIN.
96
+
97
+ **LICENSOR MAKES NO REPRESENTATION OR WARRANTY THAT THE SDK DOES NOT INFRINGE ANY THIRD-PARTY INTELLECTUAL PROPERTY RIGHTS, AND LICENSOR HAS NO OBLIGATION TO DEFEND, INDEMNIFY, OR HOLD YOU HARMLESS AGAINST ANY CLAIM OF INFRINGEMENT, EXCEPT TO THE EXTENT EXPRESSLY AGREED IN A SEPARATE SIGNED COMMERCIAL AGREEMENT BETWEEN YOU AND LICENSOR.**
98
+
99
+ ---
100
+
101
+ ## 8. Limitation of Liability
102
+
103
+ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL LICENSOR, ITS AFFILIATES, OR THEIR RESPECTIVE OFFICERS, DIRECTORS, EMPLOYEES, AGENTS, OR LICENSORS BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR FOR ANY LOSS OF PROFITS, REVENUE, DATA, GOODWILL, USE, OR BUSINESS OPPORTUNITY, ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT OR THE SDK, WHETHER IN CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY, OR ANY OTHER LEGAL THEORY, AND WHETHER OR NOT LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
104
+
105
+ LICENSOR'S TOTAL AGGREGATE LIABILITY ARISING OUT OF OR IN CONNECTION WITH THIS AGREEMENT AND THE SDK SHALL NOT EXCEED THE GREATER OF (A) THE AMOUNTS, IF ANY, PAID BY YOU TO LICENSOR FOR THE RECOMNEXT SERVICE IN THE TWELVE (12) MONTHS PRECEDING THE EVENT GIVING RISE TO THE CLAIM, OR (B) ONE HUNDRED US DOLLARS (US$100). THIS LIMITATION IS A FUNDAMENTAL ELEMENT OF THE BARGAIN AND APPLIES EVEN IF ANY LIMITED REMEDY HEREIN FAILS OF ITS ESSENTIAL PURPOSE.
106
+
107
+ ---
108
+
109
+ ## 9. Indemnification by Licensee
110
+
111
+ **9.1 Indemnity.** You shall defend, indemnify, and hold harmless Licensor, its affiliates, and their respective officers, directors, employees, and agents (each an "**Indemnified Party**") from and against any and all third-party claims, actions, proceedings, damages, losses, liabilities, fines, penalties, and reasonable expenses (including attorneys' fees and costs of investigation) arising out of or relating to:
112
+
113
+ (a) Your Application, including any defect, error, or unlawful conduct therein;
114
+
115
+ (b) Your breach of this Agreement;
116
+
117
+ (c) Your use of the SDK or the Recomnext Service in violation of applicable law, including data protection, privacy, consumer protection, export control, or intellectual property laws;
118
+
119
+ (d) any data, content, prompts, instructions, or other inputs provided by You or by any user of Your Application; or
120
+
121
+ (e) any claim alleging that Your Application, or any modification, configuration, integration, or combination of the SDK undertaken by You or on Your behalf, infringes or misappropriates any third-party right.
122
+
123
+ **9.2 Procedure.** Licensor shall promptly notify You in writing of any claim for which indemnification is sought. Licensor may, at its option and expense, participate in the defense of any such claim with counsel of its own choosing. You shall reasonably cooperate with the defense of the claim, at Your expense. You shall not settle, compromise, or consent to the entry of any judgment with respect to any such claim without Licensor's prior written consent if such settlement, compromise, or judgment would impose any liability, obligation, payment, admission, or restriction on any Indemnified Party.
124
+
125
+ **9.3 No Reciprocal Obligation.** For the avoidance of doubt, Licensor provides no indemnity to You under this Agreement. Any obligation of Licensor to indemnify You with respect to intellectual property infringement or otherwise shall arise solely under, and on the terms of, a separate signed commercial agreement, if any, between You and Licensor.
126
+
127
+ ---
128
+
129
+ ## 10. Export, Sanctions, and Compliance
130
+
131
+ You represent and warrant that You are not located in, under the control of, or a national or resident of any country, or on any list of restricted parties, to which export, re-export, or transfer of the SDK is prohibited under applicable export control, sanctions, or anti-terrorism laws of India, the United States, the European Union, the United Kingdom, or any other applicable jurisdiction. You shall comply with all such laws and shall not export, re-export, or transfer the SDK, or permit the use of the Recomnext Service, in violation thereof.
132
+
133
+ ---
134
+
135
+ ## 11. General
136
+
137
+ **11.1 Governing Law.** This Agreement is governed by and construed in accordance with the laws of the **Republic of India**, without regard to its conflict-of-laws principles. The United Nations Convention on Contracts for the International Sale of Goods does not apply to this Agreement.
138
+
139
+ **11.2 Jurisdiction.** The parties submit to the exclusive jurisdiction of **the courts of New Delhi and Haryana, India** for any dispute arising out of or relating to this Agreement, except that Licensor may seek injunctive or equitable relief in any court of competent jurisdiction to protect its intellectual property or confidential information.
140
+
141
+ **11.3 Equitable Relief.** You acknowledge that breach of Sections 3, 4, or 6.5 would cause Licensor irreparable harm for which monetary damages would be inadequate. Licensor is therefore entitled to seek injunctive and other equitable relief without the requirement of posting a bond or proving actual damages, in addition to any other remedies available at law or in equity.
142
+
143
+ **11.4 Assignment.** You may not assign or transfer this Agreement, by operation of law, change of control, or otherwise, without Licensor's prior written consent; any attempted assignment in violation of this Section is void. Licensor may assign this Agreement freely.
144
+
145
+ **11.5 Entire Agreement; Order of Precedence.** This Agreement, together with any separate signed commercial agreement between You and Licensor governing the Recomnext Service, constitutes the entire agreement between the parties with respect to its subject matter and supersedes all prior or contemporaneous understandings, communications, and proposals. In the event of a conflict between this Agreement and a separate signed commercial agreement, the signed commercial agreement controls solely with respect to the matters expressly addressed therein (including, where applicable, any negotiated intellectual property indemnification, service-level commitments, or modified liability provisions).
146
+
147
+ **11.6 Severability; Waiver.** If any provision of this Agreement is held unenforceable, that provision shall be modified to the minimum extent necessary to make it enforceable, and the remaining provisions shall remain in full force. No waiver by Licensor is effective unless in writing and signed by an authorized representative; no waiver constitutes a continuing waiver of any subsequent breach.
148
+
149
+ **11.7 Notices.** Notices to Licensor must be sent in writing to **legal@driffle.com** or to Licensor's registered address set out above. Notices to You may be sent to the email address associated with Your Authorized Subscription or, where no Authorized Subscription exists, posted on **https://recomnext.com**.
150
+
151
+ **11.8 Updates to the Agreement.** Licensor may publish updated versions of this Agreement together with subsequent releases of the SDK. The version of this Agreement distributed with a given release of the SDK governs Your use of that release. Your installation or use of a release of the SDK constitutes acceptance of the version of this Agreement distributed with it.
152
+
153
+ **11.9 No Agency.** Nothing in this Agreement creates any agency, partnership, joint venture, fiduciary, or employment relationship between the parties. Neither party has authority to bind the other.
154
+
155
+ **11.10 Headings; Construction.** Section headings are for convenience only and do not affect interpretation. The words "include" and "including" are not limiting.
156
+
157
+ ---
158
+
159
+ **© 2026 Remittx Private Limited. All rights reserved.**
160
+ **Recomnext** and the Recomnext logo are trademarks of Remittx Private Limited.
161
+ For licensing inquiries: **legal@driffle.com** · **https://recomnext.com**
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @recomnext/browser
2
+
3
+ > **Proprietary Software — Restricted License. Not Open Source.**
4
+ >
5
+ > This package is the proprietary property of **Remittx Private Limited**.
6
+ > Your use of it is governed by the **Recomnext Public SDK License Agreement**
7
+ > ([LICENSE.md](./LICENSE.md)). An **active Recomnext subscription and a valid
8
+ > API key are required** for this SDK to function.
9
+ >
10
+ > **By installing, copying, or using this package, You agree to be bound by the
11
+ > License.** If You do not agree, do not install or use this package.
12
+ >
13
+ > | | |
14
+ > |---|---|
15
+ > | **License** | Strict proprietary — see [LICENSE.md](./LICENSE.md) |
16
+ > | **Forbidden** | Modification, reverse engineering, redistribution, use with non-Recomnext backends, use to build competing services |
17
+ > | **Required** | Active subscription + API key — sign up at [recomnext.com](https://recomnext.com) |
18
+ > | **Data Processing** | A separate Data Processing Addendum applies — request via [legal@driffle.com](mailto:legal@driffle.com) |
19
+ > | **Licensing inquiries** | [legal@driffle.com](mailto:legal@driffle.com) |
20
+ >
21
+ > © 2026 Remittx Private Limited. All rights reserved. "Recomnext" is a trademark of Remittx Private Limited.
22
+
23
+ Lightweight browser SDK for recomnext — recommendations, event tracking, and automatic impression tracking.
24
+
25
+ Full documentation: [recomnext.com/resources/sdks](https://recomnext.com/resources/sdks) · License: [recomnext.com/license](https://recomnext.com/license)
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install @recomnext/browser
31
+ ```
32
+
33
+ Or via script tag:
34
+
35
+ ```html
36
+ <script src="recomnext-browser.umd.js"></script>
37
+ <script>
38
+ const client = new RecomnextBrowser.RecomnextBrowserClient({ ... });
39
+ </script>
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ```typescript
45
+ import { RecomnextBrowserClient } from '@recomnext/browser';
46
+
47
+ const recomnext = new RecomnextBrowserClient({
48
+ baseUrl: '/api',
49
+ tenantId: 'my-tenant',
50
+ userId: 'user-123', // optional
51
+ });
52
+
53
+ // Get recommendations
54
+ const { items } = await recomnext.getRecommendations({ count: 10, scenario: 'home' });
55
+
56
+ // Track events
57
+ recomnext.trackView('product-1');
58
+ recomnext.trackCart('product-1');
59
+ recomnext.trackPurchase('product-1');
60
+
61
+ // Automatic impression tracking
62
+ const cleanup = recomnext.trackImpressions(document.querySelector('.recs-container'), {
63
+ scenario: 'home-for-you',
64
+ itemSelector: '[data-item-id]',
65
+ threshold: 0.5,
66
+ once: true,
67
+ });
68
+
69
+ // Identity merge on login
70
+ await recomnext.mergeIdentity('authenticated-user-id');
71
+
72
+ // Cleanup
73
+ cleanup(); // stop observing
74
+ recomnext.destroy(); // flush pending impressions
75
+ ```
76
+
77
+ ## Features
78
+
79
+ - Zero dependencies in the published bundle (~3KB gzipped)
80
+ - Automatic sessionId generation
81
+ - IntersectionObserver-based impression tracking
82
+ - Batched impression flush (every 2s + sendBeacon on page unload)
83
+ - Tree-shakeable ESM build
@@ -0,0 +1,40 @@
1
+ import { RetryQueue, type QueuedItem } from '@recomnext/shared-browser-auth';
2
+ import type { BrowserClientConfig, RecommendationResult, RecommendationOptions, RecommendOptions, ImpressionTrackerOptions } from './types';
3
+ export declare class RecomnextBrowserClient {
4
+ private readonly baseUrl;
5
+ private readonly tenantId;
6
+ private readonly publicToken;
7
+ private userId;
8
+ private sessionId;
9
+ private readonly tracker;
10
+ readonly fireAndForgetQueue: RetryQueue<FireAndForgetPayload>;
11
+ constructor(config: BrowserClientConfig);
12
+ private resolveSessionId;
13
+ private effectiveUserId;
14
+ private request;
15
+ signFireAndForget(item: QueuedItem<FireAndForgetPayload>): Promise<{
16
+ url: string;
17
+ headers: Record<string, string>;
18
+ }>;
19
+ private buildSignedRequest;
20
+ private qs;
21
+ private includeQuery;
22
+ getRecommendations(opts?: RecommendationOptions): Promise<RecommendationResult>;
23
+ getRelatedItems(itemId: string, opts?: RecommendationOptions): Promise<RecommendationResult>;
24
+ getSimilarProducts(itemId: string, opts?: RecommendationOptions): Promise<RecommendationResult>;
25
+ getRecentlyViewed(opts?: RecommendationOptions): Promise<RecommendationResult>;
26
+ getCartRecommendations(itemIds: string[], opts?: RecommendationOptions): Promise<RecommendationResult>;
27
+ recommend(opts: RecommendOptions): Promise<RecommendationResult>;
28
+ trackView(itemId: string, scenarioSlug?: string): void;
29
+ trackCart(itemId: string, scenarioSlug?: string): void;
30
+ trackPurchase(itemId: string, scenarioSlug?: string): void;
31
+ trackImpressions(container: Element, opts?: ImpressionTrackerOptions): () => void;
32
+ setUserId(userId: string): void;
33
+ mergeIdentity(authenticatedUserId: string): Promise<unknown>;
34
+ destroy(): void;
35
+ }
36
+ export type FireAndForgetPayload = {
37
+ interactions?: unknown[];
38
+ impressions?: unknown[];
39
+ };
40
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,UAAU,EAEV,KAAK,UAAU,EAChB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,KAAK,EACV,mBAAmB,EACnB,oBAAoB,EACpB,qBAAqB,EACrB,gBAAgB,EAChB,wBAAwB,EACzB,MAAM,SAAS,CAAC;AAsBjB,qBAAa,sBAAsB;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAG5C,QAAQ,CAAC,kBAAkB,EAAE,UAAU,CAAC,oBAAoB,CAAC,CAAC;gBAElD,MAAM,EAAE,mBAAmB;IA2BvC,OAAO,CAAC,gBAAgB;IAUxB,OAAO,CAAC,eAAe;YAIT,OAAO;IAyEf,iBAAiB,CAAC,IAAI,EAAE,UAAU,CAAC,oBAAoB,CAAC;;;;YAUhD,kBAAkB;IAoChC,OAAO,CAAC,EAAE;IAMV,OAAO,CAAC,YAAY;IAepB,kBAAkB,CAAC,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAa/E,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAa5F,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAa/F,iBAAiB,CAAC,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAa9E,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IActG,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAShE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI;IAUtD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI;IAUtD,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI;IAY1D,gBAAgB,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,wBAAwB,GAAG,MAAM,IAAI;IAMjF,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI/B,aAAa,CAAC,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa5D,OAAO,IAAI,IAAI;CAIhB;AAID,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,EAAE,CAAC;CACzB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { RecomnextBrowserClient } from './client';
2
+ export { ImpressionTracker } from './tracker';
3
+ export type { FireAndForgetPayload } from './client';
4
+ export type { BrowserClientConfig, RecommendedItem, RecommendationResult, ProjectionOptions, RecommendationOptions, RecommendOptions, ImpressionTrackerOptions, } from './types';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,YAAY,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrD,YAAY,EACV,mBAAmB,EACnB,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,SAAS,CAAC"}
@@ -0,0 +1,4 @@
1
+ function w(n){return I(n)}function I(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(I).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)+":"+I(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 A=new Map;async function b(n){let e=n.method.toUpperCase(),t=k(n.path),r=n.now??Math.floor(Date.now()/1e3),s=e==="GET"||e==="DELETE"||e==="HEAD"||e==="OPTIONS"?"":await x(w(n.body??null)),i=new Map;for(let[f,u]of Object.entries(n.query??{}))u==null||u===""||i.set(f,String(u));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(([f],[u])=>f<u?-1:f>u?1:0).map(([f,u])=>`${encodeURIComponent(f)}=${encodeURIComponent(u)}`),c=n.mode==="public"?"frontend_timestamp":"hmac_timestamp",h=n.mode==="public"?"frontend_sign":"hmac_sign",l=`${c}=${r}&body_hash=${s}`,a=o.length?`${o.join("&")}&${l}`:l,d=`${e}
2
+ ${t}?${a}
3
+ X-Tenant-Id:${n.tenantId}`,m=await $(n.token,d);return{signedPath:`${t}?${a}&${h}=${m}`,timestamp:r,bodyHash:s,signature:m,idempotencyKey:E()}}async function x(n){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(n));return v(new Uint8Array(e))}async function $(n,e){let t=await T(n),r=await crypto.subtle.sign("HMAC",t,new TextEncoder().encode(e));return v(new Uint8Array(r))}async function T(n){let e=A.get(n);return e||(e=crypto.subtle.importKey("raw",new TextEncoder().encode(n),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),A.set(n,e)),e}function R(n){return T(n)}function v(n){let e="";for(let t=0;t<n.length;t++)e+=n[t].toString(16).padStart(2,"0");return e}function k(n){let e=n.replace(/\/$/,"");return e.length>0?e:"/"}function E(){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=v(e);return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}var y={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??y.maxAttempts,initialBackoffMs:e.initialBackoffMs??y.initialBackoffMs,maxBackoffMs:e.maxBackoffMs??y.maxBackoffMs,flushIntervalMs:e.flushIntervalMs??y.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=q(),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??O(),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 U(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,c)=>{s.onsuccess=()=>o(s.result),s.onerror=()=>c(s.error)});Array.isArray(i)&&(this.items=i,this.drain())}catch{}}};function O(){let n=globalThis.crypto;return n?.randomUUID?n.randomUUID():`rnx-${Date.now()}-${Math.random().toString(36).slice(2,11)}`}function q(){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)})}var g=class{constructor(e,t,r,s,i){this.queue=[];this.flushing=!1;this.observer=null;this.seen=new Set;this.timer=null;this.getUserId=r,this.getSessionId=s,this.retryQueue=i,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??"";return this.observer=new IntersectionObserver(h=>{for(let l of h){if(!l.isIntersecting)continue;let a=l.target,d=a.dataset.itemId;if(!d)continue;let m=`${o}:${d}`;i&&this.seen.has(m)||(this.seen.add(m),this.queue.push({itemId:d,scenario:o}),i&&this.observer?.unobserve(a))}},{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}of t)r.has(o)||r.set(o,[]),r.get(o).push(i);let s=Array.from(r.entries()).map(([i,o])=>({userId:this.getUserId()||void 0,sessionId:this.getSessionId()||void 0,itemIds:o,scenarioSlug:i||void 0,timestamp:new Date().toISOString()}));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 P="recomnext_session_id",_=3,F=200,M=2e3,C=new Set([429,500,502,503,504]),D="auth_replay_cache_unavailable",Q=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.publicToken&&R(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 g(this.baseUrl,this.tenantId,()=>this.userId,()=>this.sessionId,this.fireAndForgetQueue)}resolveSessionId(){if(typeof sessionStorage>"u")return"";let e=sessionStorage.getItem(P);return e||(e=`sess_${Date.now()}_${Math.random().toString(36).slice(2,9)}`,sessionStorage.setItem(P,e)),e}effectiveUserId(){return this.userId||this.sessionId}async request(e,t,r,s){let i=Q.has(t)?0:_,o;for(let c=0;c<=i;c++){if(c>0){let d=Math.min(M,F*Math.pow(2,c-1)),m=.5+Math.random()*.5;await j(Math.floor(d*m))}let{url:h,headers:l}=await this.buildSignedRequest(e,t,s,r),a;try{a=await fetch(h,{method:e,headers:{"Content-Type":"application/json",...l},body:r!==void 0?JSON.stringify(r):void 0})}catch(d){if(o=d,c>=i)throw d;continue}if(a.status===503){let d=await K(a.clone());if(d===D&&c<i){o=new Error(`recomnext: ${e} ${t} ${a.status} ${d}`);continue}}if(C.has(a.status)&&c<i){o=new Error(`recomnext: ${e} ${t} returned ${a.status}`);continue}if(!a.ok){let d=await a.text().catch(()=>"");throw new Error(`recomnext: ${e} ${t} returned ${a.status}: ${d}`)}return a.status===204?void 0:a.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 c=B(r);return{url:`${this.baseUrl}${t}${c}`,headers:i}}let o=await b({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}}getRecommendations(e){return this.request("GET",`/recommendations/user-to-item/${this.effectiveUserId()}`,void 0,this.qs({count:e?.count,scenario:e?.scenario,...this.includeQuery(e)}))}getRelatedItems(e,t){return this.request("GET",`/recommendations/item-to-item/${e}`,void 0,this.qs({count:t?.count,scenario:t?.scenario,...this.includeQuery(t)}))}getSimilarProducts(e,t){return this.request("GET",`/recommendations/similar-products/${e}`,void 0,this.qs({count:t?.count,scenario:t?.scenario,...this.includeQuery(t)}))}getRecentlyViewed(e){return this.request("GET",`/recommendations/recently-viewed/${this.effectiveUserId()}`,void 0,this.qs({count:e?.count,scenario:e?.scenario,...this.includeQuery(e)}))}getCartRecommendations(e,t){return this.request("POST","/recommendations/items-to-items",{itemIds:e,count:t?.count??10,returnProperties:t?.returnProperties,includedProperties:t?.includedProperties},this.qs({scenario:t?.scenario}))}recommend(e){return this.request("POST","/recommendations",{...e,userId:this.effectiveUserId()})}trackView(e,t){this.fireAndForgetQueue.enqueue({path:"/ingestion/interactions",method:"POST",payload:{interactions:[{userId:this.effectiveUserId(),itemId:e,type:"view",scenarioSlug:t||void 0,timestamp:new Date().toISOString()}]}})}trackCart(e,t){this.fireAndForgetQueue.enqueue({path:"/ingestion/interactions",method:"POST",payload:{interactions:[{userId:this.effectiveUserId(),itemId:e,type:"cart",scenarioSlug:t||void 0,timestamp:new Date().toISOString()}]}})}trackPurchase(e,t){this.fireAndForgetQueue.enqueue({path:"/ingestion/interactions",method:"POST",payload:{interactions:[{userId:this.effectiveUserId(),itemId:e,type:"purchase",scenarioSlug:t||void 0,timestamp:new Date().toISOString()}]}})}trackImpressions(e,t){return this.tracker.observe(e,t)}setUserId(e){this.userId=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 B(n){if(!n)return"";let e=[];for(let[t,r]of Object.entries(n))r==null||r===""||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 j(n){return new Promise(e=>setTimeout(e,n))}export{g as ImpressionTracker,S as RecomnextBrowserClient};
4
+ //# sourceMappingURL=recomnext-browser.esm.js.map
@@ -0,0 +1,7 @@
1
+ {
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/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 const others = new Map<string, string>();\n for (const [k, v] of Object.entries(input.query ?? {})) {\n if (v === undefined || v === null || v === '') 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", "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 }[] = [];\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 // 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 ) {\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 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\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 });\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, string[]>();\n for (const { itemId, scenario } of batch) {\n if (!grouped.has(scenario)) grouped.set(scenario, []);\n grouped.get(scenario)!.push(itemId);\n }\n\n const impressions = Array.from(grouped.entries()).map(([scenarioSlug, itemIds]) => ({\n userId: this.getUserId() || undefined,\n sessionId: this.getSessionId() || undefined,\n itemIds,\n scenarioSlug: scenarioSlug || undefined,\n timestamp: new Date().toISOString(),\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 { ImpressionTracker } from './tracker';\nimport type {\n BrowserClientConfig,\n RecommendationResult,\n RecommendationOptions,\n RecommendOptions,\n ImpressionTrackerOptions,\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 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\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 );\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 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 /* \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 ...this.includeQuery(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 ...this.includeQuery(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 ...this.includeQuery(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 ...this.includeQuery(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 },\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 });\n }\n\n /* \u2500\u2500 Event tracking \u2500\u2500 */\n\n trackView(itemId: string, scenarioSlug?: string): void {\n this.fireAndForgetQueue.enqueue({\n path: '/ingestion/interactions',\n method: 'POST',\n payload: {\n interactions: [{ userId: this.effectiveUserId(), itemId, type: 'view', scenarioSlug: scenarioSlug || undefined, timestamp: new Date().toISOString() }],\n },\n });\n }\n\n trackCart(itemId: string, scenarioSlug?: string): void {\n this.fireAndForgetQueue.enqueue({\n path: '/ingestion/interactions',\n method: 'POST',\n payload: {\n interactions: [{ userId: this.effectiveUserId(), itemId, type: 'cart', scenarioSlug: scenarioSlug || undefined, timestamp: new Date().toISOString() }],\n },\n });\n }\n\n trackPurchase(itemId: string, scenarioSlug?: string): void {\n this.fireAndForgetQueue.enqueue({\n path: '/ingestion/interactions',\n method: 'POST',\n payload: {\n interactions: [{ userId: this.effectiveUserId(), itemId, type: 'purchase', scenarioSlug: scenarioSlug || undefined, timestamp: new Date().toISOString() }],\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 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 if (v === undefined || v === null || v === '') 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", "ImpressionTracker", "_baseUrl", "_tenantId", "getUserId", "getSessionId", "retryQueue", "container", "opts", "selector", "threshold", "once", "scenario", "entries", "entry", "el", "itemId", "key", "urgent", "batch", "grouped", "impressions", "scenarioSlug", "itemIds", "SESSION_KEY", "SYNC_RETRIES", "SYNC_INITIAL_BACKOFF_MS", "SYNC_MAX_BACKOFF_MS", "RETRYABLE_STATUS", "AUTH_REPLAY_CACHE_UNAVAILABLE", "NEVER_RETRY_PATHS", "RecomnextBrowserClient", "config", "preloadSignerKey", "RetryQueue", "item", "ImpressionTracker", "sid", "method", "path", "body", "query", "maxRetries", "lastErr", "attempt", "exp", "jitter", "sleep", "url", "headers", "res", "err", "code", "extractErrorCode", "text", "baseHeaders", "qs", "renderQs", "signed", "sign", "params", "opts", "itemId", "itemIds", "scenarioSlug", "container", "userId", "authenticatedUserId", "anonId", "parts", "k", "v", "ms", "r"]
7
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";var RecomnextBrowser=(()=>{var w=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var O=(n,e)=>{for(var t in e)w(n,t,{get:e[t],enumerable:!0})},q=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of k(e))!E.call(n,s)&&s!==t&&w(n,s,{get:()=>e[s],enumerable:!(r=$(e,s))||r.enumerable});return n};var U=n=>q(w({},"__esModule",{value:!0}),n);var X={};O(X,{ImpressionTracker:()=>p,RecomnextBrowserClient:()=>I});function R(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 s=n[r];typeof s>"u"||t.push(JSON.stringify(r)+":"+b(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 T=new Map;async function v(n){let e=n.method.toUpperCase(),t=M(n.path),r=n.now??Math.floor(Date.now()/1e3),s=e==="GET"||e==="DELETE"||e==="HEAD"||e==="OPTIONS"?"":await _(R(n.body??null)),i=new Map;for(let[f,u]of Object.entries(n.query??{}))u==null||u===""||i.set(f,String(u));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(([f],[u])=>f<u?-1:f>u?1:0).map(([f,u])=>`${encodeURIComponent(f)}=${encodeURIComponent(u)}`),c=n.mode==="public"?"frontend_timestamp":"hmac_timestamp",h=n.mode==="public"?"frontend_sign":"hmac_sign",l=`${c}=${r}&body_hash=${s}`,a=o.length?`${o.join("&")}&${l}`:l,d=`${e}
2
+ ${t}?${a}
3
+ X-Tenant-Id:${n.tenantId}`,m=await F(n.token,d);return{signedPath:`${t}?${a}&${h}=${m}`,timestamp:r,bodyHash:s,signature:m,idempotencyKey:C()}}async function _(n){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(n));return A(new Uint8Array(e))}async function F(n,e){let t=await P(n),r=await crypto.subtle.sign("HMAC",t,new TextEncoder().encode(e));return A(new Uint8Array(r))}async function P(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 S(n){return P(n)}function A(n){let e="";for(let t=0;t<n.length;t++)e+=n[t].toString(16).padStart(2,"0");return e}function M(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=A(e);return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}var y={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??y.maxAttempts,initialBackoffMs:e.initialBackoffMs??y.initialBackoffMs,maxBackoffMs:e.maxBackoffMs??y.maxBackoffMs,flushIntervalMs:e.flushIntervalMs??y.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=Q(),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),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 B(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,c)=>{s.onsuccess=()=>o(s.result),s.onerror=()=>c(s.error)});Array.isArray(i)&&(this.items=i,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 Q(){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 B(n){return new Promise((e,t)=>{n.oncomplete=()=>e(),n.onerror=()=>t(n.error),n.onabort=()=>t(n.error)})}var p=class{constructor(e,t,r,s,i){this.queue=[];this.flushing=!1;this.observer=null;this.seen=new Set;this.timer=null;this.getUserId=r,this.getSessionId=s,this.retryQueue=i,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??"";return this.observer=new IntersectionObserver(h=>{for(let l of h){if(!l.isIntersecting)continue;let a=l.target,d=a.dataset.itemId;if(!d)continue;let m=`${o}:${d}`;i&&this.seen.has(m)||(this.seen.add(m),this.queue.push({itemId:d,scenario:o}),i&&this.observer?.unobserve(a))}},{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}of t)r.has(o)||r.set(o,[]),r.get(o).push(i);let s=Array.from(r.entries()).map(([i,o])=>({userId:this.getUserId()||void 0,sessionId:this.getSessionId()||void 0,itemIds:o,scenarioSlug:i||void 0,timestamp:new Date().toISOString()}));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 x="recomnext_session_id",K=3,j=200,H=2e3,N=new Set([429,500,502,503,504]),L="auth_replay_cache_unavailable",J=new Set(["/identity/merge"]),I=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.publicToken&&S(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)}resolveSessionId(){if(typeof sessionStorage>"u")return"";let e=sessionStorage.getItem(x);return e||(e=`sess_${Date.now()}_${Math.random().toString(36).slice(2,9)}`,sessionStorage.setItem(x,e)),e}effectiveUserId(){return this.userId||this.sessionId}async request(e,t,r,s){let i=J.has(t)?0:K,o;for(let c=0;c<=i;c++){if(c>0){let d=Math.min(H,j*Math.pow(2,c-1)),m=.5+Math.random()*.5;await G(Math.floor(d*m))}let{url:h,headers:l}=await this.buildSignedRequest(e,t,s,r),a;try{a=await fetch(h,{method:e,headers:{"Content-Type":"application/json",...l},body:r!==void 0?JSON.stringify(r):void 0})}catch(d){if(o=d,c>=i)throw d;continue}if(a.status===503){let d=await V(a.clone());if(d===L&&c<i){o=new Error(`recomnext: ${e} ${t} ${a.status} ${d}`);continue}}if(N.has(a.status)&&c<i){o=new Error(`recomnext: ${e} ${t} returned ${a.status}`);continue}if(!a.ok){let d=await a.text().catch(()=>"");throw new Error(`recomnext: ${e} ${t} returned ${a.status}: ${d}`)}return a.status===204?void 0:a.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 c=Y(r);return{url:`${this.baseUrl}${t}${c}`,headers:i}}let o=await v({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}}getRecommendations(e){return this.request("GET",`/recommendations/user-to-item/${this.effectiveUserId()}`,void 0,this.qs({count:e?.count,scenario:e?.scenario,...this.includeQuery(e)}))}getRelatedItems(e,t){return this.request("GET",`/recommendations/item-to-item/${e}`,void 0,this.qs({count:t?.count,scenario:t?.scenario,...this.includeQuery(t)}))}getSimilarProducts(e,t){return this.request("GET",`/recommendations/similar-products/${e}`,void 0,this.qs({count:t?.count,scenario:t?.scenario,...this.includeQuery(t)}))}getRecentlyViewed(e){return this.request("GET",`/recommendations/recently-viewed/${this.effectiveUserId()}`,void 0,this.qs({count:e?.count,scenario:e?.scenario,...this.includeQuery(e)}))}getCartRecommendations(e,t){return this.request("POST","/recommendations/items-to-items",{itemIds:e,count:t?.count??10,returnProperties:t?.returnProperties,includedProperties:t?.includedProperties},this.qs({scenario:t?.scenario}))}recommend(e){return this.request("POST","/recommendations",{...e,userId:this.effectiveUserId()})}trackView(e,t){this.fireAndForgetQueue.enqueue({path:"/ingestion/interactions",method:"POST",payload:{interactions:[{userId:this.effectiveUserId(),itemId:e,type:"view",scenarioSlug:t||void 0,timestamp:new Date().toISOString()}]}})}trackCart(e,t){this.fireAndForgetQueue.enqueue({path:"/ingestion/interactions",method:"POST",payload:{interactions:[{userId:this.effectiveUserId(),itemId:e,type:"cart",scenarioSlug:t||void 0,timestamp:new Date().toISOString()}]}})}trackPurchase(e,t){this.fireAndForgetQueue.enqueue({path:"/ingestion/interactions",method:"POST",payload:{interactions:[{userId:this.effectiveUserId(),itemId:e,type:"purchase",scenarioSlug:t||void 0,timestamp:new Date().toISOString()}]}})}trackImpressions(e,t){return this.tracker.observe(e,t)}setUserId(e){this.userId=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||r===""||e.push(`${encodeURIComponent(t)}=${encodeURIComponent(String(r))}`);return e.length?`?${e.join("&")}`:""}async function V(n){try{let e=await n.json();return typeof e.errorCode=="string"?e.errorCode:void 0}catch{return}}function G(n){return new Promise(e=>setTimeout(e,n))}return U(X);})();
4
+ //# sourceMappingURL=recomnext-browser.umd.js.map
@@ -0,0 +1,7 @@
1
+ {
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/tracker.ts", "../src/client.ts"],
4
+ "sourcesContent": ["export { RecomnextBrowserClient } from './client';\nexport { ImpressionTracker } from './tracker';\nexport type { FireAndForgetPayload } from './client';\nexport type {\n BrowserClientConfig,\n RecommendedItem,\n RecommendationResult,\n ProjectionOptions,\n RecommendationOptions,\n RecommendOptions,\n ImpressionTrackerOptions,\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 const others = new Map<string, string>();\n for (const [k, v] of Object.entries(input.query ?? {})) {\n if (v === undefined || v === null || v === '') 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", "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 }[] = [];\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 // 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 ) {\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 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\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 });\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, string[]>();\n for (const { itemId, scenario } of batch) {\n if (!grouped.has(scenario)) grouped.set(scenario, []);\n grouped.get(scenario)!.push(itemId);\n }\n\n const impressions = Array.from(grouped.entries()).map(([scenarioSlug, itemIds]) => ({\n userId: this.getUserId() || undefined,\n sessionId: this.getSessionId() || undefined,\n itemIds,\n scenarioSlug: scenarioSlug || undefined,\n timestamp: new Date().toISOString(),\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 { ImpressionTracker } from './tracker';\nimport type {\n BrowserClientConfig,\n RecommendationResult,\n RecommendationOptions,\n RecommendOptions,\n ImpressionTrackerOptions,\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 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\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 );\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 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 /* \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 ...this.includeQuery(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 ...this.includeQuery(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 ...this.includeQuery(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 ...this.includeQuery(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 },\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 });\n }\n\n /* \u2500\u2500 Event tracking \u2500\u2500 */\n\n trackView(itemId: string, scenarioSlug?: string): void {\n this.fireAndForgetQueue.enqueue({\n path: '/ingestion/interactions',\n method: 'POST',\n payload: {\n interactions: [{ userId: this.effectiveUserId(), itemId, type: 'view', scenarioSlug: scenarioSlug || undefined, timestamp: new Date().toISOString() }],\n },\n });\n }\n\n trackCart(itemId: string, scenarioSlug?: string): void {\n this.fireAndForgetQueue.enqueue({\n path: '/ingestion/interactions',\n method: 'POST',\n payload: {\n interactions: [{ userId: this.effectiveUserId(), itemId, type: 'cart', scenarioSlug: scenarioSlug || undefined, timestamp: new Date().toISOString() }],\n },\n });\n }\n\n trackPurchase(itemId: string, scenarioSlug?: string): void {\n this.fireAndForgetQueue.enqueue({\n path: '/ingestion/interactions',\n method: 'POST',\n payload: {\n interactions: [{ userId: this.effectiveUserId(), itemId, type: 'purchase', scenarioSlug: scenarioSlug || undefined, timestamp: new Date().toISOString() }],\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 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 if (v === undefined || v === null || v === '') 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", "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", "ImpressionTracker", "_baseUrl", "_tenantId", "getUserId", "getSessionId", "retryQueue", "container", "opts", "selector", "threshold", "once", "scenario", "entries", "entry", "el", "itemId", "key", "urgent", "batch", "grouped", "impressions", "scenarioSlug", "itemIds", "SESSION_KEY", "SYNC_RETRIES", "SYNC_INITIAL_BACKOFF_MS", "SYNC_MAX_BACKOFF_MS", "RETRYABLE_STATUS", "AUTH_REPLAY_CACHE_UNAVAILABLE", "NEVER_RETRY_PATHS", "RecomnextBrowserClient", "config", "preloadSignerKey", "RetryQueue", "item", "ImpressionTracker", "sid", "method", "path", "body", "query", "maxRetries", "lastErr", "attempt", "exp", "jitter", "sleep", "url", "headers", "res", "err", "code", "extractErrorCode", "text", "baseHeaders", "qs", "renderQs", "signed", "sign", "params", "opts", "itemId", "itemIds", "scenarioSlug", "container", "userId", "authenticatedUserId", "anonId", "parts", "k", "v", "ms", "r"]
7
+ }
@@ -0,0 +1,18 @@
1
+ import type { RetryQueue } from '@recomnext/shared-browser-auth';
2
+ import type { FireAndForgetPayload } from './client';
3
+ import type { ImpressionTrackerOptions } from './types';
4
+ export declare class ImpressionTracker {
5
+ private queue;
6
+ private flushing;
7
+ private observer;
8
+ private seen;
9
+ private timer;
10
+ private readonly getUserId;
11
+ private readonly getSessionId;
12
+ private readonly retryQueue;
13
+ constructor(_baseUrl: string, _tenantId: string, getUserId: () => string, getSessionId: () => string, retryQueue: RetryQueue<FireAndForgetPayload>);
14
+ observe(container: Element, opts?: ImpressionTrackerOptions): () => void;
15
+ private flush;
16
+ destroy(): void;
17
+ }
18
+ //# sourceMappingURL=tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../src/tracker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AACrD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;AAIxD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,KAAK,CAA8C;IAC3D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAK5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAmC;gBAG5D,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,MAAM,EACvB,YAAY,EAAE,MAAM,MAAM,EAC1B,UAAU,EAAE,UAAU,CAAC,oBAAoB,CAAC;IAmB9C,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,GAAE,wBAA6B,GAAG,MAAM,IAAI;IAoC5E,OAAO,CAAC,KAAK;IA6Bb,OAAO,IAAI,IAAI;CAKhB"}
@@ -0,0 +1,67 @@
1
+ export interface BrowserClientConfig {
2
+ baseUrl: string;
3
+ tenantId: string;
4
+ userId?: string;
5
+ sessionId?: string;
6
+ /**
7
+ * PUBLIC HMAC token (`rnx_pub_<48 hex>`) issued for this tenant.
8
+ * The browser SDK ALWAYS uses public-mode signing — there is no escape
9
+ * hatch for a private token in a JS bundle. Once the engine flips to
10
+ * default-deny (Phase 7), unsigned requests are rejected; supply this
11
+ * value as soon as you've provisioned a frontend SDK token.
12
+ */
13
+ publicToken?: string;
14
+ /**
15
+ * Persist queued impressions/interactions across page reloads via
16
+ * IndexedDB. Off by default — opt in for high-value telemetry pages
17
+ * where dropping a batch hurts (e.g. checkout funnel).
18
+ */
19
+ persistRetries?: boolean;
20
+ /**
21
+ * Invoked when the RetryQueue drops a queued item after exhausting
22
+ * attempts (default 8 attempts / 10m backoff). Hook this up to your
23
+ * observability stack to spot tenant-wide outages.
24
+ */
25
+ onRetryDrop?: (item: {
26
+ path: string;
27
+ payload: unknown;
28
+ attempts?: number;
29
+ }, error: unknown) => void;
30
+ }
31
+ export interface RecommendedItem {
32
+ itemId?: string;
33
+ /**
34
+ * Compatibility alias for `itemId`.
35
+ * @deprecated Use `itemId` instead.
36
+ */
37
+ externalId?: string;
38
+ score?: number;
39
+ [key: string]: unknown;
40
+ }
41
+ export interface RecommendationResult {
42
+ items: RecommendedItem[];
43
+ count: number;
44
+ algorithm?: string;
45
+ }
46
+ export interface ProjectionOptions {
47
+ returnProperties?: boolean;
48
+ includedProperties?: string[];
49
+ }
50
+ export interface RecommendationOptions extends ProjectionOptions {
51
+ count?: number;
52
+ scenario?: string;
53
+ }
54
+ export interface RecommendOptions extends ProjectionOptions {
55
+ scenario?: string;
56
+ logicType?: string;
57
+ itemId?: string;
58
+ itemIds?: string[];
59
+ count?: number;
60
+ }
61
+ export interface ImpressionTrackerOptions {
62
+ scenario?: string;
63
+ itemSelector?: string;
64
+ threshold?: number;
65
+ once?: boolean;
66
+ }
67
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACrG;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAiB,SAAQ,iBAAiB;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@recomnext/browser",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight browser SDK for recomnext — recommendations, event tracking, and impression tracking",
5
+ "type": "module",
6
+ "main": "dist/recomnext-browser.umd.js",
7
+ "module": "dist/recomnext-browser.esm.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/recomnext-browser.esm.js",
12
+ "require": "./dist/recomnext-browser.umd.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "LICENSE.md",
19
+ "README.md",
20
+ "scripts/postinstall-notice.js"
21
+ ],
22
+ "scripts": {
23
+ "build": "node build.mjs",
24
+ "build:types": "tsc --emitDeclarationOnly",
25
+ "clean": "rm -rf dist",
26
+ "pretest": "npm run build",
27
+ "test": "node --test test/*.test.mjs",
28
+ "postinstall": "node ./scripts/postinstall-notice.js || true"
29
+ },
30
+ "license": "SEE LICENSE IN LICENSE.md",
31
+ "author": {
32
+ "name": "Remittx Private Limited",
33
+ "email": "legal@driffle.com",
34
+ "url": "https://recomnext.com"
35
+ },
36
+ "homepage": "https://recomnext.com",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/Driffle/recomnext.git",
40
+ "directory": "sdks/js-client"
41
+ },
42
+ "bugs": {
43
+ "email": "support@driffle.com"
44
+ },
45
+ "keywords": [
46
+ "recomnext",
47
+ "recommendations",
48
+ "personalization",
49
+ "proprietary",
50
+ "commercial",
51
+ "subscription-required"
52
+ ],
53
+ "publishConfig": {
54
+ "access": "public",
55
+ "registry": "https://registry.npmjs.org/"
56
+ },
57
+ "dependencies": {},
58
+ "devDependencies": {
59
+ "@recomnext/shared-browser-auth": "file:../shared-browser-auth",
60
+ "esbuild": "^0.25.0",
61
+ "typescript": "^5.7.0"
62
+ }
63
+ }
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ if (
4
+ process.env.CI ||
5
+ process.env.NODE_ENV === 'production' ||
6
+ process.env.RECOMNEXT_NO_POSTINSTALL ||
7
+ process.env.npm_config_production === 'true'
8
+ ) {
9
+ process.exit(0);
10
+ }
11
+
12
+ let pkgName = '@recomnext/sdk';
13
+ let pkgVersion = '';
14
+ try {
15
+ const pkg = require('../package.json');
16
+ pkgName = pkg.name || pkgName;
17
+ pkgVersion = pkg.version ? '@' + pkg.version : '';
18
+ } catch (_) {}
19
+
20
+ const notice = [
21
+ '',
22
+ ' ────────────────────────────────────────────────────────────────',
23
+ ' Installed: ' + pkgName + pkgVersion,
24
+ '',
25
+ ' This is PROPRIETARY software of Remittx Private Limited.',
26
+ ' License: https://recomnext.com/license',
27
+ ' Contact: legal@driffle.com',
28
+ '',
29
+ ' An active Recomnext subscription and a valid API key are',
30
+ ' required. By using this SDK you agree to the License Agreement.',
31
+ ' ────────────────────────────────────────────────────────────────',
32
+ '',
33
+ ].join('\n');
34
+
35
+ console.log(notice);