keepa-api 0.3.2 → 0.4.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/README.md +95 -1
- package/dist/index.cjs +119 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +169 -46
- package/dist/index.d.ts +169 -46
- package/dist/index.js +113 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# keepa-api
|
|
2
2
|
|
|
3
|
-
Lightweight TypeScript SDK for the [Keepa](https://keepa.com) REST API. A single `KeepaClient` class exposes typed resources (
|
|
3
|
+
Lightweight TypeScript SDK for the [Keepa](https://keepa.com) REST API. A single `KeepaClient` class exposes typed resources (`products`, `categories`, `bestSellers`) with parsed responses, friendly currency units, and live rate-limit visibility.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -52,6 +52,7 @@ new KeepaClient(options?)
|
|
|
52
52
|
| `marketplace` | `Marketplace` | `'US'` | Case-insensitive. See [marketplaces](#marketplaces). |
|
|
53
53
|
| `days` | `number` | `1` | Days of csv history when `history: true`. |
|
|
54
54
|
| `history` | `boolean` | `false` | Populates `history.price.*` + scalar `amazonPrice` / `newPrice` / `listPrice`. Costs extra tokens. |
|
|
55
|
+
| `stats` | `boolean` | `false` | Populates the `stats` namespace (buy-box saving basis, etc.). Costs extra tokens. |
|
|
55
56
|
|
|
56
57
|
### `keepa.products.retrieve(params)` → `Promise<KeepaProduct>`
|
|
57
58
|
|
|
@@ -71,6 +72,9 @@ The SDK reshapes Keepa's wire format into something usable:
|
|
|
71
72
|
| `history.price.amazon` | `PriceHistoryEntry[]` | Amazon's own price over time. Empty without `history: true`. |
|
|
72
73
|
| `history.price.new` | `PriceHistoryEntry[]` | Lowest-3rd-party-new price over time. |
|
|
73
74
|
| `history.price.list` | `PriceHistoryEntry[]` | List price (MSRP) over time. |
|
|
75
|
+
| `stats.buyBoxSavingBasis` | `number \| null` | Buy box strikethrough reference price in the marketplace's major unit. Null without `stats: true` or when Keepa has no saving-basis data. |
|
|
76
|
+
| `stats.buyBoxSavingBasisType` | `SavingBasisType \| null` | Reference type for the strikethrough — `SavingBasisType.LIST_PRICE` or `SavingBasisType.WAS_PRICE`. Null when unavailable. |
|
|
77
|
+
| `monthlySold` | `number \| null` | Keepa's estimate from Amazon's "X+ bought in past month" widget. Null when Amazon doesn't show the widget for this ASIN — common for lower-velocity / non-US listings. A genuine zero surfaces as `0`. |
|
|
74
78
|
|
|
75
79
|
`title`, `description`, `parentAsin`, `categoryTree`, `salesRanks`, `variations`, `features` pass through from Keepa unchanged.
|
|
76
80
|
|
|
@@ -109,6 +113,96 @@ const usedPrices = parsePriceHistory(rawProduct.csv?.[CsvType.USED]);
|
|
|
109
113
|
|
|
110
114
|
`CsvType` covers all 36 of Keepa's csv series names verbatim.
|
|
111
115
|
|
|
116
|
+
## Categories
|
|
117
|
+
|
|
118
|
+
### `keepa.categories.list(params)` → `Promise<Record<number, KeepaCategory>>`
|
|
119
|
+
|
|
120
|
+
Resolve category metadata by browse-node id. Returns a map keyed by `catId` for lookup; ids Keepa doesn't recognise are silently absent from the result.
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const cats = await keepa.categories.list({
|
|
124
|
+
ids: [7141123011, 1040660],
|
|
125
|
+
marketplace: 'GB',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
cats[7141123011]?.contextFreeName; // "Women's Coats, Jackets & Gilets"
|
|
129
|
+
cats[7141123011]?.parent; // 1040660
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
| Param | Type | Default | Notes |
|
|
133
|
+
|-------|------|---------|-------|
|
|
134
|
+
| `ids` | `number[]` | (required) | Browse-node ids. Empty array → no HTTP call, returns `{}`. |
|
|
135
|
+
| `marketplace` | `Marketplace` | `'US'` | |
|
|
136
|
+
| `withParents` | `boolean` | `false` | Asks Keepa for ancestor records. Empirically the shape doesn't change for most categories — walk `categoryTree` on the product if you need a breadcrumb. |
|
|
137
|
+
|
|
138
|
+
### `KeepaCategory`
|
|
139
|
+
|
|
140
|
+
| Field | Type | Notes |
|
|
141
|
+
|-------|------|-------|
|
|
142
|
+
| `catId` | `number` | Amazon browse-node id. |
|
|
143
|
+
| `name` | `string` | Leaf-only name (collides across departments). |
|
|
144
|
+
| `contextFreeName` | `string \| undefined` | Pre-disambiguated label (e.g. "Women's Coats, Jackets & Gilets"). Missing on very old / experimental nodes — fall back to `name`. |
|
|
145
|
+
| `parent` | `number` | Parent catId; `0` marks a root. |
|
|
146
|
+
| `children` | `number[] \| null` | Direct children, or `null` for leaves. |
|
|
147
|
+
| `productCount` | `number` | Active listings in the category. |
|
|
148
|
+
|
|
149
|
+
### `keepa.categories.search(params)` → `Promise<CategorySearchHit[]>`
|
|
150
|
+
|
|
151
|
+
Free-text search against Keepa's category index. Hits Keepa's `/search?type=category` endpoint — that endpoint is category-specific despite the generic-sounding path, so it lives here as a Categories method. Empty array on "no matches".
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const hits = await keepa.categories.search({
|
|
155
|
+
term: 'yoga mat',
|
|
156
|
+
marketplace: 'GB',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
hits[0]?.name; // "Yoga Mats"
|
|
160
|
+
hits[0]?.lowestRank; // Lowest BSR of any product currently in this category
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
| Param | Type | Default | Notes |
|
|
164
|
+
|-------|------|---------|-------|
|
|
165
|
+
| `term` | `string` | (required) | URL-encoded for you. |
|
|
166
|
+
| `marketplace` | `Marketplace` | `'US'` | |
|
|
167
|
+
|
|
168
|
+
### `CategorySearchHit`
|
|
169
|
+
|
|
170
|
+
| Field | Type | Notes |
|
|
171
|
+
|-------|------|-------|
|
|
172
|
+
| `catId` | `number` | Amazon browse-node id. |
|
|
173
|
+
| `name` | `string` | Leaf-only display name. |
|
|
174
|
+
| `lowestRank` | `number` | Lowest BSR (= strongest top performer) in the category. |
|
|
175
|
+
| `highestRank` | `number` | Highest BSR (= weakest top performer) in the category. |
|
|
176
|
+
| `productCount` | `number` | Active listings. |
|
|
177
|
+
|
|
178
|
+
## Best sellers
|
|
179
|
+
|
|
180
|
+
### `keepa.bestSellers.retrieve(params)` → `Promise<KeepaBestSellerList \| null>`
|
|
181
|
+
|
|
182
|
+
Returns `null` (not an error) when Keepa has no list for the category — typical for non-leaf nodes and sparse leaves.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const list = await keepa.bestSellers.retrieve({
|
|
186
|
+
categoryId: 7141123011,
|
|
187
|
+
marketplace: 'GB',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
list?.asinList[0]; // top bestseller ASIN
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
| Param | Type | Default | Notes |
|
|
194
|
+
|-------|------|---------|-------|
|
|
195
|
+
| `categoryId` | `number` | (required) | Browse-node id. |
|
|
196
|
+
| `marketplace` | `Marketplace` | `'US'` | |
|
|
197
|
+
| `sublist` | `boolean` | `true` | When true, Keepa returns the sub-category top list (shorter). |
|
|
198
|
+
|
|
199
|
+
### `KeepaBestSellerList`
|
|
200
|
+
|
|
201
|
+
| Field | Type | Notes |
|
|
202
|
+
|-------|------|-------|
|
|
203
|
+
| `categoryId` | `number` | Echoed from Keepa, or the caller's id when Keepa omits it. |
|
|
204
|
+
| `asinList` | `string[]` | Top-to-bottom ordered. May be empty when the response carried no list. |
|
|
205
|
+
|
|
112
206
|
## Marketplaces
|
|
113
207
|
|
|
114
208
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,8 @@ __export(index_exports, {
|
|
|
26
26
|
ASIN_LENGTH: () => ASIN_LENGTH,
|
|
27
27
|
ASIN_REGEX: () => ASIN_REGEX,
|
|
28
28
|
AuthenticationError: () => AuthenticationError,
|
|
29
|
+
BestSellers: () => BestSellers,
|
|
30
|
+
Categories: () => Categories,
|
|
29
31
|
CsvType: () => CsvType,
|
|
30
32
|
DEFAULT_DAYS: () => DEFAULT_DAYS,
|
|
31
33
|
KEEPA_NO_DATA_SENTINEL: () => KEEPA_NO_DATA_SENTINEL,
|
|
@@ -38,13 +40,17 @@ __export(index_exports, {
|
|
|
38
40
|
ProductNotFoundError: () => ProductNotFoundError,
|
|
39
41
|
Products: () => Products,
|
|
40
42
|
RateLimitError: () => RateLimitError,
|
|
43
|
+
SavingBasisType: () => SavingBasisType,
|
|
41
44
|
VERSION: () => VERSION,
|
|
42
45
|
default: () => KeepaClient,
|
|
43
46
|
extractBsr: () => extractBsr,
|
|
44
47
|
isFoundProduct: () => isFoundProduct,
|
|
45
48
|
isValidAsin: () => isValidAsin,
|
|
46
49
|
normalizeAsins: () => normalizeAsins,
|
|
50
|
+
parseMonthlySold: () => parseMonthlySold,
|
|
51
|
+
parsePrice: () => parsePrice,
|
|
47
52
|
parsePriceHistory: () => parsePriceHistory,
|
|
53
|
+
parseSavingBasisType: () => parseSavingBasisType,
|
|
48
54
|
resolveDomainId: () => resolveDomainId
|
|
49
55
|
});
|
|
50
56
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -264,6 +270,10 @@ var CsvType = {
|
|
|
264
270
|
};
|
|
265
271
|
var KEEPA_EPOCH_UNIX_MS = 129384e7;
|
|
266
272
|
var VALID_IMAGE_FILENAME = /^[A-Za-z0-9._-]+\.(jpg|jpeg|png|webp)$/i;
|
|
273
|
+
var SavingBasisType = {
|
|
274
|
+
LIST_PRICE: 0,
|
|
275
|
+
WAS_PRICE: 1
|
|
276
|
+
};
|
|
267
277
|
|
|
268
278
|
// src/resources/products/error.ts
|
|
269
279
|
var ProductNotFoundError = class extends KeepaError {
|
|
@@ -295,11 +305,29 @@ function toKeepaProduct(raw) {
|
|
|
295
305
|
amazonPrice: amazon.at(-1)?.price ?? null,
|
|
296
306
|
newPrice: new_.at(-1)?.price ?? null,
|
|
297
307
|
listPrice: list.at(-1)?.price ?? null,
|
|
308
|
+
monthlySold: parseMonthlySold(raw.monthlySold),
|
|
298
309
|
history: {
|
|
299
310
|
price: { amazon, new: new_, list }
|
|
311
|
+
},
|
|
312
|
+
stats: {
|
|
313
|
+
buyBoxSavingBasis: parsePrice(raw.stats?.buyBoxSavingBasis),
|
|
314
|
+
buyBoxSavingBasisType: parseSavingBasisType(raw.stats?.buyBoxSavingBasisType)
|
|
300
315
|
}
|
|
301
316
|
};
|
|
302
317
|
}
|
|
318
|
+
function parsePrice(value) {
|
|
319
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
320
|
+
if (value === KEEPA_NO_DATA_SENTINEL) return null;
|
|
321
|
+
return value / 100;
|
|
322
|
+
}
|
|
323
|
+
function parseMonthlySold(value) {
|
|
324
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
325
|
+
if (value === KEEPA_NO_DATA_SENTINEL) return null;
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
function parseSavingBasisType(value) {
|
|
329
|
+
return value === SavingBasisType.LIST_PRICE || value === SavingBasisType.WAS_PRICE ? value : null;
|
|
330
|
+
}
|
|
303
331
|
function pairKeepaSeries(series) {
|
|
304
332
|
if (!series) return [];
|
|
305
333
|
const points = [];
|
|
@@ -314,10 +342,10 @@ function keepaMinutesToDate(minutes) {
|
|
|
314
342
|
return new Date(minutes * 6e4 + KEEPA_EPOCH_UNIX_MS);
|
|
315
343
|
}
|
|
316
344
|
function parsePriceHistory(series) {
|
|
317
|
-
return pairKeepaSeries(series).
|
|
318
|
-
|
|
319
|
-
price:
|
|
320
|
-
})
|
|
345
|
+
return pairKeepaSeries(series).flatMap(({ timestamp, value }) => {
|
|
346
|
+
const price = parsePrice(value);
|
|
347
|
+
return price === null ? [] : [{ timestamp: keepaMinutesToDate(timestamp), price }];
|
|
348
|
+
});
|
|
321
349
|
}
|
|
322
350
|
function rawImagesToUrls(images) {
|
|
323
351
|
if (!images || images.length === 0) return [];
|
|
@@ -348,7 +376,10 @@ var Products = class extends APIResource {
|
|
|
348
376
|
domain,
|
|
349
377
|
asin: asins,
|
|
350
378
|
days: params.days ?? DEFAULT_DAYS,
|
|
351
|
-
history: params.history ? 1 : 0
|
|
379
|
+
history: params.history ? 1 : 0,
|
|
380
|
+
// Keepa's `stats` parameter is the number of days to compute stats over;
|
|
381
|
+
// we pass `days` when the flag is on, 0 to disable.
|
|
382
|
+
stats: params.stats ? params.days ?? DEFAULT_DAYS : 0
|
|
352
383
|
},
|
|
353
384
|
context: PRODUCT_LIST_CONTEXT
|
|
354
385
|
});
|
|
@@ -365,6 +396,79 @@ var Products = class extends APIResource {
|
|
|
365
396
|
}
|
|
366
397
|
};
|
|
367
398
|
|
|
399
|
+
// src/resources/categories/categories.ts
|
|
400
|
+
var Categories = class extends APIResource {
|
|
401
|
+
/**
|
|
402
|
+
* Resolve category metadata by browse-node id. Returns a map keyed by
|
|
403
|
+
* `catId` for easy lookup; missing ids in the response (Keepa silently
|
|
404
|
+
* omits unknown nodes) are simply absent from the result.
|
|
405
|
+
*/
|
|
406
|
+
async list(params) {
|
|
407
|
+
if (params.ids.length === 0) return {};
|
|
408
|
+
const data = await this._client._request({
|
|
409
|
+
path: "/category",
|
|
410
|
+
query: {
|
|
411
|
+
domain: resolveDomainId(params.marketplace),
|
|
412
|
+
category: params.ids,
|
|
413
|
+
parents: params.withParents ? 1 : 0
|
|
414
|
+
},
|
|
415
|
+
context: "categories.list"
|
|
416
|
+
});
|
|
417
|
+
const out = {};
|
|
418
|
+
for (const [idStr, cat] of Object.entries(data.categories ?? {})) {
|
|
419
|
+
out[Number(idStr)] = cat;
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Free-text search against Keepa's category index. Hits the
|
|
425
|
+
* `/search?type=category` endpoint — Keepa's `/search` is category-
|
|
426
|
+
* specific despite the generic-sounding path, so it lives here as a
|
|
427
|
+
* Categories method rather than a separate resource.
|
|
428
|
+
*
|
|
429
|
+
* Empty array on "no matches" — Keepa doesn't distinguish that from a
|
|
430
|
+
* missing `categories` field in the response.
|
|
431
|
+
*/
|
|
432
|
+
async search(params) {
|
|
433
|
+
const data = await this._client._request({
|
|
434
|
+
path: "/search",
|
|
435
|
+
query: {
|
|
436
|
+
domain: resolveDomainId(params.marketplace),
|
|
437
|
+
type: "category",
|
|
438
|
+
term: params.term
|
|
439
|
+
},
|
|
440
|
+
context: "categories.search"
|
|
441
|
+
});
|
|
442
|
+
return Object.values(data.categories ?? {});
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// src/resources/bestsellers/bestsellers.ts
|
|
447
|
+
var BestSellers = class extends APIResource {
|
|
448
|
+
/**
|
|
449
|
+
* Fetch the best-seller list for a category. Returns null when Keepa has
|
|
450
|
+
* no list (typical for non-leaf nodes and sparse leaves) rather than
|
|
451
|
+
* throwing — callers usually want to surface "no data" as a row state,
|
|
452
|
+
* not an error.
|
|
453
|
+
*/
|
|
454
|
+
async retrieve(params) {
|
|
455
|
+
const data = await this._client._request({
|
|
456
|
+
path: "/bestsellers",
|
|
457
|
+
query: {
|
|
458
|
+
domain: resolveDomainId(params.marketplace),
|
|
459
|
+
category: params.categoryId,
|
|
460
|
+
sublist: params.sublist === false ? 0 : 1
|
|
461
|
+
},
|
|
462
|
+
context: "bestsellers.retrieve"
|
|
463
|
+
});
|
|
464
|
+
if (!data.bestSellersList) return null;
|
|
465
|
+
return {
|
|
466
|
+
categoryId: data.bestSellersList.categoryId ?? params.categoryId,
|
|
467
|
+
asinList: data.bestSellersList.asinList ?? []
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
368
472
|
// src/client.ts
|
|
369
473
|
var DEFAULT_BASE_URL = "https://api.keepa.com";
|
|
370
474
|
var KeepaClient = class {
|
|
@@ -372,6 +476,8 @@ var KeepaClient = class {
|
|
|
372
476
|
baseURL;
|
|
373
477
|
fetch;
|
|
374
478
|
products;
|
|
479
|
+
categories;
|
|
480
|
+
bestSellers;
|
|
375
481
|
/** Latest rate-limit snapshot from Keepa, updated after every response.
|
|
376
482
|
* Null until the first request completes. */
|
|
377
483
|
rateLimit = null;
|
|
@@ -387,6 +493,8 @@ var KeepaClient = class {
|
|
|
387
493
|
this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
388
494
|
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
389
495
|
this.products = new Products(this);
|
|
496
|
+
this.categories = new Categories(this);
|
|
497
|
+
this.bestSellers = new BestSellers(this);
|
|
390
498
|
}
|
|
391
499
|
_request(args) {
|
|
392
500
|
const config = {
|
|
@@ -411,6 +519,8 @@ var VERSION = "0.1.0";
|
|
|
411
519
|
ASIN_LENGTH,
|
|
412
520
|
ASIN_REGEX,
|
|
413
521
|
AuthenticationError,
|
|
522
|
+
BestSellers,
|
|
523
|
+
Categories,
|
|
414
524
|
CsvType,
|
|
415
525
|
DEFAULT_DAYS,
|
|
416
526
|
KEEPA_NO_DATA_SENTINEL,
|
|
@@ -423,12 +533,16 @@ var VERSION = "0.1.0";
|
|
|
423
533
|
ProductNotFoundError,
|
|
424
534
|
Products,
|
|
425
535
|
RateLimitError,
|
|
536
|
+
SavingBasisType,
|
|
426
537
|
VERSION,
|
|
427
538
|
extractBsr,
|
|
428
539
|
isFoundProduct,
|
|
429
540
|
isValidAsin,
|
|
430
541
|
normalizeAsins,
|
|
542
|
+
parseMonthlySold,
|
|
543
|
+
parsePrice,
|
|
431
544
|
parsePriceHistory,
|
|
545
|
+
parseSavingBasisType,
|
|
432
546
|
resolveDomainId
|
|
433
547
|
});
|
|
434
548
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/core/error.ts","../src/core/rate-limit.ts","../src/core/request.ts","../src/core/resource.ts","../src/lib/marketplace.ts","../src/lib/asin.ts","../src/resources/products/constant.ts","../src/resources/products/error.ts","../src/resources/products/product.util.ts","../src/resources/products/products.ts","../src/client.ts","../src/version.ts"],"sourcesContent":["export { KeepaClient as default, KeepaClient, type ClientOptions } from './client.js';\n\nexport {\n KeepaError,\n APIError,\n RateLimitError,\n AuthenticationError,\n NetworkError,\n} from './core/error.js';\n\nexport type { RateLimitInfo } from './core/rate-limit.js';\n\nexport { APIResource } from './core/resource.js';\n\nexport {\n MARKETPLACE_DOMAINS,\n resolveDomainId,\n type Marketplace,\n type DomainId,\n} from './lib/marketplace.js';\n\nexport {\n ASIN_LENGTH,\n ASIN_REGEX,\n isValidAsin,\n normalizeAsins,\n} from './lib/asin.js';\n\nexport * from './resources/index.js';\n\nexport { VERSION } from './version.js';\n","import type { RateLimitInfo } from './rate-limit.js';\n\n/** Defense-in-depth — keeps the API key out of error messages even when a\n * transport error or upstream response echoes the request URL back. */\nexport function scrubApiKey(text: string): string {\n return text.replace(/([?&])key=[^&\\s]*/gi, '$1key=REDACTED');\n}\n\nexport class KeepaError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'KeepaError';\n }\n}\n\nexport class APIError extends KeepaError {\n readonly status: number;\n readonly context: string;\n readonly body: string;\n\n constructor(status: number, context: string, body: string, message?: string) {\n const safeBody = scrubApiKey(body);\n super(scrubApiKey(message ?? `Keepa ${context} error (${status}): ${safeBody}`));\n this.name = 'APIError';\n this.status = status;\n this.context = context;\n this.body = safeBody;\n }\n\n static from(\n status: number,\n context: string,\n body: string,\n rateLimit: RateLimitInfo | null = null,\n ): APIError {\n switch (status) {\n case 429:\n return new RateLimitError(context, body, rateLimit);\n case 401:\n return new AuthenticationError(context, body);\n default:\n return new APIError(status, context, body);\n }\n }\n}\n\nexport class RateLimitError extends APIError {\n /** Bucket snapshot at the moment the 429 was returned. Null if Keepa's body\n * didn't carry the usual rate-limit fields. */\n readonly rateLimit: RateLimitInfo | null;\n\n constructor(context: string, body: string, rateLimit: RateLimitInfo | null = null) {\n super(429, context, body, 'Keepa rate limit exceeded, please wait or upgrade plan');\n this.name = 'RateLimitError';\n this.rateLimit = rateLimit;\n }\n}\n\nexport class AuthenticationError extends APIError {\n constructor(context: string, body: string) {\n super(\n 401,\n context,\n body,\n `Keepa authentication failed for ${context}: invalid or missing API key`,\n );\n this.name = 'AuthenticationError';\n }\n}\n\nexport class NetworkError extends KeepaError {\n readonly context: string;\n override readonly cause: unknown;\n\n constructor(context: string, cause: unknown) {\n const raw = cause instanceof Error ? cause.message : String(cause);\n super(`Keepa ${context} network error: ${scrubApiKey(raw)}`);\n this.name = 'NetworkError';\n this.context = context;\n this.cause = cause;\n }\n}\n","export interface RateLimitInfo {\n /** Tokens remaining in the bucket after this response. */\n tokensLeft: number;\n /** Milliseconds until the next token refills. */\n refillIn: number;\n /** Tokens added to the bucket per minute. */\n refillRate: number;\n /** Reduction applied to the refill rate during high system load. */\n tokenFlowReduction: number;\n /** Local time the snapshot was received. */\n receivedAt: Date;\n}\n\n// Keepa includes these fields on every response body — 200 and 429 alike — so a\n// single extractor works for both the success path and the rate-limit error path.\nexport function extractRateLimit(body: unknown): RateLimitInfo | null {\n if (!body || typeof body !== 'object') return null;\n const b = body as Record<string, unknown>;\n if (typeof b.tokensLeft !== 'number') return null;\n return {\n tokensLeft: b.tokensLeft,\n refillIn: typeof b.refillIn === 'number' ? b.refillIn : 0,\n refillRate: typeof b.refillRate === 'number' ? b.refillRate : 0,\n tokenFlowReduction: typeof b.tokenFlowReduction === 'number' ? b.tokenFlowReduction : 0,\n receivedAt: new Date(),\n };\n}\n","import { APIError, NetworkError } from './error.js';\nimport { extractRateLimit, type RateLimitInfo } from './rate-limit.js';\n\nexport type QueryValue = string | number | string[] | number[] | undefined;\nexport type QueryParams = Record<string, QueryValue>;\n\nexport interface RequestArgs {\n path: string;\n query?: QueryParams;\n context: string;\n}\n\nexport interface RequestConfig {\n baseURL: string;\n apiKey: string;\n fetch: typeof globalThis.fetch;\n /** Fired once per response (200 or 429) when Keepa's bucket fields are\n * present on the body. */\n onRateLimit?: (info: RateLimitInfo) => void;\n}\n\nexport function buildUrl(baseURL: string, path: string, query?: QueryParams): string {\n if (!query) return `${baseURL}${path}`;\n const parts: string[] = [];\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n const stringValue = Array.isArray(value) ? value.join(',') : String(value);\n parts.push(`${encodeURIComponent(key)}=${encodeKeepaValue(stringValue)}`);\n }\n return parts.length ? `${baseURL}${path}?${parts.join('&')}` : `${baseURL}${path}`;\n}\n\n// Keep commas literal — Keepa's category endpoint rejects %2C-encoded commas\n// in comma-separated lists (asin=A,B,C, category=1,2,3).\nfunction encodeKeepaValue(value: string): string {\n return encodeURIComponent(value).replace(/%2C/g, ',');\n}\n\nexport async function request<T>(config: RequestConfig, args: RequestArgs): Promise<T> {\n const query: QueryParams = { key: config.apiKey, ...args.query };\n const url = buildUrl(config.baseURL, args.path, query);\n let res: Response;\n try {\n res = await config.fetch(url);\n } catch (cause) {\n throw new NetworkError(args.context, cause);\n }\n\n // Read the body once — Response bodies are single-use streams, and we need\n // the same payload for both rate-limit extraction and (on errors) the body\n // attached to the thrown error.\n const text = await res.text().catch(() => '');\n const body = safeJsonParse(text);\n\n const rl = extractRateLimit(body);\n if (rl) config.onRateLimit?.(rl);\n\n if (!res.ok) throw APIError.from(res.status, args.context, text, rl);\n return body as T;\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n","import type { KeepaClient } from '../client.js';\n\nexport abstract class APIResource {\n protected _client: KeepaClient;\n\n constructor(client: KeepaClient) {\n this._client = client;\n }\n}\n","// Keys are ISO 3166-1 alpha-2; values are Keepa's `domain` query parameter.\n// Domain 7 is skipped — reserved (formerly Amazon China, retired).\nexport const MARKETPLACE_DOMAINS = {\n US: 1,\n GB: 2,\n DE: 3,\n FR: 4,\n JP: 5,\n CA: 6,\n IT: 8,\n ES: 9,\n IN: 10,\n MX: 11,\n BR: 12,\n} as const;\n\nexport type Marketplace = keyof typeof MARKETPLACE_DOMAINS;\nexport type DomainId = (typeof MARKETPLACE_DOMAINS)[Marketplace];\n\nconst SUPPORTED_LIST = Object.keys(MARKETPLACE_DOMAINS).join(', ');\n\nexport function resolveDomainId(marketplace: string | undefined): DomainId {\n const key = (marketplace ?? 'US').toUpperCase() as Marketplace;\n const domainId = MARKETPLACE_DOMAINS[key];\n if (!domainId) {\n throw new Error(\n `Invalid marketplace \"${marketplace}\". Supported: ${SUPPORTED_LIST}`,\n );\n }\n return domainId;\n}\n","export const ASIN_LENGTH = 10;\nexport const ASIN_REGEX = /^[A-Z0-9]{10}$/;\n\n/** Structural check only — a passing ASIN may still not exist in Keepa's\n * database; use `isFoundProduct` for that. */\nexport function isValidAsin(value: string): boolean {\n return ASIN_REGEX.test(value);\n}\n\nexport function normalizeAsins(asins: string[]): string[] {\n const normalized = asins.map((asin) => asin.trim().toUpperCase());\n const invalid = normalized.filter((asin) => !isValidAsin(asin));\n if (invalid.length > 0) {\n throw new Error(\n `Invalid ASIN(s): ${invalid.join(', ')}. ASINs must be ${ASIN_LENGTH} alphanumeric characters (e.g. B00MNV8E0C).`,\n );\n }\n return normalized;\n}\n","export const PRODUCT_PATH = '/product';\n// `<resource>.<method>` tag carried on errors so log aggregators can filter cleanly.\nexport const PRODUCT_LIST_CONTEXT = 'products.list';\nexport const DEFAULT_DAYS = 1;\n\n// Region-neutral CDN — serves identical images regardless of the marketplace.\nexport const AMAZON_IMAGE_BASE = 'https://m.media-amazon.com/images/I';\n\n// Keepa stores `-1` in salesRanks/price arrays as \"no data captured at that timestamp\".\nexport const KEEPA_NO_DATA_SENTINEL = -1;\n\n// First-dim index into Keepa's csv history matrix. Names mirror Keepa's own enum\n// verbatim (see https://keepa.com/#!discuss/t/product-object/116).\nexport const CsvType = {\n AMAZON: 0,\n NEW: 1,\n USED: 2,\n SALES: 3,\n LISTPRICE: 4,\n COLLECTIBLE: 5,\n REFURBISHED: 6,\n NEW_FBM_SHIPPING: 7,\n LIGHTNING_DEAL: 8,\n WAREHOUSE: 9,\n NEW_FBA: 10,\n COUNT_NEW: 11,\n COUNT_USED: 12,\n COUNT_REFURBISHED: 13,\n COUNT_COLLECTIBLE: 14,\n EXTRA_INFO_UPDATES: 15,\n RATING: 16,\n COUNT_REVIEWS: 17,\n BUY_BOX_SHIPPING: 18,\n USED_NEW_SHIPPING: 19,\n USED_VERY_GOOD_SHIPPING: 20,\n USED_GOOD_SHIPPING: 21,\n USED_ACCEPTABLE_SHIPPING: 22,\n COLLECTIBLE_NEW_SHIPPING: 23,\n COLLECTIBLE_VERY_GOOD_SHIPPING: 24,\n COLLECTIBLE_GOOD_SHIPPING: 25,\n COLLECTIBLE_ACCEPTABLE_SHIPPING: 26,\n REFURBISHED_SHIPPING: 27,\n EBAY_NEW_SHIPPING: 28,\n EBAY_USED_SHIPPING: 29,\n TRADE_IN: 30,\n RENTAL: 31,\n BUY_BOX_USED_SHIPPING: 32,\n PRIME_EXCL: 33,\n COUNT_NEW_FBA: 34,\n COUNT_NEW_FBM: 35,\n} as const;\nexport type CsvType = (typeof CsvType)[keyof typeof CsvType];\n\n// 2011-01-01 00:00 UTC — origin of Keepa's csv minute timestamps. Convert with\n// `new Date(km * 60_000 + KEEPA_EPOCH_UNIX_MS)`.\nexport const KEEPA_EPOCH_UNIX_MS = 1_293_840_000_000;\n\n// Defense-in-depth allowlist for filenames in imagesCSV — rejects path-traversal\n// / SSRF-shaped strings if the CSV is ever influenced by untrusted input.\nexport const VALID_IMAGE_FILENAME = /^[A-Za-z0-9._-]+\\.(jpg|jpeg|png|webp)$/i;\n","import { KeepaError } from '../../core/error.js';\n\n/** Thrown when Keepa returns no product, or returns a stub (title === null).\n * `asin` carries the caller's original input unchanged. */\nexport class ProductNotFoundError extends KeepaError {\n readonly asin: string;\n\n constructor(asin: string) {\n super(`Keepa: no product found for ASIN ${asin}`);\n this.name = 'ProductNotFoundError';\n this.asin = asin;\n }\n}\n","import {\n AMAZON_IMAGE_BASE,\n CsvType,\n KEEPA_EPOCH_UNIX_MS,\n KEEPA_NO_DATA_SENTINEL,\n VALID_IMAGE_FILENAME,\n} from './constant.js';\nimport type { KeepaProduct, PriceHistoryEntry } from './product.type.js';\nimport type { KeepaImageRaw, KeepaProductRaw } from './product.raw.type.js';\n\nexport function toKeepaProduct(raw: KeepaProductRaw): KeepaProduct {\n const amazon = parsePriceHistory(raw.csv?.[CsvType.AMAZON]);\n const new_ = parsePriceHistory(raw.csv?.[CsvType.NEW]);\n const list = parsePriceHistory(raw.csv?.[CsvType.LISTPRICE]);\n return {\n asin: raw.asin,\n title: raw.title,\n description: raw.description,\n parentAsin: raw.parentAsin,\n categoryTree: raw.categoryTree,\n rootCategory: raw.rootCategory,\n salesRanks: raw.salesRanks,\n variations: raw.variations,\n features: raw.features,\n images: rawImagesToUrls(raw.images),\n bsr: extractBsr(raw.salesRanks, raw.rootCategory),\n amazonPrice: amazon.at(-1)?.price ?? null,\n newPrice: new_.at(-1)?.price ?? null,\n listPrice: list.at(-1)?.price ?? null,\n history: {\n price: { amazon, new: new_, list },\n },\n };\n}\n\ninterface KeepaSeriesPoint {\n /** Keepa minutes — use `keepaMinutesToDate` for a JS Date. */\n timestamp: number;\n value: number;\n}\n\n// Pair up Keepa's flat `[ts, value, ts, value, ...]` series. Drops `-1` no-data\n// sentinels; the `i + 1 < length` bound silently skips a dangling odd element.\nfunction pairKeepaSeries(series: number[] | undefined): KeepaSeriesPoint[] {\n if (!series) return [];\n const points: KeepaSeriesPoint[] = [];\n for (let i = 0; i + 1 < series.length; i += 2) {\n const value = series[i + 1]!;\n if (value === KEEPA_NO_DATA_SENTINEL) continue;\n points.push({ timestamp: series[i]!, value });\n }\n return points;\n}\n\nfunction keepaMinutesToDate(minutes: number): Date {\n return new Date(minutes * 60_000 + KEEPA_EPOCH_UNIX_MS);\n}\n\n// Keepa stores prices as integers in the smallest unit (cents/pence/etc.) and\n// scales JPY/INR/BRL the same way, so /100 produces the marketplace's major unit\n// uniformly across all supported regions.\nexport function parsePriceHistory(series: number[] | undefined): PriceHistoryEntry[] {\n return pairKeepaSeries(series).map(({ timestamp, value }) => ({\n timestamp: keepaMinutesToDate(timestamp),\n price: value / 100,\n }));\n}\n\n// Defense-in-depth — rejects path-traversal / SSRF-shaped filenames in case the\n// raw CSV is ever influenced by untrusted input.\nfunction rawImagesToUrls(images: KeepaImageRaw[] | undefined): string[] {\n if (!images || images.length === 0) return [];\n return images\n .filter((img) => typeof img.l === 'string' && VALID_IMAGE_FILENAME.test(img.l))\n .map((img) => `${AMAZON_IMAGE_BASE}/${img.l}`);\n}\n\n/** Stubs for unknown ASINs come back with `title: null`. */\nexport function isFoundProduct(product: KeepaProduct): boolean {\n return product.title != null;\n}\n\n/** Latest non-sentinel rank from Keepa's `[ts, rank, ts, rank, ...]` salesRanks\n * series for the product's rootCategory. */\nexport function extractBsr(\n salesRanks: Record<string, number[]> | undefined,\n rootCategory: number | undefined,\n): number | null {\n if (!salesRanks || rootCategory === undefined) return null;\n return pairKeepaSeries(salesRanks[String(rootCategory)]).at(-1)?.value ?? null;\n}\n","import { APIResource } from '../../core/resource.js';\nimport { resolveDomainId } from '../../lib/marketplace.js';\nimport { normalizeAsins } from '../../lib/asin.js';\nimport {\n PRODUCT_PATH,\n PRODUCT_LIST_CONTEXT,\n DEFAULT_DAYS,\n} from './constant.js';\nimport type {\n KeepaProduct,\n ProductListParams,\n ProductRetrieveParams,\n} from './product.type.js';\nimport type { KeepaProductResponseRaw } from './product.raw.type.js';\nimport { ProductNotFoundError } from './error.js';\nimport { toKeepaProduct, isFoundProduct } from './product.util.js';\n\nexport class Products extends APIResource {\n async list(params: ProductListParams): Promise<KeepaProduct[]> {\n if (params.asins.length === 0) {\n throw new Error('At least one ASIN is required');\n }\n if (\n params.days !== undefined &&\n (!Number.isInteger(params.days) || params.days < 1)\n ) {\n throw new Error(`Invalid days: ${params.days}. Must be a positive integer.`);\n }\n const asins = normalizeAsins(params.asins);\n const domain = resolveDomainId(params.marketplace);\n const data = await this._client._request<KeepaProductResponseRaw>({\n path: PRODUCT_PATH,\n query: {\n domain,\n asin: asins,\n days: params.days ?? DEFAULT_DAYS,\n history: params.history ? 1 : 0,\n },\n context: PRODUCT_LIST_CONTEXT,\n });\n return (data.products ?? []).map(toKeepaProduct);\n }\n\n /** Throws `ProductNotFoundError` when Keepa returns no product or a stub. */\n async retrieve(params: ProductRetrieveParams): Promise<KeepaProduct> {\n const { asin, ...rest } = params;\n const [product] = await this.list({ ...rest, asins: [asin] });\n if (!product || !isFoundProduct(product)) {\n throw new ProductNotFoundError(asin);\n }\n return product;\n }\n}\n","import { request } from './core/request.js';\nimport type { RequestArgs, RequestConfig } from './core/request.js';\nimport type { RateLimitInfo } from './core/rate-limit.js';\nimport { Products } from './resources/products/products.js';\n\nexport interface ClientOptions {\n /** Falls back to `process.env.KEEPA_API_KEY` when omitted. */\n apiKey?: string;\n baseURL?: string;\n fetch?: typeof globalThis.fetch;\n}\n\nconst DEFAULT_BASE_URL = 'https://api.keepa.com';\n\nexport class KeepaClient {\n readonly apiKey: string;\n readonly baseURL: string;\n readonly fetch: typeof globalThis.fetch;\n\n readonly products: Products;\n\n /** Latest rate-limit snapshot from Keepa, updated after every response.\n * Null until the first request completes. */\n rateLimit: RateLimitInfo | null = null;\n\n constructor(options: ClientOptions = {}) {\n // `process` is undefined in browsers / Workers / edge runtimes — guard so the\n // constructor throws our friendly error instead of a raw ReferenceError there.\n const envApiKey =\n typeof process !== 'undefined' ? process.env?.KEEPA_API_KEY : undefined;\n const apiKey = options.apiKey || envApiKey;\n if (!apiKey) {\n throw new Error(\n 'Missing Keepa API key. Pass it as `new KeepaClient({ apiKey })` or set KEEPA_API_KEY in your environment.',\n );\n }\n this.apiKey = apiKey;\n // Strip trailing slash so `${baseURL}${path}` never produces `//product`.\n this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n // Unbound Node fetch throws \"Illegal invocation\" when called with undefined `this`.\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);\n\n this.products = new Products(this);\n }\n\n _request<T>(args: RequestArgs): Promise<T> {\n const config: RequestConfig = {\n apiKey: this.apiKey,\n baseURL: this.baseURL,\n fetch: this.fetch,\n onRateLimit: (info) => {\n this.rateLimit = info;\n },\n };\n return request<T>(config, args);\n }\n}\n\nexport default KeepaClient;\n","export const VERSION = '0.1.0';\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/core/error.ts","../src/core/rate-limit.ts","../src/core/request.ts","../src/core/resource.ts","../src/lib/marketplace.ts","../src/lib/asin.ts","../src/resources/products/constant.ts","../src/resources/products/error.ts","../src/resources/products/product.util.ts","../src/resources/products/products.ts","../src/resources/categories/categories.ts","../src/resources/bestsellers/bestsellers.ts","../src/client.ts","../src/version.ts"],"sourcesContent":["export { KeepaClient as default, KeepaClient, type ClientOptions } from './client.js';\n\nexport {\n KeepaError,\n APIError,\n RateLimitError,\n AuthenticationError,\n NetworkError,\n} from './core/error.js';\n\nexport type { RateLimitInfo } from './core/rate-limit.js';\n\nexport { APIResource } from './core/resource.js';\n\nexport {\n MARKETPLACE_DOMAINS,\n resolveDomainId,\n type Marketplace,\n type DomainId,\n} from './lib/marketplace.js';\n\nexport {\n ASIN_LENGTH,\n ASIN_REGEX,\n isValidAsin,\n normalizeAsins,\n} from './lib/asin.js';\n\nexport * from './resources/index.js';\n\nexport { VERSION } from './version.js';\n","import type { RateLimitInfo } from './rate-limit.js';\n\n/** Defense-in-depth — keeps the API key out of error messages even when a\n * transport error or upstream response echoes the request URL back. */\nexport function scrubApiKey(text: string): string {\n return text.replace(/([?&])key=[^&\\s]*/gi, '$1key=REDACTED');\n}\n\nexport class KeepaError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'KeepaError';\n }\n}\n\nexport class APIError extends KeepaError {\n readonly status: number;\n readonly context: string;\n readonly body: string;\n\n constructor(status: number, context: string, body: string, message?: string) {\n const safeBody = scrubApiKey(body);\n super(scrubApiKey(message ?? `Keepa ${context} error (${status}): ${safeBody}`));\n this.name = 'APIError';\n this.status = status;\n this.context = context;\n this.body = safeBody;\n }\n\n static from(\n status: number,\n context: string,\n body: string,\n rateLimit: RateLimitInfo | null = null,\n ): APIError {\n switch (status) {\n case 429:\n return new RateLimitError(context, body, rateLimit);\n case 401:\n return new AuthenticationError(context, body);\n default:\n return new APIError(status, context, body);\n }\n }\n}\n\nexport class RateLimitError extends APIError {\n /** Bucket snapshot at the moment the 429 was returned. Null if Keepa's body\n * didn't carry the usual rate-limit fields. */\n readonly rateLimit: RateLimitInfo | null;\n\n constructor(context: string, body: string, rateLimit: RateLimitInfo | null = null) {\n super(429, context, body, 'Keepa rate limit exceeded, please wait or upgrade plan');\n this.name = 'RateLimitError';\n this.rateLimit = rateLimit;\n }\n}\n\nexport class AuthenticationError extends APIError {\n constructor(context: string, body: string) {\n super(\n 401,\n context,\n body,\n `Keepa authentication failed for ${context}: invalid or missing API key`,\n );\n this.name = 'AuthenticationError';\n }\n}\n\nexport class NetworkError extends KeepaError {\n readonly context: string;\n override readonly cause: unknown;\n\n constructor(context: string, cause: unknown) {\n const raw = cause instanceof Error ? cause.message : String(cause);\n super(`Keepa ${context} network error: ${scrubApiKey(raw)}`);\n this.name = 'NetworkError';\n this.context = context;\n this.cause = cause;\n }\n}\n","export interface RateLimitInfo {\n /** Tokens remaining in the bucket after this response. */\n tokensLeft: number;\n /** Milliseconds until the next token refills. */\n refillIn: number;\n /** Tokens added to the bucket per minute. */\n refillRate: number;\n /** Reduction applied to the refill rate during high system load. */\n tokenFlowReduction: number;\n /** Local time the snapshot was received. */\n receivedAt: Date;\n}\n\n// Keepa includes these fields on every response body — 200 and 429 alike — so a\n// single extractor works for both the success path and the rate-limit error path.\nexport function extractRateLimit(body: unknown): RateLimitInfo | null {\n if (!body || typeof body !== 'object') return null;\n const b = body as Record<string, unknown>;\n if (typeof b.tokensLeft !== 'number') return null;\n return {\n tokensLeft: b.tokensLeft,\n refillIn: typeof b.refillIn === 'number' ? b.refillIn : 0,\n refillRate: typeof b.refillRate === 'number' ? b.refillRate : 0,\n tokenFlowReduction: typeof b.tokenFlowReduction === 'number' ? b.tokenFlowReduction : 0,\n receivedAt: new Date(),\n };\n}\n","import { APIError, NetworkError } from './error.js';\nimport { extractRateLimit, type RateLimitInfo } from './rate-limit.js';\n\nexport type QueryValue = string | number | string[] | number[] | undefined;\nexport type QueryParams = Record<string, QueryValue>;\n\nexport interface RequestArgs {\n path: string;\n query?: QueryParams;\n context: string;\n}\n\nexport interface RequestConfig {\n baseURL: string;\n apiKey: string;\n fetch: typeof globalThis.fetch;\n /** Fired once per response (200 or 429) when Keepa's bucket fields are\n * present on the body. */\n onRateLimit?: (info: RateLimitInfo) => void;\n}\n\nexport function buildUrl(baseURL: string, path: string, query?: QueryParams): string {\n if (!query) return `${baseURL}${path}`;\n const parts: string[] = [];\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n const stringValue = Array.isArray(value) ? value.join(',') : String(value);\n parts.push(`${encodeURIComponent(key)}=${encodeKeepaValue(stringValue)}`);\n }\n return parts.length ? `${baseURL}${path}?${parts.join('&')}` : `${baseURL}${path}`;\n}\n\n// Keep commas literal — Keepa's category endpoint rejects %2C-encoded commas\n// in comma-separated lists (asin=A,B,C, category=1,2,3).\nfunction encodeKeepaValue(value: string): string {\n return encodeURIComponent(value).replace(/%2C/g, ',');\n}\n\nexport async function request<T>(config: RequestConfig, args: RequestArgs): Promise<T> {\n const query: QueryParams = { key: config.apiKey, ...args.query };\n const url = buildUrl(config.baseURL, args.path, query);\n let res: Response;\n try {\n res = await config.fetch(url);\n } catch (cause) {\n throw new NetworkError(args.context, cause);\n }\n\n // Read the body once — Response bodies are single-use streams, and we need\n // the same payload for both rate-limit extraction and (on errors) the body\n // attached to the thrown error.\n const text = await res.text().catch(() => '');\n const body = safeJsonParse(text);\n\n const rl = extractRateLimit(body);\n if (rl) config.onRateLimit?.(rl);\n\n if (!res.ok) throw APIError.from(res.status, args.context, text, rl);\n return body as T;\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n","import type { KeepaClient } from '../client.js';\n\nexport abstract class APIResource {\n protected _client: KeepaClient;\n\n constructor(client: KeepaClient) {\n this._client = client;\n }\n}\n","// Keys are ISO 3166-1 alpha-2; values are Keepa's `domain` query parameter.\n// Domain 7 is skipped — reserved (formerly Amazon China, retired).\nexport const MARKETPLACE_DOMAINS = {\n US: 1,\n GB: 2,\n DE: 3,\n FR: 4,\n JP: 5,\n CA: 6,\n IT: 8,\n ES: 9,\n IN: 10,\n MX: 11,\n BR: 12,\n} as const;\n\nexport type Marketplace = keyof typeof MARKETPLACE_DOMAINS;\nexport type DomainId = (typeof MARKETPLACE_DOMAINS)[Marketplace];\n\nconst SUPPORTED_LIST = Object.keys(MARKETPLACE_DOMAINS).join(', ');\n\nexport function resolveDomainId(marketplace: string | undefined): DomainId {\n const key = (marketplace ?? 'US').toUpperCase() as Marketplace;\n const domainId = MARKETPLACE_DOMAINS[key];\n if (!domainId) {\n throw new Error(\n `Invalid marketplace \"${marketplace}\". Supported: ${SUPPORTED_LIST}`,\n );\n }\n return domainId;\n}\n","export const ASIN_LENGTH = 10;\nexport const ASIN_REGEX = /^[A-Z0-9]{10}$/;\n\n/** Structural check only — a passing ASIN may still not exist in Keepa's\n * database; use `isFoundProduct` for that. */\nexport function isValidAsin(value: string): boolean {\n return ASIN_REGEX.test(value);\n}\n\nexport function normalizeAsins(asins: string[]): string[] {\n const normalized = asins.map((asin) => asin.trim().toUpperCase());\n const invalid = normalized.filter((asin) => !isValidAsin(asin));\n if (invalid.length > 0) {\n throw new Error(\n `Invalid ASIN(s): ${invalid.join(', ')}. ASINs must be ${ASIN_LENGTH} alphanumeric characters (e.g. B00MNV8E0C).`,\n );\n }\n return normalized;\n}\n","export const PRODUCT_PATH = '/product';\n// `<resource>.<method>` tag carried on errors so log aggregators can filter cleanly.\nexport const PRODUCT_LIST_CONTEXT = 'products.list';\nexport const DEFAULT_DAYS = 1;\n\n// Region-neutral CDN — serves identical images regardless of the marketplace.\nexport const AMAZON_IMAGE_BASE = 'https://m.media-amazon.com/images/I';\n\n// Keepa stores `-1` in salesRanks/price arrays as \"no data captured at that timestamp\".\nexport const KEEPA_NO_DATA_SENTINEL = -1;\n\n// First-dim index into Keepa's csv history matrix. Names mirror Keepa's own enum\n// verbatim (see https://keepa.com/#!discuss/t/product-object/116).\nexport const CsvType = {\n AMAZON: 0,\n NEW: 1,\n USED: 2,\n SALES: 3,\n LISTPRICE: 4,\n COLLECTIBLE: 5,\n REFURBISHED: 6,\n NEW_FBM_SHIPPING: 7,\n LIGHTNING_DEAL: 8,\n WAREHOUSE: 9,\n NEW_FBA: 10,\n COUNT_NEW: 11,\n COUNT_USED: 12,\n COUNT_REFURBISHED: 13,\n COUNT_COLLECTIBLE: 14,\n EXTRA_INFO_UPDATES: 15,\n RATING: 16,\n COUNT_REVIEWS: 17,\n BUY_BOX_SHIPPING: 18,\n USED_NEW_SHIPPING: 19,\n USED_VERY_GOOD_SHIPPING: 20,\n USED_GOOD_SHIPPING: 21,\n USED_ACCEPTABLE_SHIPPING: 22,\n COLLECTIBLE_NEW_SHIPPING: 23,\n COLLECTIBLE_VERY_GOOD_SHIPPING: 24,\n COLLECTIBLE_GOOD_SHIPPING: 25,\n COLLECTIBLE_ACCEPTABLE_SHIPPING: 26,\n REFURBISHED_SHIPPING: 27,\n EBAY_NEW_SHIPPING: 28,\n EBAY_USED_SHIPPING: 29,\n TRADE_IN: 30,\n RENTAL: 31,\n BUY_BOX_USED_SHIPPING: 32,\n PRIME_EXCL: 33,\n COUNT_NEW_FBA: 34,\n COUNT_NEW_FBM: 35,\n} as const;\nexport type CsvType = (typeof CsvType)[keyof typeof CsvType];\n\n// 2011-01-01 00:00 UTC — origin of Keepa's csv minute timestamps. Convert with\n// `new Date(km * 60_000 + KEEPA_EPOCH_UNIX_MS)`.\nexport const KEEPA_EPOCH_UNIX_MS = 1_293_840_000_000;\n\n// Defense-in-depth allowlist for filenames in imagesCSV — rejects path-traversal\n// / SSRF-shaped strings if the CSV is ever influenced by untrusted input.\nexport const VALID_IMAGE_FILENAME = /^[A-Za-z0-9._-]+\\.(jpg|jpeg|png|webp)$/i;\n\nexport const SavingBasisType = {\n LIST_PRICE: 0,\n WAS_PRICE: 1,\n} as const;\nexport type SavingBasisType = (typeof SavingBasisType)[keyof typeof SavingBasisType];\n","import { KeepaError } from '../../core/error.js';\n\n/** Thrown when Keepa returns no product, or returns a stub (title === null).\n * `asin` carries the caller's original input unchanged. */\nexport class ProductNotFoundError extends KeepaError {\n readonly asin: string;\n\n constructor(asin: string) {\n super(`Keepa: no product found for ASIN ${asin}`);\n this.name = 'ProductNotFoundError';\n this.asin = asin;\n }\n}\n","import {\n AMAZON_IMAGE_BASE,\n CsvType,\n KEEPA_EPOCH_UNIX_MS,\n KEEPA_NO_DATA_SENTINEL,\n SavingBasisType,\n VALID_IMAGE_FILENAME,\n} from './constant.js';\nimport type { KeepaProduct, PriceHistoryEntry } from './product.type.js';\nimport type { KeepaImageRaw, KeepaProductRaw } from './product.raw.type.js';\n\nexport function toKeepaProduct(raw: KeepaProductRaw): KeepaProduct {\n const amazon = parsePriceHistory(raw.csv?.[CsvType.AMAZON]);\n const new_ = parsePriceHistory(raw.csv?.[CsvType.NEW]);\n const list = parsePriceHistory(raw.csv?.[CsvType.LISTPRICE]);\n return {\n asin: raw.asin,\n title: raw.title,\n description: raw.description,\n parentAsin: raw.parentAsin,\n categoryTree: raw.categoryTree,\n rootCategory: raw.rootCategory,\n salesRanks: raw.salesRanks,\n variations: raw.variations,\n features: raw.features,\n images: rawImagesToUrls(raw.images),\n bsr: extractBsr(raw.salesRanks, raw.rootCategory),\n amazonPrice: amazon.at(-1)?.price ?? null,\n newPrice: new_.at(-1)?.price ?? null,\n listPrice: list.at(-1)?.price ?? null,\n monthlySold: parseMonthlySold(raw.monthlySold),\n history: {\n price: { amazon, new: new_, list },\n },\n stats: {\n buyBoxSavingBasis: parsePrice(raw.stats?.buyBoxSavingBasis),\n buyBoxSavingBasisType: parseSavingBasisType(raw.stats?.buyBoxSavingBasisType),\n },\n };\n}\n\n/** Keepa's smallest-currency unit (cents/pence/…) → marketplace's major unit.\n * Keepa scales JPY/INR/BRL by 100 too, so /100 produces the right unit across\n * every supported region. Returns null for missing values, non-finite numbers,\n * and the `-1` no-data sentinel. */\nexport function parsePrice(value: unknown): number | null {\n if (typeof value !== 'number' || !Number.isFinite(value)) return null;\n if (value === KEEPA_NO_DATA_SENTINEL) return null;\n return value / 100;\n}\n\n/** Keepa returns `-1` when Amazon doesn't show the \"bought in past month\"\n * widget for the ASIN; surface that as null so callers can distinguish \"no\n * data\" from a genuine zero. */\nexport function parseMonthlySold(value: unknown): number | null {\n if (typeof value !== 'number' || !Number.isFinite(value)) return null;\n if (value === KEEPA_NO_DATA_SENTINEL) return null;\n return value;\n}\n\nexport function parseSavingBasisType(value: unknown): SavingBasisType | null {\n return value === SavingBasisType.LIST_PRICE || value === SavingBasisType.WAS_PRICE\n ? value\n : null;\n}\n\ninterface KeepaSeriesPoint {\n /** Keepa minutes — use `keepaMinutesToDate` for a JS Date. */\n timestamp: number;\n value: number;\n}\n\n// Pair up Keepa's flat `[ts, value, ts, value, ...]` series. Drops `-1` no-data\n// sentinels; the `i + 1 < length` bound silently skips a dangling odd element.\nfunction pairKeepaSeries(series: number[] | undefined): KeepaSeriesPoint[] {\n if (!series) return [];\n const points: KeepaSeriesPoint[] = [];\n for (let i = 0; i + 1 < series.length; i += 2) {\n const value = series[i + 1]!;\n if (value === KEEPA_NO_DATA_SENTINEL) continue;\n points.push({ timestamp: series[i]!, value });\n }\n return points;\n}\n\nfunction keepaMinutesToDate(minutes: number): Date {\n return new Date(minutes * 60_000 + KEEPA_EPOCH_UNIX_MS);\n}\n\nexport function parsePriceHistory(series: number[] | undefined): PriceHistoryEntry[] {\n return pairKeepaSeries(series).flatMap(({ timestamp, value }) => {\n const price = parsePrice(value);\n return price === null ? [] : [{ timestamp: keepaMinutesToDate(timestamp), price }];\n });\n}\n\n// Defense-in-depth — rejects path-traversal / SSRF-shaped filenames in case the\n// raw CSV is ever influenced by untrusted input.\nfunction rawImagesToUrls(images: KeepaImageRaw[] | undefined): string[] {\n if (!images || images.length === 0) return [];\n return images\n .filter((img) => typeof img.l === 'string' && VALID_IMAGE_FILENAME.test(img.l))\n .map((img) => `${AMAZON_IMAGE_BASE}/${img.l}`);\n}\n\n/** Stubs for unknown ASINs come back with `title: null`. */\nexport function isFoundProduct(product: KeepaProduct): boolean {\n return product.title != null;\n}\n\n/** Latest non-sentinel rank from Keepa's `[ts, rank, ts, rank, ...]` salesRanks\n * series for the product's rootCategory. */\nexport function extractBsr(\n salesRanks: Record<string, number[]> | undefined,\n rootCategory: number | undefined,\n): number | null {\n if (!salesRanks || rootCategory === undefined) return null;\n return pairKeepaSeries(salesRanks[String(rootCategory)]).at(-1)?.value ?? null;\n}\n","import { APIResource } from '../../core/resource.js';\nimport { resolveDomainId } from '../../lib/marketplace.js';\nimport { normalizeAsins } from '../../lib/asin.js';\nimport {\n PRODUCT_PATH,\n PRODUCT_LIST_CONTEXT,\n DEFAULT_DAYS,\n} from './constant.js';\nimport type {\n KeepaProduct,\n ProductListParams,\n ProductRetrieveParams,\n} from './product.type.js';\nimport type { KeepaProductResponseRaw } from './product.raw.type.js';\nimport { ProductNotFoundError } from './error.js';\nimport { toKeepaProduct, isFoundProduct } from './product.util.js';\n\nexport class Products extends APIResource {\n async list(params: ProductListParams): Promise<KeepaProduct[]> {\n if (params.asins.length === 0) {\n throw new Error('At least one ASIN is required');\n }\n if (\n params.days !== undefined &&\n (!Number.isInteger(params.days) || params.days < 1)\n ) {\n throw new Error(`Invalid days: ${params.days}. Must be a positive integer.`);\n }\n const asins = normalizeAsins(params.asins);\n const domain = resolveDomainId(params.marketplace);\n const data = await this._client._request<KeepaProductResponseRaw>({\n path: PRODUCT_PATH,\n query: {\n domain,\n asin: asins,\n days: params.days ?? DEFAULT_DAYS,\n history: params.history ? 1 : 0,\n // Keepa's `stats` parameter is the number of days to compute stats over;\n // we pass `days` when the flag is on, 0 to disable.\n stats: params.stats ? (params.days ?? DEFAULT_DAYS) : 0,\n },\n context: PRODUCT_LIST_CONTEXT,\n });\n return (data.products ?? []).map(toKeepaProduct);\n }\n\n /** Throws `ProductNotFoundError` when Keepa returns no product or a stub. */\n async retrieve(params: ProductRetrieveParams): Promise<KeepaProduct> {\n const { asin, ...rest } = params;\n const [product] = await this.list({ ...rest, asins: [asin] });\n if (!product || !isFoundProduct(product)) {\n throw new ProductNotFoundError(asin);\n }\n return product;\n }\n}\n","import { APIResource } from '../../core/resource.js';\nimport { resolveDomainId } from '../../lib/marketplace.js';\nimport type {\n CategoryListParams,\n CategorySearchHit,\n CategorySearchParams,\n KeepaCategory,\n} from './category.type.js';\n\ninterface CategoryListResponseRaw {\n categories?: Record<string, KeepaCategory>;\n}\n\ninterface CategorySearchResponseRaw {\n categories?: Record<string, CategorySearchHit>;\n}\n\nexport class Categories extends APIResource {\n /**\n * Resolve category metadata by browse-node id. Returns a map keyed by\n * `catId` for easy lookup; missing ids in the response (Keepa silently\n * omits unknown nodes) are simply absent from the result.\n */\n async list(\n params: CategoryListParams,\n ): Promise<Record<number, KeepaCategory>> {\n if (params.ids.length === 0) return {};\n const data = await this._client._request<CategoryListResponseRaw>({\n path: '/category',\n query: {\n domain: resolveDomainId(params.marketplace),\n category: params.ids,\n parents: params.withParents ? 1 : 0,\n },\n context: 'categories.list',\n });\n const out: Record<number, KeepaCategory> = {};\n for (const [idStr, cat] of Object.entries(data.categories ?? {})) {\n out[Number(idStr)] = cat;\n }\n return out;\n }\n\n /**\n * Free-text search against Keepa's category index. Hits the\n * `/search?type=category` endpoint — Keepa's `/search` is category-\n * specific despite the generic-sounding path, so it lives here as a\n * Categories method rather than a separate resource.\n *\n * Empty array on \"no matches\" — Keepa doesn't distinguish that from a\n * missing `categories` field in the response.\n */\n async search(params: CategorySearchParams): Promise<CategorySearchHit[]> {\n const data = await this._client._request<CategorySearchResponseRaw>({\n path: '/search',\n query: {\n domain: resolveDomainId(params.marketplace),\n type: 'category',\n term: params.term,\n },\n context: 'categories.search',\n });\n return Object.values(data.categories ?? {});\n }\n}\n","import { APIResource } from '../../core/resource.js';\nimport { resolveDomainId } from '../../lib/marketplace.js';\nimport type {\n BestSellerRetrieveParams,\n KeepaBestSellerList,\n} from './bestseller.type.js';\n\ninterface BestSellersResponseRaw {\n bestSellersList?: {\n asinList?: string[];\n categoryId?: number;\n } | null;\n}\n\nexport class BestSellers extends APIResource {\n /**\n * Fetch the best-seller list for a category. Returns null when Keepa has\n * no list (typical for non-leaf nodes and sparse leaves) rather than\n * throwing — callers usually want to surface \"no data\" as a row state,\n * not an error.\n */\n async retrieve(\n params: BestSellerRetrieveParams,\n ): Promise<KeepaBestSellerList | null> {\n const data = await this._client._request<BestSellersResponseRaw>({\n path: '/bestsellers',\n query: {\n domain: resolveDomainId(params.marketplace),\n category: params.categoryId,\n sublist: params.sublist === false ? 0 : 1,\n },\n context: 'bestsellers.retrieve',\n });\n if (!data.bestSellersList) return null;\n return {\n categoryId: data.bestSellersList.categoryId ?? params.categoryId,\n asinList: data.bestSellersList.asinList ?? [],\n };\n }\n}\n","import { request } from './core/request.js';\nimport type { RequestArgs, RequestConfig } from './core/request.js';\nimport type { RateLimitInfo } from './core/rate-limit.js';\nimport { Products } from './resources/products/products.js';\nimport { Categories } from './resources/categories/categories.js';\nimport { BestSellers } from './resources/bestsellers/bestsellers.js';\n\nexport interface ClientOptions {\n /** Falls back to `process.env.KEEPA_API_KEY` when omitted. */\n apiKey?: string;\n baseURL?: string;\n fetch?: typeof globalThis.fetch;\n}\n\nconst DEFAULT_BASE_URL = 'https://api.keepa.com';\n\nexport class KeepaClient {\n readonly apiKey: string;\n readonly baseURL: string;\n readonly fetch: typeof globalThis.fetch;\n\n readonly products: Products;\n readonly categories: Categories;\n readonly bestSellers: BestSellers;\n\n /** Latest rate-limit snapshot from Keepa, updated after every response.\n * Null until the first request completes. */\n rateLimit: RateLimitInfo | null = null;\n\n constructor(options: ClientOptions = {}) {\n // `process` is undefined in browsers / Workers / edge runtimes — guard so the\n // constructor throws our friendly error instead of a raw ReferenceError there.\n const envApiKey =\n typeof process !== 'undefined' ? process.env?.KEEPA_API_KEY : undefined;\n const apiKey = options.apiKey || envApiKey;\n if (!apiKey) {\n throw new Error(\n 'Missing Keepa API key. Pass it as `new KeepaClient({ apiKey })` or set KEEPA_API_KEY in your environment.',\n );\n }\n this.apiKey = apiKey;\n // Strip trailing slash so `${baseURL}${path}` never produces `//product`.\n this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n // Unbound Node fetch throws \"Illegal invocation\" when called with undefined `this`.\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);\n\n this.products = new Products(this);\n this.categories = new Categories(this);\n this.bestSellers = new BestSellers(this);\n }\n\n _request<T>(args: RequestArgs): Promise<T> {\n const config: RequestConfig = {\n apiKey: this.apiKey,\n baseURL: this.baseURL,\n fetch: this.fetch,\n onRateLimit: (info) => {\n this.rateLimit = info;\n },\n };\n return request<T>(config, args);\n }\n}\n\nexport default KeepaClient;\n","export const VERSION = '0.1.0';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIO,SAAS,YAAY,MAAsB;AAChD,SAAO,KAAK,QAAQ,uBAAuB,gBAAgB;AAC7D;AAEO,IAAM,aAAN,cAAyB,MAAM;AAAA,EACpC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,WAAN,MAAM,kBAAiB,WAAW;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAAgB,SAAiB,MAAc,SAAkB;AAC3E,UAAM,WAAW,YAAY,IAAI;AACjC,UAAM,YAAY,WAAW,SAAS,OAAO,WAAW,MAAM,MAAM,QAAQ,EAAE,CAAC;AAC/E,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,UAAU;AACf,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,OAAO,KACL,QACA,SACA,MACA,YAAkC,MACxB;AACV,YAAQ,QAAQ;AAAA,MACd,KAAK;AACH,eAAO,IAAI,eAAe,SAAS,MAAM,SAAS;AAAA,MACpD,KAAK;AACH,eAAO,IAAI,oBAAoB,SAAS,IAAI;AAAA,MAC9C;AACE,eAAO,IAAI,UAAS,QAAQ,SAAS,IAAI;AAAA,IAC7C;AAAA,EACF;AACF;AAEO,IAAM,iBAAN,cAA6B,SAAS;AAAA;AAAA;AAAA,EAGlC;AAAA,EAET,YAAY,SAAiB,MAAc,YAAkC,MAAM;AACjF,UAAM,KAAK,SAAS,MAAM,wDAAwD;AAClF,SAAK,OAAO;AACZ,SAAK,YAAY;AAAA,EACnB;AACF;AAEO,IAAM,sBAAN,cAAkC,SAAS;AAAA,EAChD,YAAY,SAAiB,MAAc;AACzC;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA,mCAAmC,OAAO;AAAA,IAC5C;AACA,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,eAAN,cAA2B,WAAW;AAAA,EAClC;AAAA,EACS;AAAA,EAElB,YAAY,SAAiB,OAAgB;AAC3C,UAAM,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACjE,UAAM,SAAS,OAAO,mBAAmB,YAAY,GAAG,CAAC,EAAE;AAC3D,SAAK,OAAO;AACZ,SAAK,UAAU;AACf,SAAK,QAAQ;AAAA,EACf;AACF;;;AClEO,SAAS,iBAAiB,MAAqC;AACpE,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC9C,QAAM,IAAI;AACV,MAAI,OAAO,EAAE,eAAe,SAAU,QAAO;AAC7C,SAAO;AAAA,IACL,YAAY,EAAE;AAAA,IACd,UAAU,OAAO,EAAE,aAAa,WAAW,EAAE,WAAW;AAAA,IACxD,YAAY,OAAO,EAAE,eAAe,WAAW,EAAE,aAAa;AAAA,IAC9D,oBAAoB,OAAO,EAAE,uBAAuB,WAAW,EAAE,qBAAqB;AAAA,IACtF,YAAY,oBAAI,KAAK;AAAA,EACvB;AACF;;;ACLO,SAAS,SAAS,SAAiB,MAAc,OAA6B;AACnF,MAAI,CAAC,MAAO,QAAO,GAAG,OAAO,GAAG,IAAI;AACpC,QAAM,QAAkB,CAAC;AACzB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,UAAU,OAAW;AACzB,UAAM,cAAc,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,GAAG,IAAI,OAAO,KAAK;AACzE,UAAM,KAAK,GAAG,mBAAmB,GAAG,CAAC,IAAI,iBAAiB,WAAW,CAAC,EAAE;AAAA,EAC1E;AACA,SAAO,MAAM,SAAS,GAAG,OAAO,GAAG,IAAI,IAAI,MAAM,KAAK,GAAG,CAAC,KAAK,GAAG,OAAO,GAAG,IAAI;AAClF;AAIA,SAAS,iBAAiB,OAAuB;AAC/C,SAAO,mBAAmB,KAAK,EAAE,QAAQ,QAAQ,GAAG;AACtD;AAEA,eAAsB,QAAW,QAAuB,MAA+B;AACrF,QAAM,QAAqB,EAAE,KAAK,OAAO,QAAQ,GAAG,KAAK,MAAM;AAC/D,QAAM,MAAM,SAAS,OAAO,SAAS,KAAK,MAAM,KAAK;AACrD,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,OAAO,MAAM,GAAG;AAAA,EAC9B,SAAS,OAAO;AACd,UAAM,IAAI,aAAa,KAAK,SAAS,KAAK;AAAA,EAC5C;AAKA,QAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE;AAC5C,QAAM,OAAO,cAAc,IAAI;AAE/B,QAAM,KAAK,iBAAiB,IAAI;AAChC,MAAI,GAAI,QAAO,cAAc,EAAE;AAE/B,MAAI,CAAC,IAAI,GAAI,OAAM,SAAS,KAAK,IAAI,QAAQ,KAAK,SAAS,MAAM,EAAE;AACnE,SAAO;AACT;AAEA,SAAS,cAAc,MAAuB;AAC5C,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACjEO,IAAe,cAAf,MAA2B;AAAA,EACtB;AAAA,EAEV,YAAY,QAAqB;AAC/B,SAAK,UAAU;AAAA,EACjB;AACF;;;ACNO,IAAM,sBAAsB;AAAA,EACjC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAKA,IAAM,iBAAiB,OAAO,KAAK,mBAAmB,EAAE,KAAK,IAAI;AAE1D,SAAS,gBAAgB,aAA2C;AACzE,QAAM,OAAO,eAAe,MAAM,YAAY;AAC9C,QAAM,WAAW,oBAAoB,GAAG;AACxC,MAAI,CAAC,UAAU;AACb,UAAM,IAAI;AAAA,MACR,wBAAwB,WAAW,iBAAiB,cAAc;AAAA,IACpE;AAAA,EACF;AACA,SAAO;AACT;;;AC9BO,IAAM,cAAc;AACpB,IAAM,aAAa;AAInB,SAAS,YAAY,OAAwB;AAClD,SAAO,WAAW,KAAK,KAAK;AAC9B;AAEO,SAAS,eAAe,OAA2B;AACxD,QAAM,aAAa,MAAM,IAAI,CAAC,SAAS,KAAK,KAAK,EAAE,YAAY,CAAC;AAChE,QAAM,UAAU,WAAW,OAAO,CAAC,SAAS,CAAC,YAAY,IAAI,CAAC;AAC9D,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,IAAI;AAAA,MACR,oBAAoB,QAAQ,KAAK,IAAI,CAAC,mBAAmB,WAAW;AAAA,IACtE;AAAA,EACF;AACA,SAAO;AACT;;;AClBO,IAAM,eAAe;AAErB,IAAM,uBAAuB;AAC7B,IAAM,eAAe;AAGrB,IAAM,oBAAoB;AAG1B,IAAM,yBAAyB;AAI/B,IAAM,UAAU;AAAA,EACrB,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,MAAM;AAAA,EACN,OAAO;AAAA,EACP,WAAW;AAAA,EACX,aAAa;AAAA,EACb,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,WAAW;AAAA,EACX,SAAS;AAAA,EACT,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,oBAAoB;AAAA,EACpB,QAAQ;AAAA,EACR,eAAe;AAAA,EACf,kBAAkB;AAAA,EAClB,mBAAmB;AAAA,EACnB,yBAAyB;AAAA,EACzB,oBAAoB;AAAA,EACpB,0BAA0B;AAAA,EAC1B,0BAA0B;AAAA,EAC1B,gCAAgC;AAAA,EAChC,2BAA2B;AAAA,EAC3B,iCAAiC;AAAA,EACjC,sBAAsB;AAAA,EACtB,mBAAmB;AAAA,EACnB,oBAAoB;AAAA,EACpB,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,uBAAuB;AAAA,EACvB,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,eAAe;AACjB;AAKO,IAAM,sBAAsB;AAI5B,IAAM,uBAAuB;AAE7B,IAAM,kBAAkB;AAAA,EAC7B,YAAY;AAAA,EACZ,WAAW;AACb;;;AC5DO,IAAM,uBAAN,cAAmC,WAAW;AAAA,EAC1C;AAAA,EAET,YAAY,MAAc;AACxB,UAAM,oCAAoC,IAAI,EAAE;AAChD,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;;;ACDO,SAAS,eAAe,KAAoC;AACjE,QAAM,SAAS,kBAAkB,IAAI,MAAM,QAAQ,MAAM,CAAC;AAC1D,QAAM,OAAO,kBAAkB,IAAI,MAAM,QAAQ,GAAG,CAAC;AACrD,QAAM,OAAO,kBAAkB,IAAI,MAAM,QAAQ,SAAS,CAAC;AAC3D,SAAO;AAAA,IACL,MAAM,IAAI;AAAA,IACV,OAAO,IAAI;AAAA,IACX,aAAa,IAAI;AAAA,IACjB,YAAY,IAAI;AAAA,IAChB,cAAc,IAAI;AAAA,IAClB,cAAc,IAAI;AAAA,IAClB,YAAY,IAAI;AAAA,IAChB,YAAY,IAAI;AAAA,IAChB,UAAU,IAAI;AAAA,IACd,QAAQ,gBAAgB,IAAI,MAAM;AAAA,IAClC,KAAK,WAAW,IAAI,YAAY,IAAI,YAAY;AAAA,IAChD,aAAa,OAAO,GAAG,EAAE,GAAG,SAAS;AAAA,IACrC,UAAU,KAAK,GAAG,EAAE,GAAG,SAAS;AAAA,IAChC,WAAW,KAAK,GAAG,EAAE,GAAG,SAAS;AAAA,IACjC,aAAa,iBAAiB,IAAI,WAAW;AAAA,IAC7C,SAAS;AAAA,MACP,OAAO,EAAE,QAAQ,KAAK,MAAM,KAAK;AAAA,IACnC;AAAA,IACA,OAAO;AAAA,MACL,mBAAmB,WAAW,IAAI,OAAO,iBAAiB;AAAA,MAC1D,uBAAuB,qBAAqB,IAAI,OAAO,qBAAqB;AAAA,IAC9E;AAAA,EACF;AACF;AAMO,SAAS,WAAW,OAA+B;AACxD,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACjE,MAAI,UAAU,uBAAwB,QAAO;AAC7C,SAAO,QAAQ;AACjB;AAKO,SAAS,iBAAiB,OAA+B;AAC9D,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACjE,MAAI,UAAU,uBAAwB,QAAO;AAC7C,SAAO;AACT;AAEO,SAAS,qBAAqB,OAAwC;AAC3E,SAAO,UAAU,gBAAgB,cAAc,UAAU,gBAAgB,YACrE,QACA;AACN;AAUA,SAAS,gBAAgB,QAAkD;AACzE,MAAI,CAAC,OAAQ,QAAO,CAAC;AACrB,QAAM,SAA6B,CAAC;AACpC,WAAS,IAAI,GAAG,IAAI,IAAI,OAAO,QAAQ,KAAK,GAAG;AAC7C,UAAM,QAAQ,OAAO,IAAI,CAAC;AAC1B,QAAI,UAAU,uBAAwB;AACtC,WAAO,KAAK,EAAE,WAAW,OAAO,CAAC,GAAI,MAAM,CAAC;AAAA,EAC9C;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,SAAuB;AACjD,SAAO,IAAI,KAAK,UAAU,MAAS,mBAAmB;AACxD;AAEO,SAAS,kBAAkB,QAAmD;AACnF,SAAO,gBAAgB,MAAM,EAAE,QAAQ,CAAC,EAAE,WAAW,MAAM,MAAM;AAC/D,UAAM,QAAQ,WAAW,KAAK;AAC9B,WAAO,UAAU,OAAO,CAAC,IAAI,CAAC,EAAE,WAAW,mBAAmB,SAAS,GAAG,MAAM,CAAC;AAAA,EACnF,CAAC;AACH;AAIA,SAAS,gBAAgB,QAA+C;AACtE,MAAI,CAAC,UAAU,OAAO,WAAW,EAAG,QAAO,CAAC;AAC5C,SAAO,OACJ,OAAO,CAAC,QAAQ,OAAO,IAAI,MAAM,YAAY,qBAAqB,KAAK,IAAI,CAAC,CAAC,EAC7E,IAAI,CAAC,QAAQ,GAAG,iBAAiB,IAAI,IAAI,CAAC,EAAE;AACjD;AAGO,SAAS,eAAe,SAAgC;AAC7D,SAAO,QAAQ,SAAS;AAC1B;AAIO,SAAS,WACd,YACA,cACe;AACf,MAAI,CAAC,cAAc,iBAAiB,OAAW,QAAO;AACtD,SAAO,gBAAgB,WAAW,OAAO,YAAY,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,SAAS;AAC5E;;;ACrGO,IAAM,WAAN,cAAuB,YAAY;AAAA,EACxC,MAAM,KAAK,QAAoD;AAC7D,QAAI,OAAO,MAAM,WAAW,GAAG;AAC7B,YAAM,IAAI,MAAM,+BAA+B;AAAA,IACjD;AACA,QACE,OAAO,SAAS,WACf,CAAC,OAAO,UAAU,OAAO,IAAI,KAAK,OAAO,OAAO,IACjD;AACA,YAAM,IAAI,MAAM,iBAAiB,OAAO,IAAI,+BAA+B;AAAA,IAC7E;AACA,UAAM,QAAQ,eAAe,OAAO,KAAK;AACzC,UAAM,SAAS,gBAAgB,OAAO,WAAW;AACjD,UAAM,OAAO,MAAM,KAAK,QAAQ,SAAkC;AAAA,MAChE,MAAM;AAAA,MACN,OAAO;AAAA,QACL;AAAA,QACA,MAAM;AAAA,QACN,MAAM,OAAO,QAAQ;AAAA,QACrB,SAAS,OAAO,UAAU,IAAI;AAAA;AAAA;AAAA,QAG9B,OAAO,OAAO,QAAS,OAAO,QAAQ,eAAgB;AAAA,MACxD;AAAA,MACA,SAAS;AAAA,IACX,CAAC;AACD,YAAQ,KAAK,YAAY,CAAC,GAAG,IAAI,cAAc;AAAA,EACjD;AAAA;AAAA,EAGA,MAAM,SAAS,QAAsD;AACnE,UAAM,EAAE,MAAM,GAAG,KAAK,IAAI;AAC1B,UAAM,CAAC,OAAO,IAAI,MAAM,KAAK,KAAK,EAAE,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;AAC5D,QAAI,CAAC,WAAW,CAAC,eAAe,OAAO,GAAG;AACxC,YAAM,IAAI,qBAAqB,IAAI;AAAA,IACrC;AACA,WAAO;AAAA,EACT;AACF;;;ACtCO,IAAM,aAAN,cAAyB,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM1C,MAAM,KACJ,QACwC;AACxC,QAAI,OAAO,IAAI,WAAW,EAAG,QAAO,CAAC;AACrC,UAAM,OAAO,MAAM,KAAK,QAAQ,SAAkC;AAAA,MAChE,MAAM;AAAA,MACN,OAAO;AAAA,QACL,QAAQ,gBAAgB,OAAO,WAAW;AAAA,QAC1C,UAAU,OAAO;AAAA,QACjB,SAAS,OAAO,cAAc,IAAI;AAAA,MACpC;AAAA,MACA,SAAS;AAAA,IACX,CAAC;AACD,UAAM,MAAqC,CAAC;AAC5C,eAAW,CAAC,OAAO,GAAG,KAAK,OAAO,QAAQ,KAAK,cAAc,CAAC,CAAC,GAAG;AAChE,UAAI,OAAO,KAAK,CAAC,IAAI;AAAA,IACvB;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,OAAO,QAA4D;AACvE,UAAM,OAAO,MAAM,KAAK,QAAQ,SAAoC;AAAA,MAClE,MAAM;AAAA,MACN,OAAO;AAAA,QACL,QAAQ,gBAAgB,OAAO,WAAW;AAAA,QAC1C,MAAM;AAAA,QACN,MAAM,OAAO;AAAA,MACf;AAAA,MACA,SAAS;AAAA,IACX,CAAC;AACD,WAAO,OAAO,OAAO,KAAK,cAAc,CAAC,CAAC;AAAA,EAC5C;AACF;;;AClDO,IAAM,cAAN,cAA0B,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO3C,MAAM,SACJ,QACqC;AACrC,UAAM,OAAO,MAAM,KAAK,QAAQ,SAAiC;AAAA,MAC/D,MAAM;AAAA,MACN,OAAO;AAAA,QACL,QAAQ,gBAAgB,OAAO,WAAW;AAAA,QAC1C,UAAU,OAAO;AAAA,QACjB,SAAS,OAAO,YAAY,QAAQ,IAAI;AAAA,MAC1C;AAAA,MACA,SAAS;AAAA,IACX,CAAC;AACD,QAAI,CAAC,KAAK,gBAAiB,QAAO;AAClC,WAAO;AAAA,MACL,YAAY,KAAK,gBAAgB,cAAc,OAAO;AAAA,MACtD,UAAU,KAAK,gBAAgB,YAAY,CAAC;AAAA,IAC9C;AAAA,EACF;AACF;;;ACzBA,IAAM,mBAAmB;AAElB,IAAM,cAAN,MAAkB;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAIT,YAAkC;AAAA,EAElC,YAAY,UAAyB,CAAC,GAAG;AAGvC,UAAM,YACJ,OAAO,YAAY,cAAc,QAAQ,KAAK,gBAAgB;AAChE,UAAM,SAAS,QAAQ,UAAU;AACjC,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,SAAK,SAAS;AAEd,SAAK,WAAW,QAAQ,WAAW,kBAAkB,QAAQ,QAAQ,EAAE;AAEvE,SAAK,QAAQ,QAAQ,SAAS,WAAW,MAAM,KAAK,UAAU;AAE9D,SAAK,WAAW,IAAI,SAAS,IAAI;AACjC,SAAK,aAAa,IAAI,WAAW,IAAI;AACrC,SAAK,cAAc,IAAI,YAAY,IAAI;AAAA,EACzC;AAAA,EAEA,SAAY,MAA+B;AACzC,UAAM,SAAwB;AAAA,MAC5B,QAAQ,KAAK;AAAA,MACb,SAAS,KAAK;AAAA,MACd,OAAO,KAAK;AAAA,MACZ,aAAa,CAAC,SAAS;AACrB,aAAK,YAAY;AAAA,MACnB;AAAA,IACF;AACA,WAAO,QAAW,QAAQ,IAAI;AAAA,EAChC;AACF;;;AC9DO,IAAM,UAAU;","names":[]}
|