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.
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/client.d.ts +21 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +38 -0
- package/dist/client.js.map +1 -0
- package/dist/core/error.d.ts +27 -0
- package/dist/core/error.d.ts.map +1 -0
- package/dist/core/error.js +61 -0
- package/dist/core/error.js.map +1 -0
- package/dist/core/request.d.ts +15 -0
- package/dist/core/request.d.ts.map +1 -0
- package/dist/core/request.js +36 -0
- package/dist/core/request.js.map +1 -0
- package/dist/core/resource.d.ts +6 -0
- package/dist/core/resource.d.ts.map +1 -0
- package/dist/core/resource.js +7 -0
- package/dist/core/resource.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/asin.d.ts +10 -0
- package/dist/lib/asin.d.ts.map +1 -0
- package/dist/lib/asin.js +19 -0
- package/dist/lib/asin.js.map +1 -0
- package/dist/lib/marketplace.d.ts +17 -0
- package/dist/lib/marketplace.d.ts.map +1 -0
- package/dist/lib/marketplace.js +26 -0
- package/dist/lib/marketplace.js.map +1 -0
- package/dist/resources/index.d.ts +2 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +2 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/products/constant.d.ts +7 -0
- package/dist/resources/products/constant.d.ts.map +1 -0
- package/dist/resources/products/constant.js +15 -0
- package/dist/resources/products/constant.js.map +1 -0
- package/dist/resources/products/index.d.ts +4 -0
- package/dist/resources/products/index.d.ts.map +1 -0
- package/dist/resources/products/index.js +6 -0
- package/dist/resources/products/index.js.map +1 -0
- package/dist/resources/products/product.raw.type.d.ts +28 -0
- package/dist/resources/products/product.raw.type.d.ts.map +1 -0
- package/dist/resources/products/product.raw.type.js +6 -0
- package/dist/resources/products/product.raw.type.js.map +1 -0
- package/dist/resources/products/product.type.d.ts +41 -0
- package/dist/resources/products/product.type.d.ts.map +1 -0
- package/dist/resources/products/product.type.js +2 -0
- package/dist/resources/products/product.type.js.map +1 -0
- package/dist/resources/products/products.d.ts +31 -0
- package/dist/resources/products/products.d.ts.map +1 -0
- package/dist/resources/products/products.js +96 -0
- package/dist/resources/products/products.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- 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).
|
package/dist/client.d.ts
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/lib/asin.js
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC"}
|
|
@@ -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 @@
|
|
|
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":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,eAAe,CAAC;AAIvB,MAAM,OAAO,QAAS,SAAQ,WAAW;IACvC,KAAK,CAAC,IAAI,CAAC,MAAyB;QAClC,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACnD,CAAC;QACD,IACE,MAAM,CAAC,IAAI,KAAK,SAAS;YACzB,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,EACnD,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,iBAAiB,MAAM,CAAC,IAAI,+BAA+B,CAAC,CAAC;QAC/E,CAAC;QACD,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3C,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QACnD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAA0B;YAChE,IAAI,EAAE,YAAY;YAClB,KAAK,EAAE;gBACL,MAAM;gBACN,IAAI,EAAE,KAAK;gBACX,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,YAAY;aAClC;YACD,OAAO,EAAE,oBAAoB;SAC9B,CAAC,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACnD,CAAC;CACF;AAED,0EAA0E;AAC1E,SAAS,cAAc,CAAC,GAAoB;IAC1C,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,MAAM,EAAE,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC;QACrC,GAAG,EAAE,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,YAAY,CAAC;KAClD,CAAC;AACJ,CAAC;AAED;;;;;;;kDAOkD;AAClD,MAAM,UAAU,cAAc,CAAC,OAAqB;IAClD,OAAO,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;AACvE,CAAC;AAED;;;;0DAI0D;AAC1D,MAAM,UAAU,cAAc,CAAC,SAA6B;IAC1D,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,CAAC;IAC1B,OAAO,SAAS;SACb,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;SACnD,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,GAAG,iBAAiB,IAAI,QAAQ,EAAE,CAAC,CAAC;AAC3D,CAAC;AAED;;;;8DAI8D;AAC9D,MAAM,UAAU,UAAU,CACxB,UAAgD,EAChD,YAAgC;IAEhC,IAAI,CAAC,UAAU,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAC3D,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/C,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5C,+EAA+E;IAC/E,+EAA+E;IAC/E,8CAA8C;IAC9C,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IAC3E,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,sBAAsB;YAAE,OAAO,IAAI,CAAC;IACzE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;4BAE4B;AAC5B,MAAM,UAAU,eAAe,CAAC,SAA6B;IAC3D,OAAO,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,UAAU,CAAC"}
|
package/dist/version.js
ADDED
|
@@ -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
|
+
}
|