keepa-api 0.3.0 → 0.3.2

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.
@@ -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/rate-limit.ts","../src/core/request.ts","../src/core/resource.ts","../src/lib/marketplace.ts","../src/lib/asin.ts","../src/resources/products/constant.ts","../src/resources/products/error.ts","../src/resources/products/product.util.ts","../src/resources/products/products.ts","../src/client.ts","../src/version.ts"],"sourcesContent":["export { KeepaClient as default, KeepaClient, type ClientOptions } from './client.js';\n\nexport {\n KeepaError,\n APIError,\n RateLimitError,\n AuthenticationError,\n NetworkError,\n} from './core/error.js';\n\nexport type { RateLimitInfo } from './core/rate-limit.js';\n\nexport { APIResource } from './core/resource.js';\n\nexport {\n MARKETPLACE_DOMAINS,\n resolveDomainId,\n type Marketplace,\n type DomainId,\n} from './lib/marketplace.js';\n\nexport {\n ASIN_LENGTH,\n ASIN_REGEX,\n isValidAsin,\n normalizeAsins,\n} from './lib/asin.js';\n\nexport * from './resources/index.js';\n\nexport { VERSION } from './version.js';\n","import type { RateLimitInfo } from './rate-limit.js';\n\n/** Defense-in-depth — keeps the API key out of error messages even when a\n * transport error or upstream response echoes the request URL back. */\nexport function scrubApiKey(text: string): string {\n return text.replace(/([?&])key=[^&\\s]*/gi, '$1key=REDACTED');\n}\n\nexport class KeepaError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'KeepaError';\n }\n}\n\nexport class APIError extends KeepaError {\n readonly status: number;\n readonly context: string;\n readonly body: string;\n\n constructor(status: number, context: string, body: string, message?: string) {\n const safeBody = scrubApiKey(body);\n super(scrubApiKey(message ?? `Keepa ${context} error (${status}): ${safeBody}`));\n this.name = 'APIError';\n this.status = status;\n this.context = context;\n this.body = safeBody;\n }\n\n static from(\n status: number,\n context: string,\n body: string,\n rateLimit: RateLimitInfo | null = null,\n ): APIError {\n switch (status) {\n case 429:\n return new RateLimitError(context, body, rateLimit);\n case 401:\n return new AuthenticationError(context, body);\n default:\n return new APIError(status, context, body);\n }\n }\n}\n\nexport class RateLimitError extends APIError {\n /** Bucket snapshot at the moment the 429 was returned. Null if Keepa's body\n * didn't carry the usual rate-limit fields. */\n readonly rateLimit: RateLimitInfo | null;\n\n constructor(context: string, body: string, rateLimit: RateLimitInfo | null = null) {\n super(429, context, body, 'Keepa rate limit exceeded, please wait or upgrade plan');\n this.name = 'RateLimitError';\n this.rateLimit = rateLimit;\n }\n}\n\nexport class AuthenticationError extends APIError {\n constructor(context: string, body: string) {\n super(\n 401,\n context,\n body,\n `Keepa authentication failed for ${context}: invalid or missing API key`,\n );\n this.name = 'AuthenticationError';\n }\n}\n\nexport class NetworkError extends KeepaError {\n readonly context: string;\n override readonly cause: unknown;\n\n constructor(context: string, cause: unknown) {\n const raw = cause instanceof Error ? cause.message : String(cause);\n super(`Keepa ${context} network error: ${scrubApiKey(raw)}`);\n this.name = 'NetworkError';\n this.context = context;\n this.cause = cause;\n }\n}\n","export interface RateLimitInfo {\n /** Tokens remaining in the bucket after this response. */\n tokensLeft: number;\n /** Milliseconds until the next token refills. */\n refillIn: number;\n /** Tokens added to the bucket per minute. */\n refillRate: number;\n /** Reduction applied to the refill rate during high system load. */\n tokenFlowReduction: number;\n /** Local time the snapshot was received. */\n receivedAt: Date;\n}\n\n// Keepa includes these fields on every response body — 200 and 429 alike — so a\n// single extractor works for both the success path and the rate-limit error path.\nexport function extractRateLimit(body: unknown): RateLimitInfo | null {\n if (!body || typeof body !== 'object') return null;\n const b = body as Record<string, unknown>;\n if (typeof b.tokensLeft !== 'number') return null;\n return {\n tokensLeft: b.tokensLeft,\n refillIn: typeof b.refillIn === 'number' ? b.refillIn : 0,\n refillRate: typeof b.refillRate === 'number' ? b.refillRate : 0,\n tokenFlowReduction: typeof b.tokenFlowReduction === 'number' ? b.tokenFlowReduction : 0,\n receivedAt: new Date(),\n };\n}\n","import { APIError, NetworkError } from './error.js';\nimport { extractRateLimit, type RateLimitInfo } from './rate-limit.js';\n\nexport type QueryValue = string | number | string[] | number[] | undefined;\nexport type QueryParams = Record<string, QueryValue>;\n\nexport interface RequestArgs {\n path: string;\n query?: QueryParams;\n context: string;\n}\n\nexport interface RequestConfig {\n baseURL: string;\n apiKey: string;\n fetch: typeof globalThis.fetch;\n /** Fired once per response (200 or 429) when Keepa's bucket fields are\n * present on the body. */\n onRateLimit?: (info: RateLimitInfo) => void;\n}\n\nexport function buildUrl(baseURL: string, path: string, query?: QueryParams): string {\n if (!query) return `${baseURL}${path}`;\n const parts: string[] = [];\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n const stringValue = Array.isArray(value) ? value.join(',') : String(value);\n parts.push(`${encodeURIComponent(key)}=${encodeKeepaValue(stringValue)}`);\n }\n return parts.length ? `${baseURL}${path}?${parts.join('&')}` : `${baseURL}${path}`;\n}\n\n// Keep commas literal — Keepa's category endpoint rejects %2C-encoded commas\n// in comma-separated lists (asin=A,B,C, category=1,2,3).\nfunction encodeKeepaValue(value: string): string {\n return encodeURIComponent(value).replace(/%2C/g, ',');\n}\n\nexport async function request<T>(config: RequestConfig, args: RequestArgs): Promise<T> {\n const query: QueryParams = { key: config.apiKey, ...args.query };\n const url = buildUrl(config.baseURL, args.path, query);\n let res: Response;\n try {\n res = await config.fetch(url);\n } catch (cause) {\n throw new NetworkError(args.context, cause);\n }\n\n // Read the body once — Response bodies are single-use streams, and we need\n // the same payload for both rate-limit extraction and (on errors) the body\n // attached to the thrown error.\n const text = await res.text().catch(() => '');\n const body = safeJsonParse(text);\n\n const rl = extractRateLimit(body);\n if (rl) config.onRateLimit?.(rl);\n\n if (!res.ok) throw APIError.from(res.status, args.context, text, rl);\n return body as T;\n}\n\nfunction safeJsonParse(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return null;\n }\n}\n","import type { KeepaClient } from '../client.js';\n\nexport abstract class APIResource {\n protected _client: KeepaClient;\n\n constructor(client: KeepaClient) {\n this._client = client;\n }\n}\n","// Keys are ISO 3166-1 alpha-2; values are Keepa's `domain` query parameter.\n// Domain 7 is skipped — reserved (formerly Amazon China, retired).\nexport const MARKETPLACE_DOMAINS = {\n US: 1,\n GB: 2,\n DE: 3,\n FR: 4,\n JP: 5,\n CA: 6,\n IT: 8,\n ES: 9,\n IN: 10,\n MX: 11,\n BR: 12,\n} as const;\n\nexport type Marketplace = keyof typeof MARKETPLACE_DOMAINS;\nexport type DomainId = (typeof MARKETPLACE_DOMAINS)[Marketplace];\n\nconst SUPPORTED_LIST = Object.keys(MARKETPLACE_DOMAINS).join(', ');\n\nexport function resolveDomainId(marketplace: string | undefined): DomainId {\n const key = (marketplace ?? 'US').toUpperCase() as Marketplace;\n const domainId = MARKETPLACE_DOMAINS[key];\n if (!domainId) {\n throw new Error(\n `Invalid marketplace \"${marketplace}\". Supported: ${SUPPORTED_LIST}`,\n );\n }\n return domainId;\n}\n","export const ASIN_LENGTH = 10;\nexport const ASIN_REGEX = /^[A-Z0-9]{10}$/;\n\n/** Structural check only — a passing ASIN may still not exist in Keepa's\n * database; use `isFoundProduct` for that. */\nexport function isValidAsin(value: string): boolean {\n return ASIN_REGEX.test(value);\n}\n\nexport function normalizeAsins(asins: string[]): string[] {\n const normalized = asins.map((asin) => asin.trim().toUpperCase());\n const invalid = normalized.filter((asin) => !isValidAsin(asin));\n if (invalid.length > 0) {\n throw new Error(\n `Invalid ASIN(s): ${invalid.join(', ')}. ASINs must be ${ASIN_LENGTH} alphanumeric characters (e.g. B00MNV8E0C).`,\n );\n }\n return normalized;\n}\n","export const PRODUCT_PATH = '/product';\n// `<resource>.<method>` tag carried on errors so log aggregators can filter cleanly.\nexport const PRODUCT_LIST_CONTEXT = 'products.list';\nexport const DEFAULT_DAYS = 1;\n\n// Region-neutral CDN — serves identical images regardless of the marketplace.\nexport const AMAZON_IMAGE_BASE = 'https://m.media-amazon.com/images/I';\n\n// Keepa stores `-1` in salesRanks/price arrays as \"no data captured at that timestamp\".\nexport const KEEPA_NO_DATA_SENTINEL = -1;\n\n// First-dim index into Keepa's csv history matrix. Names mirror Keepa's own enum\n// verbatim (see https://keepa.com/#!discuss/t/product-object/116).\nexport const CsvType = {\n AMAZON: 0,\n NEW: 1,\n USED: 2,\n SALES: 3,\n LISTPRICE: 4,\n COLLECTIBLE: 5,\n REFURBISHED: 6,\n NEW_FBM_SHIPPING: 7,\n LIGHTNING_DEAL: 8,\n WAREHOUSE: 9,\n NEW_FBA: 10,\n COUNT_NEW: 11,\n COUNT_USED: 12,\n COUNT_REFURBISHED: 13,\n COUNT_COLLECTIBLE: 14,\n EXTRA_INFO_UPDATES: 15,\n RATING: 16,\n COUNT_REVIEWS: 17,\n BUY_BOX_SHIPPING: 18,\n USED_NEW_SHIPPING: 19,\n USED_VERY_GOOD_SHIPPING: 20,\n USED_GOOD_SHIPPING: 21,\n USED_ACCEPTABLE_SHIPPING: 22,\n COLLECTIBLE_NEW_SHIPPING: 23,\n COLLECTIBLE_VERY_GOOD_SHIPPING: 24,\n COLLECTIBLE_GOOD_SHIPPING: 25,\n COLLECTIBLE_ACCEPTABLE_SHIPPING: 26,\n REFURBISHED_SHIPPING: 27,\n EBAY_NEW_SHIPPING: 28,\n EBAY_USED_SHIPPING: 29,\n TRADE_IN: 30,\n RENTAL: 31,\n BUY_BOX_USED_SHIPPING: 32,\n PRIME_EXCL: 33,\n COUNT_NEW_FBA: 34,\n COUNT_NEW_FBM: 35,\n} as const;\nexport type CsvType = (typeof CsvType)[keyof typeof CsvType];\n\n// 2011-01-01 00:00 UTC — origin of Keepa's csv minute timestamps. Convert with\n// `new Date(km * 60_000 + KEEPA_EPOCH_UNIX_MS)`.\nexport const KEEPA_EPOCH_UNIX_MS = 1_293_840_000_000;\n\n// Defense-in-depth allowlist for filenames in imagesCSV — rejects path-traversal\n// / SSRF-shaped strings if the CSV is ever influenced by untrusted input.\nexport const VALID_IMAGE_FILENAME = /^[A-Za-z0-9._-]+\\.(jpg|jpeg|png|webp)$/i;\n","import { KeepaError } from '../../core/error.js';\n\n/** Thrown when Keepa returns no product, or returns a stub (title === null).\n * `asin` carries the caller's original input unchanged. */\nexport class ProductNotFoundError extends KeepaError {\n readonly asin: string;\n\n constructor(asin: string) {\n super(`Keepa: no product found for ASIN ${asin}`);\n this.name = 'ProductNotFoundError';\n this.asin = asin;\n }\n}\n","import {\n AMAZON_IMAGE_BASE,\n CsvType,\n KEEPA_EPOCH_UNIX_MS,\n KEEPA_NO_DATA_SENTINEL,\n VALID_IMAGE_FILENAME,\n} from './constant.js';\nimport type { KeepaProduct, PriceHistoryEntry } from './product.type.js';\nimport type { KeepaImageRaw, KeepaProductRaw } from './product.raw.type.js';\n\nexport function toKeepaProduct(raw: KeepaProductRaw): KeepaProduct {\n const amazon = parsePriceHistory(raw.csv?.[CsvType.AMAZON]);\n const new_ = parsePriceHistory(raw.csv?.[CsvType.NEW]);\n const list = parsePriceHistory(raw.csv?.[CsvType.LISTPRICE]);\n return {\n asin: raw.asin,\n title: raw.title,\n description: raw.description,\n parentAsin: raw.parentAsin,\n categoryTree: raw.categoryTree,\n rootCategory: raw.rootCategory,\n salesRanks: raw.salesRanks,\n variations: raw.variations,\n features: raw.features,\n images: rawImagesToUrls(raw.images),\n bsr: extractBsr(raw.salesRanks, raw.rootCategory),\n amazonPrice: amazon.at(-1)?.price ?? null,\n newPrice: new_.at(-1)?.price ?? null,\n listPrice: list.at(-1)?.price ?? null,\n history: {\n price: { amazon, new: new_, list },\n },\n };\n}\n\ninterface KeepaSeriesPoint {\n /** Keepa minutes — use `keepaMinutesToDate` for a JS Date. */\n timestamp: number;\n value: number;\n}\n\n// Pair up Keepa's flat `[ts, value, ts, value, ...]` series. Drops `-1` no-data\n// sentinels; the `i + 1 < length` bound silently skips a dangling odd element.\nfunction pairKeepaSeries(series: number[] | undefined): KeepaSeriesPoint[] {\n if (!series) return [];\n const points: KeepaSeriesPoint[] = [];\n for (let i = 0; i + 1 < series.length; i += 2) {\n const value = series[i + 1]!;\n if (value === KEEPA_NO_DATA_SENTINEL) continue;\n points.push({ timestamp: series[i]!, value });\n }\n return points;\n}\n\nfunction keepaMinutesToDate(minutes: number): Date {\n return new Date(minutes * 60_000 + KEEPA_EPOCH_UNIX_MS);\n}\n\n// Keepa stores prices as integers in the smallest unit (cents/pence/etc.) and\n// scales JPY/INR/BRL the same way, so /100 produces the marketplace's major unit\n// uniformly across all supported regions.\nexport function parsePriceHistory(series: number[] | undefined): PriceHistoryEntry[] {\n return pairKeepaSeries(series).map(({ timestamp, value }) => ({\n timestamp: keepaMinutesToDate(timestamp),\n price: value / 100,\n }));\n}\n\n// Defense-in-depth — rejects path-traversal / SSRF-shaped filenames in case the\n// raw CSV is ever influenced by untrusted input.\nfunction rawImagesToUrls(images: KeepaImageRaw[] | undefined): string[] {\n if (!images || images.length === 0) return [];\n return images\n .filter((img) => typeof img.l === 'string' && VALID_IMAGE_FILENAME.test(img.l))\n .map((img) => `${AMAZON_IMAGE_BASE}/${img.l}`);\n}\n\n/** Stubs for unknown ASINs come back with `title: null`. */\nexport function isFoundProduct(product: KeepaProduct): boolean {\n return product.title != null;\n}\n\n/** Latest non-sentinel rank from Keepa's `[ts, rank, ts, rank, ...]` salesRanks\n * series for the product's rootCategory. */\nexport function extractBsr(\n salesRanks: Record<string, number[]> | undefined,\n rootCategory: number | undefined,\n): number | null {\n if (!salesRanks || rootCategory === undefined) return null;\n return pairKeepaSeries(salesRanks[String(rootCategory)]).at(-1)?.value ?? null;\n}\n","import { APIResource } from '../../core/resource.js';\nimport { resolveDomainId } from '../../lib/marketplace.js';\nimport { normalizeAsins } from '../../lib/asin.js';\nimport {\n PRODUCT_PATH,\n PRODUCT_LIST_CONTEXT,\n DEFAULT_DAYS,\n} from './constant.js';\nimport type {\n KeepaProduct,\n ProductListParams,\n ProductRetrieveParams,\n} from './product.type.js';\nimport type { KeepaProductResponseRaw } from './product.raw.type.js';\nimport { ProductNotFoundError } from './error.js';\nimport { toKeepaProduct, isFoundProduct } from './product.util.js';\n\nexport class Products extends APIResource {\n async list(params: ProductListParams): Promise<KeepaProduct[]> {\n if (params.asins.length === 0) {\n throw new Error('At least one ASIN is required');\n }\n if (\n params.days !== undefined &&\n (!Number.isInteger(params.days) || params.days < 1)\n ) {\n throw new Error(`Invalid days: ${params.days}. Must be a positive integer.`);\n }\n const asins = normalizeAsins(params.asins);\n const domain = resolveDomainId(params.marketplace);\n const data = await this._client._request<KeepaProductResponseRaw>({\n path: PRODUCT_PATH,\n query: {\n domain,\n asin: asins,\n days: params.days ?? DEFAULT_DAYS,\n history: params.history ? 1 : 0,\n },\n context: PRODUCT_LIST_CONTEXT,\n });\n return (data.products ?? []).map(toKeepaProduct);\n }\n\n /** Throws `ProductNotFoundError` when Keepa returns no product or a stub. */\n async retrieve(params: ProductRetrieveParams): Promise<KeepaProduct> {\n const { asin, ...rest } = params;\n const [product] = await this.list({ ...rest, asins: [asin] });\n if (!product || !isFoundProduct(product)) {\n throw new ProductNotFoundError(asin);\n }\n return product;\n }\n}\n","import { request } from './core/request.js';\nimport type { RequestArgs, RequestConfig } from './core/request.js';\nimport type { RateLimitInfo } from './core/rate-limit.js';\nimport { Products } from './resources/products/products.js';\n\nexport interface ClientOptions {\n /** Falls back to `process.env.KEEPA_API_KEY` when omitted. */\n apiKey?: string;\n baseURL?: string;\n fetch?: typeof globalThis.fetch;\n}\n\nconst DEFAULT_BASE_URL = 'https://api.keepa.com';\n\nexport class KeepaClient {\n readonly apiKey: string;\n readonly baseURL: string;\n readonly fetch: typeof globalThis.fetch;\n\n readonly products: Products;\n\n /** Latest rate-limit snapshot from Keepa, updated after every response.\n * Null until the first request completes. */\n rateLimit: RateLimitInfo | null = null;\n\n constructor(options: ClientOptions = {}) {\n // `process` is undefined in browsers / Workers / edge runtimes — guard so the\n // constructor throws our friendly error instead of a raw ReferenceError there.\n const envApiKey =\n typeof process !== 'undefined' ? process.env?.KEEPA_API_KEY : undefined;\n const apiKey = options.apiKey || envApiKey;\n if (!apiKey) {\n throw new Error(\n 'Missing Keepa API key. Pass it as `new KeepaClient({ apiKey })` or set KEEPA_API_KEY in your environment.',\n );\n }\n this.apiKey = apiKey;\n // Strip trailing slash so `${baseURL}${path}` never produces `//product`.\n this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\\/+$/, '');\n // Unbound Node fetch throws \"Illegal invocation\" when called with undefined `this`.\n this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);\n\n this.products = new Products(this);\n }\n\n _request<T>(args: RequestArgs): Promise<T> {\n const config: RequestConfig = {\n apiKey: this.apiKey,\n baseURL: this.baseURL,\n fetch: this.fetch,\n onRateLimit: (info) => {\n this.rateLimit = info;\n },\n };\n return request<T>(config, args);\n }\n}\n\nexport default KeepaClient;\n","export const VERSION = '0.1.0';\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,3 +1,16 @@
1
+ interface RateLimitInfo {
2
+ /** Tokens remaining in the bucket after this response. */
3
+ tokensLeft: number;
4
+ /** Milliseconds until the next token refills. */
5
+ refillIn: number;
6
+ /** Tokens added to the bucket per minute. */
7
+ refillRate: number;
8
+ /** Reduction applied to the refill rate during high system load. */
9
+ tokenFlowReduction: number;
10
+ /** Local time the snapshot was received. */
11
+ receivedAt: Date;
12
+ }
13
+
1
14
  type QueryValue = string | number | string[] | number[] | undefined;
2
15
  type QueryParams = Record<string, QueryValue>;
3
16
  interface RequestArgs {
@@ -28,19 +41,20 @@ type Marketplace = keyof typeof MARKETPLACE_DOMAINS;
28
41
  type DomainId = (typeof MARKETPLACE_DOMAINS)[Marketplace];
29
42
  declare function resolveDomainId(marketplace: string | undefined): DomainId;
30
43
 
31
- interface ProductListParams {
32
- asins: string[];
33
- /** Marketplace code (case-insensitive at runtime). Defaults to 'US'. */
44
+ interface ProductRequestOptions {
45
+ /** Case-insensitive at runtime. Defaults to 'US'. */
34
46
  marketplace?: Marketplace;
35
- /** Days of price history to include. Defaults to 1. */
47
+ /** Defaults to 1. */
36
48
  days?: number;
49
+ /** When true, populates `history.price.*` and the scalar `price` / `listPrice`
50
+ * fields on each returned product. Increases token cost. */
51
+ history?: boolean;
52
+ }
53
+ interface ProductListParams extends ProductRequestOptions {
54
+ asins: string[];
37
55
  }
38
- interface ProductRetrieveParams {
56
+ interface ProductRetrieveParams extends ProductRequestOptions {
39
57
  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
58
  }
45
59
  interface KeepaCategoryNode {
46
60
  catId: number;
@@ -54,10 +68,14 @@ interface KeepaVariation {
54
68
  asin: string;
55
69
  attributes?: KeepaVariationAttribute[];
56
70
  }
57
- /** A Keepa product in the consumer-friendly shape. Derived from the raw Keepa
58
- * response: `imagesCSV` is split into `images: string[]`, and the most-recent
59
- * real BSR is surfaced as `bsr`. The raw `salesRanks` is preserved for
60
- * consumers that need to walk the rank history themselves. */
71
+ interface PriceHistoryEntry {
72
+ timestamp: Date;
73
+ /** Marketplace's major unit dollars for US, pounds for GB, yen for JP, etc. */
74
+ price: number;
75
+ }
76
+ /** Flag-gated fields (`amazonPrice`, `newPrice`, `listPrice`, `history.price.*`)
77
+ * are always present on the type and default to empty/null when the
78
+ * corresponding request flag wasn't set. */
61
79
  interface KeepaProduct {
62
80
  asin: string;
63
81
  title?: string;
@@ -68,26 +86,40 @@ interface KeepaProduct {
68
86
  salesRanks?: Record<string, number[]>;
69
87
  variations?: KeepaVariation[];
70
88
  features?: string[];
71
- /** Full image URLs derived from imagesCSV using a region-neutral Amazon CDN. */
72
89
  images: string[];
73
- /** Most recent real BSR for the rootCategory. Null when missing or every entry is
74
- * Keepa's `-1` "no data captured" sentinel. */
90
+ /** Null when missing or every history entry is Keepa's `-1` no-data sentinel. */
75
91
  bsr: number | null;
92
+ /** Latest Amazon-sold price (csv[0]) in the marketplace's major unit;
93
+ * null when history wasn't requested or Keepa has no data. */
94
+ amazonPrice: number | null;
95
+ /** Latest lowest-3rd-party-new price (csv[1]) in the marketplace's major
96
+ * unit. Distinct from `amazonPrice` — Amazon itself may not be the cheapest
97
+ * new offer. */
98
+ newPrice: number | null;
99
+ /** Latest list price / MSRP (csv[4]) in the marketplace's major unit. */
100
+ listPrice: number | null;
101
+ history: {
102
+ price: {
103
+ /** Empty when `history: false`. Sentinel-filtered. */
104
+ amazon: PriceHistoryEntry[];
105
+ /** Empty when `history: false`. Sentinel-filtered. */
106
+ new: PriceHistoryEntry[];
107
+ /** Empty when `history: false`. Sentinel-filtered. */
108
+ list: PriceHistoryEntry[];
109
+ };
110
+ };
76
111
  }
77
112
 
78
113
  declare class Products extends APIResource {
79
114
  list(params: ProductListParams): Promise<KeepaProduct[]>;
80
- /** Fetch a single product by ASIN. Throws `ProductNotFoundError` when Keepa
81
- * has no record for the ASIN (no product returned, or a stub with no title). */
115
+ /** Throws `ProductNotFoundError` when Keepa returns no product or a stub. */
82
116
  retrieve(params: ProductRetrieveParams): Promise<KeepaProduct>;
83
117
  }
84
118
 
85
119
  interface ClientOptions {
86
- /** Keepa API key. Falls back to `process.env.KEEPA_API_KEY`. */
120
+ /** Falls back to `process.env.KEEPA_API_KEY` when omitted. */
87
121
  apiKey?: string;
88
- /** Base URL for the Keepa API. Defaults to `https://api.keepa.com`. */
89
122
  baseURL?: string;
90
- /** Custom fetch implementation. Defaults to `globalThis.fetch`. */
91
123
  fetch?: typeof globalThis.fetch;
92
124
  }
93
125
  declare class KeepaClient {
@@ -95,8 +127,10 @@ declare class KeepaClient {
95
127
  readonly baseURL: string;
96
128
  readonly fetch: typeof globalThis.fetch;
97
129
  readonly products: Products;
130
+ /** Latest rate-limit snapshot from Keepa, updated after every response.
131
+ * Null until the first request completes. */
132
+ rateLimit: RateLimitInfo | null;
98
133
  constructor(options?: ClientOptions);
99
- /** Internal: used by APIResource subclasses to perform a request. */
100
134
  _request<T>(args: RequestArgs): Promise<T>;
101
135
  }
102
136
 
@@ -108,10 +142,13 @@ declare class APIError extends KeepaError {
108
142
  readonly context: string;
109
143
  readonly body: string;
110
144
  constructor(status: number, context: string, body: string, message?: string);
111
- static from(response: Response, context: string): Promise<APIError>;
145
+ static from(status: number, context: string, body: string, rateLimit?: RateLimitInfo | null): APIError;
112
146
  }
113
147
  declare class RateLimitError extends APIError {
114
- constructor(context: string, body: string);
148
+ /** Bucket snapshot at the moment the 429 was returned. Null if Keepa's body
149
+ * didn't carry the usual rate-limit fields. */
150
+ readonly rateLimit: RateLimitInfo | null;
151
+ constructor(context: string, body: string, rateLimit?: RateLimitInfo | null);
115
152
  }
116
153
  declare class AuthenticationError extends APIError {
117
154
  constructor(context: string, body: string);
@@ -124,29 +161,20 @@ declare class NetworkError extends KeepaError {
124
161
 
125
162
  declare const ASIN_LENGTH = 10;
126
163
  declare const ASIN_REGEX: RegExp;
127
- /** Returns true if the given string is a structurally valid ASIN
128
- * (10 uppercase alphanumeric characters). Note: a passing ASIN may still
129
- * not exist in Keepa's database — use `isFoundProduct` to check that. */
164
+ /** Structural check only a passing ASIN may still not exist in Keepa's
165
+ * database; use `isFoundProduct` for that. */
130
166
  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
167
  declare function normalizeAsins(asins: string[]): string[];
134
168
 
135
- /** Returns true when Keepa returned an actual product record (not just a stub
136
- * for an unknown ASIN). Stubs come back with `title: null`; real listings
137
- * always have a string. Use as a `.filter()` predicate to drop empty matches. */
169
+ declare function parsePriceHistory(series: number[] | undefined): PriceHistoryEntry[];
170
+ /** Stubs for unknown ASINs come back with `title: null`. */
138
171
  declare function isFoundProduct(product: KeepaProduct): boolean;
139
- /** Extract the most recent real BSR from Keepa's `[ts, rank, ts, rank, ...]` salesRanks array.
140
- * Walks backward through rank entries (odd indices) and skips Keepa's `-1` sentinel which
141
- * marks "no data captured at that timestamp". Returns `null` if every entry is sentinel.
142
- * Most consumers won't need this — `Products.list` already fills `bsr` on every returned
143
- * product. Useful when working with a raw Keepa response. */
172
+ /** Latest non-sentinel rank from Keepa's `[ts, rank, ts, rank, ...]` salesRanks
173
+ * series for the product's rootCategory. */
144
174
  declare function extractBsr(salesRanks: Record<string, number[]> | undefined, rootCategory: number | undefined): number | null;
145
175
 
146
- /** Thrown by `Products.retrieve` when Keepa has no record for the requested ASIN
147
- * (either no product object returned, or a stub with no title). The `asin`
148
- * property carries the value the caller passed in, unchanged, so it can be
149
- * surfaced in user-facing messages without re-deriving it. */
176
+ /** Thrown when Keepa returns no product, or returns a stub (title === null).
177
+ * `asin` carries the caller's original input unchanged. */
150
178
  declare class ProductNotFoundError extends KeepaError {
151
179
  readonly asin: string;
152
180
  constructor(asin: string);
@@ -157,7 +185,46 @@ declare const PRODUCT_LIST_CONTEXT = "products.list";
157
185
  declare const DEFAULT_DAYS = 1;
158
186
  declare const AMAZON_IMAGE_BASE = "https://m.media-amazon.com/images/I";
159
187
  declare const KEEPA_NO_DATA_SENTINEL = -1;
188
+ declare const CsvType: {
189
+ readonly AMAZON: 0;
190
+ readonly NEW: 1;
191
+ readonly USED: 2;
192
+ readonly SALES: 3;
193
+ readonly LISTPRICE: 4;
194
+ readonly COLLECTIBLE: 5;
195
+ readonly REFURBISHED: 6;
196
+ readonly NEW_FBM_SHIPPING: 7;
197
+ readonly LIGHTNING_DEAL: 8;
198
+ readonly WAREHOUSE: 9;
199
+ readonly NEW_FBA: 10;
200
+ readonly COUNT_NEW: 11;
201
+ readonly COUNT_USED: 12;
202
+ readonly COUNT_REFURBISHED: 13;
203
+ readonly COUNT_COLLECTIBLE: 14;
204
+ readonly EXTRA_INFO_UPDATES: 15;
205
+ readonly RATING: 16;
206
+ readonly COUNT_REVIEWS: 17;
207
+ readonly BUY_BOX_SHIPPING: 18;
208
+ readonly USED_NEW_SHIPPING: 19;
209
+ readonly USED_VERY_GOOD_SHIPPING: 20;
210
+ readonly USED_GOOD_SHIPPING: 21;
211
+ readonly USED_ACCEPTABLE_SHIPPING: 22;
212
+ readonly COLLECTIBLE_NEW_SHIPPING: 23;
213
+ readonly COLLECTIBLE_VERY_GOOD_SHIPPING: 24;
214
+ readonly COLLECTIBLE_GOOD_SHIPPING: 25;
215
+ readonly COLLECTIBLE_ACCEPTABLE_SHIPPING: 26;
216
+ readonly REFURBISHED_SHIPPING: 27;
217
+ readonly EBAY_NEW_SHIPPING: 28;
218
+ readonly EBAY_USED_SHIPPING: 29;
219
+ readonly TRADE_IN: 30;
220
+ readonly RENTAL: 31;
221
+ readonly BUY_BOX_USED_SHIPPING: 32;
222
+ readonly PRIME_EXCL: 33;
223
+ readonly COUNT_NEW_FBA: 34;
224
+ readonly COUNT_NEW_FBM: 35;
225
+ };
226
+ type CsvType = (typeof CsvType)[keyof typeof CsvType];
160
227
 
161
228
  declare const VERSION = "0.1.0";
162
229
 
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 };
230
+ 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, type RateLimitInfo, VERSION, KeepaClient as default, extractBsr, isFoundProduct, isValidAsin, normalizeAsins, parsePriceHistory, resolveDomainId };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,16 @@
1
+ interface RateLimitInfo {
2
+ /** Tokens remaining in the bucket after this response. */
3
+ tokensLeft: number;
4
+ /** Milliseconds until the next token refills. */
5
+ refillIn: number;
6
+ /** Tokens added to the bucket per minute. */
7
+ refillRate: number;
8
+ /** Reduction applied to the refill rate during high system load. */
9
+ tokenFlowReduction: number;
10
+ /** Local time the snapshot was received. */
11
+ receivedAt: Date;
12
+ }
13
+
1
14
  type QueryValue = string | number | string[] | number[] | undefined;
2
15
  type QueryParams = Record<string, QueryValue>;
3
16
  interface RequestArgs {
@@ -28,19 +41,20 @@ type Marketplace = keyof typeof MARKETPLACE_DOMAINS;
28
41
  type DomainId = (typeof MARKETPLACE_DOMAINS)[Marketplace];
29
42
  declare function resolveDomainId(marketplace: string | undefined): DomainId;
30
43
 
31
- interface ProductListParams {
32
- asins: string[];
33
- /** Marketplace code (case-insensitive at runtime). Defaults to 'US'. */
44
+ interface ProductRequestOptions {
45
+ /** Case-insensitive at runtime. Defaults to 'US'. */
34
46
  marketplace?: Marketplace;
35
- /** Days of price history to include. Defaults to 1. */
47
+ /** Defaults to 1. */
36
48
  days?: number;
49
+ /** When true, populates `history.price.*` and the scalar `price` / `listPrice`
50
+ * fields on each returned product. Increases token cost. */
51
+ history?: boolean;
52
+ }
53
+ interface ProductListParams extends ProductRequestOptions {
54
+ asins: string[];
37
55
  }
38
- interface ProductRetrieveParams {
56
+ interface ProductRetrieveParams extends ProductRequestOptions {
39
57
  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
58
  }
45
59
  interface KeepaCategoryNode {
46
60
  catId: number;
@@ -54,10 +68,14 @@ interface KeepaVariation {
54
68
  asin: string;
55
69
  attributes?: KeepaVariationAttribute[];
56
70
  }
57
- /** A Keepa product in the consumer-friendly shape. Derived from the raw Keepa
58
- * response: `imagesCSV` is split into `images: string[]`, and the most-recent
59
- * real BSR is surfaced as `bsr`. The raw `salesRanks` is preserved for
60
- * consumers that need to walk the rank history themselves. */
71
+ interface PriceHistoryEntry {
72
+ timestamp: Date;
73
+ /** Marketplace's major unit dollars for US, pounds for GB, yen for JP, etc. */
74
+ price: number;
75
+ }
76
+ /** Flag-gated fields (`amazonPrice`, `newPrice`, `listPrice`, `history.price.*`)
77
+ * are always present on the type and default to empty/null when the
78
+ * corresponding request flag wasn't set. */
61
79
  interface KeepaProduct {
62
80
  asin: string;
63
81
  title?: string;
@@ -68,26 +86,40 @@ interface KeepaProduct {
68
86
  salesRanks?: Record<string, number[]>;
69
87
  variations?: KeepaVariation[];
70
88
  features?: string[];
71
- /** Full image URLs derived from imagesCSV using a region-neutral Amazon CDN. */
72
89
  images: string[];
73
- /** Most recent real BSR for the rootCategory. Null when missing or every entry is
74
- * Keepa's `-1` "no data captured" sentinel. */
90
+ /** Null when missing or every history entry is Keepa's `-1` no-data sentinel. */
75
91
  bsr: number | null;
92
+ /** Latest Amazon-sold price (csv[0]) in the marketplace's major unit;
93
+ * null when history wasn't requested or Keepa has no data. */
94
+ amazonPrice: number | null;
95
+ /** Latest lowest-3rd-party-new price (csv[1]) in the marketplace's major
96
+ * unit. Distinct from `amazonPrice` — Amazon itself may not be the cheapest
97
+ * new offer. */
98
+ newPrice: number | null;
99
+ /** Latest list price / MSRP (csv[4]) in the marketplace's major unit. */
100
+ listPrice: number | null;
101
+ history: {
102
+ price: {
103
+ /** Empty when `history: false`. Sentinel-filtered. */
104
+ amazon: PriceHistoryEntry[];
105
+ /** Empty when `history: false`. Sentinel-filtered. */
106
+ new: PriceHistoryEntry[];
107
+ /** Empty when `history: false`. Sentinel-filtered. */
108
+ list: PriceHistoryEntry[];
109
+ };
110
+ };
76
111
  }
77
112
 
78
113
  declare class Products extends APIResource {
79
114
  list(params: ProductListParams): Promise<KeepaProduct[]>;
80
- /** Fetch a single product by ASIN. Throws `ProductNotFoundError` when Keepa
81
- * has no record for the ASIN (no product returned, or a stub with no title). */
115
+ /** Throws `ProductNotFoundError` when Keepa returns no product or a stub. */
82
116
  retrieve(params: ProductRetrieveParams): Promise<KeepaProduct>;
83
117
  }
84
118
 
85
119
  interface ClientOptions {
86
- /** Keepa API key. Falls back to `process.env.KEEPA_API_KEY`. */
120
+ /** Falls back to `process.env.KEEPA_API_KEY` when omitted. */
87
121
  apiKey?: string;
88
- /** Base URL for the Keepa API. Defaults to `https://api.keepa.com`. */
89
122
  baseURL?: string;
90
- /** Custom fetch implementation. Defaults to `globalThis.fetch`. */
91
123
  fetch?: typeof globalThis.fetch;
92
124
  }
93
125
  declare class KeepaClient {
@@ -95,8 +127,10 @@ declare class KeepaClient {
95
127
  readonly baseURL: string;
96
128
  readonly fetch: typeof globalThis.fetch;
97
129
  readonly products: Products;
130
+ /** Latest rate-limit snapshot from Keepa, updated after every response.
131
+ * Null until the first request completes. */
132
+ rateLimit: RateLimitInfo | null;
98
133
  constructor(options?: ClientOptions);
99
- /** Internal: used by APIResource subclasses to perform a request. */
100
134
  _request<T>(args: RequestArgs): Promise<T>;
101
135
  }
102
136
 
@@ -108,10 +142,13 @@ declare class APIError extends KeepaError {
108
142
  readonly context: string;
109
143
  readonly body: string;
110
144
  constructor(status: number, context: string, body: string, message?: string);
111
- static from(response: Response, context: string): Promise<APIError>;
145
+ static from(status: number, context: string, body: string, rateLimit?: RateLimitInfo | null): APIError;
112
146
  }
113
147
  declare class RateLimitError extends APIError {
114
- constructor(context: string, body: string);
148
+ /** Bucket snapshot at the moment the 429 was returned. Null if Keepa's body
149
+ * didn't carry the usual rate-limit fields. */
150
+ readonly rateLimit: RateLimitInfo | null;
151
+ constructor(context: string, body: string, rateLimit?: RateLimitInfo | null);
115
152
  }
116
153
  declare class AuthenticationError extends APIError {
117
154
  constructor(context: string, body: string);
@@ -124,29 +161,20 @@ declare class NetworkError extends KeepaError {
124
161
 
125
162
  declare const ASIN_LENGTH = 10;
126
163
  declare const ASIN_REGEX: RegExp;
127
- /** Returns true if the given string is a structurally valid ASIN
128
- * (10 uppercase alphanumeric characters). Note: a passing ASIN may still
129
- * not exist in Keepa's database — use `isFoundProduct` to check that. */
164
+ /** Structural check only a passing ASIN may still not exist in Keepa's
165
+ * database; use `isFoundProduct` for that. */
130
166
  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
167
  declare function normalizeAsins(asins: string[]): string[];
134
168
 
135
- /** Returns true when Keepa returned an actual product record (not just a stub
136
- * for an unknown ASIN). Stubs come back with `title: null`; real listings
137
- * always have a string. Use as a `.filter()` predicate to drop empty matches. */
169
+ declare function parsePriceHistory(series: number[] | undefined): PriceHistoryEntry[];
170
+ /** Stubs for unknown ASINs come back with `title: null`. */
138
171
  declare function isFoundProduct(product: KeepaProduct): boolean;
139
- /** Extract the most recent real BSR from Keepa's `[ts, rank, ts, rank, ...]` salesRanks array.
140
- * Walks backward through rank entries (odd indices) and skips Keepa's `-1` sentinel which
141
- * marks "no data captured at that timestamp". Returns `null` if every entry is sentinel.
142
- * Most consumers won't need this — `Products.list` already fills `bsr` on every returned
143
- * product. Useful when working with a raw Keepa response. */
172
+ /** Latest non-sentinel rank from Keepa's `[ts, rank, ts, rank, ...]` salesRanks
173
+ * series for the product's rootCategory. */
144
174
  declare function extractBsr(salesRanks: Record<string, number[]> | undefined, rootCategory: number | undefined): number | null;
145
175
 
146
- /** Thrown by `Products.retrieve` when Keepa has no record for the requested ASIN
147
- * (either no product object returned, or a stub with no title). The `asin`
148
- * property carries the value the caller passed in, unchanged, so it can be
149
- * surfaced in user-facing messages without re-deriving it. */
176
+ /** Thrown when Keepa returns no product, or returns a stub (title === null).
177
+ * `asin` carries the caller's original input unchanged. */
150
178
  declare class ProductNotFoundError extends KeepaError {
151
179
  readonly asin: string;
152
180
  constructor(asin: string);
@@ -157,7 +185,46 @@ declare const PRODUCT_LIST_CONTEXT = "products.list";
157
185
  declare const DEFAULT_DAYS = 1;
158
186
  declare const AMAZON_IMAGE_BASE = "https://m.media-amazon.com/images/I";
159
187
  declare const KEEPA_NO_DATA_SENTINEL = -1;
188
+ declare const CsvType: {
189
+ readonly AMAZON: 0;
190
+ readonly NEW: 1;
191
+ readonly USED: 2;
192
+ readonly SALES: 3;
193
+ readonly LISTPRICE: 4;
194
+ readonly COLLECTIBLE: 5;
195
+ readonly REFURBISHED: 6;
196
+ readonly NEW_FBM_SHIPPING: 7;
197
+ readonly LIGHTNING_DEAL: 8;
198
+ readonly WAREHOUSE: 9;
199
+ readonly NEW_FBA: 10;
200
+ readonly COUNT_NEW: 11;
201
+ readonly COUNT_USED: 12;
202
+ readonly COUNT_REFURBISHED: 13;
203
+ readonly COUNT_COLLECTIBLE: 14;
204
+ readonly EXTRA_INFO_UPDATES: 15;
205
+ readonly RATING: 16;
206
+ readonly COUNT_REVIEWS: 17;
207
+ readonly BUY_BOX_SHIPPING: 18;
208
+ readonly USED_NEW_SHIPPING: 19;
209
+ readonly USED_VERY_GOOD_SHIPPING: 20;
210
+ readonly USED_GOOD_SHIPPING: 21;
211
+ readonly USED_ACCEPTABLE_SHIPPING: 22;
212
+ readonly COLLECTIBLE_NEW_SHIPPING: 23;
213
+ readonly COLLECTIBLE_VERY_GOOD_SHIPPING: 24;
214
+ readonly COLLECTIBLE_GOOD_SHIPPING: 25;
215
+ readonly COLLECTIBLE_ACCEPTABLE_SHIPPING: 26;
216
+ readonly REFURBISHED_SHIPPING: 27;
217
+ readonly EBAY_NEW_SHIPPING: 28;
218
+ readonly EBAY_USED_SHIPPING: 29;
219
+ readonly TRADE_IN: 30;
220
+ readonly RENTAL: 31;
221
+ readonly BUY_BOX_USED_SHIPPING: 32;
222
+ readonly PRIME_EXCL: 33;
223
+ readonly COUNT_NEW_FBA: 34;
224
+ readonly COUNT_NEW_FBM: 35;
225
+ };
226
+ type CsvType = (typeof CsvType)[keyof typeof CsvType];
160
227
 
161
228
  declare const VERSION = "0.1.0";
162
229
 
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 };
230
+ 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, type RateLimitInfo, VERSION, KeepaClient as default, extractBsr, isFoundProduct, isValidAsin, normalizeAsins, parsePriceHistory, resolveDomainId };