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 +31 -3
- package/dist/index.cjs +74 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -40
- package/dist/index.d.ts +83 -40
- package/dist/index.js +72 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
| `
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
package/dist/index.cjs.map
CHANGED
|
@@ -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
|
|
32
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
128
|
-
*
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
/**
|
|
140
|
-
*
|
|
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
|
|
147
|
-
*
|
|
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
|
|
32
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
128
|
-
*
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
/**
|
|
140
|
-
*
|
|
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
|
|
147
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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":[]}
|