keepa-api 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,17 +79,46 @@ The client reads `process.env.KEEPA_API_KEY` if you don't pass `apiKey` explicit
79
79
  |-------|------|---------|-------|
80
80
  | `asins` | `string[]` | (required) | Validated + uppercased. Throws on malformed input. |
81
81
  | `marketplace` | `'US' \| 'GB' \| 'DE' \| 'FR' \| 'JP' \| 'CA' \| 'IT' \| 'ES' \| 'IN' \| 'MX' \| 'BR'` | `'US'` | Case-insensitive. Throws on unknown. |
82
- | `days` | `number` | `1` | Days of price history. Must be a positive integer (validated pre-flight). |
82
+ | `days` | `number` | `1` | Days of price history to scope csv data when `history: true`. Must be a positive integer (validated pre-flight). |
83
+ | `history` | `boolean` | `false` | When `true`, requests Keepa's csv history matrix and parses it into `product.history.price.{amazon,list}` (and the scalar `price`/`listPrice` counterparts) on each returned product. When `false`, those fields are empty/null. Affects token cost — leave off when you don't need it. |
83
84
 
84
85
  The SDK maps Keepa's raw wire shape into a friendlier `KeepaProduct`:
85
86
 
86
87
  - `images: string[]` — full image URLs (region-neutral CDN). Replaces Keepa's awkward `imagesCSV` string.
87
88
  - `bsr: number | null` — most recent real BSR for the product's `rootCategory`. `null` when missing or every history entry is Keepa's `-1` "no data captured" sentinel.
89
+ - `price: number | null` / `listPrice: number | null` — latest Amazon price and list price in the marketplace's **major unit** (dollars for US, pounds for GB, yen for JP, etc.). Derived from the last entry of `history.price.amazon` / `history.price.list`. `null` when `history: false` was used or Keepa has no data.
90
+ - `history.price.amazon: PriceHistoryEntry[]` / `history.price.list: PriceHistoryEntry[]` — full parsed price series. Empty `[]` when `history: false` was used. Prices are in the marketplace's major unit (same as the scalar fields).
88
91
 
89
92
  The raw `salesRanks` record is preserved for consumers that need to walk the full rank history. Other Keepa fields (`title`, `parentAsin`, `categoryTree`, `variations`, `features`, …) pass through unchanged.
90
93
 
91
94
  **Stub records:** Keepa returns one record per requested ASIN even when it has no data — these stubs have `title === null`. Filter with `isFoundProduct` (below).
92
95
 
96
+ ### Price and price history
97
+
98
+ Every `KeepaProduct` has the price fields — what changes with the `history` flag is whether they're populated:
99
+
100
+ ```ts
101
+ // Default: history fields exist but are empty/null. Cheapest call.
102
+ const [product] = await keepa.products.list({ asins: ['B00MNV8E0C'] });
103
+ product.price; // null
104
+ product.history.price.amazon; // []
105
+
106
+ // With history: arrays filled, scalar fields derive from the latest entry.
107
+ const [detailed] = await keepa.products.list({
108
+ asins: ['B00MNV8E0C'],
109
+ history: true,
110
+ days: 30,
111
+ });
112
+ detailed.price; // 18.99 — latest Amazon price (USD dollars)
113
+ detailed.listPrice; // 29.99 — latest list price (USD dollars)
114
+ detailed.history.price.amazon; // PriceHistoryEntry[] — Amazon's own price over time (csv[0])
115
+ detailed.history.price.list; // PriceHistoryEntry[] — list price / MSRP over time (csv[4])
116
+ ```
117
+
118
+ Each `PriceHistoryEntry` is `{ timestamp: Date, price: number }` where `price` is in the marketplace's major unit (dollars for `'US'`, pounds for `'GB'`, yen for `'JP'`, etc. — the consumer chose the marketplace, so the currency is known). Keepa's `-1` "no data captured" sentinel entries are filtered out, so iterating the arrays only yields real price points.
119
+
120
+ If you need a csv type other than `CsvType.AMAZON` or `CsvType.LISTPRICE` (e.g. `CsvType.NEW`, `CsvType.USED`, `CsvType.REFURBISHED`, `CsvType.RATING`, …) — pull the row off the raw Keepa response and pass it through `parsePriceHistory`. The full 36-value mapping is exported as `CsvType` (matches Keepa's own enum names exactly).
121
+
93
122
  ### Helpers
94
123
 
95
124
  ```ts
@@ -106,9 +135,8 @@ for (const product of real) {
106
135
  | Helper | Signature | Returns |
107
136
  |--------|-----------|---------|
108
137
  | `isFoundProduct(product)` | `(product: KeepaProduct) => boolean` | `true` only if Keepa returned real data (stubs have `title === null`). |
109
- | `parseImagesCsv(csv)` | `(csv: string \| undefined) => string[]` | Build the full image-URL list from a raw Keepa imagesCSV. Used internally to fill `images`; exported for advanced use. |
110
138
  | `extractBsr(salesRanks, rootCategory)` | `(salesRanks: Record<string, number[]> \| undefined, rootCategory: number \| undefined) => number \| null` | Most recent real BSR from Keepa's raw `[ts, rank, ...]` history. Used internally to fill `bsr`; exported for advanced use. |
111
- | `extractImageUrl(imagesCSV)` | `(imagesCSV: string \| undefined) => string \| null` | Single URL equivalent to `parseImagesCsv(imagesCSV)[0] ?? null`. Exported for advanced use. |
139
+ | `parsePriceHistory(series)` | `(series: number[] \| undefined) => PriceHistoryEntry[]` | Parse one row of Keepa's csv matrix (e.g. `csv[CsvType.AMAZON]`, `csv[CsvType.LISTPRICE]`) into `{ timestamp, price }` entries — `price` in the marketplace's major unit. `-1` sentinels filtered out. Used internally to fill `history.price.*`; exported so callers can parse other csv types via `CsvType`. |
112
140
 
113
141
  ### ASIN validation
114
142
 
package/dist/index.cjs CHANGED
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  ASIN_LENGTH: () => ASIN_LENGTH,
27
27
  ASIN_REGEX: () => ASIN_REGEX,
28
28
  AuthenticationError: () => AuthenticationError,
29
+ CsvType: () => CsvType,
29
30
  DEFAULT_DAYS: () => DEFAULT_DAYS,
30
31
  KEEPA_NO_DATA_SENTINEL: () => KEEPA_NO_DATA_SENTINEL,
31
32
  KeepaClient: () => KeepaClient,
@@ -43,6 +44,7 @@ __export(index_exports, {
43
44
  isFoundProduct: () => isFoundProduct,
44
45
  isValidAsin: () => isValidAsin,
45
46
  normalizeAsins: () => normalizeAsins,
47
+ parsePriceHistory: () => parsePriceHistory,
46
48
  resolveDomainId: () => resolveDomainId
47
49
  });
48
50
  module.exports = __toCommonJS(index_exports);
@@ -194,6 +196,45 @@ var PRODUCT_LIST_CONTEXT = "products.list";
194
196
  var DEFAULT_DAYS = 1;
195
197
  var AMAZON_IMAGE_BASE = "https://m.media-amazon.com/images/I";
196
198
  var KEEPA_NO_DATA_SENTINEL = -1;
199
+ var CsvType = {
200
+ AMAZON: 0,
201
+ NEW: 1,
202
+ USED: 2,
203
+ SALES: 3,
204
+ LISTPRICE: 4,
205
+ COLLECTIBLE: 5,
206
+ REFURBISHED: 6,
207
+ NEW_FBM_SHIPPING: 7,
208
+ LIGHTNING_DEAL: 8,
209
+ WAREHOUSE: 9,
210
+ NEW_FBA: 10,
211
+ COUNT_NEW: 11,
212
+ COUNT_USED: 12,
213
+ COUNT_REFURBISHED: 13,
214
+ COUNT_COLLECTIBLE: 14,
215
+ EXTRA_INFO_UPDATES: 15,
216
+ RATING: 16,
217
+ COUNT_REVIEWS: 17,
218
+ BUY_BOX_SHIPPING: 18,
219
+ USED_NEW_SHIPPING: 19,
220
+ USED_VERY_GOOD_SHIPPING: 20,
221
+ USED_GOOD_SHIPPING: 21,
222
+ USED_ACCEPTABLE_SHIPPING: 22,
223
+ COLLECTIBLE_NEW_SHIPPING: 23,
224
+ COLLECTIBLE_VERY_GOOD_SHIPPING: 24,
225
+ COLLECTIBLE_GOOD_SHIPPING: 25,
226
+ COLLECTIBLE_ACCEPTABLE_SHIPPING: 26,
227
+ REFURBISHED_SHIPPING: 27,
228
+ EBAY_NEW_SHIPPING: 28,
229
+ EBAY_USED_SHIPPING: 29,
230
+ TRADE_IN: 30,
231
+ RENTAL: 31,
232
+ BUY_BOX_USED_SHIPPING: 32,
233
+ PRIME_EXCL: 33,
234
+ COUNT_NEW_FBA: 34,
235
+ COUNT_NEW_FBM: 35
236
+ };
237
+ var KEEPA_EPOCH_UNIX_MS = 129384e7;
197
238
  var VALID_IMAGE_FILENAME = /^[A-Za-z0-9._-]+\.(jpg|jpeg|png|webp)$/i;
198
239
 
199
240
  // src/resources/products/error.ts
@@ -208,6 +249,8 @@ var ProductNotFoundError = class extends KeepaError {
208
249
 
209
250
  // src/resources/products/product.util.ts
210
251
  function toKeepaProduct(raw) {
252
+ const amazon = parsePriceHistory(raw.csv?.[CsvType.AMAZON]);
253
+ const list = parsePriceHistory(raw.csv?.[CsvType.LISTPRICE]);
211
254
  return {
212
255
  asin: raw.asin,
213
256
  title: raw.title,
@@ -219,9 +262,33 @@ function toKeepaProduct(raw) {
219
262
  variations: raw.variations,
220
263
  features: raw.features,
221
264
  images: rawImagesToUrls(raw.images),
222
- bsr: extractBsr(raw.salesRanks, raw.rootCategory)
265
+ bsr: extractBsr(raw.salesRanks, raw.rootCategory),
266
+ price: amazon.at(-1)?.price ?? null,
267
+ listPrice: list.at(-1)?.price ?? null,
268
+ history: {
269
+ price: { amazon, list }
270
+ }
223
271
  };
224
272
  }
273
+ function pairKeepaSeries(series) {
274
+ if (!series) return [];
275
+ const points = [];
276
+ for (let i = 0; i + 1 < series.length; i += 2) {
277
+ const value = series[i + 1];
278
+ if (value === KEEPA_NO_DATA_SENTINEL) continue;
279
+ points.push({ timestamp: series[i], value });
280
+ }
281
+ return points;
282
+ }
283
+ function keepaMinutesToDate(minutes) {
284
+ return new Date(minutes * 6e4 + KEEPA_EPOCH_UNIX_MS);
285
+ }
286
+ function parsePriceHistory(series) {
287
+ return pairKeepaSeries(series).map(({ timestamp, value }) => ({
288
+ timestamp: keepaMinutesToDate(timestamp),
289
+ price: value / 100
290
+ }));
291
+ }
225
292
  function rawImagesToUrls(images) {
226
293
  if (!images || images.length === 0) return [];
227
294
  return images.filter((img) => typeof img.l === "string" && VALID_IMAGE_FILENAME.test(img.l)).map((img) => `${AMAZON_IMAGE_BASE}/${img.l}`);
@@ -231,14 +298,7 @@ function isFoundProduct(product) {
231
298
  }
232
299
  function extractBsr(salesRanks, rootCategory) {
233
300
  if (!salesRanks || rootCategory === void 0) return null;
234
- const ranks = salesRanks[String(rootCategory)];
235
- if (!ranks || ranks.length < 2) return null;
236
- const start = ranks.length % 2 === 0 ? ranks.length - 1 : ranks.length - 2;
237
- for (let i = start; i >= 1; i -= 2) {
238
- const rank = ranks[i];
239
- if (rank !== void 0 && rank !== KEEPA_NO_DATA_SENTINEL) return rank;
240
- }
241
- return null;
301
+ return pairKeepaSeries(salesRanks[String(rootCategory)]).at(-1)?.value ?? null;
242
302
  }
243
303
 
244
304
  // src/resources/products/products.ts
@@ -257,14 +317,14 @@ var Products = class extends APIResource {
257
317
  query: {
258
318
  domain,
259
319
  asin: asins,
260
- days: params.days ?? DEFAULT_DAYS
320
+ days: params.days ?? DEFAULT_DAYS,
321
+ history: params.history ? 1 : 0
261
322
  },
262
323
  context: PRODUCT_LIST_CONTEXT
263
324
  });
264
325
  return (data.products ?? []).map(toKeepaProduct);
265
326
  }
266
- /** Fetch a single product by ASIN. Throws `ProductNotFoundError` when Keepa
267
- * has no record for the ASIN (no product returned, or a stub with no title). */
327
+ /** Throws `ProductNotFoundError` when Keepa returns no product or a stub. */
268
328
  async retrieve(params) {
269
329
  const { asin, ...rest } = params;
270
330
  const [product] = await this.list({ ...rest, asins: [asin] });
@@ -295,7 +355,6 @@ var KeepaClient = class {
295
355
  this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
296
356
  this.products = new Products(this);
297
357
  }
298
- /** Internal: used by APIResource subclasses to perform a request. */
299
358
  _request(args) {
300
359
  const config = {
301
360
  apiKey: this.apiKey,
@@ -316,6 +375,7 @@ var VERSION = "0.1.0";
316
375
  ASIN_LENGTH,
317
376
  ASIN_REGEX,
318
377
  AuthenticationError,
378
+ CsvType,
319
379
  DEFAULT_DAYS,
320
380
  KEEPA_NO_DATA_SENTINEL,
321
381
  KeepaClient,
@@ -332,6 +392,7 @@ var VERSION = "0.1.0";
332
392
  isFoundProduct,
333
393
  isValidAsin,
334
394
  normalizeAsins,
395
+ parsePriceHistory,
335
396
  resolveDomainId
336
397
  });
337
398
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/core/error.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 { 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","/** Replace any `?key=...` or `&key=...` segment with a REDACTED placeholder.\n * Defense-in-depth: keeps the API key out of error messages even when an\n * underlying transport error or upstream response echoes the request URL.\n * Exported for use by callers that build their own error wrappers. */\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 async from(response: Response, context: string): Promise<APIError> {\n const body = await response.text().catch(() => '');\n switch (response.status) {\n case 429:\n return new RateLimitError(context, body);\n case 401:\n return new AuthenticationError(context, body);\n default:\n return new APIError(response.status, context, body);\n }\n }\n}\n\nexport class RateLimitError extends APIError {\n constructor(context: string, body: string) {\n super(429, context, body, 'Keepa rate limit exceeded, please wait or upgrade plan');\n this.name = 'RateLimitError';\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","import { APIError, NetworkError } from './error.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}\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 accepts comma-separated lists (asin=A,B,C, category=1,2,3)\n// and the category endpoint specifically rejects %2C-encoded commas.\n// Currently private to this module: if a future helper builds Keepa URLs outside\n// `request()` (e.g. a paginator) and needs the same encoding, lift this into\n// `core/encoding.ts` rather than duplicating it.\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 if (!res.ok) throw await APIError.from(res, args.context);\n return (await res.json()) as T;\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","// The full set of marketplaces Keepa currently supports. Codes are ISO 3166-1\n// alpha-2; the numeric id is Keepa's `domain` query parameter value. Domain 7\n// is reserved (formerly Amazon China, retired). Update the README marketplace\n// table alongside any change here.\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/** Returns true if the given string is a structurally valid ASIN\n * (10 uppercase alphanumeric characters). Note: a passing ASIN may still\n * not exist in Keepa's database — use `isFoundProduct` to check that. */\nexport function isValidAsin(value: string): boolean {\n return ASIN_REGEX.test(value);\n}\n\n/** Trim + uppercase a list of ASINs and validate each. Returns the normalized\n * list. Throws with all invalid ASINs listed in the error message. */\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// Structured tag of the form `<resource>.<method>`. Used as the `context` field on\n// errors thrown from this method — keeps log aggregators able to filter cleanly as\n// more methods land (categories.list, categories.search, bestsellers.retrieve, …).\nexport const PRODUCT_LIST_CONTEXT = 'products.list';\nexport const DEFAULT_DAYS = 1;\n\n// Region-neutral Amazon image CDN. Serves the same images globally regardless of marketplace.\nexport const AMAZON_IMAGE_BASE = 'https://m.media-amazon.com/images/I';\n\n// Keepa stores `-1` in salesRanks/price arrays to indicate \"no data captured at that timestamp\".\nexport const KEEPA_NO_DATA_SENTINEL = -1;\n\n// Allowlist for image filenames in imagesCSV. Defends against path-traversal / SSRF\n// shaped strings (e.g. \"../../etc/passwd\") in case the CSV is ever influenced by\n// untrusted input. Matches typical Keepa image hashes (e.g. \"61abcDEF.jpg\").\nexport const VALID_IMAGE_FILENAME = /^[A-Za-z0-9._-]+\\.(jpg|jpeg|png|webp)$/i;\n","import { KeepaError } from '../../core/error.js';\n\n/** Thrown by `Products.retrieve` when Keepa has no record for the requested ASIN\n * (either no product object returned, or a stub with no title). The `asin`\n * property carries the value the caller passed in, unchanged, so it can be\n * surfaced in user-facing messages without re-deriving it. */\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 KEEPA_NO_DATA_SENTINEL,\n VALID_IMAGE_FILENAME,\n} from './constant.js';\nimport type { KeepaProduct } from './product.type.js';\nimport type { KeepaImageRaw, KeepaProductRaw } from './product.raw.type.js';\n\n/** Map Keepa's raw wire shape into the consumer-friendly KeepaProduct. */\nexport function toKeepaProduct(raw: KeepaProductRaw): KeepaProduct {\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 };\n}\n\n/** Convert Keepa's raw images array into full Amazon image URLs (the large\n * variant). Filters entries whose filename doesn't match the expected\n * alphanumeric image-name pattern as defense-in-depth. */\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/** Returns true when Keepa returned an actual product record (not just a stub\n * for an unknown ASIN). Stubs come back with `title: null`; real listings\n * always have a string. Use as a `.filter()` predicate to drop empty matches. */\nexport function isFoundProduct(product: KeepaProduct): boolean {\n return product.title != null;\n}\n\n/** Extract the most recent real BSR from Keepa's `[ts, rank, ts, rank, ...]` salesRanks array.\n * Walks backward through rank entries (odd indices) and skips Keepa's `-1` sentinel which\n * marks \"no data captured at that timestamp\". Returns `null` if every entry is sentinel.\n * Most consumers won't need this — `Products.list` already fills `bsr` on every returned\n * product. Useful when working with a raw Keepa response. */\nexport function extractBsr(\n salesRanks: Record<string, number[]> | undefined,\n rootCategory: number | undefined,\n): number | null {\n if (!salesRanks || rootCategory === undefined) return null;\n const ranks = salesRanks[String(rootCategory)];\n if (!ranks || ranks.length < 2) return null;\n // Keepa pairs are [ts, rank, ts, rank, ...]. If length is even, the last index\n // is a rank; if odd (truncated/schema drift), it's a dangling timestamp — skip\n // back one slot so we always start on a rank.\n const start = ranks.length % 2 === 0 ? ranks.length - 1 : ranks.length - 2;\n for (let i = start; i >= 1; i -= 2) {\n const rank = ranks[i];\n if (rank !== undefined && rank !== KEEPA_NO_DATA_SENTINEL) return rank;\n }\n return 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 },\n context: PRODUCT_LIST_CONTEXT,\n });\n return (data.products ?? []).map(toKeepaProduct);\n }\n\n /** Fetch a single product by ASIN. Throws `ProductNotFoundError` when Keepa\n * has no record for the ASIN (no product returned, or a stub with no title). */\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 { Products } from './resources/products/products.js';\n\nexport interface ClientOptions {\n /** Keepa API key. Falls back to `process.env.KEEPA_API_KEY`. */\n apiKey?: string;\n /** Base URL for the Keepa API. Defaults to `https://api.keepa.com`. */\n baseURL?: string;\n /** Custom fetch implementation. Defaults to `globalThis.fetch`. */\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 constructor(options: ClientOptions = {}) {\n // `process` is undefined in browsers / Cloudflare Workers / edge runtimes — guard it\n // so the constructor throws our friendly \"Missing Keepa API key\" message instead of a\n // raw ReferenceError on those targets.\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 a trailing slash so `${baseURL}${path}` never produces `//product`.\n this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n // Bind `globalThis` once: an unbound reference to Node's fetch throws\n // \"Illegal invocation\" when called with an undefined `this`. Bind once at\n // construction so every _request call uses the cached bound function.\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);\n\n this.products = new Products(this);\n }\n\n /** Internal: used by APIResource subclasses to perform a request. */\n _request<T>(args: RequestArgs): Promise<T> {\n const config: RequestConfig = {\n apiKey: this.apiKey,\n baseURL: this.baseURL,\n fetch: this.fetch,\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/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 { 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","/** 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 async from(response: Response, context: string): Promise<APIError> {\n const body = await response.text().catch(() => '');\n switch (response.status) {\n case 429:\n return new RateLimitError(context, body);\n case 401:\n return new AuthenticationError(context, body);\n default:\n return new APIError(response.status, context, body);\n }\n }\n}\n\nexport class RateLimitError extends APIError {\n constructor(context: string, body: string) {\n super(429, context, body, 'Keepa rate limit exceeded, please wait or upgrade plan');\n this.name = 'RateLimitError';\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","import { APIError, NetworkError } from './error.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}\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 if (!res.ok) throw await APIError.from(res, args.context);\n return (await res.json()) as T;\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 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 price: amazon.at(-1)?.price ?? null,\n listPrice: list.at(-1)?.price ?? null,\n history: {\n price: { amazon, 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 { 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 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 };\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":[]}
package/dist/index.d.cts CHANGED
@@ -28,19 +28,20 @@ type Marketplace = keyof typeof MARKETPLACE_DOMAINS;
28
28
  type DomainId = (typeof MARKETPLACE_DOMAINS)[Marketplace];
29
29
  declare function resolveDomainId(marketplace: string | undefined): DomainId;
30
30
 
31
- interface ProductListParams {
32
- asins: string[];
33
- /** Marketplace code (case-insensitive at runtime). Defaults to 'US'. */
31
+ interface ProductRequestOptions {
32
+ /** Case-insensitive at runtime. Defaults to 'US'. */
34
33
  marketplace?: Marketplace;
35
- /** Days of price history to include. Defaults to 1. */
34
+ /** Defaults to 1. */
36
35
  days?: number;
36
+ /** When true, populates `history.price.*` and the scalar `price` / `listPrice`
37
+ * fields on each returned product. Increases token cost. */
38
+ history?: boolean;
39
+ }
40
+ interface ProductListParams extends ProductRequestOptions {
41
+ asins: string[];
37
42
  }
38
- interface ProductRetrieveParams {
43
+ interface ProductRetrieveParams extends ProductRequestOptions {
39
44
  asin: string;
40
- /** Marketplace code (case-insensitive at runtime). Defaults to 'US'. */
41
- marketplace?: Marketplace;
42
- /** Days of price history to include. Defaults to 1. */
43
- days?: number;
44
45
  }
45
46
  interface KeepaCategoryNode {
46
47
  catId: number;
@@ -54,10 +55,14 @@ interface KeepaVariation {
54
55
  asin: string;
55
56
  attributes?: KeepaVariationAttribute[];
56
57
  }
57
- /** A Keepa product in the consumer-friendly shape. Derived from the raw Keepa
58
- * response: `imagesCSV` is split into `images: string[]`, and the most-recent
59
- * real BSR is surfaced as `bsr`. The raw `salesRanks` is preserved for
60
- * consumers that need to walk the rank history themselves. */
58
+ interface PriceHistoryEntry {
59
+ timestamp: Date;
60
+ /** Marketplace's major unit dollars for US, pounds for GB, yen for JP, etc. */
61
+ price: number;
62
+ }
63
+ /** Flag-gated fields (`price`, `listPrice`, `history.price.*`) are always present
64
+ * on the type and default to empty/null when the corresponding request flag
65
+ * wasn't set. */
61
66
  interface KeepaProduct {
62
67
  asin: string;
63
68
  title?: string;
@@ -68,26 +73,35 @@ interface KeepaProduct {
68
73
  salesRanks?: Record<string, number[]>;
69
74
  variations?: KeepaVariation[];
70
75
  features?: string[];
71
- /** Full image URLs derived from imagesCSV using a region-neutral Amazon CDN. */
72
76
  images: string[];
73
- /** Most recent real BSR for the rootCategory. Null when missing or every entry is
74
- * Keepa's `-1` "no data captured" sentinel. */
77
+ /** Null when missing or every history entry is Keepa's `-1` no-data sentinel. */
75
78
  bsr: number | null;
79
+ /** Latest entry from `history.price.amazon`; null when history wasn't
80
+ * requested or Keepa has no data. Marketplace's major unit. */
81
+ price: number | null;
82
+ /** Latest entry from `history.price.list`; null when unavailable.
83
+ * Marketplace's major unit. */
84
+ listPrice: number | null;
85
+ history: {
86
+ price: {
87
+ /** Empty when `history: false`. Sentinel-filtered. */
88
+ amazon: PriceHistoryEntry[];
89
+ /** Empty when `history: false`. Sentinel-filtered. */
90
+ list: PriceHistoryEntry[];
91
+ };
92
+ };
76
93
  }
77
94
 
78
95
  declare class Products extends APIResource {
79
96
  list(params: ProductListParams): Promise<KeepaProduct[]>;
80
- /** Fetch a single product by ASIN. Throws `ProductNotFoundError` when Keepa
81
- * has no record for the ASIN (no product returned, or a stub with no title). */
97
+ /** Throws `ProductNotFoundError` when Keepa returns no product or a stub. */
82
98
  retrieve(params: ProductRetrieveParams): Promise<KeepaProduct>;
83
99
  }
84
100
 
85
101
  interface ClientOptions {
86
- /** Keepa API key. Falls back to `process.env.KEEPA_API_KEY`. */
102
+ /** Falls back to `process.env.KEEPA_API_KEY` when omitted. */
87
103
  apiKey?: string;
88
- /** Base URL for the Keepa API. Defaults to `https://api.keepa.com`. */
89
104
  baseURL?: string;
90
- /** Custom fetch implementation. Defaults to `globalThis.fetch`. */
91
105
  fetch?: typeof globalThis.fetch;
92
106
  }
93
107
  declare class KeepaClient {
@@ -96,7 +110,6 @@ declare class KeepaClient {
96
110
  readonly fetch: typeof globalThis.fetch;
97
111
  readonly products: Products;
98
112
  constructor(options?: ClientOptions);
99
- /** Internal: used by APIResource subclasses to perform a request. */
100
113
  _request<T>(args: RequestArgs): Promise<T>;
101
114
  }
102
115
 
@@ -124,29 +137,20 @@ declare class NetworkError extends KeepaError {
124
137
 
125
138
  declare const ASIN_LENGTH = 10;
126
139
  declare const ASIN_REGEX: RegExp;
127
- /** Returns true if the given string is a structurally valid ASIN
128
- * (10 uppercase alphanumeric characters). Note: a passing ASIN may still
129
- * not exist in Keepa's database — use `isFoundProduct` to check that. */
140
+ /** Structural check only a passing ASIN may still not exist in Keepa's
141
+ * database; use `isFoundProduct` for that. */
130
142
  declare function isValidAsin(value: string): boolean;
131
- /** Trim + uppercase a list of ASINs and validate each. Returns the normalized
132
- * list. Throws with all invalid ASINs listed in the error message. */
133
143
  declare function normalizeAsins(asins: string[]): string[];
134
144
 
135
- /** Returns true when Keepa returned an actual product record (not just a stub
136
- * for an unknown ASIN). Stubs come back with `title: null`; real listings
137
- * always have a string. Use as a `.filter()` predicate to drop empty matches. */
145
+ declare function parsePriceHistory(series: number[] | undefined): PriceHistoryEntry[];
146
+ /** Stubs for unknown ASINs come back with `title: null`. */
138
147
  declare function isFoundProduct(product: KeepaProduct): boolean;
139
- /** Extract the most recent real BSR from Keepa's `[ts, rank, ts, rank, ...]` salesRanks array.
140
- * Walks backward through rank entries (odd indices) and skips Keepa's `-1` sentinel which
141
- * marks "no data captured at that timestamp". Returns `null` if every entry is sentinel.
142
- * Most consumers won't need this — `Products.list` already fills `bsr` on every returned
143
- * product. Useful when working with a raw Keepa response. */
148
+ /** Latest non-sentinel rank from Keepa's `[ts, rank, ts, rank, ...]` salesRanks
149
+ * series for the product's rootCategory. */
144
150
  declare function extractBsr(salesRanks: Record<string, number[]> | undefined, rootCategory: number | undefined): number | null;
145
151
 
146
- /** Thrown by `Products.retrieve` when Keepa has no record for the requested ASIN
147
- * (either no product object returned, or a stub with no title). The `asin`
148
- * property carries the value the caller passed in, unchanged, so it can be
149
- * surfaced in user-facing messages without re-deriving it. */
152
+ /** Thrown when Keepa returns no product, or returns a stub (title === null).
153
+ * `asin` carries the caller's original input unchanged. */
150
154
  declare class ProductNotFoundError extends KeepaError {
151
155
  readonly asin: string;
152
156
  constructor(asin: string);
@@ -157,7 +161,46 @@ declare const PRODUCT_LIST_CONTEXT = "products.list";
157
161
  declare const DEFAULT_DAYS = 1;
158
162
  declare const AMAZON_IMAGE_BASE = "https://m.media-amazon.com/images/I";
159
163
  declare const KEEPA_NO_DATA_SENTINEL = -1;
164
+ declare const CsvType: {
165
+ readonly AMAZON: 0;
166
+ readonly NEW: 1;
167
+ readonly USED: 2;
168
+ readonly SALES: 3;
169
+ readonly LISTPRICE: 4;
170
+ readonly COLLECTIBLE: 5;
171
+ readonly REFURBISHED: 6;
172
+ readonly NEW_FBM_SHIPPING: 7;
173
+ readonly LIGHTNING_DEAL: 8;
174
+ readonly WAREHOUSE: 9;
175
+ readonly NEW_FBA: 10;
176
+ readonly COUNT_NEW: 11;
177
+ readonly COUNT_USED: 12;
178
+ readonly COUNT_REFURBISHED: 13;
179
+ readonly COUNT_COLLECTIBLE: 14;
180
+ readonly EXTRA_INFO_UPDATES: 15;
181
+ readonly RATING: 16;
182
+ readonly COUNT_REVIEWS: 17;
183
+ readonly BUY_BOX_SHIPPING: 18;
184
+ readonly USED_NEW_SHIPPING: 19;
185
+ readonly USED_VERY_GOOD_SHIPPING: 20;
186
+ readonly USED_GOOD_SHIPPING: 21;
187
+ readonly USED_ACCEPTABLE_SHIPPING: 22;
188
+ readonly COLLECTIBLE_NEW_SHIPPING: 23;
189
+ readonly COLLECTIBLE_VERY_GOOD_SHIPPING: 24;
190
+ readonly COLLECTIBLE_GOOD_SHIPPING: 25;
191
+ readonly COLLECTIBLE_ACCEPTABLE_SHIPPING: 26;
192
+ readonly REFURBISHED_SHIPPING: 27;
193
+ readonly EBAY_NEW_SHIPPING: 28;
194
+ readonly EBAY_USED_SHIPPING: 29;
195
+ readonly TRADE_IN: 30;
196
+ readonly RENTAL: 31;
197
+ readonly BUY_BOX_USED_SHIPPING: 32;
198
+ readonly PRIME_EXCL: 33;
199
+ readonly COUNT_NEW_FBA: 34;
200
+ readonly COUNT_NEW_FBM: 35;
201
+ };
202
+ type CsvType = (typeof CsvType)[keyof typeof CsvType];
160
203
 
161
204
  declare const VERSION = "0.1.0";
162
205
 
163
- export { AMAZON_IMAGE_BASE, APIError, APIResource, ASIN_LENGTH, ASIN_REGEX, AuthenticationError, type ClientOptions, DEFAULT_DAYS, type DomainId, KEEPA_NO_DATA_SENTINEL, type KeepaCategoryNode, KeepaClient, KeepaError, type KeepaProduct, type KeepaVariation, type KeepaVariationAttribute, MARKETPLACE_DOMAINS, type Marketplace, NetworkError, PRODUCT_LIST_CONTEXT, PRODUCT_PATH, type ProductListParams, ProductNotFoundError, type ProductRetrieveParams, Products, RateLimitError, VERSION, KeepaClient as default, extractBsr, isFoundProduct, isValidAsin, normalizeAsins, resolveDomainId };
206
+ export { AMAZON_IMAGE_BASE, APIError, APIResource, ASIN_LENGTH, ASIN_REGEX, AuthenticationError, type ClientOptions, CsvType, DEFAULT_DAYS, type DomainId, KEEPA_NO_DATA_SENTINEL, type KeepaCategoryNode, KeepaClient, KeepaError, type KeepaProduct, type KeepaVariation, type KeepaVariationAttribute, MARKETPLACE_DOMAINS, type Marketplace, NetworkError, PRODUCT_LIST_CONTEXT, PRODUCT_PATH, type PriceHistoryEntry, type ProductListParams, ProductNotFoundError, type ProductRequestOptions, type ProductRetrieveParams, Products, RateLimitError, VERSION, KeepaClient as default, extractBsr, isFoundProduct, isValidAsin, normalizeAsins, parsePriceHistory, resolveDomainId };
package/dist/index.d.ts CHANGED
@@ -28,19 +28,20 @@ type Marketplace = keyof typeof MARKETPLACE_DOMAINS;
28
28
  type DomainId = (typeof MARKETPLACE_DOMAINS)[Marketplace];
29
29
  declare function resolveDomainId(marketplace: string | undefined): DomainId;
30
30
 
31
- interface ProductListParams {
32
- asins: string[];
33
- /** Marketplace code (case-insensitive at runtime). Defaults to 'US'. */
31
+ interface ProductRequestOptions {
32
+ /** Case-insensitive at runtime. Defaults to 'US'. */
34
33
  marketplace?: Marketplace;
35
- /** Days of price history to include. Defaults to 1. */
34
+ /** Defaults to 1. */
36
35
  days?: number;
36
+ /** When true, populates `history.price.*` and the scalar `price` / `listPrice`
37
+ * fields on each returned product. Increases token cost. */
38
+ history?: boolean;
39
+ }
40
+ interface ProductListParams extends ProductRequestOptions {
41
+ asins: string[];
37
42
  }
38
- interface ProductRetrieveParams {
43
+ interface ProductRetrieveParams extends ProductRequestOptions {
39
44
  asin: string;
40
- /** Marketplace code (case-insensitive at runtime). Defaults to 'US'. */
41
- marketplace?: Marketplace;
42
- /** Days of price history to include. Defaults to 1. */
43
- days?: number;
44
45
  }
45
46
  interface KeepaCategoryNode {
46
47
  catId: number;
@@ -54,10 +55,14 @@ interface KeepaVariation {
54
55
  asin: string;
55
56
  attributes?: KeepaVariationAttribute[];
56
57
  }
57
- /** A Keepa product in the consumer-friendly shape. Derived from the raw Keepa
58
- * response: `imagesCSV` is split into `images: string[]`, and the most-recent
59
- * real BSR is surfaced as `bsr`. The raw `salesRanks` is preserved for
60
- * consumers that need to walk the rank history themselves. */
58
+ interface PriceHistoryEntry {
59
+ timestamp: Date;
60
+ /** Marketplace's major unit dollars for US, pounds for GB, yen for JP, etc. */
61
+ price: number;
62
+ }
63
+ /** Flag-gated fields (`price`, `listPrice`, `history.price.*`) are always present
64
+ * on the type and default to empty/null when the corresponding request flag
65
+ * wasn't set. */
61
66
  interface KeepaProduct {
62
67
  asin: string;
63
68
  title?: string;
@@ -68,26 +73,35 @@ interface KeepaProduct {
68
73
  salesRanks?: Record<string, number[]>;
69
74
  variations?: KeepaVariation[];
70
75
  features?: string[];
71
- /** Full image URLs derived from imagesCSV using a region-neutral Amazon CDN. */
72
76
  images: string[];
73
- /** Most recent real BSR for the rootCategory. Null when missing or every entry is
74
- * Keepa's `-1` "no data captured" sentinel. */
77
+ /** Null when missing or every history entry is Keepa's `-1` no-data sentinel. */
75
78
  bsr: number | null;
79
+ /** Latest entry from `history.price.amazon`; null when history wasn't
80
+ * requested or Keepa has no data. Marketplace's major unit. */
81
+ price: number | null;
82
+ /** Latest entry from `history.price.list`; null when unavailable.
83
+ * Marketplace's major unit. */
84
+ listPrice: number | null;
85
+ history: {
86
+ price: {
87
+ /** Empty when `history: false`. Sentinel-filtered. */
88
+ amazon: PriceHistoryEntry[];
89
+ /** Empty when `history: false`. Sentinel-filtered. */
90
+ list: PriceHistoryEntry[];
91
+ };
92
+ };
76
93
  }
77
94
 
78
95
  declare class Products extends APIResource {
79
96
  list(params: ProductListParams): Promise<KeepaProduct[]>;
80
- /** Fetch a single product by ASIN. Throws `ProductNotFoundError` when Keepa
81
- * has no record for the ASIN (no product returned, or a stub with no title). */
97
+ /** Throws `ProductNotFoundError` when Keepa returns no product or a stub. */
82
98
  retrieve(params: ProductRetrieveParams): Promise<KeepaProduct>;
83
99
  }
84
100
 
85
101
  interface ClientOptions {
86
- /** Keepa API key. Falls back to `process.env.KEEPA_API_KEY`. */
102
+ /** Falls back to `process.env.KEEPA_API_KEY` when omitted. */
87
103
  apiKey?: string;
88
- /** Base URL for the Keepa API. Defaults to `https://api.keepa.com`. */
89
104
  baseURL?: string;
90
- /** Custom fetch implementation. Defaults to `globalThis.fetch`. */
91
105
  fetch?: typeof globalThis.fetch;
92
106
  }
93
107
  declare class KeepaClient {
@@ -96,7 +110,6 @@ declare class KeepaClient {
96
110
  readonly fetch: typeof globalThis.fetch;
97
111
  readonly products: Products;
98
112
  constructor(options?: ClientOptions);
99
- /** Internal: used by APIResource subclasses to perform a request. */
100
113
  _request<T>(args: RequestArgs): Promise<T>;
101
114
  }
102
115
 
@@ -124,29 +137,20 @@ declare class NetworkError extends KeepaError {
124
137
 
125
138
  declare const ASIN_LENGTH = 10;
126
139
  declare const ASIN_REGEX: RegExp;
127
- /** Returns true if the given string is a structurally valid ASIN
128
- * (10 uppercase alphanumeric characters). Note: a passing ASIN may still
129
- * not exist in Keepa's database — use `isFoundProduct` to check that. */
140
+ /** Structural check only a passing ASIN may still not exist in Keepa's
141
+ * database; use `isFoundProduct` for that. */
130
142
  declare function isValidAsin(value: string): boolean;
131
- /** Trim + uppercase a list of ASINs and validate each. Returns the normalized
132
- * list. Throws with all invalid ASINs listed in the error message. */
133
143
  declare function normalizeAsins(asins: string[]): string[];
134
144
 
135
- /** Returns true when Keepa returned an actual product record (not just a stub
136
- * for an unknown ASIN). Stubs come back with `title: null`; real listings
137
- * always have a string. Use as a `.filter()` predicate to drop empty matches. */
145
+ declare function parsePriceHistory(series: number[] | undefined): PriceHistoryEntry[];
146
+ /** Stubs for unknown ASINs come back with `title: null`. */
138
147
  declare function isFoundProduct(product: KeepaProduct): boolean;
139
- /** Extract the most recent real BSR from Keepa's `[ts, rank, ts, rank, ...]` salesRanks array.
140
- * Walks backward through rank entries (odd indices) and skips Keepa's `-1` sentinel which
141
- * marks "no data captured at that timestamp". Returns `null` if every entry is sentinel.
142
- * Most consumers won't need this — `Products.list` already fills `bsr` on every returned
143
- * product. Useful when working with a raw Keepa response. */
148
+ /** Latest non-sentinel rank from Keepa's `[ts, rank, ts, rank, ...]` salesRanks
149
+ * series for the product's rootCategory. */
144
150
  declare function extractBsr(salesRanks: Record<string, number[]> | undefined, rootCategory: number | undefined): number | null;
145
151
 
146
- /** Thrown by `Products.retrieve` when Keepa has no record for the requested ASIN
147
- * (either no product object returned, or a stub with no title). The `asin`
148
- * property carries the value the caller passed in, unchanged, so it can be
149
- * surfaced in user-facing messages without re-deriving it. */
152
+ /** Thrown when Keepa returns no product, or returns a stub (title === null).
153
+ * `asin` carries the caller's original input unchanged. */
150
154
  declare class ProductNotFoundError extends KeepaError {
151
155
  readonly asin: string;
152
156
  constructor(asin: string);
@@ -157,7 +161,46 @@ declare const PRODUCT_LIST_CONTEXT = "products.list";
157
161
  declare const DEFAULT_DAYS = 1;
158
162
  declare const AMAZON_IMAGE_BASE = "https://m.media-amazon.com/images/I";
159
163
  declare const KEEPA_NO_DATA_SENTINEL = -1;
164
+ declare const CsvType: {
165
+ readonly AMAZON: 0;
166
+ readonly NEW: 1;
167
+ readonly USED: 2;
168
+ readonly SALES: 3;
169
+ readonly LISTPRICE: 4;
170
+ readonly COLLECTIBLE: 5;
171
+ readonly REFURBISHED: 6;
172
+ readonly NEW_FBM_SHIPPING: 7;
173
+ readonly LIGHTNING_DEAL: 8;
174
+ readonly WAREHOUSE: 9;
175
+ readonly NEW_FBA: 10;
176
+ readonly COUNT_NEW: 11;
177
+ readonly COUNT_USED: 12;
178
+ readonly COUNT_REFURBISHED: 13;
179
+ readonly COUNT_COLLECTIBLE: 14;
180
+ readonly EXTRA_INFO_UPDATES: 15;
181
+ readonly RATING: 16;
182
+ readonly COUNT_REVIEWS: 17;
183
+ readonly BUY_BOX_SHIPPING: 18;
184
+ readonly USED_NEW_SHIPPING: 19;
185
+ readonly USED_VERY_GOOD_SHIPPING: 20;
186
+ readonly USED_GOOD_SHIPPING: 21;
187
+ readonly USED_ACCEPTABLE_SHIPPING: 22;
188
+ readonly COLLECTIBLE_NEW_SHIPPING: 23;
189
+ readonly COLLECTIBLE_VERY_GOOD_SHIPPING: 24;
190
+ readonly COLLECTIBLE_GOOD_SHIPPING: 25;
191
+ readonly COLLECTIBLE_ACCEPTABLE_SHIPPING: 26;
192
+ readonly REFURBISHED_SHIPPING: 27;
193
+ readonly EBAY_NEW_SHIPPING: 28;
194
+ readonly EBAY_USED_SHIPPING: 29;
195
+ readonly TRADE_IN: 30;
196
+ readonly RENTAL: 31;
197
+ readonly BUY_BOX_USED_SHIPPING: 32;
198
+ readonly PRIME_EXCL: 33;
199
+ readonly COUNT_NEW_FBA: 34;
200
+ readonly COUNT_NEW_FBM: 35;
201
+ };
202
+ type CsvType = (typeof CsvType)[keyof typeof CsvType];
160
203
 
161
204
  declare const VERSION = "0.1.0";
162
205
 
163
- export { AMAZON_IMAGE_BASE, APIError, APIResource, ASIN_LENGTH, ASIN_REGEX, AuthenticationError, type ClientOptions, DEFAULT_DAYS, type DomainId, KEEPA_NO_DATA_SENTINEL, type KeepaCategoryNode, KeepaClient, KeepaError, type KeepaProduct, type KeepaVariation, type KeepaVariationAttribute, MARKETPLACE_DOMAINS, type Marketplace, NetworkError, PRODUCT_LIST_CONTEXT, PRODUCT_PATH, type ProductListParams, ProductNotFoundError, type ProductRetrieveParams, Products, RateLimitError, VERSION, KeepaClient as default, extractBsr, isFoundProduct, isValidAsin, normalizeAsins, resolveDomainId };
206
+ export { AMAZON_IMAGE_BASE, APIError, APIResource, ASIN_LENGTH, ASIN_REGEX, AuthenticationError, type ClientOptions, CsvType, DEFAULT_DAYS, type DomainId, KEEPA_NO_DATA_SENTINEL, type KeepaCategoryNode, KeepaClient, KeepaError, type KeepaProduct, type KeepaVariation, type KeepaVariationAttribute, MARKETPLACE_DOMAINS, type Marketplace, NetworkError, PRODUCT_LIST_CONTEXT, PRODUCT_PATH, type PriceHistoryEntry, type ProductListParams, ProductNotFoundError, type ProductRequestOptions, type ProductRetrieveParams, Products, RateLimitError, VERSION, KeepaClient as default, extractBsr, isFoundProduct, isValidAsin, normalizeAsins, parsePriceHistory, resolveDomainId };
package/dist/index.js CHANGED
@@ -145,6 +145,45 @@ var PRODUCT_LIST_CONTEXT = "products.list";
145
145
  var DEFAULT_DAYS = 1;
146
146
  var AMAZON_IMAGE_BASE = "https://m.media-amazon.com/images/I";
147
147
  var KEEPA_NO_DATA_SENTINEL = -1;
148
+ var CsvType = {
149
+ AMAZON: 0,
150
+ NEW: 1,
151
+ USED: 2,
152
+ SALES: 3,
153
+ LISTPRICE: 4,
154
+ COLLECTIBLE: 5,
155
+ REFURBISHED: 6,
156
+ NEW_FBM_SHIPPING: 7,
157
+ LIGHTNING_DEAL: 8,
158
+ WAREHOUSE: 9,
159
+ NEW_FBA: 10,
160
+ COUNT_NEW: 11,
161
+ COUNT_USED: 12,
162
+ COUNT_REFURBISHED: 13,
163
+ COUNT_COLLECTIBLE: 14,
164
+ EXTRA_INFO_UPDATES: 15,
165
+ RATING: 16,
166
+ COUNT_REVIEWS: 17,
167
+ BUY_BOX_SHIPPING: 18,
168
+ USED_NEW_SHIPPING: 19,
169
+ USED_VERY_GOOD_SHIPPING: 20,
170
+ USED_GOOD_SHIPPING: 21,
171
+ USED_ACCEPTABLE_SHIPPING: 22,
172
+ COLLECTIBLE_NEW_SHIPPING: 23,
173
+ COLLECTIBLE_VERY_GOOD_SHIPPING: 24,
174
+ COLLECTIBLE_GOOD_SHIPPING: 25,
175
+ COLLECTIBLE_ACCEPTABLE_SHIPPING: 26,
176
+ REFURBISHED_SHIPPING: 27,
177
+ EBAY_NEW_SHIPPING: 28,
178
+ EBAY_USED_SHIPPING: 29,
179
+ TRADE_IN: 30,
180
+ RENTAL: 31,
181
+ BUY_BOX_USED_SHIPPING: 32,
182
+ PRIME_EXCL: 33,
183
+ COUNT_NEW_FBA: 34,
184
+ COUNT_NEW_FBM: 35
185
+ };
186
+ var KEEPA_EPOCH_UNIX_MS = 129384e7;
148
187
  var VALID_IMAGE_FILENAME = /^[A-Za-z0-9._-]+\.(jpg|jpeg|png|webp)$/i;
149
188
 
150
189
  // src/resources/products/error.ts
@@ -159,6 +198,8 @@ var ProductNotFoundError = class extends KeepaError {
159
198
 
160
199
  // src/resources/products/product.util.ts
161
200
  function toKeepaProduct(raw) {
201
+ const amazon = parsePriceHistory(raw.csv?.[CsvType.AMAZON]);
202
+ const list = parsePriceHistory(raw.csv?.[CsvType.LISTPRICE]);
162
203
  return {
163
204
  asin: raw.asin,
164
205
  title: raw.title,
@@ -170,9 +211,33 @@ function toKeepaProduct(raw) {
170
211
  variations: raw.variations,
171
212
  features: raw.features,
172
213
  images: rawImagesToUrls(raw.images),
173
- bsr: extractBsr(raw.salesRanks, raw.rootCategory)
214
+ bsr: extractBsr(raw.salesRanks, raw.rootCategory),
215
+ price: amazon.at(-1)?.price ?? null,
216
+ listPrice: list.at(-1)?.price ?? null,
217
+ history: {
218
+ price: { amazon, list }
219
+ }
174
220
  };
175
221
  }
222
+ function pairKeepaSeries(series) {
223
+ if (!series) return [];
224
+ const points = [];
225
+ for (let i = 0; i + 1 < series.length; i += 2) {
226
+ const value = series[i + 1];
227
+ if (value === KEEPA_NO_DATA_SENTINEL) continue;
228
+ points.push({ timestamp: series[i], value });
229
+ }
230
+ return points;
231
+ }
232
+ function keepaMinutesToDate(minutes) {
233
+ return new Date(minutes * 6e4 + KEEPA_EPOCH_UNIX_MS);
234
+ }
235
+ function parsePriceHistory(series) {
236
+ return pairKeepaSeries(series).map(({ timestamp, value }) => ({
237
+ timestamp: keepaMinutesToDate(timestamp),
238
+ price: value / 100
239
+ }));
240
+ }
176
241
  function rawImagesToUrls(images) {
177
242
  if (!images || images.length === 0) return [];
178
243
  return images.filter((img) => typeof img.l === "string" && VALID_IMAGE_FILENAME.test(img.l)).map((img) => `${AMAZON_IMAGE_BASE}/${img.l}`);
@@ -182,14 +247,7 @@ function isFoundProduct(product) {
182
247
  }
183
248
  function extractBsr(salesRanks, rootCategory) {
184
249
  if (!salesRanks || rootCategory === void 0) return null;
185
- const ranks = salesRanks[String(rootCategory)];
186
- if (!ranks || ranks.length < 2) return null;
187
- const start = ranks.length % 2 === 0 ? ranks.length - 1 : ranks.length - 2;
188
- for (let i = start; i >= 1; i -= 2) {
189
- const rank = ranks[i];
190
- if (rank !== void 0 && rank !== KEEPA_NO_DATA_SENTINEL) return rank;
191
- }
192
- return null;
250
+ return pairKeepaSeries(salesRanks[String(rootCategory)]).at(-1)?.value ?? null;
193
251
  }
194
252
 
195
253
  // src/resources/products/products.ts
@@ -208,14 +266,14 @@ var Products = class extends APIResource {
208
266
  query: {
209
267
  domain,
210
268
  asin: asins,
211
- days: params.days ?? DEFAULT_DAYS
269
+ days: params.days ?? DEFAULT_DAYS,
270
+ history: params.history ? 1 : 0
212
271
  },
213
272
  context: PRODUCT_LIST_CONTEXT
214
273
  });
215
274
  return (data.products ?? []).map(toKeepaProduct);
216
275
  }
217
- /** Fetch a single product by ASIN. Throws `ProductNotFoundError` when Keepa
218
- * has no record for the ASIN (no product returned, or a stub with no title). */
276
+ /** Throws `ProductNotFoundError` when Keepa returns no product or a stub. */
219
277
  async retrieve(params) {
220
278
  const { asin, ...rest } = params;
221
279
  const [product] = await this.list({ ...rest, asins: [asin] });
@@ -246,7 +304,6 @@ var KeepaClient = class {
246
304
  this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
247
305
  this.products = new Products(this);
248
306
  }
249
- /** Internal: used by APIResource subclasses to perform a request. */
250
307
  _request(args) {
251
308
  const config = {
252
309
  apiKey: this.apiKey,
@@ -266,6 +323,7 @@ export {
266
323
  ASIN_LENGTH,
267
324
  ASIN_REGEX,
268
325
  AuthenticationError,
326
+ CsvType,
269
327
  DEFAULT_DAYS,
270
328
  KEEPA_NO_DATA_SENTINEL,
271
329
  KeepaClient,
@@ -283,6 +341,7 @@ export {
283
341
  isFoundProduct,
284
342
  isValidAsin,
285
343
  normalizeAsins,
344
+ parsePriceHistory,
286
345
  resolveDomainId
287
346
  };
288
347
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/error.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":["/** Replace any `?key=...` or `&key=...` segment with a REDACTED placeholder.\n * Defense-in-depth: keeps the API key out of error messages even when an\n * underlying transport error or upstream response echoes the request URL.\n * Exported for use by callers that build their own error wrappers. */\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 async from(response: Response, context: string): Promise<APIError> {\n const body = await response.text().catch(() => '');\n switch (response.status) {\n case 429:\n return new RateLimitError(context, body);\n case 401:\n return new AuthenticationError(context, body);\n default:\n return new APIError(response.status, context, body);\n }\n }\n}\n\nexport class RateLimitError extends APIError {\n constructor(context: string, body: string) {\n super(429, context, body, 'Keepa rate limit exceeded, please wait or upgrade plan');\n this.name = 'RateLimitError';\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","import { APIError, NetworkError } from './error.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}\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 accepts comma-separated lists (asin=A,B,C, category=1,2,3)\n// and the category endpoint specifically rejects %2C-encoded commas.\n// Currently private to this module: if a future helper builds Keepa URLs outside\n// `request()` (e.g. a paginator) and needs the same encoding, lift this into\n// `core/encoding.ts` rather than duplicating it.\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 if (!res.ok) throw await APIError.from(res, args.context);\n return (await res.json()) as T;\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","// The full set of marketplaces Keepa currently supports. Codes are ISO 3166-1\n// alpha-2; the numeric id is Keepa's `domain` query parameter value. Domain 7\n// is reserved (formerly Amazon China, retired). Update the README marketplace\n// table alongside any change here.\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/** Returns true if the given string is a structurally valid ASIN\n * (10 uppercase alphanumeric characters). Note: a passing ASIN may still\n * not exist in Keepa's database — use `isFoundProduct` to check that. */\nexport function isValidAsin(value: string): boolean {\n return ASIN_REGEX.test(value);\n}\n\n/** Trim + uppercase a list of ASINs and validate each. Returns the normalized\n * list. Throws with all invalid ASINs listed in the error message. */\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// Structured tag of the form `<resource>.<method>`. Used as the `context` field on\n// errors thrown from this method — keeps log aggregators able to filter cleanly as\n// more methods land (categories.list, categories.search, bestsellers.retrieve, …).\nexport const PRODUCT_LIST_CONTEXT = 'products.list';\nexport const DEFAULT_DAYS = 1;\n\n// Region-neutral Amazon image CDN. Serves the same images globally regardless of marketplace.\nexport const AMAZON_IMAGE_BASE = 'https://m.media-amazon.com/images/I';\n\n// Keepa stores `-1` in salesRanks/price arrays to indicate \"no data captured at that timestamp\".\nexport const KEEPA_NO_DATA_SENTINEL = -1;\n\n// Allowlist for image filenames in imagesCSV. Defends against path-traversal / SSRF\n// shaped strings (e.g. \"../../etc/passwd\") in case the CSV is ever influenced by\n// untrusted input. Matches typical Keepa image hashes (e.g. \"61abcDEF.jpg\").\nexport const VALID_IMAGE_FILENAME = /^[A-Za-z0-9._-]+\\.(jpg|jpeg|png|webp)$/i;\n","import { KeepaError } from '../../core/error.js';\n\n/** Thrown by `Products.retrieve` when Keepa has no record for the requested ASIN\n * (either no product object returned, or a stub with no title). The `asin`\n * property carries the value the caller passed in, unchanged, so it can be\n * surfaced in user-facing messages without re-deriving it. */\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 KEEPA_NO_DATA_SENTINEL,\n VALID_IMAGE_FILENAME,\n} from './constant.js';\nimport type { KeepaProduct } from './product.type.js';\nimport type { KeepaImageRaw, KeepaProductRaw } from './product.raw.type.js';\n\n/** Map Keepa's raw wire shape into the consumer-friendly KeepaProduct. */\nexport function toKeepaProduct(raw: KeepaProductRaw): KeepaProduct {\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 };\n}\n\n/** Convert Keepa's raw images array into full Amazon image URLs (the large\n * variant). Filters entries whose filename doesn't match the expected\n * alphanumeric image-name pattern as defense-in-depth. */\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/** Returns true when Keepa returned an actual product record (not just a stub\n * for an unknown ASIN). Stubs come back with `title: null`; real listings\n * always have a string. Use as a `.filter()` predicate to drop empty matches. */\nexport function isFoundProduct(product: KeepaProduct): boolean {\n return product.title != null;\n}\n\n/** Extract the most recent real BSR from Keepa's `[ts, rank, ts, rank, ...]` salesRanks array.\n * Walks backward through rank entries (odd indices) and skips Keepa's `-1` sentinel which\n * marks \"no data captured at that timestamp\". Returns `null` if every entry is sentinel.\n * Most consumers won't need this — `Products.list` already fills `bsr` on every returned\n * product. Useful when working with a raw Keepa response. */\nexport function extractBsr(\n salesRanks: Record<string, number[]> | undefined,\n rootCategory: number | undefined,\n): number | null {\n if (!salesRanks || rootCategory === undefined) return null;\n const ranks = salesRanks[String(rootCategory)];\n if (!ranks || ranks.length < 2) return null;\n // Keepa pairs are [ts, rank, ts, rank, ...]. If length is even, the last index\n // is a rank; if odd (truncated/schema drift), it's a dangling timestamp — skip\n // back one slot so we always start on a rank.\n const start = ranks.length % 2 === 0 ? ranks.length - 1 : ranks.length - 2;\n for (let i = start; i >= 1; i -= 2) {\n const rank = ranks[i];\n if (rank !== undefined && rank !== KEEPA_NO_DATA_SENTINEL) return rank;\n }\n return 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 },\n context: PRODUCT_LIST_CONTEXT,\n });\n return (data.products ?? []).map(toKeepaProduct);\n }\n\n /** Fetch a single product by ASIN. Throws `ProductNotFoundError` when Keepa\n * has no record for the ASIN (no product returned, or a stub with no title). */\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 { Products } from './resources/products/products.js';\n\nexport interface ClientOptions {\n /** Keepa API key. Falls back to `process.env.KEEPA_API_KEY`. */\n apiKey?: string;\n /** Base URL for the Keepa API. Defaults to `https://api.keepa.com`. */\n baseURL?: string;\n /** Custom fetch implementation. Defaults to `globalThis.fetch`. */\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 constructor(options: ClientOptions = {}) {\n // `process` is undefined in browsers / Cloudflare Workers / edge runtimes — guard it\n // so the constructor throws our friendly \"Missing Keepa API key\" message instead of a\n // raw ReferenceError on those targets.\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 a trailing slash so `${baseURL}${path}` never produces `//product`.\n this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n // Bind `globalThis` once: an unbound reference to Node's fetch throws\n // \"Illegal invocation\" when called with an undefined `this`. Bind once at\n // construction so every _request call uses the cached bound function.\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);\n\n this.products = new Products(this);\n }\n\n /** Internal: used by APIResource subclasses to perform a request. */\n _request<T>(args: RequestArgs): Promise<T> {\n const config: RequestConfig = {\n apiKey: this.apiKey,\n baseURL: this.baseURL,\n fetch: this.fetch,\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/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":["/** 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 async from(response: Response, context: string): Promise<APIError> {\n const body = await response.text().catch(() => '');\n switch (response.status) {\n case 429:\n return new RateLimitError(context, body);\n case 401:\n return new AuthenticationError(context, body);\n default:\n return new APIError(response.status, context, body);\n }\n }\n}\n\nexport class RateLimitError extends APIError {\n constructor(context: string, body: string) {\n super(429, context, body, 'Keepa rate limit exceeded, please wait or upgrade plan');\n this.name = 'RateLimitError';\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","import { APIError, NetworkError } from './error.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}\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 if (!res.ok) throw await APIError.from(res, args.context);\n return (await res.json()) as T;\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 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 price: amazon.at(-1)?.price ?? null,\n listPrice: list.at(-1)?.price ?? null,\n history: {\n price: { amazon, 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 { 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 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 };\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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keepa-api",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "TypeScript SDK for the Keepa API.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",