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/dist/index.js
CHANGED
|
@@ -213,6 +213,10 @@ var CsvType = {
|
|
|
213
213
|
};
|
|
214
214
|
var KEEPA_EPOCH_UNIX_MS = 129384e7;
|
|
215
215
|
var VALID_IMAGE_FILENAME = /^[A-Za-z0-9._-]+\.(jpg|jpeg|png|webp)$/i;
|
|
216
|
+
var SavingBasisType = {
|
|
217
|
+
LIST_PRICE: 0,
|
|
218
|
+
WAS_PRICE: 1
|
|
219
|
+
};
|
|
216
220
|
|
|
217
221
|
// src/resources/products/error.ts
|
|
218
222
|
var ProductNotFoundError = class extends KeepaError {
|
|
@@ -244,11 +248,29 @@ function toKeepaProduct(raw) {
|
|
|
244
248
|
amazonPrice: amazon.at(-1)?.price ?? null,
|
|
245
249
|
newPrice: new_.at(-1)?.price ?? null,
|
|
246
250
|
listPrice: list.at(-1)?.price ?? null,
|
|
251
|
+
monthlySold: parseMonthlySold(raw.monthlySold),
|
|
247
252
|
history: {
|
|
248
253
|
price: { amazon, new: new_, list }
|
|
254
|
+
},
|
|
255
|
+
stats: {
|
|
256
|
+
buyBoxSavingBasis: parsePrice(raw.stats?.buyBoxSavingBasis),
|
|
257
|
+
buyBoxSavingBasisType: parseSavingBasisType(raw.stats?.buyBoxSavingBasisType)
|
|
249
258
|
}
|
|
250
259
|
};
|
|
251
260
|
}
|
|
261
|
+
function parsePrice(value) {
|
|
262
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
263
|
+
if (value === KEEPA_NO_DATA_SENTINEL) return null;
|
|
264
|
+
return value / 100;
|
|
265
|
+
}
|
|
266
|
+
function parseMonthlySold(value) {
|
|
267
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
268
|
+
if (value === KEEPA_NO_DATA_SENTINEL) return null;
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
function parseSavingBasisType(value) {
|
|
272
|
+
return value === SavingBasisType.LIST_PRICE || value === SavingBasisType.WAS_PRICE ? value : null;
|
|
273
|
+
}
|
|
252
274
|
function pairKeepaSeries(series) {
|
|
253
275
|
if (!series) return [];
|
|
254
276
|
const points = [];
|
|
@@ -263,10 +285,10 @@ function keepaMinutesToDate(minutes) {
|
|
|
263
285
|
return new Date(minutes * 6e4 + KEEPA_EPOCH_UNIX_MS);
|
|
264
286
|
}
|
|
265
287
|
function parsePriceHistory(series) {
|
|
266
|
-
return pairKeepaSeries(series).
|
|
267
|
-
|
|
268
|
-
price:
|
|
269
|
-
})
|
|
288
|
+
return pairKeepaSeries(series).flatMap(({ timestamp, value }) => {
|
|
289
|
+
const price = parsePrice(value);
|
|
290
|
+
return price === null ? [] : [{ timestamp: keepaMinutesToDate(timestamp), price }];
|
|
291
|
+
});
|
|
270
292
|
}
|
|
271
293
|
function rawImagesToUrls(images) {
|
|
272
294
|
if (!images || images.length === 0) return [];
|
|
@@ -297,7 +319,10 @@ var Products = class extends APIResource {
|
|
|
297
319
|
domain,
|
|
298
320
|
asin: asins,
|
|
299
321
|
days: params.days ?? DEFAULT_DAYS,
|
|
300
|
-
history: params.history ? 1 : 0
|
|
322
|
+
history: params.history ? 1 : 0,
|
|
323
|
+
// Keepa's `stats` parameter is the number of days to compute stats over;
|
|
324
|
+
// we pass `days` when the flag is on, 0 to disable.
|
|
325
|
+
stats: params.stats ? params.days ?? DEFAULT_DAYS : 0
|
|
301
326
|
},
|
|
302
327
|
context: PRODUCT_LIST_CONTEXT
|
|
303
328
|
});
|
|
@@ -314,6 +339,79 @@ var Products = class extends APIResource {
|
|
|
314
339
|
}
|
|
315
340
|
};
|
|
316
341
|
|
|
342
|
+
// src/resources/categories/categories.ts
|
|
343
|
+
var Categories = class extends APIResource {
|
|
344
|
+
/**
|
|
345
|
+
* Resolve category metadata by browse-node id. Returns a map keyed by
|
|
346
|
+
* `catId` for easy lookup; missing ids in the response (Keepa silently
|
|
347
|
+
* omits unknown nodes) are simply absent from the result.
|
|
348
|
+
*/
|
|
349
|
+
async list(params) {
|
|
350
|
+
if (params.ids.length === 0) return {};
|
|
351
|
+
const data = await this._client._request({
|
|
352
|
+
path: "/category",
|
|
353
|
+
query: {
|
|
354
|
+
domain: resolveDomainId(params.marketplace),
|
|
355
|
+
category: params.ids,
|
|
356
|
+
parents: params.withParents ? 1 : 0
|
|
357
|
+
},
|
|
358
|
+
context: "categories.list"
|
|
359
|
+
});
|
|
360
|
+
const out = {};
|
|
361
|
+
for (const [idStr, cat] of Object.entries(data.categories ?? {})) {
|
|
362
|
+
out[Number(idStr)] = cat;
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Free-text search against Keepa's category index. Hits the
|
|
368
|
+
* `/search?type=category` endpoint — Keepa's `/search` is category-
|
|
369
|
+
* specific despite the generic-sounding path, so it lives here as a
|
|
370
|
+
* Categories method rather than a separate resource.
|
|
371
|
+
*
|
|
372
|
+
* Empty array on "no matches" — Keepa doesn't distinguish that from a
|
|
373
|
+
* missing `categories` field in the response.
|
|
374
|
+
*/
|
|
375
|
+
async search(params) {
|
|
376
|
+
const data = await this._client._request({
|
|
377
|
+
path: "/search",
|
|
378
|
+
query: {
|
|
379
|
+
domain: resolveDomainId(params.marketplace),
|
|
380
|
+
type: "category",
|
|
381
|
+
term: params.term
|
|
382
|
+
},
|
|
383
|
+
context: "categories.search"
|
|
384
|
+
});
|
|
385
|
+
return Object.values(data.categories ?? {});
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// src/resources/bestsellers/bestsellers.ts
|
|
390
|
+
var BestSellers = class extends APIResource {
|
|
391
|
+
/**
|
|
392
|
+
* Fetch the best-seller list for a category. Returns null when Keepa has
|
|
393
|
+
* no list (typical for non-leaf nodes and sparse leaves) rather than
|
|
394
|
+
* throwing — callers usually want to surface "no data" as a row state,
|
|
395
|
+
* not an error.
|
|
396
|
+
*/
|
|
397
|
+
async retrieve(params) {
|
|
398
|
+
const data = await this._client._request({
|
|
399
|
+
path: "/bestsellers",
|
|
400
|
+
query: {
|
|
401
|
+
domain: resolveDomainId(params.marketplace),
|
|
402
|
+
category: params.categoryId,
|
|
403
|
+
sublist: params.sublist === false ? 0 : 1
|
|
404
|
+
},
|
|
405
|
+
context: "bestsellers.retrieve"
|
|
406
|
+
});
|
|
407
|
+
if (!data.bestSellersList) return null;
|
|
408
|
+
return {
|
|
409
|
+
categoryId: data.bestSellersList.categoryId ?? params.categoryId,
|
|
410
|
+
asinList: data.bestSellersList.asinList ?? []
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
317
415
|
// src/client.ts
|
|
318
416
|
var DEFAULT_BASE_URL = "https://api.keepa.com";
|
|
319
417
|
var KeepaClient = class {
|
|
@@ -321,6 +419,8 @@ var KeepaClient = class {
|
|
|
321
419
|
baseURL;
|
|
322
420
|
fetch;
|
|
323
421
|
products;
|
|
422
|
+
categories;
|
|
423
|
+
bestSellers;
|
|
324
424
|
/** Latest rate-limit snapshot from Keepa, updated after every response.
|
|
325
425
|
* Null until the first request completes. */
|
|
326
426
|
rateLimit = null;
|
|
@@ -336,6 +436,8 @@ var KeepaClient = class {
|
|
|
336
436
|
this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
337
437
|
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
338
438
|
this.products = new Products(this);
|
|
439
|
+
this.categories = new Categories(this);
|
|
440
|
+
this.bestSellers = new BestSellers(this);
|
|
339
441
|
}
|
|
340
442
|
_request(args) {
|
|
341
443
|
const config = {
|
|
@@ -359,6 +461,8 @@ export {
|
|
|
359
461
|
ASIN_LENGTH,
|
|
360
462
|
ASIN_REGEX,
|
|
361
463
|
AuthenticationError,
|
|
464
|
+
BestSellers,
|
|
465
|
+
Categories,
|
|
362
466
|
CsvType,
|
|
363
467
|
DEFAULT_DAYS,
|
|
364
468
|
KEEPA_NO_DATA_SENTINEL,
|
|
@@ -371,13 +475,17 @@ export {
|
|
|
371
475
|
ProductNotFoundError,
|
|
372
476
|
Products,
|
|
373
477
|
RateLimitError,
|
|
478
|
+
SavingBasisType,
|
|
374
479
|
VERSION,
|
|
375
480
|
KeepaClient as default,
|
|
376
481
|
extractBsr,
|
|
377
482
|
isFoundProduct,
|
|
378
483
|
isValidAsin,
|
|
379
484
|
normalizeAsins,
|
|
485
|
+
parseMonthlySold,
|
|
486
|
+
parsePrice,
|
|
380
487
|
parsePriceHistory,
|
|
488
|
+
parseSavingBasisType,
|
|
381
489
|
resolveDomainId
|
|
382
490
|
};
|
|
383
491
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../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":["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/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":["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":"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","names":[]}
|