keepa-api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +211 -0
  3. package/dist/client.d.ts +21 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +38 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/core/error.d.ts +27 -0
  8. package/dist/core/error.d.ts.map +1 -0
  9. package/dist/core/error.js +61 -0
  10. package/dist/core/error.js.map +1 -0
  11. package/dist/core/request.d.ts +15 -0
  12. package/dist/core/request.d.ts.map +1 -0
  13. package/dist/core/request.js +36 -0
  14. package/dist/core/request.js.map +1 -0
  15. package/dist/core/resource.d.ts +6 -0
  16. package/dist/core/resource.d.ts.map +1 -0
  17. package/dist/core/resource.js +7 -0
  18. package/dist/core/resource.js.map +1 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +8 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/lib/asin.d.ts +10 -0
  24. package/dist/lib/asin.d.ts.map +1 -0
  25. package/dist/lib/asin.js +19 -0
  26. package/dist/lib/asin.js.map +1 -0
  27. package/dist/lib/marketplace.d.ts +17 -0
  28. package/dist/lib/marketplace.d.ts.map +1 -0
  29. package/dist/lib/marketplace.js +26 -0
  30. package/dist/lib/marketplace.js.map +1 -0
  31. package/dist/resources/index.d.ts +2 -0
  32. package/dist/resources/index.d.ts.map +1 -0
  33. package/dist/resources/index.js +2 -0
  34. package/dist/resources/index.js.map +1 -0
  35. package/dist/resources/products/constant.d.ts +7 -0
  36. package/dist/resources/products/constant.d.ts.map +1 -0
  37. package/dist/resources/products/constant.js +15 -0
  38. package/dist/resources/products/constant.js.map +1 -0
  39. package/dist/resources/products/index.d.ts +4 -0
  40. package/dist/resources/products/index.d.ts.map +1 -0
  41. package/dist/resources/products/index.js +6 -0
  42. package/dist/resources/products/index.js.map +1 -0
  43. package/dist/resources/products/product.raw.type.d.ts +28 -0
  44. package/dist/resources/products/product.raw.type.d.ts.map +1 -0
  45. package/dist/resources/products/product.raw.type.js +6 -0
  46. package/dist/resources/products/product.raw.type.js.map +1 -0
  47. package/dist/resources/products/product.type.d.ts +41 -0
  48. package/dist/resources/products/product.type.d.ts.map +1 -0
  49. package/dist/resources/products/product.type.js +2 -0
  50. package/dist/resources/products/product.type.js.map +1 -0
  51. package/dist/resources/products/products.d.ts +31 -0
  52. package/dist/resources/products/products.d.ts.map +1 -0
  53. package/dist/resources/products/products.js +96 -0
  54. package/dist/resources/products/products.js.map +1 -0
  55. package/dist/version.d.ts +2 -0
  56. package/dist/version.d.ts.map +1 -0
  57. package/dist/version.js +2 -0
  58. package/dist/version.js.map +1 -0
  59. package/package.json +45 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wajeeh Ul Hassan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # keepa-api
2
+
3
+ Lightweight TypeScript SDK for the [Keepa](https://keepa.com) REST API. Mirrors the organizational style of [openai-node](https://github.com/openai/openai-node) — a single `Keepa` client class exposes resources (`products`, etc.) that wrap each endpoint with typed inputs and responses.
4
+
5
+ Phase 1 ships the **Products** resource only. Categories, Search, and Bestsellers are planned but not yet implemented.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js **≥ 18** (uses native `fetch`)
10
+ - A Keepa API key — sign up at [keepa.com/api](https://keepa.com/api.html)
11
+ - ESM consumer (your `package.json` should have `"type": "module"`, or you must be able to `import()` an ESM package)
12
+
13
+ ## Installation
14
+
15
+ This package is **not yet published to npm**. Install it locally or from GitHub.
16
+
17
+ ### From a local checkout (during development)
18
+
19
+ ```bash
20
+ # In keepa-api/
21
+ npm install
22
+ npm run build
23
+ npm link
24
+
25
+ # In your other project:
26
+ npm link keepa-api
27
+ ```
28
+
29
+ After this, edits in `keepa-api/src/` only need `npm run build` — the consumer sees the new `dist/` immediately.
30
+
31
+ ### From GitHub (no npm publish needed)
32
+
33
+ ```bash
34
+ npm install github:bilalyasin1616/keepa-api#feature/keepa-products-package
35
+ ```
36
+
37
+ (Replace the branch with `main` once the work is merged.)
38
+
39
+ ### From a tarball
40
+
41
+ ```bash
42
+ # In keepa-api/
43
+ npm pack # produces keepa-api-0.1.0.tgz
44
+
45
+ # In your other project:
46
+ npm install /absolute/path/to/keepa-api-0.1.0.tgz
47
+ ```
48
+
49
+ ## Quickstart
50
+
51
+ ```ts
52
+ import Keepa from 'keepa-api';
53
+
54
+ const keepa = new Keepa({ apiKey: process.env.KEEPA_API_KEY });
55
+
56
+ const products = await keepa.products.list({
57
+ asins: ['B00MNV8E0C'],
58
+ marketplace: 'US',
59
+ });
60
+
61
+ console.log(products[0]?.title);
62
+ ```
63
+
64
+ The client reads `process.env.KEEPA_API_KEY` if you don't pass `apiKey` explicitly. Put your key in a gitignored `.env` (see `.env.example` for the shape).
65
+
66
+ ## API
67
+
68
+ ### `new Keepa(options?)`
69
+
70
+ | Option | Type | Default | Notes |
71
+ |--------|------|---------|-------|
72
+ | `apiKey` | `string` | `process.env.KEEPA_API_KEY` | Throws if neither is set. |
73
+ | `baseURL` | `string` | `'https://api.keepa.com'` | Override for testing/proxying. |
74
+ | `fetch` | `typeof fetch` | `globalThis.fetch` | Plug in a custom fetch (mocks, retries, etc.). |
75
+
76
+ ### `keepa.products.list(params)` → `Promise<KeepaProduct[]>`
77
+
78
+ | Param | Type | Default | Notes |
79
+ |-------|------|---------|-------|
80
+ | `asins` | `string[]` | (required) | Validated + uppercased. Throws on malformed input. |
81
+ | `marketplace` | `'US' \| 'UK' \| 'DE' \| 'FR' \| 'JP' \| 'CA' \| 'IT' \| 'ES' \| 'IN' \| 'MX' \| 'AU'` | `'US'` | Case-insensitive. Throws on unknown. |
82
+ | `days` | `number` | `1` | Days of price history. Must be a positive integer (validated pre-flight). |
83
+
84
+ The SDK maps Keepa's raw wire shape into a friendlier `KeepaProduct`:
85
+
86
+ - `images: string[]` — full image URLs (region-neutral CDN). Replaces Keepa's awkward `imagesCSV` string.
87
+ - `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.
88
+
89
+ The raw `salesRanks` record is preserved for consumers that need to walk the full rank history. Other Keepa fields (`title`, `parentAsin`, `categoryTree`, `variations`, `bulletPoints`, …) pass through unchanged.
90
+
91
+ **Stub records:** Keepa returns one record per requested ASIN even when it has no data — these stubs have `title === undefined`. Filter with `isFoundProduct` (below).
92
+
93
+ ### Helpers
94
+
95
+ ```ts
96
+ import { isFoundProduct } from 'keepa-api';
97
+
98
+ const real = products.filter(isFoundProduct);
99
+
100
+ for (const product of real) {
101
+ console.log(product.bsr); // already populated by the SDK
102
+ console.log(product.images[0]); // already populated by the SDK
103
+ }
104
+ ```
105
+
106
+ | Helper | Signature | Returns |
107
+ |--------|-----------|---------|
108
+ | `isFoundProduct(product)` | `(product: KeepaProduct) => boolean` | `true` only if Keepa returned real data (`title` is non-empty). |
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
+ | `extractBsr(salesRanks, rootCategory)` | `(salesRanks: Record<string, number[]> \| undefined, rootCategory: number \| undefined) => number \| null` | Most recent real BSR from Keepa's raw `[ts, rank, ...]` history. Used internally to fill `bsr`; exported for advanced use. |
111
+ | `extractImageUrl(imagesCSV)` | `(imagesCSV: string \| undefined) => string \| null` | Single URL — equivalent to `parseImagesCsv(imagesCSV)[0] ?? null`. Exported for advanced use. |
112
+
113
+ ### ASIN validation
114
+
115
+ ```ts
116
+ import { ASIN_REGEX, ASIN_LENGTH, isValidAsin, normalizeAsins } from 'keepa-api';
117
+
118
+ isValidAsin('B00MNV8E0C'); // true
119
+ isValidAsin('b00mnv8e0c'); // false (lowercase — use normalizeAsins to coerce)
120
+ isValidAsin('B07XYZ'); // false (too short)
121
+
122
+ normalizeAsins([' b00mnv8e0c ']); // ['B00MNV8E0C']
123
+ normalizeAsins(['B07XYZ']); // throws: Invalid ASIN(s): B07XYZ. ...
124
+ ```
125
+
126
+ `Products.list` calls `normalizeAsins` for you, so you don't need to pre-validate unless you're doing form-level checking.
127
+
128
+ **Caveat:** The regex catches *malformed* input. It cannot catch "Keepa has no record" — for example `1234567890` is a structurally valid ISBN-10 shape and passes the regex, but Keepa returns a stub record. Use `isFoundProduct` to filter those.
129
+
130
+ ### Marketplaces
131
+
132
+ ```ts
133
+ import { MARKETPLACE_DOMAINS, resolveDomainId } from 'keepa-api';
134
+
135
+ MARKETPLACE_DOMAINS.US; // 1
136
+ resolveDomainId('uk'); // 2 (case-insensitive)
137
+ resolveDomainId(undefined); // 1 (defaults to US)
138
+ ```
139
+
140
+ | Code | Domain ID | Code | Domain ID |
141
+ |------|-----------|------|-----------|
142
+ | US | 1 | IT | 8 |
143
+ | UK | 2 | ES | 9 |
144
+ | DE | 3 | IN | 10 |
145
+ | FR | 4 | MX | 11 |
146
+ | JP | 5 | AU | 13 |
147
+ | CA | 6 | | |
148
+
149
+ ### Errors
150
+
151
+ All errors thrown by API calls extend `KeepaError`. Catch the specific subclasses for status-aware handling:
152
+
153
+ ```ts
154
+ import {
155
+ RateLimitError,
156
+ AuthenticationError,
157
+ APIError,
158
+ NetworkError,
159
+ KeepaError,
160
+ } from 'keepa-api';
161
+
162
+ try {
163
+ await keepa.products.list({ asins: ['B00MNV8E0C'] });
164
+ } catch (err) {
165
+ if (err instanceof RateLimitError) /* 429 */;
166
+ else if (err instanceof AuthenticationError) /* 401 — bad API key */;
167
+ else if (err instanceof APIError) /* 4xx/5xx — err.status, err.body */;
168
+ else if (err instanceof NetworkError) /* DNS/ECONNREFUSED/abort — err.cause has the original */;
169
+ else if (err instanceof KeepaError) /* something else from this SDK */;
170
+ else throw err;
171
+ }
172
+ ```
173
+
174
+ ### API key handling
175
+
176
+ Keepa's REST API requires the key as a `?key=...` query parameter — that's their contract, not a choice we made. Practical implications:
177
+
178
+ - **Server-side proxy / access logs** will record full URLs (key included). Configure log scrubbing if you can't trust the layer.
179
+ - **Don't run this in the browser.** A client-side request would expose your key in DevTools' Network tab and to any browser extension or middlebox.
180
+ - The `NetworkError.body` and `APIError.body` fields capture Keepa's response body. Keepa doesn't echo your key in error bodies, but if you log them to a customer-facing surface, sanity-check the contents first.
181
+
182
+ ## Scripts
183
+
184
+ ```bash
185
+ npm test # vitest run — unit tests
186
+ npm run test:watch # vitest watch
187
+ npm run build # tsc → dist/
188
+ npm run example # runs examples/basic.ts against real Keepa (needs .env)
189
+ npm run clean # rm -rf dist
190
+ ```
191
+
192
+ ## Roadmap
193
+
194
+ **Resources**
195
+ - [ ] `Categories` resource (`fetchKeepaCategories`)
196
+ - [ ] `Categories.search` (`searchKeepaCategories`)
197
+ - [ ] `Bestsellers.retrieve` (`fetchKeepaBestSeller`)
198
+
199
+ **Request layer**
200
+ - [ ] `AbortSignal` / configurable timeout on `Products.list` (default ~30s) — currently a hung Keepa server keeps the request pending forever
201
+ - [ ] Pass-through `init` (method, headers, signal) on `core/request` for resources that need POST or custom headers
202
+ - [ ] Cap `asins.length` at Keepa's per-call limit (100) instead of letting Keepa silently truncate
203
+ - [ ] Optional `retryOn429` (with backoff) — currently we throw `RateLimitError` immediately; consumers handle pacing in their own code
204
+
205
+ **Distribution**
206
+ - [ ] CommonJS build alongside ESM
207
+ - [ ] Publish to npm
208
+
209
+ ## License
210
+
211
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,21 @@
1
+ import type { RequestArgs } from './core/request.js';
2
+ import { Products } from './resources/products/products.js';
3
+ export interface ClientOptions {
4
+ /** Keepa API key. Falls back to `process.env.KEEPA_API_KEY`. */
5
+ apiKey?: string;
6
+ /** Base URL for the Keepa API. Defaults to `https://api.keepa.com`. */
7
+ baseURL?: string;
8
+ /** Custom fetch implementation. Defaults to `globalThis.fetch`. */
9
+ fetch?: typeof globalThis.fetch;
10
+ }
11
+ export declare class Keepa {
12
+ readonly apiKey: string;
13
+ readonly baseURL: string;
14
+ readonly fetch: typeof globalThis.fetch;
15
+ readonly products: Products;
16
+ constructor(options?: ClientOptions);
17
+ /** Internal: used by APIResource subclasses to perform a request. */
18
+ _request<T>(args: RequestArgs): Promise<T>;
19
+ }
20
+ export default Keepa;
21
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAE5D,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAID,qBAAa,KAAK;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAExC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;gBAEhB,OAAO,GAAE,aAAkB;IAuBvC,qEAAqE;IACrE,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;CAQ3C;AAED,eAAe,KAAK,CAAC"}
package/dist/client.js ADDED
@@ -0,0 +1,38 @@
1
+ import { request } from './core/request.js';
2
+ import { Products } from './resources/products/products.js';
3
+ const DEFAULT_BASE_URL = 'https://api.keepa.com';
4
+ export class Keepa {
5
+ apiKey;
6
+ baseURL;
7
+ fetch;
8
+ products;
9
+ constructor(options = {}) {
10
+ // `process` is undefined in browsers / Cloudflare Workers / edge runtimes — guard it
11
+ // so the constructor throws our friendly "Missing Keepa API key" message instead of a
12
+ // raw ReferenceError on those targets.
13
+ const envApiKey = typeof process !== 'undefined' ? process.env?.KEEPA_API_KEY : undefined;
14
+ const apiKey = options.apiKey || envApiKey;
15
+ if (!apiKey) {
16
+ throw new Error('Missing Keepa API key. Pass it as `new Keepa({ apiKey })` or set KEEPA_API_KEY in your environment.');
17
+ }
18
+ this.apiKey = apiKey;
19
+ // Strip a trailing slash so `${baseURL}${path}` never produces `//product`.
20
+ this.baseURL = (options.baseURL ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
21
+ // Bind `globalThis` once: an unbound reference to Node's fetch throws
22
+ // "Illegal invocation" when called with an undefined `this`. Bind once at
23
+ // construction so every _request call uses the cached bound function.
24
+ this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
25
+ this.products = new Products(this);
26
+ }
27
+ /** Internal: used by APIResource subclasses to perform a request. */
28
+ _request(args) {
29
+ const config = {
30
+ apiKey: this.apiKey,
31
+ baseURL: this.baseURL,
32
+ fetch: this.fetch,
33
+ };
34
+ return request(config, args);
35
+ }
36
+ }
37
+ export default Keepa;
38
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAW5D,MAAM,gBAAgB,GAAG,uBAAuB,CAAC;AAEjD,MAAM,OAAO,KAAK;IACP,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,KAAK,CAA0B;IAE/B,QAAQ,CAAW;IAE5B,YAAY,UAAyB,EAAE;QACrC,qFAAqF;QACrF,sFAAsF;QACtF,uCAAuC;QACvC,MAAM,SAAS,GACb,OAAO,OAAO,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;QAC1E,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,SAAS,CAAC;QAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CACb,qGAAqG,CACtG,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,4EAA4E;QAC5E,IAAI,CAAC,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACzE,sEAAsE;QACtE,0EAA0E;QAC1E,sEAAsE;QACtE,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAEhE,IAAI,CAAC,QAAQ,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IAED,qEAAqE;IACrE,QAAQ,CAAI,IAAiB;QAC3B,MAAM,MAAM,GAAkB;YAC5B,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,KAAK,EAAE,IAAI,CAAC,KAAK;SAClB,CAAC;QACF,OAAO,OAAO,CAAI,MAAM,EAAE,IAAI,CAAC,CAAC;IAClC,CAAC;CACF;AAED,eAAe,KAAK,CAAC"}
@@ -0,0 +1,27 @@
1
+ /** Replace any `?key=...` or `&key=...` segment with a REDACTED placeholder.
2
+ * Defense-in-depth: keeps the API key out of error messages even when an
3
+ * underlying transport error or upstream response echoes the request URL.
4
+ * Exported for use by callers that build their own error wrappers. */
5
+ export declare function scrubApiKey(text: string): string;
6
+ export declare class KeepaError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ export declare class APIError extends KeepaError {
10
+ readonly status: number;
11
+ readonly context: string;
12
+ readonly body: string;
13
+ constructor(status: number, context: string, body: string, message?: string);
14
+ static from(response: Response, context: string): Promise<APIError>;
15
+ }
16
+ export declare class RateLimitError extends APIError {
17
+ constructor(context: string, body: string);
18
+ }
19
+ export declare class AuthenticationError extends APIError {
20
+ constructor(context: string, body: string);
21
+ }
22
+ export declare class NetworkError extends KeepaError {
23
+ readonly context: string;
24
+ readonly cause: unknown;
25
+ constructor(context: string, cause: unknown);
26
+ }
27
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../src/core/error.ts"],"names":[],"mappings":"AAAA;;;uEAGuE;AACvE,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,QAAS,SAAQ,UAAU;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;WAS9D,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;CAW1E;AAED,qBAAa,cAAe,SAAQ,QAAQ;gBAC9B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAI1C;AAED,qBAAa,mBAAoB,SAAQ,QAAQ;gBACnC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAS1C;AAED,qBAAa,YAAa,SAAQ,UAAU;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,SAAkB,KAAK,EAAE,OAAO,CAAC;gBAErB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CAO5C"}
@@ -0,0 +1,61 @@
1
+ /** Replace any `?key=...` or `&key=...` segment with a REDACTED placeholder.
2
+ * Defense-in-depth: keeps the API key out of error messages even when an
3
+ * underlying transport error or upstream response echoes the request URL.
4
+ * Exported for use by callers that build their own error wrappers. */
5
+ export function scrubApiKey(text) {
6
+ return text.replace(/([?&])key=[^&\s]*/gi, '$1key=REDACTED');
7
+ }
8
+ export class KeepaError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = 'KeepaError';
12
+ }
13
+ }
14
+ export class APIError extends KeepaError {
15
+ status;
16
+ context;
17
+ body;
18
+ constructor(status, context, body, message) {
19
+ const safeBody = scrubApiKey(body);
20
+ super(scrubApiKey(message ?? `Keepa ${context} error (${status}): ${safeBody}`));
21
+ this.name = 'APIError';
22
+ this.status = status;
23
+ this.context = context;
24
+ this.body = safeBody;
25
+ }
26
+ static async from(response, context) {
27
+ const body = await response.text().catch(() => '');
28
+ switch (response.status) {
29
+ case 429:
30
+ return new RateLimitError(context, body);
31
+ case 401:
32
+ return new AuthenticationError(context, body);
33
+ default:
34
+ return new APIError(response.status, context, body);
35
+ }
36
+ }
37
+ }
38
+ export class RateLimitError extends APIError {
39
+ constructor(context, body) {
40
+ super(429, context, body, 'Keepa rate limit exceeded, please wait or upgrade plan');
41
+ this.name = 'RateLimitError';
42
+ }
43
+ }
44
+ export class AuthenticationError extends APIError {
45
+ constructor(context, body) {
46
+ super(401, context, body, `Keepa authentication failed for ${context}: invalid or missing API key`);
47
+ this.name = 'AuthenticationError';
48
+ }
49
+ }
50
+ export class NetworkError extends KeepaError {
51
+ context;
52
+ cause;
53
+ constructor(context, cause) {
54
+ const raw = cause instanceof Error ? cause.message : String(cause);
55
+ super(`Keepa ${context} network error: ${scrubApiKey(raw)}`);
56
+ this.name = 'NetworkError';
57
+ this.context = context;
58
+ this.cause = cause;
59
+ }
60
+ }
61
+ //# sourceMappingURL=error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.js","sourceRoot":"","sources":["../../src/core/error.ts"],"names":[],"mappings":"AAAA;;;uEAGuE;AACvE,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,gBAAgB,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,OAAO,UAAW,SAAQ,KAAK;IACnC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,OAAO,QAAS,SAAQ,UAAU;IAC7B,MAAM,CAAS;IACf,OAAO,CAAS;IAChB,IAAI,CAAS;IAEtB,YAAY,MAAc,EAAE,OAAe,EAAE,IAAY,EAAE,OAAgB;QACzE,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QACnC,KAAK,CAAC,WAAW,CAAC,OAAO,IAAI,SAAS,OAAO,WAAW,MAAM,MAAM,QAAQ,EAAE,CAAC,CAAC,CAAC;QACjF,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC;IACvB,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,QAAkB,EAAE,OAAe;QACnD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACnD,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAC;YACxB,KAAK,GAAG;gBACN,OAAO,IAAI,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAC3C,KAAK,GAAG;gBACN,OAAO,IAAI,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAChD;gBACE,OAAO,IAAI,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,QAAQ;IAC1C,YAAY,OAAe,EAAE,IAAY;QACvC,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,wDAAwD,CAAC,CAAC;QACpF,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED,MAAM,OAAO,mBAAoB,SAAQ,QAAQ;IAC/C,YAAY,OAAe,EAAE,IAAY;QACvC,KAAK,CACH,GAAG,EACH,OAAO,EACP,IAAI,EACJ,mCAAmC,OAAO,8BAA8B,CACzE,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,MAAM,OAAO,YAAa,SAAQ,UAAU;IACjC,OAAO,CAAS;IACP,KAAK,CAAU;IAEjC,YAAY,OAAe,EAAE,KAAc;QACzC,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnE,KAAK,CAAC,SAAS,OAAO,mBAAmB,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;CACF"}
@@ -0,0 +1,15 @@
1
+ export type QueryValue = string | number | string[] | number[] | undefined;
2
+ export type QueryParams = Record<string, QueryValue>;
3
+ export interface RequestArgs {
4
+ path: string;
5
+ query?: QueryParams;
6
+ context: string;
7
+ }
8
+ export interface RequestConfig {
9
+ baseURL: string;
10
+ apiKey: string;
11
+ fetch: typeof globalThis.fetch;
12
+ }
13
+ export declare function buildUrl(baseURL: string, path: string, query?: QueryParams): string;
14
+ export declare function request<T>(config: RequestConfig, args: RequestArgs): Promise<T>;
15
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../../src/core/request.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;AAC3E,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAChC;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,MAAM,CASnF;AAWD,wBAAsB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,CAWrF"}
@@ -0,0 +1,36 @@
1
+ import { APIError, NetworkError } from './error.js';
2
+ export function buildUrl(baseURL, path, query) {
3
+ if (!query)
4
+ return `${baseURL}${path}`;
5
+ const parts = [];
6
+ for (const [key, value] of Object.entries(query)) {
7
+ if (value === undefined)
8
+ continue;
9
+ const stringValue = Array.isArray(value) ? value.join(',') : String(value);
10
+ parts.push(`${encodeURIComponent(key)}=${encodeKeepaValue(stringValue)}`);
11
+ }
12
+ return parts.length ? `${baseURL}${path}?${parts.join('&')}` : `${baseURL}${path}`;
13
+ }
14
+ // Keep commas literal — Keepa accepts comma-separated lists (asin=A,B,C, category=1,2,3)
15
+ // and the category endpoint specifically rejects %2C-encoded commas.
16
+ // Currently private to this module: if a future helper builds Keepa URLs outside
17
+ // `request()` (e.g. a paginator) and needs the same encoding, lift this into
18
+ // `core/encoding.ts` rather than duplicating it.
19
+ function encodeKeepaValue(value) {
20
+ return encodeURIComponent(value).replace(/%2C/g, ',');
21
+ }
22
+ export async function request(config, args) {
23
+ const query = { key: config.apiKey, ...args.query };
24
+ const url = buildUrl(config.baseURL, args.path, query);
25
+ let res;
26
+ try {
27
+ res = await config.fetch(url);
28
+ }
29
+ catch (cause) {
30
+ throw new NetworkError(args.context, cause);
31
+ }
32
+ if (!res.ok)
33
+ throw await APIError.from(res, args.context);
34
+ return (await res.json());
35
+ }
36
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/core/request.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAiBpD,MAAM,UAAU,QAAQ,CAAC,OAAe,EAAE,IAAY,EAAE,KAAmB;IACzE,IAAI,CAAC,KAAK;QAAE,OAAO,GAAG,OAAO,GAAG,IAAI,EAAE,CAAC;IACvC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3E,KAAK,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,gBAAgB,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,IAAI,EAAE,CAAC;AACrF,CAAC;AAED,yFAAyF;AACzF,qEAAqE;AACrE,iFAAiF;AACjF,6EAA6E;AAC7E,iDAAiD;AACjD,SAAS,gBAAgB,CAAC,KAAa;IACrC,OAAO,kBAAkB,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAI,MAAqB,EAAE,IAAiB;IACvE,MAAM,KAAK,GAAgB,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;IACjE,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACvD,IAAI,GAAa,CAAC;IAClB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,MAAM,MAAM,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1D,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAM,CAAC;AACjC,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { Keepa } from '../client.js';
2
+ export declare abstract class APIResource {
3
+ protected _client: Keepa;
4
+ constructor(client: Keepa);
5
+ }
6
+ //# sourceMappingURL=resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/core/resource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,8BAAsB,WAAW;IAC/B,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC;gBAEb,MAAM,EAAE,KAAK;CAG1B"}
@@ -0,0 +1,7 @@
1
+ export class APIResource {
2
+ _client;
3
+ constructor(client) {
4
+ this._client = client;
5
+ }
6
+ }
7
+ //# sourceMappingURL=resource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.js","sourceRoot":"","sources":["../../src/core/resource.ts"],"names":[],"mappings":"AAEA,MAAM,OAAgB,WAAW;IACrB,OAAO,CAAQ;IAEzB,YAAY,MAAa;QACvB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACxB,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ export { Keepa as default, Keepa, type ClientOptions } from './client.js';
2
+ export { KeepaError, APIError, RateLimitError, AuthenticationError, NetworkError, } from './core/error.js';
3
+ export { APIResource } from './core/resource.js';
4
+ export { MARKETPLACE_DOMAINS, resolveDomainId, type Marketplace, type DomainId, } from './lib/marketplace.js';
5
+ export { ASIN_LENGTH, ASIN_REGEX, isValidAsin, normalizeAsins, } from './lib/asin.js';
6
+ export * from './resources/index.js';
7
+ export { VERSION } from './version.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,OAAO,EAAE,KAAK,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAE1E,OAAO,EACL,UAAU,EACV,QAAQ,EACR,cAAc,EACd,mBAAmB,EACnB,YAAY,GACb,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,QAAQ,GACd,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,cAAc,GACf,MAAM,eAAe,CAAC;AAEvB,cAAc,sBAAsB,CAAC;AAErC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { Keepa as default, Keepa } from './client.js';
2
+ export { KeepaError, APIError, RateLimitError, AuthenticationError, NetworkError, } from './core/error.js';
3
+ export { APIResource } from './core/resource.js';
4
+ export { MARKETPLACE_DOMAINS, resolveDomainId, } from './lib/marketplace.js';
5
+ export { ASIN_LENGTH, ASIN_REGEX, isValidAsin, normalizeAsins, } from './lib/asin.js';
6
+ export * from './resources/index.js';
7
+ export { VERSION } from './version.js';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,OAAO,EAAE,KAAK,EAAsB,MAAM,aAAa,CAAC;AAE1E,OAAO,EACL,UAAU,EACV,QAAQ,EACR,cAAc,EACd,mBAAmB,EACnB,YAAY,GACb,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,EACL,mBAAmB,EACnB,eAAe,GAGhB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,cAAc,GACf,MAAM,eAAe,CAAC;AAEvB,cAAc,sBAAsB,CAAC;AAErC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,10 @@
1
+ export declare const ASIN_LENGTH = 10;
2
+ export declare const ASIN_REGEX: RegExp;
3
+ /** Returns true if the given string is a structurally valid ASIN
4
+ * (10 uppercase alphanumeric characters). Note: a passing ASIN may still
5
+ * not exist in Keepa's database — use `isFoundProduct` to check that. */
6
+ export declare function isValidAsin(value: string): boolean;
7
+ /** Trim + uppercase a list of ASINs and validate each. Returns the normalized
8
+ * list. Throws with all invalid ASINs listed in the error message. */
9
+ export declare function normalizeAsins(asins: string[]): string[];
10
+ //# sourceMappingURL=asin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asin.d.ts","sourceRoot":"","sources":["../../src/lib/asin.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,KAAK,CAAC;AAC9B,eAAO,MAAM,UAAU,QAAmB,CAAC;AAE3C;;0EAE0E;AAC1E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;uEACuE;AACvE,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CASxD"}
@@ -0,0 +1,19 @@
1
+ export const ASIN_LENGTH = 10;
2
+ export const ASIN_REGEX = /^[A-Z0-9]{10}$/;
3
+ /** Returns true if the given string is a structurally valid ASIN
4
+ * (10 uppercase alphanumeric characters). Note: a passing ASIN may still
5
+ * not exist in Keepa's database — use `isFoundProduct` to check that. */
6
+ export function isValidAsin(value) {
7
+ return ASIN_REGEX.test(value);
8
+ }
9
+ /** Trim + uppercase a list of ASINs and validate each. Returns the normalized
10
+ * list. Throws with all invalid ASINs listed in the error message. */
11
+ export function normalizeAsins(asins) {
12
+ const normalized = asins.map((asin) => asin.trim().toUpperCase());
13
+ const invalid = normalized.filter((asin) => !isValidAsin(asin));
14
+ if (invalid.length > 0) {
15
+ throw new Error(`Invalid ASIN(s): ${invalid.join(', ')}. ASINs must be ${ASIN_LENGTH} alphanumeric characters (e.g. B00MNV8E0C).`);
16
+ }
17
+ return normalized;
18
+ }
19
+ //# sourceMappingURL=asin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asin.js","sourceRoot":"","sources":["../../src/lib/asin.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAC9B,MAAM,CAAC,MAAM,UAAU,GAAG,gBAAgB,CAAC;AAE3C;;0EAE0E;AAC1E,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC;AAED;uEACuE;AACvE,MAAM,UAAU,cAAc,CAAC,KAAe;IAC5C,MAAM,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IAClE,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;IAChE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,oBAAoB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,WAAW,6CAA6C,CAClH,CAAC;IACJ,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC"}
@@ -0,0 +1,17 @@
1
+ export declare const MARKETPLACE_DOMAINS: {
2
+ readonly US: 1;
3
+ readonly UK: 2;
4
+ readonly DE: 3;
5
+ readonly FR: 4;
6
+ readonly JP: 5;
7
+ readonly CA: 6;
8
+ readonly IT: 8;
9
+ readonly ES: 9;
10
+ readonly IN: 10;
11
+ readonly MX: 11;
12
+ readonly AU: 13;
13
+ };
14
+ export type Marketplace = keyof typeof MARKETPLACE_DOMAINS;
15
+ export type DomainId = (typeof MARKETPLACE_DOMAINS)[Marketplace];
16
+ export declare function resolveDomainId(marketplace: string | undefined): DomainId;
17
+ //# sourceMappingURL=marketplace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marketplace.d.ts","sourceRoot":"","sources":["../../src/lib/marketplace.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB;;;;;;;;;;;;CAYtB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,mBAAmB,CAAC;AAC3D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,WAAW,CAAC,CAAC;AAIjE,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CASzE"}
@@ -0,0 +1,26 @@
1
+ // The 11 marketplaces consumed by the existing internal projects. Keepa supports more
2
+ // (AE, NL, PL, SE, BR, TR, …); extend this map and the README marketplace table when
3
+ // a new region is needed. The numeric id is Keepa's `domain` query parameter value.
4
+ export const MARKETPLACE_DOMAINS = {
5
+ US: 1,
6
+ UK: 2,
7
+ DE: 3,
8
+ FR: 4,
9
+ JP: 5,
10
+ CA: 6,
11
+ IT: 8,
12
+ ES: 9,
13
+ IN: 10,
14
+ MX: 11,
15
+ AU: 13,
16
+ };
17
+ const SUPPORTED_LIST = Object.keys(MARKETPLACE_DOMAINS).join(', ');
18
+ export function resolveDomainId(marketplace) {
19
+ const key = (marketplace ?? 'US').toUpperCase();
20
+ const domainId = MARKETPLACE_DOMAINS[key];
21
+ if (!domainId) {
22
+ throw new Error(`Invalid marketplace "${marketplace}". Supported: ${SUPPORTED_LIST}`);
23
+ }
24
+ return domainId;
25
+ }
26
+ //# sourceMappingURL=marketplace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marketplace.js","sourceRoot":"","sources":["../../src/lib/marketplace.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,qFAAqF;AACrF,oFAAoF;AACpF,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,EAAE,EAAE,CAAC;IACL,EAAE,EAAE,CAAC;IACL,EAAE,EAAE,CAAC;IACL,EAAE,EAAE,CAAC;IACL,EAAE,EAAE,CAAC;IACL,EAAE,EAAE,CAAC;IACL,EAAE,EAAE,CAAC;IACL,EAAE,EAAE,CAAC;IACL,EAAE,EAAE,EAAE;IACN,EAAE,EAAE,EAAE;IACN,EAAE,EAAE,EAAE;CACE,CAAC;AAKX,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEnE,MAAM,UAAU,eAAe,CAAC,WAA+B;IAC7D,MAAM,GAAG,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC,WAAW,EAAiB,CAAC;IAC/D,MAAM,QAAQ,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;IAC1C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,wBAAwB,WAAW,iBAAiB,cAAc,EAAE,CACrE,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './products/index.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './products/index.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,7 @@
1
+ export declare const PRODUCT_PATH = "/product";
2
+ export declare const PRODUCT_LIST_CONTEXT = "products.list";
3
+ export declare const DEFAULT_DAYS = 1;
4
+ export declare const AMAZON_IMAGE_BASE = "https://m.media-amazon.com/images/I";
5
+ export declare const KEEPA_NO_DATA_SENTINEL = -1;
6
+ export declare const VALID_IMAGE_FILENAME: RegExp;
7
+ //# sourceMappingURL=constant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constant.d.ts","sourceRoot":"","sources":["../../../src/resources/products/constant.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,aAAa,CAAC;AAIvC,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AACpD,eAAO,MAAM,YAAY,IAAI,CAAC;AAG9B,eAAO,MAAM,iBAAiB,wCAAwC,CAAC;AAGvE,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAKzC,eAAO,MAAM,oBAAoB,QAA4C,CAAC"}
@@ -0,0 +1,15 @@
1
+ export const PRODUCT_PATH = '/product';
2
+ // Structured tag of the form `<resource>.<method>`. Used as the `context` field on
3
+ // errors thrown from this method — keeps log aggregators able to filter cleanly as
4
+ // more methods land (categories.list, categories.search, bestsellers.retrieve, …).
5
+ export const PRODUCT_LIST_CONTEXT = 'products.list';
6
+ export const DEFAULT_DAYS = 1;
7
+ // Region-neutral Amazon image CDN. Serves the same images globally regardless of marketplace.
8
+ export const AMAZON_IMAGE_BASE = 'https://m.media-amazon.com/images/I';
9
+ // Keepa stores `-1` in salesRanks/price arrays to indicate "no data captured at that timestamp".
10
+ export const KEEPA_NO_DATA_SENTINEL = -1;
11
+ // Allowlist for image filenames in imagesCSV. Defends against path-traversal / SSRF
12
+ // shaped strings (e.g. "../../etc/passwd") in case the CSV is ever influenced by
13
+ // untrusted input. Matches typical Keepa image hashes (e.g. "61abcDEF.jpg").
14
+ export const VALID_IMAGE_FILENAME = /^[A-Za-z0-9._-]+\.(jpg|jpeg|png|webp)$/i;
15
+ //# sourceMappingURL=constant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constant.js","sourceRoot":"","sources":["../../../src/resources/products/constant.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAC;AACvC,mFAAmF;AACnF,mFAAmF;AACnF,mFAAmF;AACnF,MAAM,CAAC,MAAM,oBAAoB,GAAG,eAAe,CAAC;AACpD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAE9B,8FAA8F;AAC9F,MAAM,CAAC,MAAM,iBAAiB,GAAG,qCAAqC,CAAC;AAEvE,iGAAiG;AACjG,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,CAAC;AAEzC,oFAAoF;AACpF,iFAAiF;AACjF,6EAA6E;AAC7E,MAAM,CAAC,MAAM,oBAAoB,GAAG,yCAAyC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { Products, extractBsr, extractImageUrl, isFoundProduct, parseImagesCsv, } from './products.js';
2
+ export type { ProductListParams, KeepaProduct, KeepaVariation, KeepaVariationAttribute, KeepaCategoryNode, } from './product.type.js';
3
+ export { PRODUCT_PATH, PRODUCT_LIST_CONTEXT, DEFAULT_DAYS, AMAZON_IMAGE_BASE, KEEPA_NO_DATA_SENTINEL, } from './constant.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/resources/products/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,UAAU,EACV,eAAe,EACf,cAAc,EACd,cAAc,GACf,MAAM,eAAe,CAAC;AAEvB,YAAY,EACV,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACd,uBAAuB,EACvB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC;AAM3B,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,eAAe,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { Products, extractBsr, extractImageUrl, isFoundProduct, parseImagesCsv, } from './products.js';
2
+ // NOTE: raw Keepa wire types (KeepaProductRaw, KeepaProductResponseRaw) are
3
+ // intentionally NOT re-exported. They are an implementation detail — Products.list
4
+ // maps them to the consumer-friendly KeepaProduct shape before returning.
5
+ export { PRODUCT_PATH, PRODUCT_LIST_CONTEXT, DEFAULT_DAYS, AMAZON_IMAGE_BASE, KEEPA_NO_DATA_SENTINEL, } from './constant.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/resources/products/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,UAAU,EACV,eAAe,EACf,cAAc,EACd,cAAc,GACf,MAAM,eAAe,CAAC;AAUvB,4EAA4E;AAC5E,mFAAmF;AACnF,0EAA0E;AAE1E,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,eAAe,CAAC"}
@@ -0,0 +1,28 @@
1
+ export interface KeepaCategoryNodeRaw {
2
+ catId: number;
3
+ name: string;
4
+ }
5
+ export interface KeepaVariationAttributeRaw {
6
+ dimension: string;
7
+ value: string;
8
+ }
9
+ export interface KeepaVariationRaw {
10
+ asin: string;
11
+ attributes?: KeepaVariationAttributeRaw[];
12
+ }
13
+ export interface KeepaProductRaw {
14
+ asin: string;
15
+ title?: string;
16
+ description?: string;
17
+ parentAsin?: string;
18
+ categoryTree?: KeepaCategoryNodeRaw[];
19
+ rootCategory?: number;
20
+ salesRanks?: Record<string, number[]>;
21
+ imagesCSV?: string;
22
+ variations?: KeepaVariationRaw[];
23
+ bulletPoints?: string[];
24
+ }
25
+ export interface KeepaProductResponseRaw {
26
+ products?: KeepaProductRaw[];
27
+ }
28
+ //# sourceMappingURL=product.raw.type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product.raw.type.d.ts","sourceRoot":"","sources":["../../../src/resources/products/product.raw.type.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,0BAA0B,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAC;CAC9B"}
@@ -0,0 +1,6 @@
1
+ // Internal: shapes Keepa actually returns over the wire. The Products resource maps
2
+ // these into the consumer-friendly KeepaProduct (see product.type.ts) before returning,
3
+ // so callers never have to deal with the awkward forms (e.g. comma-separated imagesCSV).
4
+ // Not re-exported through resources/index.ts.
5
+ export {};
6
+ //# sourceMappingURL=product.raw.type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product.raw.type.js","sourceRoot":"","sources":["../../../src/resources/products/product.raw.type.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,wFAAwF;AACxF,yFAAyF;AACzF,8CAA8C"}
@@ -0,0 +1,41 @@
1
+ import type { Marketplace } from '../../lib/marketplace.js';
2
+ export interface ProductListParams {
3
+ asins: string[];
4
+ /** Marketplace code (case-insensitive at runtime). Defaults to 'US'. */
5
+ marketplace?: Marketplace;
6
+ /** Days of price history to include. Defaults to 1. */
7
+ days?: number;
8
+ }
9
+ export interface KeepaCategoryNode {
10
+ catId: number;
11
+ name: string;
12
+ }
13
+ export interface KeepaVariationAttribute {
14
+ dimension: string;
15
+ value: string;
16
+ }
17
+ export interface KeepaVariation {
18
+ asin: string;
19
+ attributes?: KeepaVariationAttribute[];
20
+ }
21
+ /** A Keepa product in the consumer-friendly shape. Derived from the raw Keepa
22
+ * response: `imagesCSV` is split into `images: string[]`, and the most-recent
23
+ * real BSR is surfaced as `bsr`. The raw `salesRanks` is preserved for
24
+ * consumers that need to walk the rank history themselves. */
25
+ export interface KeepaProduct {
26
+ asin: string;
27
+ title?: string;
28
+ description?: string;
29
+ parentAsin?: string;
30
+ categoryTree?: KeepaCategoryNode[];
31
+ rootCategory?: number;
32
+ salesRanks?: Record<string, number[]>;
33
+ variations?: KeepaVariation[];
34
+ bulletPoints?: string[];
35
+ /** Full image URLs derived from imagesCSV using a region-neutral Amazon CDN. */
36
+ images: string[];
37
+ /** Most recent real BSR for the rootCategory. Null when missing or every entry is
38
+ * Keepa's `-1` "no data captured" sentinel. */
39
+ bsr: number | null;
40
+ }
41
+ //# sourceMappingURL=product.type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product.type.d.ts","sourceRoot":"","sources":["../../../src/resources/products/product.type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,wEAAwE;IACxE,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,uBAAuB,EAAE,CAAC;CACxC;AAED;;;+DAG+D;AAC/D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,iBAAiB,EAAE,CAAC;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IAExB,gFAAgF;IAChF,MAAM,EAAE,MAAM,EAAE,CAAC;IAEjB;oDACgD;IAChD,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=product.type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product.type.js","sourceRoot":"","sources":["../../../src/resources/products/product.type.ts"],"names":[],"mappings":""}
@@ -0,0 +1,31 @@
1
+ import { APIResource } from '../../core/resource.js';
2
+ import type { KeepaProduct, ProductListParams } from './product.type.js';
3
+ export declare class Products extends APIResource {
4
+ list(params: ProductListParams): Promise<KeepaProduct[]>;
5
+ }
6
+ /** Returns true when Keepa returned an actual product record (not just a stub
7
+ * for an unknown ASIN). Use as a `.filter()` predicate to drop empty matches.
8
+ *
9
+ * We use `title.length > 0` because it's the strongest single correlate of
10
+ * "Keepa has data" — stubs for unknown ASINs come back without a title, and
11
+ * every real listing has one. If Keepa's stub format changes (e.g. they start
12
+ * returning a placeholder title), tighten this predicate to also require
13
+ * `categoryTree` or another non-string field. */
14
+ export declare function isFoundProduct(product: KeepaProduct): boolean;
15
+ /** Build the full image-URL list from a Keepa imagesCSV string. Region-neutral CDN.
16
+ * Filters out entries that don't look like image filenames (path traversal,
17
+ * arbitrary extensions, etc.) — defense in case the CSV is ever influenced by
18
+ * untrusted input. Most consumers won't need this directly: `Products.list`
19
+ * already fills `images[]` on every returned product. */
20
+ export declare function parseImagesCsv(imagesCSV: string | undefined): string[];
21
+ /** Extract the most recent real BSR from Keepa's `[ts, rank, ts, rank, ...]` salesRanks array.
22
+ * Walks backward through rank entries (odd indices) and skips Keepa's `-1` sentinel which
23
+ * marks "no data captured at that timestamp". Returns `null` if every entry is sentinel.
24
+ * Most consumers won't need this — `Products.list` already fills `bsr` on every returned
25
+ * product. Useful when working with a raw Keepa response. */
26
+ export declare function extractBsr(salesRanks: Record<string, number[]> | undefined, rootCategory: number | undefined): number | null;
27
+ /** Build a single Amazon image URL from the first entry in Keepa's imagesCSV.
28
+ * Equivalent to `parseImagesCsv(imagesCSV)[0] ?? null`. Useful when working with
29
+ * a raw Keepa response. */
30
+ export declare function extractImageUrl(imagesCSV: string | undefined): string | null;
31
+ //# sourceMappingURL=products.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"products.d.ts","sourceRoot":"","sources":["../../../src/resources/products/products.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAWrD,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGzE,qBAAa,QAAS,SAAQ,WAAW;IACjC,IAAI,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;CAuB/D;AAmBD;;;;;;;kDAOkD;AAClD,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAE7D;AAED;;;;0DAI0D;AAC1D,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAMtE;AAED;;;;8DAI8D;AAC9D,wBAAgB,UAAU,CACxB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,SAAS,EAChD,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,MAAM,GAAG,IAAI,CAaf;AAED;;4BAE4B;AAC5B,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAE5E"}
@@ -0,0 +1,96 @@
1
+ import { APIResource } from '../../core/resource.js';
2
+ import { resolveDomainId } from '../../lib/marketplace.js';
3
+ import { normalizeAsins } from '../../lib/asin.js';
4
+ import { PRODUCT_PATH, PRODUCT_LIST_CONTEXT, DEFAULT_DAYS, AMAZON_IMAGE_BASE, KEEPA_NO_DATA_SENTINEL, VALID_IMAGE_FILENAME, } from './constant.js';
5
+ export class Products extends APIResource {
6
+ async list(params) {
7
+ if (params.asins.length === 0) {
8
+ throw new Error('At least one ASIN is required');
9
+ }
10
+ if (params.days !== undefined &&
11
+ (!Number.isInteger(params.days) || params.days < 1)) {
12
+ throw new Error(`Invalid days: ${params.days}. Must be a positive integer.`);
13
+ }
14
+ const asins = normalizeAsins(params.asins);
15
+ const domain = resolveDomainId(params.marketplace);
16
+ const data = await this._client._request({
17
+ path: PRODUCT_PATH,
18
+ query: {
19
+ domain,
20
+ asin: asins,
21
+ days: params.days ?? DEFAULT_DAYS,
22
+ },
23
+ context: PRODUCT_LIST_CONTEXT,
24
+ });
25
+ return (data.products ?? []).map(toKeepaProduct);
26
+ }
27
+ }
28
+ /** Map Keepa's raw wire shape into the consumer-friendly KeepaProduct. */
29
+ function toKeepaProduct(raw) {
30
+ return {
31
+ asin: raw.asin,
32
+ title: raw.title,
33
+ description: raw.description,
34
+ parentAsin: raw.parentAsin,
35
+ categoryTree: raw.categoryTree,
36
+ rootCategory: raw.rootCategory,
37
+ salesRanks: raw.salesRanks,
38
+ variations: raw.variations,
39
+ bulletPoints: raw.bulletPoints,
40
+ images: parseImagesCsv(raw.imagesCSV),
41
+ bsr: extractBsr(raw.salesRanks, raw.rootCategory),
42
+ };
43
+ }
44
+ /** Returns true when Keepa returned an actual product record (not just a stub
45
+ * for an unknown ASIN). Use as a `.filter()` predicate to drop empty matches.
46
+ *
47
+ * We use `title.length > 0` because it's the strongest single correlate of
48
+ * "Keepa has data" — stubs for unknown ASINs come back without a title, and
49
+ * every real listing has one. If Keepa's stub format changes (e.g. they start
50
+ * returning a placeholder title), tighten this predicate to also require
51
+ * `categoryTree` or another non-string field. */
52
+ export function isFoundProduct(product) {
53
+ return typeof product.title === 'string' && product.title.length > 0;
54
+ }
55
+ /** Build the full image-URL list from a Keepa imagesCSV string. Region-neutral CDN.
56
+ * Filters out entries that don't look like image filenames (path traversal,
57
+ * arbitrary extensions, etc.) — defense in case the CSV is ever influenced by
58
+ * untrusted input. Most consumers won't need this directly: `Products.list`
59
+ * already fills `images[]` on every returned product. */
60
+ export function parseImagesCsv(imagesCSV) {
61
+ if (!imagesCSV)
62
+ return [];
63
+ return imagesCSV
64
+ .split(',')
65
+ .filter((entry) => VALID_IMAGE_FILENAME.test(entry))
66
+ .map((filename) => `${AMAZON_IMAGE_BASE}/${filename}`);
67
+ }
68
+ /** Extract the most recent real BSR from Keepa's `[ts, rank, ts, rank, ...]` salesRanks array.
69
+ * Walks backward through rank entries (odd indices) and skips Keepa's `-1` sentinel which
70
+ * marks "no data captured at that timestamp". Returns `null` if every entry is sentinel.
71
+ * Most consumers won't need this — `Products.list` already fills `bsr` on every returned
72
+ * product. Useful when working with a raw Keepa response. */
73
+ export function extractBsr(salesRanks, rootCategory) {
74
+ if (!salesRanks || rootCategory === undefined)
75
+ return null;
76
+ const ranks = salesRanks[String(rootCategory)];
77
+ if (!ranks || ranks.length < 2)
78
+ return null;
79
+ // Keepa pairs are [ts, rank, ts, rank, ...]. If length is even, the last index
80
+ // is a rank; if odd (truncated/schema drift), it's a dangling timestamp — skip
81
+ // back one slot so we always start on a rank.
82
+ const start = ranks.length % 2 === 0 ? ranks.length - 1 : ranks.length - 2;
83
+ for (let i = start; i >= 1; i -= 2) {
84
+ const rank = ranks[i];
85
+ if (rank !== undefined && rank !== KEEPA_NO_DATA_SENTINEL)
86
+ return rank;
87
+ }
88
+ return null;
89
+ }
90
+ /** Build a single Amazon image URL from the first entry in Keepa's imagesCSV.
91
+ * Equivalent to `parseImagesCsv(imagesCSV)[0] ?? null`. Useful when working with
92
+ * a raw Keepa response. */
93
+ export function extractImageUrl(imagesCSV) {
94
+ return parseImagesCsv(imagesCSV)[0] ?? null;
95
+ }
96
+ //# sourceMappingURL=products.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"products.js","sourceRoot":"","sources":["../../../src/resources/products/products.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const VERSION = "0.1.0";
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,UAAU,CAAC"}
@@ -0,0 +1,2 @@
1
+ export const VERSION = '0.1.0';
2
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "keepa-api",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript SDK for the Keepa API.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "example": "tsx examples/basic.ts",
24
+ "clean": "rm -rf dist",
25
+ "prepublishOnly": "npm run clean && npm run build && npm test"
26
+ },
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "keywords": [
31
+ "keepa",
32
+ "amazon",
33
+ "api",
34
+ "sdk",
35
+ "asin"
36
+ ],
37
+ "license": "MIT",
38
+ "devDependencies": {
39
+ "@types/node": "^22.10.0",
40
+ "dotenv": "^16.4.0",
41
+ "tsx": "^4.19.0",
42
+ "typescript": "^5.6.0",
43
+ "vitest": "^2.1.0"
44
+ }
45
+ }