@likyly/sdk 0.0.0-stage → 1.1.1

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 +286 -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 +60 -4
@@ -0,0 +1,226 @@
1
+ /** Free-form JSON object: price, currency, orderId, category, any field of your own. Keys are stored exactly as you send them. */
2
+ export type Properties = Record<string, unknown>;
3
+ export interface Item {
4
+ /** Your own identifier - any string (`SKU-123`, a UUID, `gid://shopify/Product/123`). */
5
+ itemId: string;
6
+ title: string;
7
+ description?: string;
8
+ properties: Properties;
9
+ }
10
+ /** What you send to create or replace an item. Only `title` is required. */
11
+ export interface ItemInput {
12
+ title: string;
13
+ description?: string;
14
+ /** `category` and `description` feed content similarity; everything else is stored as is. */
15
+ properties?: Properties;
16
+ }
17
+ export interface ItemImport extends ItemInput {
18
+ itemId: string;
19
+ }
20
+ export interface ListOptions {
21
+ /** Page size (1-1000, default 100). */
22
+ limit?: number;
23
+ /** Rows to skip. */
24
+ offset?: number;
25
+ }
26
+ export interface ItemList {
27
+ items: Item[];
28
+ /** Total number of items in the catalog (`X-Total-Count`), when the API reported it. */
29
+ total?: number;
30
+ /** The page size you asked for (undefined = the API's default). */
31
+ limit?: number;
32
+ offset: number;
33
+ }
34
+ export interface User {
35
+ userId: string;
36
+ properties: Properties;
37
+ }
38
+ export interface UserInput {
39
+ /** country, segment, language, ... - whatever describes your users. */
40
+ properties?: Properties;
41
+ }
42
+ export interface UserImport extends UserInput {
43
+ userId: string;
44
+ }
45
+ export interface UserList {
46
+ users: User[];
47
+ total?: number;
48
+ limit?: number;
49
+ offset: number;
50
+ }
51
+ /** Result of a batch call. A failing entry is reported in `errors`, it does not throw. */
52
+ export interface BatchResult {
53
+ received: number;
54
+ succeeded: number;
55
+ failed: number;
56
+ errors: {
57
+ index: number;
58
+ id?: string;
59
+ message: string;
60
+ }[];
61
+ }
62
+ /**
63
+ * An interaction. `itemId` and at least one of `userId` / `sessionId` are required; sending both
64
+ * ties an anonymous session to the user.
65
+ */
66
+ export interface EventInput {
67
+ itemId: string;
68
+ userId?: string;
69
+ /** Anonymous visitor / browsing session - no login needed. */
70
+ sessionId?: string;
71
+ /** The `recommendationId` of the recommendation that surfaced this item - enables attribution. */
72
+ recommendationId?: string;
73
+ /** Free-form label of where this happened: `homepage`, `product_page`, `cart`, ... */
74
+ placement?: string;
75
+ quantity?: number;
76
+ /** ISO 8601 string or Date. Defaults to the server's time. */
77
+ occurredAt?: string | Date;
78
+ properties?: Properties;
79
+ /**
80
+ * Idempotency key, unique per account: replaying an event with the same `eventId` records nothing
81
+ * and returns `duplicate: true`. Set it on purchases - it makes retries safe.
82
+ */
83
+ eventId?: string;
84
+ }
85
+ /** An event of any type, for `events.trackMany`. `type` is an open string (`view`, `favorite`, ...). */
86
+ export interface TypedEventInput extends EventInput {
87
+ type: string;
88
+ }
89
+ export interface EventResult {
90
+ message: string;
91
+ eventId?: string;
92
+ /** True if this `eventId` was already recorded - nothing was written. */
93
+ duplicate: boolean;
94
+ }
95
+ export interface EventBatchResult {
96
+ received: number;
97
+ accepted: number;
98
+ /** Events skipped because their `eventId` was already recorded. */
99
+ duplicates: number;
100
+ }
101
+ /** Call right after login/signup, once you know `userId`, for the `sessionId` the visitor was
102
+ * anonymous under - see `events.identify`. */
103
+ export interface IdentifyInput {
104
+ userId: string;
105
+ sessionId: string;
106
+ }
107
+ export interface IdentifyResult {
108
+ /** How many of this session's past interactions now belong to `userId`. */
109
+ linkedInteractions: number;
110
+ }
111
+ /** Send whatever you know: LIKYLY chooses the best strategy. All fields are optional. */
112
+ export interface RecommendationRequest {
113
+ userId?: string;
114
+ sessionId?: string;
115
+ /** The item being looked at (e.g. the product page). */
116
+ itemId?: string;
117
+ /** Recently viewed items, oldest first. */
118
+ viewedItemIds?: string[];
119
+ /** Free-form label of where the recommendations will be shown. */
120
+ placement?: string;
121
+ /** How many items to return (1-100, default 10). */
122
+ limit?: number;
123
+ /** Diagnostic detail in `explanation`. Secret key only. */
124
+ debug?: boolean;
125
+ }
126
+ export interface SimilarUser {
127
+ userId: string;
128
+ sharedItemIds: string[];
129
+ }
130
+ export interface Explanation {
131
+ reason: string;
132
+ contentSimilarity?: number;
133
+ semanticSimilarity?: number;
134
+ popularityScore?: number;
135
+ interactionCount?: number;
136
+ interactionLabel?: string;
137
+ collaborativeScore?: number;
138
+ sourceItemIds?: string[];
139
+ /** Only with `debug` and a secret key. */
140
+ similarUsers?: SimilarUser[];
141
+ }
142
+ export interface RecommendedItem {
143
+ itemId: string;
144
+ score?: number;
145
+ title?: string;
146
+ description?: string;
147
+ properties: Properties;
148
+ explanation?: Explanation;
149
+ }
150
+ export interface RecommendationResponse {
151
+ /** Send it back on the impression / click / add_to_cart / purchase events for these items. */
152
+ recommendationId: string;
153
+ /** What LIKYLY used: `hybrid`, `content`, `collaborative`, `session` or `popular`. */
154
+ strategy: string;
155
+ placement?: string;
156
+ items: RecommendedItem[];
157
+ }
158
+ interface AdvancedBase {
159
+ placement?: string;
160
+ /** Attach the recommendation to an anonymous session, for attribution. */
161
+ sessionId?: string;
162
+ /** How many items to return (default 10). */
163
+ limit?: number;
164
+ }
165
+ export interface PopularOptions extends AdvancedBase {
166
+ }
167
+ export interface SimilarOptions extends AdvancedBase {
168
+ itemId: string;
169
+ }
170
+ export interface CollaborativeOptions extends AdvancedBase {
171
+ userId: string;
172
+ }
173
+ export interface HybridOptions extends AdvancedBase {
174
+ userId: string;
175
+ itemId: string;
176
+ /** Weight of the collaborative signal against content similarity: 0 = pure content, 1 = pure collaborative (default 0.5). */
177
+ alpha?: number;
178
+ }
179
+ /** Give `viewedItemIds` (an explicit list, oldest first) OR `userId` (LIKYLY's own history of that user's views). */
180
+ export interface SessionOptions extends AdvancedBase {
181
+ viewedItemIds?: string[];
182
+ userId?: string;
183
+ }
184
+ /**
185
+ * What you know about the moment - every field optional unless the placement's own
186
+ * `signals.required` names it (see `likyly.placements` and GET .../requirements).
187
+ */
188
+ export interface PlacementContext {
189
+ /** The product/article/listing currently being viewed. */
190
+ itemId?: string;
191
+ categoryId?: string;
192
+ collectionId?: string;
193
+ /** Identified visitor. */
194
+ userId?: string;
195
+ /** Stable anonymous visitor id - used like `sessionId` when `sessionId` is omitted. See `LikylySession`. */
196
+ anonymousId?: string;
197
+ /** Anonymous browsing session. */
198
+ sessionId?: string;
199
+ cartItemIds?: string[];
200
+ locale?: string;
201
+ customContext?: Record<string, unknown>;
202
+ }
203
+ export interface PlacementRecommendOptions {
204
+ /** The placement's slug, e.g. "pdp-related". */
205
+ placement: string;
206
+ context?: PlacementContext;
207
+ /** Overrides the placement's own configured limit for this call. */
208
+ limit?: number;
209
+ }
210
+ export interface PlacementRecommendResponse {
211
+ /** Send it back on the impression / click / add_to_cart / purchase events for these items. */
212
+ recommendationId: string;
213
+ placement: string;
214
+ /** What the placement actually used: `hybrid`, `content`, `collaborative`, `session` or `popular`. */
215
+ strategyUsed: string;
216
+ items: RecommendedItem[];
217
+ }
218
+ export interface RequestOptions {
219
+ /** Cancel the call. */
220
+ signal?: AbortSignal;
221
+ /** Overrides the client's timeout (milliseconds) for this call. */
222
+ timeout?: number;
223
+ /** Overrides the client's retry count for this call. */
224
+ maxRetries?: number;
225
+ }
226
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ export declare const SDK_VERSION = "1.1.1";
2
+ export declare const DEFAULT_BASE_URL = "https://api.likyly.com";
3
+ export declare const USER_AGENT = "likyly-typescript/1.1.1";
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.USER_AGENT = exports.DEFAULT_BASE_URL = exports.SDK_VERSION = void 0;
4
+ exports.SDK_VERSION = "1.1.1";
5
+ exports.DEFAULT_BASE_URL = "https://api.likyly.com";
6
+ exports.USER_AGENT = `likyly-typescript/${exports.SDK_VERSION}`;
@@ -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,81 @@
1
+ import { ValidationError } from "./errors.js";
2
+ import { HttpClient } from "./http.js";
3
+ import { EventsResource } from "./resources/events.js";
4
+ import { ItemsResource } from "./resources/items.js";
5
+ import { PlacementsResource } from "./resources/placements.js";
6
+ import { RecommendCache } from "./recommend-cache.js";
7
+ import { RecommendationsResource } from "./resources/recommendations.js";
8
+ import { UsersResource } from "./resources/users.js";
9
+ import { DEFAULT_BASE_URL, USER_AGENT } from "./version.js";
10
+ /** Key prefixes issued from now on (older, unprefixed keys keep working) - "sk_" secret,
11
+ * "pk_" public, "lk_" developer. Lets this SDK refuse a browser-unsafe key at construction
12
+ * time instead of only documenting "don't do this" - see the constructor below. */
13
+ const UNSAFE_FOR_BROWSER_PREFIXES = ["sk_", "lk_"];
14
+ /**
15
+ * The LIKYLY client. Five things to know:
16
+ *
17
+ * likyly.items your catalog
18
+ * likyly.users your users (optional)
19
+ * likyly.events what visitors do
20
+ * likyly.recommendations what to show them (advanced, per-call)
21
+ * likyly.placements what to show them (recommended - a named, persistent config)
22
+ */
23
+ export class Likyly {
24
+ items;
25
+ users;
26
+ events;
27
+ recommendations;
28
+ placements;
29
+ constructor(options) {
30
+ if (!options || typeof options.apiKey !== "string" || options.apiKey.trim() === "") {
31
+ throw new ValidationError("apiKey is required (create one in your LIKYLY account)");
32
+ }
33
+ // A secret or developer key running where a browser could read this process's memory/
34
+ // bundle (window+document present - true in a real browser, false in Node/most edge
35
+ // runtimes) is exactly the mistake this guard exists to catch immediately, rather than
36
+ // silently shipping it. Neither key kind has any reason to run there: recommendations and
37
+ // event tracking (the only things safe in a browser) only ever need the public key.
38
+ const isBrowserLike = typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
39
+ if (isBrowserLike && UNSAFE_FOR_BROWSER_PREFIXES.some((prefix) => options.apiKey.startsWith(prefix))) {
40
+ throw new ValidationError("This looks like a secret or developer key, and this code is running in a browser. " +
41
+ "Never put a secret/developer key where browser-facing code could read it - use the " +
42
+ "restricted public key here instead (account page's \"Clé publique\"), and keep the " +
43
+ "secret/developer key server-side only.");
44
+ }
45
+ const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
46
+ if (!fetchImpl)
47
+ throw new ValidationError("no fetch implementation found: use Node 18+ or pass options.fetch");
48
+ const http = new HttpClient({
49
+ apiKey: options.apiKey,
50
+ baseUrl: (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
51
+ catalog: options.catalog,
52
+ timeout: options.timeout ?? 10_000,
53
+ maxRetries: options.maxRetries ?? 2,
54
+ userAgent: options.userAgent ? `${USER_AGENT} ${options.userAgent}` : USER_AGENT,
55
+ fetch: fetchImpl,
56
+ sleep: options._internal?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
57
+ random: options._internal?.random ?? Math.random,
58
+ });
59
+ this.items = new ItemsResource(http);
60
+ this.users = new UsersResource(http);
61
+ this.events = new EventsResource(http);
62
+ this.recommendations = new RecommendationsResource(http);
63
+ this.placements = new PlacementsResource(http, options.cache === false ? null : new RecommendCache(options.cache ?? {}));
64
+ }
65
+ /**
66
+ * Shorthand for `likyly.placements.recommend(...)` - recommendations for one placement.
67
+ * Configure the placement once (LIKYLY's MCP admin tools or dashboard), then:
68
+ *
69
+ * const result = await likyly.recommend({
70
+ * placement: "pdp-related",
71
+ * context: { itemId: product.id, userId: user?.id, sessionId },
72
+ * });
73
+ */
74
+ recommend(opts, options) {
75
+ return this.placements.recommend(opts, options);
76
+ }
77
+ /** Forget the cached placement recommendations (this browser tab or process only). */
78
+ clearRecommendationCache() {
79
+ this.placements.cache?.clear();
80
+ }
81
+ }
@@ -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,31 @@
1
+ import { ValidationError } from "./errors.js";
2
+ /**
3
+ * Percent-encodes a path segment: everything outside RFC 3986 "unreserved" (A-Z a-z 0-9 - . _ ~)
4
+ * becomes %XX (UTF-8), '/' included - so an id like "gid://shopify/Product/1" is one segment.
5
+ */
6
+ export function encodeSegment(value) {
7
+ return encodeURIComponent(value).replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
8
+ }
9
+ /** Ids are opaque, non-empty strings - never coerced to numbers. */
10
+ export function requireId(value, name) {
11
+ if (typeof value !== "string" || value.trim() === "") {
12
+ throw new ValidationError(`${name} must be a non-empty string (your own identifier, e.g. "SKU-123")`);
13
+ }
14
+ return value;
15
+ }
16
+ /** Advanced recommendation endpoints carry ids in the URL path, where '/' cannot be used. */
17
+ export function requirePathSafeId(value, name) {
18
+ const id = requireId(value, name);
19
+ if (id.includes("/")) {
20
+ throw new ValidationError(`${name} "${id}" contains "/", which this endpoint cannot carry in its URL path - use recommendations.get(), which takes ids in the request body`);
21
+ }
22
+ return id;
23
+ }
24
+ /** Removes undefined values so optional fields are simply not sent. */
25
+ export function compact(obj) {
26
+ const out = {};
27
+ for (const [key, value] of Object.entries(obj))
28
+ if (value !== undefined)
29
+ out[key] = value;
30
+ return out;
31
+ }
@@ -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,69 @@
1
+ export class LikylyError extends Error {
2
+ /** HTTP status code, when there was an HTTP response. */
3
+ statusCode;
4
+ /** The `request_id` of the failed call (also the `X-Request-ID` response header) - quote it when reporting a problem. */
5
+ requestId;
6
+ /** Seconds to wait before retrying, from the `Retry-After` header (429/503). */
7
+ retryAfter;
8
+ /** The parsed response body, when there was one. */
9
+ body;
10
+ constructor(message, details = {}) {
11
+ super(message, details.cause !== undefined ? { cause: details.cause } : undefined);
12
+ this.name = new.target.name;
13
+ this.statusCode = details.statusCode;
14
+ this.requestId = details.requestId;
15
+ this.retryAfter = details.retryAfter;
16
+ this.body = details.body;
17
+ }
18
+ }
19
+ export class NetworkError extends LikylyError {
20
+ }
21
+ export class TimeoutError extends LikylyError {
22
+ }
23
+ export class ValidationError extends LikylyError {
24
+ }
25
+ /** LIKYLY failed moments ago: the SDK skips calls for a short pause (see `cache.pauseSeconds`). */
26
+ export class PausedError extends LikylyError {
27
+ }
28
+ export class ApiError extends LikylyError {
29
+ }
30
+ export class AuthenticationError extends ApiError {
31
+ }
32
+ export class PermissionDeniedError extends ApiError {
33
+ }
34
+ export class NotFoundError extends ApiError {
35
+ }
36
+ export class RateLimitError extends ApiError {
37
+ }
38
+ export function errorFromResponse(status, body, requestId, retryAfter) {
39
+ const message = extractMessage(body) ?? `LIKYLY API error (HTTP ${status})`;
40
+ const details = { statusCode: status, requestId, retryAfter, body };
41
+ switch (status) {
42
+ case 401:
43
+ return new AuthenticationError(message, details);
44
+ case 403:
45
+ return new PermissionDeniedError(message, details);
46
+ case 404:
47
+ return new NotFoundError(message, details);
48
+ case 422:
49
+ return new ValidationError(message, details);
50
+ case 429:
51
+ return new RateLimitError(message, details);
52
+ default:
53
+ return new ApiError(message, details);
54
+ }
55
+ }
56
+ function extractMessage(body) {
57
+ if (body && typeof body === "object" && "detail" in body) {
58
+ const detail = body.detail;
59
+ if (typeof detail === "string")
60
+ return detail;
61
+ if (Array.isArray(detail)) {
62
+ return detail
63
+ .map((d) => (d && typeof d === "object" && "msg" in d ? String(d.msg) : JSON.stringify(d)))
64
+ .join("; ");
65
+ }
66
+ return JSON.stringify(detail);
67
+ }
68
+ return undefined;
69
+ }
@@ -0,0 +1,49 @@
1
+ import type { RequestOptions } from "./types.js";
2
+ export interface HttpConfig {
3
+ apiKey: string;
4
+ baseUrl: string;
5
+ catalog?: string;
6
+ timeout: number;
7
+ maxRetries: number;
8
+ userAgent: string;
9
+ fetch: typeof fetch;
10
+ /** Test hooks - not part of the public API. */
11
+ sleep: (ms: number) => Promise<void>;
12
+ random: () => number;
13
+ }
14
+ export interface HttpRequest {
15
+ method: "GET" | "POST" | "PUT" | "DELETE";
16
+ /** Path already percent-encoded, starting with "/". */
17
+ path: string;
18
+ query?: Record<string, string | number | boolean | undefined>;
19
+ body?: unknown;
20
+ /**
21
+ * Safe to send again if the outcome is unknown (a timeout, a 5xx, a dropped connection)? True for
22
+ * reads, PUT/DELETE and the upserts; false for events without an `eventId`, where a blind retry
23
+ * could record the event twice.
24
+ */
25
+ idempotent: boolean;
26
+ options?: RequestOptions;
27
+ }
28
+ export interface HttpResponse<T> {
29
+ data: T;
30
+ status: number;
31
+ headers: Headers;
32
+ requestId?: string;
33
+ }
34
+ /**
35
+ * The only place that talks HTTP. Every resource goes through `request()`: auth header, catalog
36
+ * parameter, timeout, retries with exponential backoff + jitter, and the error mapping.
37
+ */
38
+ export declare class HttpClient {
39
+ private readonly config;
40
+ constructor(config: HttpConfig);
41
+ request<T>(req: HttpRequest): Promise<HttpResponse<T>>;
42
+ /** Milliseconds to wait before the next attempt, or undefined if this failure must not be retried. */
43
+ private retryDelay;
44
+ private buildUrl;
45
+ private attempt;
46
+ }
47
+ /** Retry-After is either a number of seconds or an HTTP date. */
48
+ export declare function parseRetryAfter(value: string | null): number | undefined;
49
+ export declare function assertNonEmptyObject(value: unknown, name: string): asserts value is Record<string, unknown>;