@torvion/rascador 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Torvion
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,176 @@
1
+ # @torvion/rascador
2
+
3
+ The official TypeScript SDK for the [Rascador](https://rascador.store) product data API.
4
+ It gives you structured products, categories and live search from SHEIN, AliExpress, Amazon
5
+ and Back Market.
6
+
7
+ - Zero dependencies. Uses the platform `fetch`.
8
+ - Runs on Node 18+, Bun, Deno, Cloudflare Workers / Vercel Edge and browsers.
9
+ - Typed responses, typed error codes, retries, timeouts and auto-pagination.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ npm install @torvion/rascador
15
+ bun add @torvion/rascador
16
+ pnpm add @torvion/rascador
17
+ deno add jsr:@torvion/rascador
18
+ ```
19
+
20
+ ## Quick start
21
+
22
+ Create a key in the [dashboard](https://rascador.store/dashboard/api-keys). A `rsc_test_` key
23
+ reads stored data for free; a `rsc_live_` key adds live search.
24
+
25
+ ```ts
26
+ import { Rascador } from "@torvion/rascador"
27
+
28
+ const rascador = new Rascador({ apiKey: process.env.RASCADOR_API_KEY })
29
+
30
+ const { data: me } = await rascador.me()
31
+ console.log(me.scopes, me.quota.remaining)
32
+ ```
33
+
34
+ If `apiKey` is omitted, the SDK reads `RASCADOR_API_KEY` from the environment.
35
+
36
+ Every method resolves to the API's envelope unchanged, `{ data, meta }`, with field names exactly
37
+ as the [API reference](https://rascador.store/developers/reference) documents them.
38
+
39
+ ## Methods
40
+
41
+ | Method | Endpoint | Notes |
42
+ | --- | --- | --- |
43
+ | `me()` | `GET /v1/me` | Scopes, limits, remaining quota. Free. |
44
+ | `sources.list()` | `GET /v1/sources` | Sources and what each supports. Free. |
45
+ | `categories.list(params)` | `GET /v1/categories` | Paginated. `q`, `level`, `parent_path`. |
46
+ | `products.list(params)` | `GET /v1/products` | Paginated. Filters below. |
47
+ | `products.get(id, params)` | `GET /v1/products/:id` | Instant when stored; live otherwise. |
48
+ | `search(params)` | `GET /v1/search` | Live. 20–40s. |
49
+
50
+ ```ts
51
+ // Browse stored products
52
+ const { data: products, meta } = await rascador.products.list({
53
+ category_id: 1727,
54
+ on_sale: true,
55
+ max_price: 30,
56
+ limit: 20,
57
+ })
58
+
59
+ // Live search, then full details for a hit
60
+ const { data: hits } = await rascador.search({ q: "summer dress", pages: 1 })
61
+ const { data: product } = await rascador.products.get(hits[0]!.product_id!)
62
+ ```
63
+
64
+ `products.list` filters: `category_id`, `sku`, `q`, `brand`, `min_price`, `max_price`, `color`,
65
+ `size`, `on_sale`, `only_new`, `limit`, `offset`.
66
+
67
+ ## Pagination
68
+
69
+ `await` a list call to get one page. Use `for await` to walk every item; the SDK follows
70
+ `meta.pagination.has_more`.
71
+
72
+ ```ts
73
+ for await (const product of rascador.products.list({ brand: "SHEIN", limit: 100 })) {
74
+ console.log(product.title, product.price.current)
75
+ }
76
+
77
+ // Or page by page
78
+ for await (const page of rascador.categories.list({ level: 0 }).pages()) {
79
+ console.log(page.meta.pagination)
80
+ }
81
+ ```
82
+
83
+ ## Sources
84
+
85
+ The gateway defaults to `shein`. Set your own default on the client and override it per call.
86
+
87
+ ```ts
88
+ const rascador = new Rascador({ source: "amazon" })
89
+ await rascador.search({ q: "usb-c hub" }) // amazon
90
+ await rascador.search({ q: "refurbished iphone", source: "backmarket" })
91
+ ```
92
+
93
+ ## Errors
94
+
95
+ Every failure throws a `RascadorError`. Branch on `code`, not on `message`.
96
+
97
+ ```ts
98
+ import { RascadorError } from "@torvion/rascador"
99
+
100
+ try {
101
+ await rascador.search({ q: "linen shirt" })
102
+ } catch (err) {
103
+ if (!(err instanceof RascadorError)) throw err
104
+
105
+ switch (err.code) {
106
+ case "quota_exceeded":
107
+ console.log("Resets at", err.details?.resets_at)
108
+ break
109
+ case "insufficient_scope":
110
+ console.log("Key is missing a scope", err.details)
111
+ break
112
+ default:
113
+ console.log(err.status, err.code, err.requestId)
114
+ }
115
+ }
116
+ ```
117
+
118
+ | Field | Meaning |
119
+ | --- | --- |
120
+ | `code` | Stable machine-readable code, e.g. `rate_limited`, `product_not_found`. |
121
+ | `status` | HTTP status, or `0` when no response arrived. |
122
+ | `retryable` | The gateway says trying again may work. |
123
+ | `retryAfterSeconds` | How long the gateway asked you to wait. |
124
+ | `requestId` | Quote this to support. |
125
+ | `logUrl` | This request in your dashboard log. |
126
+ | `details` | Extra context, e.g. `resets_at`, required scopes. |
127
+
128
+ The SDK adds three codes of its own: `network_error`, `timeout` and `invalid_response`.
129
+ The full list is on [Errors & Limits](https://rascador.store/developers/errors).
130
+
131
+ ## Timeouts and retries
132
+
133
+ | Option | Default | Applies to |
134
+ | --- | --- | --- |
135
+ | `timeoutMs` | 30 000 | Stored-data calls. |
136
+ | `liveTimeoutMs` | 130 000 | `search` and `products.get(id, { refresh: true })`. |
137
+ | `maxRetries` | 2 | Errors marked `retryable`, plus network failures. |
138
+
139
+ Retries use jittered exponential backoff and never wait less than `Retry-After`. Timeouts are
140
+ not retried, because the scrape may still be running upstream. The gateway refunds quota on
141
+ every 5xx, so a retried outage doesn't cost you twice.
142
+
143
+ Every method also takes per-call options:
144
+
145
+ ```ts
146
+ const controller = new AbortController()
147
+ await rascador.search({ q: "desk lamp" }, { signal: controller.signal, maxRetries: 0 })
148
+ ```
149
+
150
+ ## Options
151
+
152
+ ```ts
153
+ new Rascador({
154
+ apiKey: "rsc_live_…",
155
+ baseUrl: "https://api.rascador.store",
156
+ source: "shein",
157
+ timeoutMs: 30_000,
158
+ liveTimeoutMs: 130_000,
159
+ maxRetries: 2,
160
+ fetch: customFetch, // proxies, tests, instrumentation
161
+ headers: { "x-trace-id": "…" },
162
+ })
163
+ ```
164
+
165
+ ## Browsers
166
+
167
+ The SDK works in browsers, but an API key in front-end code is visible to anyone who opens
168
+ DevTools. Call Rascador from your server, or use a test key scoped to read-only data.
169
+
170
+ ## Contributing
171
+
172
+ See [CONTRIBUTING.md](./CONTRIBUTING.md). Security issues: [SECURITY.md](./SECURITY.md).
173
+
174
+ ## License
175
+
176
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,387 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
24
+ PagePromise: () => PagePromise,
25
+ Rascador: () => Rascador,
26
+ RascadorError: () => RascadorError,
27
+ VERSION: () => VERSION,
28
+ isRascadorError: () => isRascadorError
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/errors.ts
33
+ var RascadorError = class extends Error {
34
+ name = "RascadorError";
35
+ code;
36
+ /** HTTP status, or 0 when no response arrived (network failure, timeout). */
37
+ status;
38
+ retryable;
39
+ retryAfterSeconds;
40
+ /** Quote this when contacting support. */
41
+ requestId;
42
+ /** This failure in your dashboard log. */
43
+ logUrl;
44
+ details;
45
+ constructor(init) {
46
+ super(init.message, init.cause === void 0 ? void 0 : { cause: init.cause });
47
+ this.code = init.code;
48
+ this.status = init.status;
49
+ this.retryable = init.retryable;
50
+ this.retryAfterSeconds = init.retryAfterSeconds;
51
+ this.requestId = init.requestId;
52
+ this.logUrl = init.logUrl;
53
+ this.details = init.details;
54
+ }
55
+ };
56
+ function isRascadorError(error) {
57
+ return error instanceof RascadorError;
58
+ }
59
+
60
+ // src/pagination.ts
61
+ var PagePromise = class {
62
+ #fetchPage;
63
+ #startOffset;
64
+ #first;
65
+ constructor(fetchPage, startOffset) {
66
+ this.#fetchPage = fetchPage;
67
+ this.#startOffset = startOffset;
68
+ }
69
+ // Lazy, so a list you only iterate doesn't also fire a request for `then`.
70
+ #firstPage() {
71
+ this.#first ??= this.#fetchPage(this.#startOffset);
72
+ return this.#first;
73
+ }
74
+ then(onfulfilled, onrejected) {
75
+ return this.#firstPage().then(onfulfilled, onrejected);
76
+ }
77
+ catch(onrejected) {
78
+ return this.#firstPage().catch(onrejected);
79
+ }
80
+ finally(onfinally) {
81
+ return this.#firstPage().finally(onfinally);
82
+ }
83
+ /** Every page, in order. */
84
+ async *pages() {
85
+ let page = await this.#firstPage();
86
+ for (; ; ) {
87
+ yield page;
88
+ const p = page.meta.pagination;
89
+ if (!p?.has_more || p.limit <= 0) return;
90
+ page = await this.#fetchPage(p.offset + p.limit);
91
+ }
92
+ }
93
+ async *[Symbol.asyncIterator]() {
94
+ for await (const page of this.pages()) yield* page.data;
95
+ }
96
+ };
97
+
98
+ // src/version.ts
99
+ var VERSION = "0.1.0";
100
+
101
+ // src/client.ts
102
+ var DEFAULT_BASE_URL = "https://api.rascador.store";
103
+ var DEFAULT_TIMEOUT_MS = 3e4;
104
+ var DEFAULT_LIVE_TIMEOUT_MS = 13e4;
105
+ var DEFAULT_MAX_RETRIES = 2;
106
+ var MAX_BACKOFF_MS = 8e3;
107
+ function readEnvKey() {
108
+ const env = globalThis.process?.env;
109
+ return env?.RASCADOR_API_KEY;
110
+ }
111
+ var Rascador = class {
112
+ #apiKey;
113
+ #baseUrl;
114
+ #source;
115
+ #timeoutMs;
116
+ #liveTimeoutMs;
117
+ #maxRetries;
118
+ #fetch;
119
+ #headers;
120
+ products;
121
+ categories;
122
+ sources;
123
+ constructor(options = {}) {
124
+ const apiKey = options.apiKey ?? readEnvKey();
125
+ if (!apiKey) {
126
+ throw new RascadorError({
127
+ code: "missing_credentials",
128
+ message: "No API key. Pass `apiKey` or set the RASCADOR_API_KEY environment variable.",
129
+ status: 0,
130
+ retryable: false
131
+ });
132
+ }
133
+ const fetchImpl = options.fetch ?? globalThis.fetch;
134
+ if (typeof fetchImpl !== "function") {
135
+ throw new TypeError("No global fetch found. Pass `fetch` in the options (Node 18+ has one built in).");
136
+ }
137
+ this.#apiKey = apiKey;
138
+ this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
139
+ this.#source = options.source;
140
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
141
+ this.#liveTimeoutMs = options.liveTimeoutMs ?? DEFAULT_LIVE_TIMEOUT_MS;
142
+ this.#maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
143
+ this.#fetch = fetchImpl.bind(globalThis);
144
+ this.#headers = options.headers ?? {};
145
+ this.products = new ProductsResource(this);
146
+ this.categories = new CategoriesResource(this);
147
+ this.sources = new SourcesResource(this);
148
+ }
149
+ /** The key's scopes, limits and remaining quota. Never spends quota. */
150
+ me(options) {
151
+ return this._get({ path: "/v1/me", options });
152
+ }
153
+ /**
154
+ * Live search on the source site. Slow (20–40s) and quota-heavy.
155
+ * Returns result cards; call `products.get` for full details.
156
+ */
157
+ search(params, options) {
158
+ const { source, ...query } = params;
159
+ return this._get({ path: "/v1/search", query: { ...query, source: this._source(source) }, live: true, options });
160
+ }
161
+ /** @internal */
162
+ _source(source) {
163
+ return source ?? this.#source;
164
+ }
165
+ /** @internal */
166
+ async _get(spec) {
167
+ const url = this.#url(spec.path, spec.query);
168
+ const maxRetries = spec.options?.maxRetries ?? this.#maxRetries;
169
+ const timeoutMs = spec.options?.timeoutMs ?? (spec.live ? this.#liveTimeoutMs : this.#timeoutMs);
170
+ for (let attempt = 0; ; attempt++) {
171
+ try {
172
+ return await this.#once(url, timeoutMs, spec.options?.signal);
173
+ } catch (error) {
174
+ if (!(error instanceof RascadorError) || !error.retryable || attempt >= maxRetries) throw error;
175
+ await sleep(backoffMs(attempt, error.retryAfterSeconds), spec.options?.signal);
176
+ }
177
+ }
178
+ }
179
+ #url(path, query) {
180
+ const url = new URL(this.#baseUrl + path);
181
+ for (const [key, value] of Object.entries(query ?? {})) {
182
+ if (value !== void 0) url.searchParams.set(key, String(value));
183
+ }
184
+ return url.toString();
185
+ }
186
+ async #once(url, timeoutMs, signal) {
187
+ const { signal: combined, cleanup, timedOut } = withTimeout(timeoutMs, signal);
188
+ let res;
189
+ try {
190
+ res = await this.#fetch(url, {
191
+ method: "GET",
192
+ headers: {
193
+ ...this.#headers,
194
+ accept: "application/json",
195
+ authorization: `Bearer ${this.#apiKey}`
196
+ },
197
+ signal: combined
198
+ });
199
+ } catch (cause) {
200
+ cleanup();
201
+ if (timedOut()) {
202
+ throw new RascadorError({
203
+ code: "timeout",
204
+ message: `No response within ${timeoutMs}ms.`,
205
+ status: 0,
206
+ retryable: false,
207
+ cause
208
+ });
209
+ }
210
+ if (signal?.aborted) throw cause;
211
+ throw new RascadorError({
212
+ code: "network_error",
213
+ message: cause instanceof Error ? cause.message : "The request could not be sent.",
214
+ status: 0,
215
+ retryable: true,
216
+ cause
217
+ });
218
+ }
219
+ let body;
220
+ try {
221
+ const text = await res.text();
222
+ body = text ? JSON.parse(text) : null;
223
+ } catch (cause) {
224
+ if (timedOut()) {
225
+ throw new RascadorError({
226
+ code: "timeout",
227
+ message: `The response did not finish within ${timeoutMs}ms.`,
228
+ status: res.status,
229
+ retryable: false,
230
+ cause
231
+ });
232
+ }
233
+ if (signal?.aborted) throw cause;
234
+ throw new RascadorError({
235
+ code: "invalid_response",
236
+ message: `Expected JSON from the gateway (HTTP ${res.status}).`,
237
+ status: res.status,
238
+ retryable: res.status >= 500,
239
+ cause
240
+ });
241
+ } finally {
242
+ cleanup();
243
+ }
244
+ if (!res.ok) throw errorFrom(res, body);
245
+ if (!body || typeof body !== "object" || !("data" in body)) {
246
+ throw new RascadorError({
247
+ code: "invalid_response",
248
+ message: "The gateway response had no `data` field.",
249
+ status: res.status,
250
+ retryable: false
251
+ });
252
+ }
253
+ return body;
254
+ }
255
+ /** The SDK version. Include it in bug reports. */
256
+ static VERSION = VERSION;
257
+ };
258
+ var ProductsResource = class {
259
+ #client;
260
+ constructor(client) {
261
+ this.#client = client;
262
+ }
263
+ /**
264
+ * One product. Instant when stored; otherwise (or with `refresh: true`) a
265
+ * live fetch, which needs the `search:live` scope.
266
+ */
267
+ get(id, params = {}, options) {
268
+ const { source, ...query } = params;
269
+ return this.#client._get({
270
+ path: `/v1/products/${encodeURIComponent(String(id))}`,
271
+ query: { ...query, source: this.#client._source(source) },
272
+ live: params.refresh === true,
273
+ options
274
+ });
275
+ }
276
+ /** Stored products. `await` for one page, `for await` for every product. */
277
+ list(params = {}, options) {
278
+ const { source, offset, ...query } = params;
279
+ return new PagePromise(
280
+ (next) => this.#client._get({
281
+ path: "/v1/products",
282
+ query: { ...query, offset: next, source: this.#client._source(source) },
283
+ options
284
+ }),
285
+ offset
286
+ );
287
+ }
288
+ };
289
+ var CategoriesResource = class {
290
+ #client;
291
+ constructor(client) {
292
+ this.#client = client;
293
+ }
294
+ /** The source's category tree. `await` for one page, `for await` for all. */
295
+ list(params = {}, options) {
296
+ const { source, offset, ...query } = params;
297
+ return new PagePromise(
298
+ (next) => this.#client._get({
299
+ path: "/v1/categories",
300
+ query: { ...query, offset: next, source: this.#client._source(source) },
301
+ options
302
+ }),
303
+ offset
304
+ );
305
+ }
306
+ };
307
+ var SourcesResource = class {
308
+ #client;
309
+ constructor(client) {
310
+ this.#client = client;
311
+ }
312
+ /** Every source and what it supports. Never spends quota. */
313
+ list(options) {
314
+ return this.#client._get({ path: "/v1/sources", options });
315
+ }
316
+ };
317
+ function errorFrom(res, body) {
318
+ const e = body?.error;
319
+ const headerRetry = Number(res.headers.get("retry-after"));
320
+ if (!e || typeof e.code !== "string") {
321
+ return new RascadorError({
322
+ code: res.status >= 500 ? "internal_error" : "invalid_response",
323
+ message: `The gateway returned HTTP ${res.status} without an error body.`,
324
+ status: res.status,
325
+ retryable: res.status >= 500 || res.status === 429,
326
+ retryAfterSeconds: Number.isFinite(headerRetry) && headerRetry > 0 ? headerRetry : void 0
327
+ });
328
+ }
329
+ const retryAfter = typeof e.retry_after_seconds === "number" ? e.retry_after_seconds : Number.isFinite(headerRetry) && headerRetry > 0 ? headerRetry : void 0;
330
+ return new RascadorError({
331
+ code: e.code,
332
+ message: typeof e.message === "string" ? e.message : e.code,
333
+ status: res.status,
334
+ retryable: e.retryable === true,
335
+ retryAfterSeconds: retryAfter,
336
+ requestId: typeof e.request_id === "string" ? e.request_id : void 0,
337
+ logUrl: typeof e.log_url === "string" ? e.log_url : void 0,
338
+ details: e.details ?? void 0
339
+ });
340
+ }
341
+ function backoffMs(attempt, retryAfterSeconds) {
342
+ const jittered = Math.random() * Math.min(MAX_BACKOFF_MS, 500 * 2 ** attempt);
343
+ return Math.max(jittered, (retryAfterSeconds ?? 0) * 1e3);
344
+ }
345
+ function sleep(ms, signal) {
346
+ return new Promise((resolve, reject) => {
347
+ if (signal?.aborted) return reject(signal.reason);
348
+ const timer = setTimeout(() => {
349
+ signal?.removeEventListener("abort", onAbort);
350
+ resolve();
351
+ }, ms);
352
+ const onAbort = () => {
353
+ clearTimeout(timer);
354
+ reject(signal?.reason);
355
+ };
356
+ signal?.addEventListener("abort", onAbort, { once: true });
357
+ });
358
+ }
359
+ function withTimeout(ms, signal) {
360
+ const controller = new AbortController();
361
+ let fired = false;
362
+ const timer = setTimeout(() => {
363
+ fired = true;
364
+ controller.abort();
365
+ }, ms);
366
+ const onAbort = () => controller.abort(signal?.reason);
367
+ if (signal?.aborted) controller.abort(signal.reason);
368
+ else signal?.addEventListener("abort", onAbort, { once: true });
369
+ return {
370
+ signal: controller.signal,
371
+ cleanup: () => {
372
+ clearTimeout(timer);
373
+ signal?.removeEventListener("abort", onAbort);
374
+ },
375
+ timedOut: () => fired
376
+ };
377
+ }
378
+ // Annotate the CommonJS export names for ESM import in node:
379
+ 0 && (module.exports = {
380
+ DEFAULT_BASE_URL,
381
+ PagePromise,
382
+ Rascador,
383
+ RascadorError,
384
+ VERSION,
385
+ isRascadorError
386
+ });
387
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/pagination.ts","../src/version.ts","../src/client.ts"],"sourcesContent":["export { Rascador, DEFAULT_BASE_URL } from \"./client.ts\"\nexport type { RascadorOptions, ProductsResource, CategoriesResource, SourcesResource } from \"./client.ts\"\nexport { RascadorError, isRascadorError } from \"./errors.ts\"\nexport type { RascadorErrorCode, RascadorErrorInit } from \"./errors.ts\"\nexport { PagePromise } from \"./pagination.ts\"\nexport { VERSION } from \"./version.ts\"\nexport type * from \"./types.ts\"\n","/**\n * Codes the gateway can return, plus three the SDK raises itself:\n * `network_error`, `timeout` and `invalid_response`.\n *\n * The `(string & {})` keeps autocomplete for the known codes while still\n * accepting any code a newer gateway adds.\n */\nexport type RascadorErrorCode =\n | \"invalid_request\"\n | \"unknown_source\"\n | \"missing_credentials\"\n | \"invalid_token\"\n | \"quota_exceeded\"\n | \"coverage_exceeded\"\n | \"payment_required\"\n | \"insufficient_scope\"\n | \"client_suspended\"\n | \"not_found\"\n | \"product_not_found\"\n | \"rate_limited\"\n | \"platform_busy\"\n | \"internal_error\"\n | \"gateway_misconfigured\"\n | \"upstream_error\"\n | \"upstream_unavailable\"\n | \"upstream_busy\"\n | \"upstream_timeout\"\n | \"network_error\"\n | \"timeout\"\n | \"invalid_response\"\n | (string & {})\n\nexport interface RascadorErrorInit {\n code: RascadorErrorCode\n message: string\n status: number\n retryable: boolean\n retryAfterSeconds?: number\n requestId?: string\n logUrl?: string\n details?: Record<string, unknown>\n cause?: unknown\n}\n\n/** Thrown for every failed call. Branch on `code`, not on `message`. */\nexport class RascadorError extends Error {\n override readonly name = \"RascadorError\"\n readonly code: RascadorErrorCode\n /** HTTP status, or 0 when no response arrived (network failure, timeout). */\n readonly status: number\n readonly retryable: boolean\n readonly retryAfterSeconds: number | undefined\n /** Quote this when contacting support. */\n readonly requestId: string | undefined\n /** This failure in your dashboard log. */\n readonly logUrl: string | undefined\n readonly details: Record<string, unknown> | undefined\n\n constructor(init: RascadorErrorInit) {\n super(init.message, init.cause === undefined ? undefined : { cause: init.cause })\n this.code = init.code\n this.status = init.status\n this.retryable = init.retryable\n this.retryAfterSeconds = init.retryAfterSeconds\n this.requestId = init.requestId\n this.logUrl = init.logUrl\n this.details = init.details\n }\n}\n\nexport function isRascadorError(error: unknown): error is RascadorError {\n return error instanceof RascadorError\n}\n","import type { Response } from \"./types.ts\"\n\ntype FetchPage<T> = (offset: number | undefined) => Promise<Response<T[]>>\n\n/**\n * A single page you can `await`, or every item you can `for await`.\n *\n * `await list(...)` makes exactly one request. Iterating makes as many as\n * `meta.pagination.has_more` calls for, starting from the offset you passed.\n */\nexport class PagePromise<T> implements PromiseLike<Response<T[]>>, AsyncIterable<T> {\n readonly #fetchPage: FetchPage<T>\n readonly #startOffset: number | undefined\n #first: Promise<Response<T[]>> | undefined\n\n constructor(fetchPage: FetchPage<T>, startOffset: number | undefined) {\n this.#fetchPage = fetchPage\n this.#startOffset = startOffset\n }\n\n // Lazy, so a list you only iterate doesn't also fire a request for `then`.\n #firstPage(): Promise<Response<T[]>> {\n this.#first ??= this.#fetchPage(this.#startOffset)\n return this.#first\n }\n\n then<R1 = Response<T[]>, R2 = never>(\n onfulfilled?: ((value: Response<T[]>) => R1 | PromiseLike<R1>) | null,\n onrejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null\n ): Promise<R1 | R2> {\n return this.#firstPage().then(onfulfilled, onrejected)\n }\n\n catch<R = never>(onrejected?: ((reason: unknown) => R | PromiseLike<R>) | null): Promise<Response<T[]> | R> {\n return this.#firstPage().catch(onrejected)\n }\n\n finally(onfinally?: (() => void) | null): Promise<Response<T[]>> {\n return this.#firstPage().finally(onfinally)\n }\n\n /** Every page, in order. */\n async *pages(): AsyncGenerator<Response<T[]>, void, undefined> {\n let page = await this.#firstPage()\n for (;;) {\n yield page\n const p = page.meta.pagination\n if (!p?.has_more || p.limit <= 0) return\n // Advance by the page size, not by what came back: with `only_new` a page\n // can be shorter than `limit` without the listing being exhausted.\n page = await this.#fetchPage(p.offset + p.limit)\n }\n }\n\n async *[Symbol.asyncIterator](): AsyncIterator<T> {\n for await (const page of this.pages()) yield* page.data\n }\n}\n","// Written by scripts/sync-version.mjs from package.json. Do not edit by hand.\nexport const VERSION = \"0.1.0\"\n","import { RascadorError } from \"./errors.ts\"\nimport { PagePromise } from \"./pagination.ts\"\nimport type {\n Category,\n GetProductParams,\n ListCategoriesParams,\n ListProductsParams,\n Me,\n Product,\n RequestOptions,\n Response,\n SearchHit,\n SearchParams,\n Source,\n SourceId,\n} from \"./types.ts\"\nimport { VERSION } from \"./version.ts\"\n\nexport const DEFAULT_BASE_URL = \"https://api.rascador.store\"\n\n/** Stored-data reads answer in well under a second. */\nconst DEFAULT_TIMEOUT_MS = 30_000\n/** Live scrapes drive a real browser: 20-40s typical, up to ~90s for a product refresh. */\nconst DEFAULT_LIVE_TIMEOUT_MS = 130_000\nconst DEFAULT_MAX_RETRIES = 2\nconst MAX_BACKOFF_MS = 8_000\n\nexport interface RascadorOptions {\n /** Defaults to the `RASCADOR_API_KEY` environment variable where one exists. */\n apiKey?: string\n /** Defaults to the production gateway. */\n baseUrl?: string\n /** Source used when a call doesn't name one. The gateway's own default is `shein`. */\n source?: SourceId\n /** Timeout for stored-data calls. Default 30s. */\n timeoutMs?: number\n /** Timeout for live calls (`search`, `products.get` with `refresh`). Default 130s. */\n liveTimeoutMs?: number\n /** Retries for errors the gateway marks retryable. Default 2. */\n maxRetries?: number\n /** Custom fetch, e.g. for a proxy or tests. Defaults to the global `fetch`. */\n fetch?: typeof fetch\n /** Extra headers sent with every request. */\n headers?: Record<string, string>\n}\n\ntype Query = Record<string, string | number | boolean | undefined>\n\ninterface CallSpec {\n path: string\n query?: Query\n live?: boolean\n options?: RequestOptions\n}\n\nfunction readEnvKey(): string | undefined {\n // `process` doesn't exist in browsers or Deno without the node shim.\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env\n return env?.RASCADOR_API_KEY\n}\n\nexport class Rascador {\n readonly #apiKey: string\n readonly #baseUrl: string\n readonly #source: SourceId | undefined\n readonly #timeoutMs: number\n readonly #liveTimeoutMs: number\n readonly #maxRetries: number\n readonly #fetch: typeof fetch\n readonly #headers: Record<string, string>\n\n readonly products: ProductsResource\n readonly categories: CategoriesResource\n readonly sources: SourcesResource\n\n constructor(options: RascadorOptions = {}) {\n const apiKey = options.apiKey ?? readEnvKey()\n if (!apiKey) {\n throw new RascadorError({\n code: \"missing_credentials\",\n message: \"No API key. Pass `apiKey` or set the RASCADOR_API_KEY environment variable.\",\n status: 0,\n retryable: false,\n })\n }\n\n const fetchImpl = options.fetch ?? globalThis.fetch\n if (typeof fetchImpl !== \"function\") {\n throw new TypeError(\"No global fetch found. Pass `fetch` in the options (Node 18+ has one built in).\")\n }\n\n this.#apiKey = apiKey\n this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\")\n this.#source = options.source\n this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n this.#liveTimeoutMs = options.liveTimeoutMs ?? DEFAULT_LIVE_TIMEOUT_MS\n this.#maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES\n // Bound so a detached `fetch` (e.g. window.fetch) keeps its receiver.\n this.#fetch = fetchImpl.bind(globalThis)\n this.#headers = options.headers ?? {}\n\n this.products = new ProductsResource(this)\n this.categories = new CategoriesResource(this)\n this.sources = new SourcesResource(this)\n }\n\n /** The key's scopes, limits and remaining quota. Never spends quota. */\n me(options?: RequestOptions): Promise<Response<Me>> {\n return this._get({ path: \"/v1/me\", options })\n }\n\n /**\n * Live search on the source site. Slow (20–40s) and quota-heavy.\n * Returns result cards; call `products.get` for full details.\n */\n search(params: SearchParams, options?: RequestOptions): Promise<Response<SearchHit[]>> {\n const { source, ...query } = params\n return this._get({ path: \"/v1/search\", query: { ...query, source: this._source(source) }, live: true, options })\n }\n\n /** @internal */\n _source(source: SourceId | undefined): SourceId | undefined {\n return source ?? this.#source\n }\n\n /** @internal */\n async _get<T>(spec: CallSpec): Promise<Response<T>> {\n const url = this.#url(spec.path, spec.query)\n const maxRetries = spec.options?.maxRetries ?? this.#maxRetries\n const timeoutMs = spec.options?.timeoutMs ?? (spec.live ? this.#liveTimeoutMs : this.#timeoutMs)\n\n for (let attempt = 0; ; attempt++) {\n try {\n return await this.#once<T>(url, timeoutMs, spec.options?.signal)\n } catch (error) {\n if (!(error instanceof RascadorError) || !error.retryable || attempt >= maxRetries) throw error\n await sleep(backoffMs(attempt, error.retryAfterSeconds), spec.options?.signal)\n }\n }\n }\n\n #url(path: string, query: Query | undefined): string {\n const url = new URL(this.#baseUrl + path)\n for (const [key, value] of Object.entries(query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, String(value))\n }\n return url.toString()\n }\n\n async #once<T>(url: string, timeoutMs: number, signal: AbortSignal | undefined): Promise<Response<T>> {\n const { signal: combined, cleanup, timedOut } = withTimeout(timeoutMs, signal)\n\n let res: globalThis.Response\n try {\n res = await this.#fetch(url, {\n method: \"GET\",\n headers: {\n ...this.#headers,\n accept: \"application/json\",\n authorization: `Bearer ${this.#apiKey}`,\n },\n signal: combined,\n })\n } catch (cause) {\n cleanup()\n if (timedOut()) {\n // Not retried: a live scrape that outran us may still be running\n // upstream, and firing another would only stack a second one on it.\n throw new RascadorError({\n code: \"timeout\",\n message: `No response within ${timeoutMs}ms.`,\n status: 0,\n retryable: false,\n cause,\n })\n }\n if (signal?.aborted) throw cause\n throw new RascadorError({\n code: \"network_error\",\n message: cause instanceof Error ? cause.message : \"The request could not be sent.\",\n status: 0,\n retryable: true,\n cause,\n })\n }\n\n let body: unknown\n try {\n const text = await res.text()\n body = text ? JSON.parse(text) : null\n } catch (cause) {\n if (timedOut()) {\n throw new RascadorError({\n code: \"timeout\",\n message: `The response did not finish within ${timeoutMs}ms.`,\n status: res.status,\n retryable: false,\n cause,\n })\n }\n if (signal?.aborted) throw cause\n throw new RascadorError({\n code: \"invalid_response\",\n message: `Expected JSON from the gateway (HTTP ${res.status}).`,\n status: res.status,\n retryable: res.status >= 500,\n cause,\n })\n } finally {\n cleanup()\n }\n\n if (!res.ok) throw errorFrom(res, body)\n\n if (!body || typeof body !== \"object\" || !(\"data\" in body)) {\n throw new RascadorError({\n code: \"invalid_response\",\n message: \"The gateway response had no `data` field.\",\n status: res.status,\n retryable: false,\n })\n }\n return body as Response<T>\n }\n\n /** The SDK version. Include it in bug reports. */\n static readonly VERSION: string = VERSION\n}\n\nexport class ProductsResource {\n readonly #client: Rascador\n constructor(client: Rascador) {\n this.#client = client\n }\n\n /**\n * One product. Instant when stored; otherwise (or with `refresh: true`) a\n * live fetch, which needs the `search:live` scope.\n */\n get(id: string | number, params: GetProductParams = {}, options?: RequestOptions): Promise<Response<Product>> {\n const { source, ...query } = params\n return this.#client._get({\n path: `/v1/products/${encodeURIComponent(String(id))}`,\n query: { ...query, source: this.#client._source(source) },\n live: params.refresh === true,\n options,\n })\n }\n\n /** Stored products. `await` for one page, `for await` for every product. */\n list(params: ListProductsParams = {}, options?: RequestOptions): PagePromise<Product> {\n const { source, offset, ...query } = params\n return new PagePromise<Product>(\n (next) =>\n this.#client._get({\n path: \"/v1/products\",\n query: { ...query, offset: next, source: this.#client._source(source) },\n options,\n }),\n offset\n )\n }\n}\n\nexport class CategoriesResource {\n readonly #client: Rascador\n constructor(client: Rascador) {\n this.#client = client\n }\n\n /** The source's category tree. `await` for one page, `for await` for all. */\n list(params: ListCategoriesParams = {}, options?: RequestOptions): PagePromise<Category> {\n const { source, offset, ...query } = params\n return new PagePromise<Category>(\n (next) =>\n this.#client._get({\n path: \"/v1/categories\",\n query: { ...query, offset: next, source: this.#client._source(source) },\n options,\n }),\n offset\n )\n }\n}\n\nexport class SourcesResource {\n readonly #client: Rascador\n constructor(client: Rascador) {\n this.#client = client\n }\n\n /** Every source and what it supports. Never spends quota. */\n list(options?: RequestOptions): Promise<Response<Source[]>> {\n return this.#client._get({ path: \"/v1/sources\", options })\n }\n}\n\nfunction errorFrom(res: globalThis.Response, body: unknown): RascadorError {\n const e = (body as { error?: Record<string, unknown> } | null)?.error\n const headerRetry = Number(res.headers.get(\"retry-after\"))\n\n if (!e || typeof e.code !== \"string\") {\n return new RascadorError({\n code: res.status >= 500 ? \"internal_error\" : \"invalid_response\",\n message: `The gateway returned HTTP ${res.status} without an error body.`,\n status: res.status,\n retryable: res.status >= 500 || res.status === 429,\n retryAfterSeconds: Number.isFinite(headerRetry) && headerRetry > 0 ? headerRetry : undefined,\n })\n }\n\n const retryAfter =\n typeof e.retry_after_seconds === \"number\"\n ? e.retry_after_seconds\n : Number.isFinite(headerRetry) && headerRetry > 0\n ? headerRetry\n : undefined\n\n return new RascadorError({\n code: e.code,\n message: typeof e.message === \"string\" ? e.message : e.code,\n status: res.status,\n retryable: e.retryable === true,\n retryAfterSeconds: retryAfter,\n requestId: typeof e.request_id === \"string\" ? e.request_id : undefined,\n logUrl: typeof e.log_url === \"string\" ? e.log_url : undefined,\n details: (e.details as Record<string, unknown> | undefined) ?? undefined,\n })\n}\n\n/** Exponential backoff with full jitter, never shorter than what the gateway asked for. */\nfunction backoffMs(attempt: number, retryAfterSeconds: number | undefined): number {\n const jittered = Math.random() * Math.min(MAX_BACKOFF_MS, 500 * 2 ** attempt)\n return Math.max(jittered, (retryAfterSeconds ?? 0) * 1000)\n}\n\nfunction sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) return reject(signal.reason)\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort)\n resolve()\n }, ms)\n const onAbort = () => {\n clearTimeout(timer)\n reject(signal?.reason)\n }\n signal?.addEventListener(\"abort\", onAbort, { once: true })\n })\n}\n\n/** AbortSignal.any isn't in Node 18, so combine the caller's signal and the timeout by hand. */\nfunction withTimeout(\n ms: number,\n signal: AbortSignal | undefined\n): { signal: AbortSignal; cleanup: () => void; timedOut: () => boolean } {\n const controller = new AbortController()\n let fired = false\n const timer = setTimeout(() => {\n fired = true\n controller.abort()\n }, ms)\n const onAbort = () => controller.abort(signal?.reason)\n if (signal?.aborted) controller.abort(signal.reason)\n else signal?.addEventListener(\"abort\", onAbort, { once: true })\n\n return {\n signal: controller.signal,\n cleanup: () => {\n clearTimeout(timer)\n signal?.removeEventListener(\"abort\", onAbort)\n },\n timedOut: () => fired,\n }\n}\n"],"mappings":"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","names":[]}