@likyly/sdk 0.0.0-stage → 1.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.
Files changed (59) hide show
  1. package/README.md +278 -2
  2. package/dist/cjs/client.d.ts +70 -0
  3. package/dist/cjs/client.js +85 -0
  4. package/dist/cjs/encoding.d.ts +11 -0
  5. package/dist/cjs/encoding.js +37 -0
  6. package/dist/cjs/errors.d.ts +52 -0
  7. package/dist/cjs/errors.js +83 -0
  8. package/dist/cjs/http.d.ts +49 -0
  9. package/dist/cjs/http.js +155 -0
  10. package/dist/cjs/index.d.ts +8 -0
  11. package/dist/cjs/index.js +20 -0
  12. package/dist/cjs/package.json +1 -0
  13. package/dist/cjs/recommend-cache.d.ts +42 -0
  14. package/dist/cjs/recommend-cache.js +114 -0
  15. package/dist/cjs/resources/events.d.ts +45 -0
  16. package/dist/cjs/resources/events.js +134 -0
  17. package/dist/cjs/resources/items.d.ts +43 -0
  18. package/dist/cjs/resources/items.js +97 -0
  19. package/dist/cjs/resources/placements.d.ts +24 -0
  20. package/dist/cjs/resources/placements.js +97 -0
  21. package/dist/cjs/resources/recommendations.d.ts +54 -0
  22. package/dist/cjs/resources/recommendations.js +130 -0
  23. package/dist/cjs/resources/users.d.ts +19 -0
  24. package/dist/cjs/resources/users.js +64 -0
  25. package/dist/cjs/session.d.ts +32 -0
  26. package/dist/cjs/session.js +76 -0
  27. package/dist/cjs/types.d.ts +226 -0
  28. package/dist/cjs/types.js +2 -0
  29. package/dist/cjs/version.d.ts +3 -0
  30. package/dist/cjs/version.js +6 -0
  31. package/dist/esm/client.d.ts +70 -0
  32. package/dist/esm/client.js +81 -0
  33. package/dist/esm/encoding.d.ts +11 -0
  34. package/dist/esm/encoding.js +31 -0
  35. package/dist/esm/errors.d.ts +52 -0
  36. package/dist/esm/errors.js +69 -0
  37. package/dist/esm/http.d.ts +49 -0
  38. package/dist/esm/http.js +149 -0
  39. package/dist/esm/index.d.ts +8 -0
  40. package/dist/esm/index.js +4 -0
  41. package/dist/esm/recommend-cache.d.ts +42 -0
  42. package/dist/esm/recommend-cache.js +110 -0
  43. package/dist/esm/resources/events.d.ts +45 -0
  44. package/dist/esm/resources/events.js +130 -0
  45. package/dist/esm/resources/items.d.ts +43 -0
  46. package/dist/esm/resources/items.js +91 -0
  47. package/dist/esm/resources/placements.d.ts +24 -0
  48. package/dist/esm/resources/placements.js +93 -0
  49. package/dist/esm/resources/recommendations.d.ts +54 -0
  50. package/dist/esm/resources/recommendations.js +124 -0
  51. package/dist/esm/resources/users.d.ts +19 -0
  52. package/dist/esm/resources/users.js +60 -0
  53. package/dist/esm/session.d.ts +32 -0
  54. package/dist/esm/session.js +72 -0
  55. package/dist/esm/types.d.ts +226 -0
  56. package/dist/esm/types.js +1 -0
  57. package/dist/esm/version.d.ts +3 -0
  58. package/dist/esm/version.js +3 -0
  59. package/package.json +40 -4
package/README.md CHANGED
@@ -1,3 +1,279 @@
1
- # Temporary Holding Version
1
+ # LIKYLY SDK for TypeScript / JavaScript
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The official TypeScript / JavaScript client for the [LIKYLY](https://likyly.com) recommendations API. Send your catalog and what your visitors do, get personalised recommendations back.
4
+
5
+ ```
6
+ catalog (items) → events → recommendations → impression → click → purchase
7
+ ```
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install @likyly/sdk
13
+ ```
14
+
15
+ Node.js 18 or later, or any runtime that has `fetch` (Bun, Deno, Cloudflare Workers, browsers). ESM and CommonJS builds, TypeScript types included, no runtime dependency.
16
+
17
+ ## Quick start
18
+
19
+ ### Initialize
20
+
21
+ Create one client and reuse it. Use your **secret** key on your server (see [Which key?](#which-key)).
22
+
23
+ ```js
24
+ import { Likyly, NotFoundError, RateLimitError } from "@likyly/sdk";
25
+
26
+ // Backend only: the secret key gives access to your catalog and your users.
27
+ const likyly = new Likyly({
28
+ apiKey: process.env.LIKYLY_SECRET_KEY,
29
+ });
30
+ ```
31
+
32
+ ### Send your catalog
33
+
34
+ ```js
35
+ // Send your catalog once (and whenever it changes). Ids are YOUR ids: SKUs, UUIDs, Shopify gids...
36
+ await likyly.items.upsertMany([
37
+ { itemId: "SKU-1", title: "Nike Air Max", description: "Running shoe with air cushioning", properties: { category: "shoes", brand: "Nike", price: 129.9 } },
38
+ { itemId: "SKU-2", title: "Adidas Ultraboost", description: "Responsive running shoe", properties: { category: "shoes", brand: "Adidas", price: 149 } },
39
+ { itemId: "SKU-3", title: "Nike Pegasus", description: "Everyday running shoe", properties: { category: "shoes", brand: "Nike", price: 119 } },
40
+ ]);
41
+
42
+ // Users are optional: describe them if you want the profile to travel with their events.
43
+ await likyly.users.upsert("user_123", { properties: { country: "FR", segment: "premium" } });
44
+ ```
45
+
46
+ Ids are your own: any string (`SKU-123`, a UUID, `gid://shopify/Product/123`). `properties` is free-form; `category` and `description` improve similarity, everything else is stored as is.
47
+
48
+ ### Track events
49
+
50
+ ```js
51
+ // Tell LIKYLY what your visitors do.
52
+ await likyly.events.view({ userId: "user_123", itemId: "SKU-1" });
53
+
54
+ // Purchases carry an eventId: replaying the call can never count the sale twice.
55
+ await likyly.events.purchase({
56
+ eventId: "purchase_order_9281_SKU-1",
57
+ userId: "user_123",
58
+ itemId: "SKU-1",
59
+ quantity: 1,
60
+ properties: { price: 129.9, currency: "EUR", orderId: "order_9281" },
61
+ });
62
+ ```
63
+
64
+ ### Get recommendations
65
+
66
+ ```js
67
+ // Ask what to show. You never pick an algorithm: LIKYLY chooses the best strategy for what it knows.
68
+ const recs = await likyly.recommendations.get({ userId: "user_123", placement: "homepage", limit: 3 });
69
+
70
+ console.log(`strategy: ${recs.strategy}`);
71
+ for (const item of recs.items) console.log(item.itemId, item.title);
72
+ ```
73
+
74
+ You never choose an algorithm: LIKYLY picks the best strategy for what it knows about the visitor and tells you which one it used (`strategy`: `hybrid`, `content`, `collaborative`, `session` or `popular`). With no user, no session and no item you still get a useful answer: what is popular.
75
+
76
+ ### Placements: the recommended way
77
+
78
+ ```js
79
+ // The recommended way: a placement ("pdp-related"...) is configured once in LIKYLY - strategy,
80
+ // fallback, filters - and your page only says where it is and what it knows.
81
+ const related = await likyly.placements.recommend({
82
+ placement: "pdp-related",
83
+ context: { itemId: "SKU-123", sessionId: "sess_abc" },
84
+ limit: 4,
85
+ });
86
+ console.log(`strategy used: ${related.strategyUsed}`);
87
+
88
+ // When an anonymous visitor signs in, attach their session's history to their account.
89
+ const { linkedInteractions } = await likyly.events.identify({ userId: "user_123", sessionId: "sess_abc" });
90
+ console.log(`linked: ${linkedInteractions}`);
91
+ ```
92
+
93
+ A placement (`pdp-related`, `home-for-you`, `cart-cross-sell`...) is configured once in LIKYLY, by your coding assistant through the MCP server or in the console: strategy, fallback, filters, how many items. Your code only names it and passes what it knows (`itemId`, `userId`, `sessionId`, ...); change the configuration later without redeploying. `identify` attaches an anonymous visitor's history to their account the moment they sign in, so their recommendations stay personal.
94
+
95
+ ### Close the loop
96
+
97
+ ```js
98
+ // Send the recommendationId back: LIKYLY measures which recommendations get seen, clicked and bought.
99
+ await likyly.events.impression({ userId: "user_123", itemId: recs.items[0].itemId, recommendationId: recs.recommendationId, placement: "homepage" });
100
+ await likyly.events.click({ userId: "user_123", itemId: recs.items[0].itemId, recommendationId: recs.recommendationId, placement: "homepage" });
101
+ ```
102
+
103
+ Send the recommendation id back on the events that follow (`impression` when it is displayed, `click`, `add_to_cart`, `purchase`) and LIKYLY can measure which recommendations actually convert.
104
+
105
+ ## Which key?
106
+
107
+ | | Secret key | Public key |
108
+ |---|---|---|
109
+ | Where | your server only | anywhere, including a web page |
110
+ | Recommendations | yes | yes |
111
+ | Record events | yes | yes |
112
+ | Items and users (create, read, delete) | yes | no (`PermissionDeniedError`) |
113
+ | `debug` explanations | yes | no |
114
+
115
+ The public key can only read recommendations and write events, so it is safe to expose. **Never put the secret key in code that ships to a browser or a mobile app.**
116
+
117
+ ```js
118
+ // In a web page, use the PUBLIC key: it can only read recommendations and record events.
119
+ const browser = new Likyly({
120
+ apiKey: process.env.LIKYLY_PUBLIC_KEY,
121
+ });
122
+
123
+ // A visitor who is not logged in: use an anonymous session id (any string you generate and keep in a cookie).
124
+ await browser.events.view({ sessionId: "sess_abc", itemId: "SKU-2" });
125
+ const forVisitor = await browser.recommendations.get({ sessionId: "sess_abc", viewedItemIds: ["SKU-2"], placement: "product_page", limit: 3 });
126
+ ```
127
+
128
+ ## Events
129
+
130
+ `track` is the one mechanism; `impression`, `view`, `click`, `add_to_cart`, `remove_from_cart` and `purchase` are shortcuts that call it with the matching type. Event types are **open strings**: use your own (`favorite`, `share`, `video_played`, ...) as freely as the official ones.
131
+
132
+ ```js
133
+ // Any string is a valid event type: track what matters to your business.
134
+ await likyly.events.track("favorite", { userId: "user_123", itemId: "SKU-2", properties: { list: "wishlist" } });
135
+
136
+ // Up to 1000 events per call, each with its own type.
137
+ await likyly.events.trackMany([
138
+ { type: "view", userId: "user_123", itemId: "SKU-3" },
139
+ { type: "add_to_cart", userId: "user_123", itemId: "SKU-3", quantity: 1 },
140
+ ]);
141
+ ```
142
+
143
+ - An event needs an item and a user, a session, or both (both ties the anonymous session to the user once they log in).
144
+ - Anonymous visitors: generate a session id (any string) and keep it in a cookie; no login needed.
145
+ - `properties` is a free-form object (price, currency, order id, ...). Its keys are stored exactly as you send them and are never renamed by the SDK.
146
+ - `event_id` (idempotency key): replaying an event with the same id records nothing and returns `duplicate: true`. Set it on purchases, for example `purchase_<orderId>_<itemId>`.
147
+ - `placement` is a free-form label of where something happened or will be displayed (`homepage`, `product_page`, `cart`, ...).
148
+
149
+ ## Advanced recommendations
150
+
151
+ `recommendations.get` is the normal way. When you want to pick the strategy yourself:
152
+
153
+ ```js
154
+ // Advanced Recommendations: one strategy at a time, when you want to choose.
155
+ const similar = await likyly.recommendations.similar({ itemId: "SKU-1", limit: 3 });
156
+ const hybrid = await likyly.recommendations.hybrid({ userId: "user_123", itemId: "SKU-1", alpha: 0.7, limit: 3 });
157
+ const session = await likyly.recommendations.session({ viewedItemIds: ["SKU-1", "SKU-2"], limit: 3 });
158
+ ```
159
+
160
+ | Strategy | What it does |
161
+ |---|---|
162
+ | `popular` | The most popular items: the fallback for a visitor with no history |
163
+ | `similar` | Items similar to one item (content similarity), no user needed |
164
+ | `collaborative` | What users with similar histories liked. Needs a trained model, otherwise a not-found error |
165
+ | `hybrid` | Similar items personalised for a user; `alpha` (0 to 1) weighs collaborative against content |
166
+ | `session` | Recency-weighted, from `viewed item ids` or from a user's own view history (exactly one) |
167
+
168
+ These endpoints carry ids in the URL path, so ids containing `/` (such as `gid://shopify/Product/1`) or, for the session list, `,` are refused with a validation error: use `recommendations.get`, which takes ids in the request body.
169
+
170
+ ## Errors
171
+
172
+ Errors are typed (every one is a `LikylyError`). Catch the specific ones you care about:
173
+
174
+ | Error | When |
175
+ |---|---|
176
+ | `ValidationError` | The request is invalid: caught by the SDK before anything is sent, or a 422 from the API (the message names the offending fields) |
177
+ | `AuthenticationError` | 401: missing, invalid or revoked API key |
178
+ | `PermissionDeniedError` | 403: for example a public key used for a secret-key operation, or a plan limit |
179
+ | `NotFoundError` | 404 |
180
+ | `RateLimitError` | 429: see the retry-after value |
181
+ | `ApiError` | Any other error status from the API (the four rows above are its subtypes) |
182
+ | `NetworkError` | No HTTP response: DNS, connection reset, TLS |
183
+ | `TimeoutError` | The request timed out |
184
+
185
+ Errors that come from the API carry `statusCode`, `requestId`, `retryAfter` (seconds), `body`. Quote the request id when you contact support.
186
+
187
+ ```js
188
+ try {
189
+ await likyly.items.get("does-not-exist");
190
+ } catch (error) {
191
+ if (error instanceof NotFoundError) console.log("no such item, request", error.requestId);
192
+ else if (error instanceof RateLimitError) console.log("slow down, retry in", error.retryAfter, "s");
193
+ else throw error;
194
+ }
195
+ ```
196
+
197
+ ## Retries and timeouts
198
+
199
+ The SDK retries transient failures for you (2 retries by default, exponential backoff from 0.5 s capped at 8 s, with full jitter):
200
+
201
+ - **429**: always retried, honouring `Retry-After`. A `Retry-After` longer than 60 s is not waited for: the rate-limit error is returned instead.
202
+ - **502, 503, 504, dropped connections and timeouts**: retried only when replaying the call is harmless: reads, upserts, deletes, events that carry an `event_id`, and batches where every event has one.
203
+ - An event **without** an `event_id` is never retried after an ambiguous failure, so a flaky network can never record a purchase twice. Set `event_id` on events you care about and they become safe to retry.
204
+ - Other 4xx errors are never retried.
205
+
206
+ ## Configuration
207
+
208
+ | Setting | Option | Default |
209
+ |---|---|---|
210
+ | API key | `apiKey` | required |
211
+ | Base URL | `baseUrl` | `https://api.likyly.com` |
212
+ | Catalog | `catalog` | your only catalog |
213
+ | Timeout | `timeout` (milliseconds) | 10 000 |
214
+ | Retries | `maxRetries` | 2 |
215
+ | User-Agent suffix | `userAgent` | none |
216
+ | Custom HTTP | `fetch` | global `fetch` |
217
+ | Recommendation cache | `cache`: `{ ttlSeconds, pauseSeconds, storage }` or `false` | 1 hour; 30 s pause after a failure |
218
+
219
+ ```js
220
+ const tuned = new Likyly({
221
+ apiKey: process.env.LIKYLY_SECRET_KEY,
222
+ timeout: 5_000, // milliseconds per attempt (default 10 000)
223
+ maxRetries: 3, // automatic retries on 429 / 502 / 503 / 504 / network errors (default 2, 0 disables)
224
+ userAgent: "my-shop/1.4", // appended to the SDK's User-Agent
225
+ });
226
+ ```
227
+
228
+ Every method takes a last `options` argument: `{ signal, timeout, maxRetries }` (an `AbortSignal` cancels the call).
229
+
230
+ ## API reference
231
+
232
+ `key` says which API key may call it: **S** = secret key only, **P** = public or secret key.
233
+
234
+ | Method | What it does | Key |
235
+ |---|---|---|
236
+ | `likyly.items.get` | One item by your own id | S |
237
+ | `likyly.items.list` | One page of the catalog (`limit` / `offset`), with the catalog's total | S |
238
+ | `likyly.items.upsert` | Create or replace an item (idempotent) | S |
239
+ | `likyly.items.delete` | Remove an item (events already recorded for it are kept) | S |
240
+ | `likyly.items.upsertMany` | Batch upsert, 1 to 1000 items | S |
241
+ | `likyly.items.import` | Alias of the batch upsert above (JSON batch) | S |
242
+ | `likyly.items.deleteMany` | Batch delete, 1 to 1000 ids; unknown ids are reported, not raised | S |
243
+ | `likyly.users.get` | One user profile | S |
244
+ | `likyly.users.list` | One page of users (`limit` / `offset`) | S |
245
+ | `likyly.users.upsert` | Create or replace a profile (free-form `properties`) | S |
246
+ | `likyly.users.delete` | Erase the profile **and every event recorded for that user** | S |
247
+ | `likyly.users.import` | Batch upsert of users, 1 to 1000 | S |
248
+ | `likyly.events.track` | Record an event of any type (open string) | P |
249
+ | `likyly.events.trackMany` | Up to 1000 events in one call, each with its own type | P |
250
+ | `likyly.events.impression` | The item was displayed to the visitor | P |
251
+ | `likyly.events.view` | The visitor looked at the item | P |
252
+ | `likyly.events.click` | The visitor clicked the item | P |
253
+ | `likyly.events.addToCart` | The visitor added the item to their cart | P |
254
+ | `likyly.events.removeFromCart` | The visitor removed the item from their cart | P |
255
+ | `likyly.events.purchase` | The visitor bought the item (set an event id) | P |
256
+ | `likyly.events.identify` | An anonymous visitor signed in: attach their session's history to their user id | P |
257
+ | `likyly.placements.recommend` | Recommendations for a named placement configured in LIKYLY (the recommended way) | P |
258
+ | `likyly.recommendations.get` | Recommendations with the automatic strategy: user, session, item or viewed items | P |
259
+ | `likyly.recommendations.popular` | Advanced: the most popular items | P |
260
+ | `likyly.recommendations.similar` | Advanced: items similar to one item | P |
261
+ | `likyly.recommendations.collaborative` | Advanced: what similar users liked (needs a trained model) | P |
262
+ | `likyly.recommendations.hybrid` | Advanced: similar items personalised for a user (`alpha`) | P |
263
+ | `likyly.recommendations.session` | Advanced: from viewed items or a user's view history | P |
264
+
265
+ Lists are paginated with `limit` and `offset` (the API's default page is 100 items); the total is reported next to the page. Users have a batch `import` but no batch delete: the API has no such endpoint. `import` on items is an alias of the batch upsert (the CSV import is a dashboard feature).
266
+
267
+ ## Notes for TypeScript
268
+
269
+ - Identifiers are plain strings and are never coerced: `itemId: 42` is a type error, and a JavaScript caller passing a number gets a `ValidationError`.
270
+ - Request and response fields are `camelCase` (`itemId`, `recommendationId`); the keys inside `properties` are yours and are never renamed.
271
+ - Placement recommendations are cached (one hour, per visitor in a browser, `sessionStorage`) and, after a network error, a timeout, a 429 or a 5xx, calls pause for 30 seconds and throw `PausedError` at once, so your page shows its fallback instead of waiting. `likyly.clearRecommendationCache()` forgets the cached answers; the LIKYLY console's "Vider le cache" empties the server side.
272
+
273
+ ## Documentation and support
274
+
275
+ - Documentation: <https://likyly.com/docs/sdk/typescript>
276
+ - REST API reference (OpenAPI): <https://api.likyly.com/docs>
277
+ - Package: @likyly/sdk on npm
278
+
279
+ MIT licensed.
@@ -0,0 +1,70 @@
1
+ import { EventsResource } from "./resources/events.js";
2
+ import { ItemsResource } from "./resources/items.js";
3
+ import { PlacementsResource } from "./resources/placements.js";
4
+ import { type RecommendCacheOptions } from "./recommend-cache.js";
5
+ import { RecommendationsResource } from "./resources/recommendations.js";
6
+ import { UsersResource } from "./resources/users.js";
7
+ import type { PlacementRecommendOptions, PlacementRecommendResponse, RequestOptions } from "./types.js";
8
+ export interface LikylyOptions {
9
+ /**
10
+ * Your API key. Two kinds: the **secret** key (backend only: items, users, everything) and the
11
+ * **public** key (safe in a web page: recommendations and event tracking only). Never put the
12
+ * secret key in code that ships to a browser.
13
+ */
14
+ apiKey: string;
15
+ /** Defaults to `https://api.likyly.com`. */
16
+ baseUrl?: string;
17
+ /**
18
+ * Which of your catalogs to use, if your account has several. Omit it if you have one: LIKYLY
19
+ * uses your only catalog.
20
+ */
21
+ catalog?: string;
22
+ /** Per-request timeout in milliseconds. Default 10000. */
23
+ timeout?: number;
24
+ /** Automatic retries on transient failures (429, 502, 503, 504, dropped connections). Default 2. Set 0 to disable. */
25
+ maxRetries?: number;
26
+ /** Appended to the SDK's User-Agent, e.g. `my-shop/1.4`. */
27
+ userAgent?: string;
28
+ /**
29
+ * Placement recommendations are cached (one hour, per visitor in a browser) and LIKYLY is not
30
+ * called again for 30 seconds after it fails, so pages fall back to their own content at once.
31
+ * Tune it (`{ ttlSeconds, pauseSeconds, storage }`) or turn it off with `false`.
32
+ */
33
+ cache?: RecommendCacheOptions | false;
34
+ /** Custom `fetch` (proxies, tests). Defaults to the global one (Node 18+, browsers, edge runtimes). */
35
+ fetch?: typeof fetch;
36
+ /** @internal test hooks */
37
+ _internal?: {
38
+ sleep?: (ms: number) => Promise<void>;
39
+ random?: () => number;
40
+ };
41
+ }
42
+ /**
43
+ * The LIKYLY client. Five things to know:
44
+ *
45
+ * likyly.items your catalog
46
+ * likyly.users your users (optional)
47
+ * likyly.events what visitors do
48
+ * likyly.recommendations what to show them (advanced, per-call)
49
+ * likyly.placements what to show them (recommended - a named, persistent config)
50
+ */
51
+ export declare class Likyly {
52
+ readonly items: ItemsResource;
53
+ readonly users: UsersResource;
54
+ readonly events: EventsResource;
55
+ readonly recommendations: RecommendationsResource;
56
+ readonly placements: PlacementsResource;
57
+ constructor(options: LikylyOptions);
58
+ /**
59
+ * Shorthand for `likyly.placements.recommend(...)` - recommendations for one placement.
60
+ * Configure the placement once (LIKYLY's MCP admin tools or dashboard), then:
61
+ *
62
+ * const result = await likyly.recommend({
63
+ * placement: "pdp-related",
64
+ * context: { itemId: product.id, userId: user?.id, sessionId },
65
+ * });
66
+ */
67
+ recommend(opts: PlacementRecommendOptions, options?: RequestOptions): Promise<PlacementRecommendResponse>;
68
+ /** Forget the cached placement recommendations (this browser tab or process only). */
69
+ clearRecommendationCache(): void;
70
+ }
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Likyly = void 0;
4
+ const errors_js_1 = require("./errors.js");
5
+ const http_js_1 = require("./http.js");
6
+ const events_js_1 = require("./resources/events.js");
7
+ const items_js_1 = require("./resources/items.js");
8
+ const placements_js_1 = require("./resources/placements.js");
9
+ const recommend_cache_js_1 = require("./recommend-cache.js");
10
+ const recommendations_js_1 = require("./resources/recommendations.js");
11
+ const users_js_1 = require("./resources/users.js");
12
+ const version_js_1 = require("./version.js");
13
+ /** Key prefixes issued from now on (older, unprefixed keys keep working) - "sk_" secret,
14
+ * "pk_" public, "lk_" developer. Lets this SDK refuse a browser-unsafe key at construction
15
+ * time instead of only documenting "don't do this" - see the constructor below. */
16
+ const UNSAFE_FOR_BROWSER_PREFIXES = ["sk_", "lk_"];
17
+ /**
18
+ * The LIKYLY client. Five things to know:
19
+ *
20
+ * likyly.items your catalog
21
+ * likyly.users your users (optional)
22
+ * likyly.events what visitors do
23
+ * likyly.recommendations what to show them (advanced, per-call)
24
+ * likyly.placements what to show them (recommended - a named, persistent config)
25
+ */
26
+ class Likyly {
27
+ items;
28
+ users;
29
+ events;
30
+ recommendations;
31
+ placements;
32
+ constructor(options) {
33
+ if (!options || typeof options.apiKey !== "string" || options.apiKey.trim() === "") {
34
+ throw new errors_js_1.ValidationError("apiKey is required (create one in your LIKYLY account)");
35
+ }
36
+ // A secret or developer key running where a browser could read this process's memory/
37
+ // bundle (window+document present - true in a real browser, false in Node/most edge
38
+ // runtimes) is exactly the mistake this guard exists to catch immediately, rather than
39
+ // silently shipping it. Neither key kind has any reason to run there: recommendations and
40
+ // event tracking (the only things safe in a browser) only ever need the public key.
41
+ const isBrowserLike = typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
42
+ if (isBrowserLike && UNSAFE_FOR_BROWSER_PREFIXES.some((prefix) => options.apiKey.startsWith(prefix))) {
43
+ throw new errors_js_1.ValidationError("This looks like a secret or developer key, and this code is running in a browser. " +
44
+ "Never put a secret/developer key where browser-facing code could read it - use the " +
45
+ "restricted public key here instead (account page's \"Clé publique\"), and keep the " +
46
+ "secret/developer key server-side only.");
47
+ }
48
+ const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
49
+ if (!fetchImpl)
50
+ throw new errors_js_1.ValidationError("no fetch implementation found: use Node 18+ or pass options.fetch");
51
+ const http = new http_js_1.HttpClient({
52
+ apiKey: options.apiKey,
53
+ baseUrl: (options.baseUrl ?? version_js_1.DEFAULT_BASE_URL).replace(/\/+$/, ""),
54
+ catalog: options.catalog,
55
+ timeout: options.timeout ?? 10_000,
56
+ maxRetries: options.maxRetries ?? 2,
57
+ userAgent: options.userAgent ? `${version_js_1.USER_AGENT} ${options.userAgent}` : version_js_1.USER_AGENT,
58
+ fetch: fetchImpl,
59
+ sleep: options._internal?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
60
+ random: options._internal?.random ?? Math.random,
61
+ });
62
+ this.items = new items_js_1.ItemsResource(http);
63
+ this.users = new users_js_1.UsersResource(http);
64
+ this.events = new events_js_1.EventsResource(http);
65
+ this.recommendations = new recommendations_js_1.RecommendationsResource(http);
66
+ this.placements = new placements_js_1.PlacementsResource(http, options.cache === false ? null : new recommend_cache_js_1.RecommendCache(options.cache ?? {}));
67
+ }
68
+ /**
69
+ * Shorthand for `likyly.placements.recommend(...)` - recommendations for one placement.
70
+ * Configure the placement once (LIKYLY's MCP admin tools or dashboard), then:
71
+ *
72
+ * const result = await likyly.recommend({
73
+ * placement: "pdp-related",
74
+ * context: { itemId: product.id, userId: user?.id, sessionId },
75
+ * });
76
+ */
77
+ recommend(opts, options) {
78
+ return this.placements.recommend(opts, options);
79
+ }
80
+ /** Forget the cached placement recommendations (this browser tab or process only). */
81
+ clearRecommendationCache() {
82
+ this.placements.cache?.clear();
83
+ }
84
+ }
85
+ exports.Likyly = Likyly;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Percent-encodes a path segment: everything outside RFC 3986 "unreserved" (A-Z a-z 0-9 - . _ ~)
3
+ * becomes %XX (UTF-8), '/' included - so an id like "gid://shopify/Product/1" is one segment.
4
+ */
5
+ export declare function encodeSegment(value: string): string;
6
+ /** Ids are opaque, non-empty strings - never coerced to numbers. */
7
+ export declare function requireId(value: unknown, name: string): string;
8
+ /** Advanced recommendation endpoints carry ids in the URL path, where '/' cannot be used. */
9
+ export declare function requirePathSafeId(value: unknown, name: string): string;
10
+ /** Removes undefined values so optional fields are simply not sent. */
11
+ export declare function compact<T extends Record<string, unknown>>(obj: T): Record<string, unknown>;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encodeSegment = encodeSegment;
4
+ exports.requireId = requireId;
5
+ exports.requirePathSafeId = requirePathSafeId;
6
+ exports.compact = compact;
7
+ const errors_js_1 = require("./errors.js");
8
+ /**
9
+ * Percent-encodes a path segment: everything outside RFC 3986 "unreserved" (A-Z a-z 0-9 - . _ ~)
10
+ * becomes %XX (UTF-8), '/' included - so an id like "gid://shopify/Product/1" is one segment.
11
+ */
12
+ function encodeSegment(value) {
13
+ return encodeURIComponent(value).replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
14
+ }
15
+ /** Ids are opaque, non-empty strings - never coerced to numbers. */
16
+ function requireId(value, name) {
17
+ if (typeof value !== "string" || value.trim() === "") {
18
+ throw new errors_js_1.ValidationError(`${name} must be a non-empty string (your own identifier, e.g. "SKU-123")`);
19
+ }
20
+ return value;
21
+ }
22
+ /** Advanced recommendation endpoints carry ids in the URL path, where '/' cannot be used. */
23
+ function requirePathSafeId(value, name) {
24
+ const id = requireId(value, name);
25
+ if (id.includes("/")) {
26
+ throw new errors_js_1.ValidationError(`${name} "${id}" contains "/", which this endpoint cannot carry in its URL path - use recommendations.get(), which takes ids in the request body`);
27
+ }
28
+ return id;
29
+ }
30
+ /** Removes undefined values so optional fields are simply not sent. */
31
+ function compact(obj) {
32
+ const out = {};
33
+ for (const [key, value] of Object.entries(obj))
34
+ if (value !== undefined)
35
+ out[key] = value;
36
+ return out;
37
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Every error the SDK throws extends LikylyError, so `catch (e) { if (e instanceof LikylyError) ... }`
3
+ * is enough to separate LIKYLY failures from your own bugs.
4
+ *
5
+ * LikylyError
6
+ * ├─ NetworkError no HTTP response (DNS, connection reset, ...)
7
+ * ├─ TimeoutError the request timed out
8
+ * ├─ ValidationError invalid request - caught by the SDK before sending, or 422 from the API
9
+ * └─ ApiError the API answered with an error status
10
+ * ├─ AuthenticationError 401 - missing / invalid / revoked API key
11
+ * ├─ PermissionDeniedError 403 - e.g. a public key used for a secret-key operation, or a plan limit
12
+ * ├─ NotFoundError 404
13
+ * └─ RateLimitError 429 - see `retryAfter`
14
+ */
15
+ export interface ErrorDetails {
16
+ statusCode?: number;
17
+ requestId?: string;
18
+ retryAfter?: number;
19
+ body?: unknown;
20
+ cause?: unknown;
21
+ }
22
+ export declare class LikylyError extends Error {
23
+ /** HTTP status code, when there was an HTTP response. */
24
+ readonly statusCode?: number;
25
+ /** The `request_id` of the failed call (also the `X-Request-ID` response header) - quote it when reporting a problem. */
26
+ readonly requestId?: string;
27
+ /** Seconds to wait before retrying, from the `Retry-After` header (429/503). */
28
+ readonly retryAfter?: number;
29
+ /** The parsed response body, when there was one. */
30
+ readonly body?: unknown;
31
+ constructor(message: string, details?: ErrorDetails);
32
+ }
33
+ export declare class NetworkError extends LikylyError {
34
+ }
35
+ export declare class TimeoutError extends LikylyError {
36
+ }
37
+ export declare class ValidationError extends LikylyError {
38
+ }
39
+ /** LIKYLY failed moments ago: the SDK skips calls for a short pause (see `cache.pauseSeconds`). */
40
+ export declare class PausedError extends LikylyError {
41
+ }
42
+ export declare class ApiError extends LikylyError {
43
+ }
44
+ export declare class AuthenticationError extends ApiError {
45
+ }
46
+ export declare class PermissionDeniedError extends ApiError {
47
+ }
48
+ export declare class NotFoundError extends ApiError {
49
+ }
50
+ export declare class RateLimitError extends ApiError {
51
+ }
52
+ export declare function errorFromResponse(status: number, body: unknown, requestId: string | undefined, retryAfter: number | undefined): ApiError | ValidationError;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RateLimitError = exports.NotFoundError = exports.PermissionDeniedError = exports.AuthenticationError = exports.ApiError = exports.PausedError = exports.ValidationError = exports.TimeoutError = exports.NetworkError = exports.LikylyError = void 0;
4
+ exports.errorFromResponse = errorFromResponse;
5
+ class LikylyError extends Error {
6
+ /** HTTP status code, when there was an HTTP response. */
7
+ statusCode;
8
+ /** The `request_id` of the failed call (also the `X-Request-ID` response header) - quote it when reporting a problem. */
9
+ requestId;
10
+ /** Seconds to wait before retrying, from the `Retry-After` header (429/503). */
11
+ retryAfter;
12
+ /** The parsed response body, when there was one. */
13
+ body;
14
+ constructor(message, details = {}) {
15
+ super(message, details.cause !== undefined ? { cause: details.cause } : undefined);
16
+ this.name = new.target.name;
17
+ this.statusCode = details.statusCode;
18
+ this.requestId = details.requestId;
19
+ this.retryAfter = details.retryAfter;
20
+ this.body = details.body;
21
+ }
22
+ }
23
+ exports.LikylyError = LikylyError;
24
+ class NetworkError extends LikylyError {
25
+ }
26
+ exports.NetworkError = NetworkError;
27
+ class TimeoutError extends LikylyError {
28
+ }
29
+ exports.TimeoutError = TimeoutError;
30
+ class ValidationError extends LikylyError {
31
+ }
32
+ exports.ValidationError = ValidationError;
33
+ /** LIKYLY failed moments ago: the SDK skips calls for a short pause (see `cache.pauseSeconds`). */
34
+ class PausedError extends LikylyError {
35
+ }
36
+ exports.PausedError = PausedError;
37
+ class ApiError extends LikylyError {
38
+ }
39
+ exports.ApiError = ApiError;
40
+ class AuthenticationError extends ApiError {
41
+ }
42
+ exports.AuthenticationError = AuthenticationError;
43
+ class PermissionDeniedError extends ApiError {
44
+ }
45
+ exports.PermissionDeniedError = PermissionDeniedError;
46
+ class NotFoundError extends ApiError {
47
+ }
48
+ exports.NotFoundError = NotFoundError;
49
+ class RateLimitError extends ApiError {
50
+ }
51
+ exports.RateLimitError = RateLimitError;
52
+ function errorFromResponse(status, body, requestId, retryAfter) {
53
+ const message = extractMessage(body) ?? `LIKYLY API error (HTTP ${status})`;
54
+ const details = { statusCode: status, requestId, retryAfter, body };
55
+ switch (status) {
56
+ case 401:
57
+ return new AuthenticationError(message, details);
58
+ case 403:
59
+ return new PermissionDeniedError(message, details);
60
+ case 404:
61
+ return new NotFoundError(message, details);
62
+ case 422:
63
+ return new ValidationError(message, details);
64
+ case 429:
65
+ return new RateLimitError(message, details);
66
+ default:
67
+ return new ApiError(message, details);
68
+ }
69
+ }
70
+ function extractMessage(body) {
71
+ if (body && typeof body === "object" && "detail" in body) {
72
+ const detail = body.detail;
73
+ if (typeof detail === "string")
74
+ return detail;
75
+ if (Array.isArray(detail)) {
76
+ return detail
77
+ .map((d) => (d && typeof d === "object" && "msg" in d ? String(d.msg) : JSON.stringify(d)))
78
+ .join("; ");
79
+ }
80
+ return JSON.stringify(detail);
81
+ }
82
+ return undefined;
83
+ }