tcgpriser 0.10.0 → 1.0.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/dist/index.cjs CHANGED
@@ -9,8 +9,21 @@ var TcgPriserError = class extends Error {
9
9
  details;
10
10
  /** The raw response body, for debugging when `code`/`details` don't cover what you need. */
11
11
  body;
12
+ /**
13
+ * Seconds to wait before retrying, from the `Retry-After` header. Present on `rateLimited`, and
14
+ * on anything else a proxy in front of the API decides to send it with. Absent otherwise — an
15
+ * error without it is not one that says retrying will help.
16
+ */
17
+ retryAfter;
18
+ /**
19
+ * Credits left in this week's allowance, from `X-Credits-Remaining`. Present on errors from
20
+ * charged routes — notably `creditsExhausted`, where it is `0`. Absent on uncharged routes and on
21
+ * anything a proxy answered instead of the API.
22
+ */
23
+ creditsRemaining;
12
24
  constructor(params) {
13
- super(`tcgpriser: ${params.statusCode} ${params.code} - ${params.message} (${params.url})`);
25
+ const status = params.statusCode === 0 ? "" : `${params.statusCode} `;
26
+ super(`tcgpriser: ${status}${params.code} - ${params.message} (${params.url})`);
14
27
  this.name = "TcgPriserError";
15
28
  this.statusCode = params.statusCode;
16
29
  this.statusText = params.statusText;
@@ -18,6 +31,8 @@ var TcgPriserError = class extends Error {
18
31
  this.code = params.code;
19
32
  this.details = params.details;
20
33
  this.body = params.body;
34
+ this.retryAfter = params.retryAfter;
35
+ this.creditsRemaining = params.creditsRemaining;
21
36
  }
22
37
  };
23
38
 
@@ -31,9 +46,9 @@ function toQueryString(params) {
31
46
  const query = search.toString();
32
47
  return query ? `?${query}` : "";
33
48
  }
34
- function splitAuthToken(params) {
35
- const { authToken, ...rest } = params;
36
- return [rest, authToken];
49
+ function splitRequestOptions(params) {
50
+ const { authToken, signal, timeoutMs, ...rest } = params;
51
+ return [rest, { authToken, signal, timeoutMs }];
37
52
  }
38
53
  var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set([
39
54
  "validationFailed",
@@ -48,6 +63,12 @@ var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set([
48
63
  "creditsExhausted",
49
64
  "internalError"
50
65
  ]);
66
+ function readIntHeader(res, name) {
67
+ const raw = res.headers.get(name);
68
+ if (raw === null) return void 0;
69
+ const parsed = Number(raw);
70
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
71
+ }
51
72
  async function toApiError(res, url) {
52
73
  const body = await res.text();
53
74
  let code = "unknown";
@@ -62,18 +83,63 @@ async function toApiError(res, url) {
62
83
  details = parsed.error?.details;
63
84
  } catch {
64
85
  }
65
- return new TcgPriserError({ statusCode: res.status, statusText: res.statusText, url, code, message, details, body });
86
+ return new TcgPriserError({
87
+ statusCode: res.status,
88
+ statusText: res.statusText,
89
+ url,
90
+ code,
91
+ message,
92
+ details,
93
+ body,
94
+ // Both are most useful on exactly the errors that carry them: `retryAfter` on `rateLimited`,
95
+ // `creditsRemaining` on `creditsExhausted` (where it is 0) and on any error from a charged
96
+ // route. Read unconditionally rather than branching on the code, since a proxy can return a
97
+ // 429 with `Retry-After` and no envelope at all.
98
+ retryAfter: readIntHeader(res, "Retry-After"),
99
+ creditsRemaining: readIntHeader(res, "X-Credits-Remaining")
100
+ });
101
+ }
102
+ var DEFAULT_TIMEOUT_MS = 6e4;
103
+ function withTimeout(timeoutMs, callerSignal) {
104
+ if (timeoutMs <= 0) return { signal: callerSignal, clear: () => {
105
+ }, timedOut: () => false };
106
+ const controller = new AbortController();
107
+ let expired = false;
108
+ const timer = setTimeout(() => {
109
+ expired = true;
110
+ controller.abort();
111
+ }, timeoutMs);
112
+ const onCallerAbort = () => controller.abort();
113
+ if (callerSignal) {
114
+ if (callerSignal.aborted) controller.abort();
115
+ else callerSignal.addEventListener("abort", onCallerAbort, { once: true });
116
+ }
117
+ return {
118
+ signal: controller.signal,
119
+ clear: () => {
120
+ clearTimeout(timer);
121
+ callerSignal?.removeEventListener("abort", onCallerAbort);
122
+ },
123
+ timedOut: () => expired
124
+ };
66
125
  }
67
126
  var HttpClient = class {
68
127
  baseUrl;
69
128
  fetchImpl;
70
129
  defaultHeaders;
71
130
  defaultAuthToken;
131
+ defaultTimeoutMs;
132
+ /**
133
+ * The `X-Credits-Remaining` value from the most recent charged response, or `undefined` if no
134
+ * charged call has been made yet. See `TcgPriser.creditsRemaining`.
135
+ */
136
+ creditsRemaining;
72
137
  constructor(options) {
73
138
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
74
139
  this.fetchImpl = options.fetch;
75
140
  this.defaultHeaders = options.headers ?? {};
76
141
  this.defaultAuthToken = options.authToken;
142
+ this.defaultTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
77
143
  }
78
144
  get(path, requestOptions) {
79
145
  return this.request("GET", path, void 0, requestOptions);
@@ -84,17 +150,42 @@ var HttpClient = class {
84
150
  patch(path, body, requestOptions) {
85
151
  return this.request("PATCH", path, body, requestOptions);
86
152
  }
153
+ delete(path, requestOptions) {
154
+ return this.request("DELETE", path, void 0, requestOptions);
155
+ }
87
156
  async request(method, path, body, requestOptions) {
88
157
  const url = `${this.baseUrl}${path}`;
89
158
  const authToken = requestOptions && "authToken" in requestOptions ? requestOptions.authToken : this.defaultAuthToken;
90
159
  const headers = { Accept: "application/json", ...this.defaultHeaders };
91
160
  if (authToken) headers.Authorization = `Bearer ${authToken}`;
92
161
  if (body !== void 0) headers["Content-Type"] = "application/json";
93
- const res = await this.fetchImpl(url, {
94
- method,
95
- headers,
96
- body: body === void 0 ? void 0 : JSON.stringify(body)
97
- });
162
+ const timeoutMs = requestOptions?.timeoutMs ?? this.defaultTimeoutMs;
163
+ const timeout = withTimeout(timeoutMs, requestOptions?.signal);
164
+ let res;
165
+ try {
166
+ res = await this.fetchImpl(url, {
167
+ method,
168
+ headers,
169
+ body: body === void 0 ? void 0 : JSON.stringify(body),
170
+ signal: timeout.signal
171
+ });
172
+ } catch (error) {
173
+ if (timeout.timedOut()) {
174
+ throw new TcgPriserError({
175
+ statusCode: 0,
176
+ statusText: "Timeout",
177
+ url,
178
+ code: "timeout",
179
+ message: `Request timed out after ${timeoutMs}ms`,
180
+ body: ""
181
+ });
182
+ }
183
+ throw error;
184
+ } finally {
185
+ timeout.clear();
186
+ }
187
+ const credits = readIntHeader(res, "X-Credits-Remaining");
188
+ if (credits !== void 0) this.creditsRemaining = credits;
98
189
  if (!res.ok) throw await toApiError(res, url);
99
190
  if (res.status === 204) return void 0;
100
191
  return nullsToUndefined(await res.json());
@@ -121,13 +212,32 @@ var BargainsResource = class {
121
212
  * count is fixed by the API (no `limit`/`skip` on the public tier); `pagination.hasMore` tells
122
213
  * you if more exist. */
123
214
  list(params = {}) {
124
- return this.http.get(`/bargains${toQueryString(params)}`);
215
+ const [query, requestOptions] = splitRequestOptions(params);
216
+ return this.http.get(`/bargains${toQueryString(query)}`, requestOptions);
125
217
  }
126
218
  /** `GET /bargains/search`: like `list()`, but with real pagination and filters (shop, discount
127
219
  * threshold, card condition/grade, free-text search). Premium. */
128
220
  search(params = {}) {
129
- const [query, authToken] = splitAuthToken(params);
130
- return this.http.get(`/bargains/search${toQueryString(query)}`, { authToken });
221
+ const [query, requestOptions] = splitRequestOptions(params);
222
+ return this.http.get(`/bargains/search${toQueryString(query)}`, requestOptions);
223
+ }
224
+ };
225
+
226
+ // src/resources/brands.ts
227
+ var BrandsResource = class {
228
+ constructor(http) {
229
+ this.http = http;
230
+ }
231
+ http;
232
+ /** `GET /brands`: every brand. Unwrapped to a plain array, nothing to paginate here — same
233
+ * shape as `expansions.list()`/`shops.list()`. */
234
+ async list(options = {}) {
235
+ const res = await this.http.get("/brands", options);
236
+ return res.data;
237
+ }
238
+ /** `GET /brands/{id}`: fetch one brand by its id or technicalName. */
239
+ get(idOrTechnicalName, options = {}) {
240
+ return this.http.get(`/brands/${encodeURIComponent(idOrTechnicalName)}`, options);
131
241
  }
132
242
  };
133
243
 
@@ -137,34 +247,45 @@ var CardsResource = class {
137
247
  this.http = http;
138
248
  }
139
249
  http;
140
- /** `GET /cards`: search or list cards. */
250
+ /** `GET /cards`: list cards, newest first. No free-text search — use `search()` for that. */
141
251
  list(params = {}) {
142
- return this.http.get(`/cards${toQueryString(params)}`);
252
+ const [query, requestOptions] = splitRequestOptions(params);
253
+ return this.http.get(`/cards${toQueryString(query)}`, requestOptions);
254
+ }
255
+ /** `GET /cards/search`: like `list()`, but with free-text search on card and set names. Premium. */
256
+ search(params = {}) {
257
+ const [query, requestOptions] = splitRequestOptions(params);
258
+ return this.http.get(`/cards/search${toQueryString(query)}`, requestOptions);
143
259
  }
144
- /** `GET /cards/{id}`: fetch one card by its id or technicalName. */
145
- get(idOrTechnicalName) {
146
- return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}`);
260
+ /** `GET /cards/{id}`: fetch one card by its id or technicalName. Pass `brand` if two brands
261
+ * might share the same technicalName — see `GetCardParams`. */
262
+ get(idOrTechnicalName, params = {}) {
263
+ const [query, requestOptions] = splitRequestOptions(params);
264
+ return this.http.get(
265
+ `/cards/${encodeURIComponent(idOrTechnicalName)}${toQueryString(query)}`,
266
+ requestOptions
267
+ );
147
268
  }
148
269
  /** `GET /cards/{id}/matches`: current shop listings matched to this card (latest per shop). */
149
270
  matches(idOrTechnicalName, params = {}) {
271
+ const [query, requestOptions] = splitRequestOptions(params);
150
272
  return this.http.get(
151
- `/cards/${encodeURIComponent(idOrTechnicalName)}/matches${toQueryString(params)}`
273
+ `/cards/${encodeURIComponent(idOrTechnicalName)}/matches${toQueryString(query)}`,
274
+ requestOptions
152
275
  );
153
276
  }
154
277
  /** `GET /cards/{id}/reference-prices`: Cardmarket/TCGplayer/eBay/Tradera price history. Premium. */
155
278
  referencePrices(idOrTechnicalName, params = {}) {
156
- const [query, authToken] = splitAuthToken(params);
279
+ const [query, requestOptions] = splitRequestOptions(params);
157
280
  return this.http.get(
158
281
  `/cards/${encodeURIComponent(idOrTechnicalName)}/reference-prices${toQueryString(query)}`,
159
- { authToken }
282
+ requestOptions
160
283
  );
161
284
  }
162
285
  /** `GET /cards/{id}/prices`: individual marketplace sale records. Premium. */
163
286
  prices(idOrTechnicalName, params = {}) {
164
- const [query, authToken] = splitAuthToken(params);
165
- return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}/prices${toQueryString(query)}`, {
166
- authToken
167
- });
287
+ const [query, requestOptions] = splitRequestOptions(params);
288
+ return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}/prices${toQueryString(query)}`, requestOptions);
168
289
  }
169
290
  /** `GET /cards/{id}/pricing/live`: computed fresh for this request, not read from the last
170
291
  * stats job. Premium. */
@@ -175,16 +296,39 @@ var CardsResource = class {
175
296
  * `estimatedValue`, `lowestShopOffer`, `referencePriceSnapshotsByProvider` — refreshed once a day
176
297
  * by the nightly pricing/scraper jobs. `get()` returns content only; this is the separate,
177
298
  * shorter-cached call for the part of a card that actually changes day to day. */
178
- pricing(idOrTechnicalName) {
179
- return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}/pricing`);
299
+ pricing(idOrTechnicalName, options = {}) {
300
+ return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}/pricing`, options);
180
301
  }
181
302
  /** `GET /cards/pricing`: pricing for up to 200 cards in one request, keyed by `id` — the batch
182
303
  * counterpart to `pricing()`, for a page of results (a search page, an expansion's contents) that
183
304
  * needs pricing for many items at once. Unlike `get()`/`pricing()`, this only accepts `id`s, not
184
305
  * technicalNames — pass the `id`s already on the cards you fetched. Ids with no match are
185
306
  * silently omitted from the result rather than causing an error. */
186
- pricingBatch(ids) {
187
- return this.http.get(`/cards/pricing?ids=${ids.map(encodeURIComponent).join(",")}`);
307
+ pricingBatch(ids, options = {}) {
308
+ return this.http.get(`/cards/pricing?ids=${ids.map(encodeURIComponent).join(",")}`, options);
309
+ }
310
+ /** `GET /cards/technical-names`: every card's `technicalName` and `updatedAt`, unpaginated and
311
+ * with no pricing joins. Built for enumerating the whole catalog cheaply — a sitemap, or working
312
+ * out which items changed since your last sync — where `list()` would make you page through full
313
+ * card documents to learn the same two fields. */
314
+ technicalNames(options = {}) {
315
+ return this.http.get("/cards/technical-names", options);
316
+ }
317
+ /** `GET /cards/price-stats/daily`: daily average price history, cards only. The same data as
318
+ * `client.priceStats.daily()`, scoped to the card catalog so a filter like `expansion` can't pull
319
+ * in that expansion's sealed products too. */
320
+ dailyStats(params = {}) {
321
+ const [query, requestOptions] = splitRequestOptions(params);
322
+ return this.http.get(`/cards/price-stats/daily${toQueryString(query)}`, requestOptions);
323
+ }
324
+ /** `GET /cards/price-stats/estimated-values`: current estimated market value, cards only. The
325
+ * card-scoped counterpart to `client.priceStats.estimatedValues()`. */
326
+ estimatedValues(params = {}) {
327
+ const [query, requestOptions] = splitRequestOptions(params);
328
+ return this.http.get(
329
+ `/cards/price-stats/estimated-values${toQueryString(query)}`,
330
+ requestOptions
331
+ );
188
332
  }
189
333
  };
190
334
 
@@ -195,30 +339,34 @@ var ExpansionsResource = class {
195
339
  }
196
340
  http;
197
341
  /** `GET /expansions`: every expansion. Unwrapped to a plain array, nothing to paginate here. */
198
- async list() {
199
- const res = await this.http.get("/expansions");
342
+ async list(params = {}) {
343
+ const [query, requestOptions] = splitRequestOptions(params);
344
+ const res = await this.http.get(
345
+ `/expansions${toQueryString(query)}`,
346
+ requestOptions
347
+ );
200
348
  return res.data;
201
349
  }
202
350
  /** `GET /expansions/{technicalName}`: metadata only — no cards or sealed products. Returns the
203
351
  * smaller `ExpansionRef`, not the full `Expansion`: this is a plain lookup by technicalName, not
204
352
  * the aggregation `list()` runs, so `sealedCount`/`cardCount`/`productCount` aren't available
205
353
  * here. See `cards()` and `sealedProducts()` for this expansion's contents. */
206
- get(technicalName) {
207
- return this.http.get(`/expansions/${encodeURIComponent(technicalName)}`);
354
+ get(technicalName, options = {}) {
355
+ return this.http.get(`/expansions/${encodeURIComponent(technicalName)}`, options);
208
356
  }
209
357
  /** `GET /expansions/{technicalName}/cards`: every card in this expansion. Content only, no
210
358
  * pricing fields — pass the `id`s from the result to `client.cards.pricingBatch()` if you need
211
359
  * pricing too. Sealed products are a separate call — see `sealedProducts()` — never merged into
212
360
  * this one. */
213
- cards(technicalName) {
214
- return this.http.get(`/expansions/${encodeURIComponent(technicalName)}/cards`);
361
+ cards(technicalName, options = {}) {
362
+ return this.http.get(`/expansions/${encodeURIComponent(technicalName)}/cards`, options);
215
363
  }
216
364
  /** `GET /expansions/{technicalName}/products`: every sealed product in this expansion. Content
217
365
  * only, no pricing fields — pass the `id`s from the result to `client.products.pricingBatch()`
218
366
  * if you need pricing too. Cards are a separate call — see `cards()` — never merged into this
219
367
  * one. */
220
- sealedProducts(technicalName) {
221
- return this.http.get(`/expansions/${encodeURIComponent(technicalName)}/products`);
368
+ sealedProducts(technicalName, options = {}) {
369
+ return this.http.get(`/expansions/${encodeURIComponent(technicalName)}/products`, options);
222
370
  }
223
371
  /** `GET /expansions/{technicalName}/cards/live-pricing`: computed fresh for every card in this
224
372
  * expansion, not read from the last stats job. Premium. */
@@ -246,13 +394,13 @@ var PackRatesResource = class {
246
394
  http;
247
395
  /** `GET /pack-rates`: pull-rate odds for every expansion that has them. Unwrapped to a plain
248
396
  * array, nothing to paginate here. */
249
- async list() {
250
- const res = await this.http.get("/pack-rates");
397
+ async list(options = {}) {
398
+ const res = await this.http.get("/pack-rates", options);
251
399
  return res.data;
252
400
  }
253
401
  /** `GET /pack-rates/{expansionId}`: pull-rate odds for one expansion. */
254
- get(expansionId) {
255
- return this.http.get(`/pack-rates/${encodeURIComponent(expansionId)}`);
402
+ get(expansionId, options = {}) {
403
+ return this.http.get(`/pack-rates/${encodeURIComponent(expansionId)}`, options);
256
404
  }
257
405
  };
258
406
 
@@ -264,17 +412,20 @@ var PriceStatsResource = class {
264
412
  http;
265
413
  /** `GET /price-stats/daily`: daily average price history, filtered to matching product(s). */
266
414
  daily(params = {}) {
267
- return this.http.get(`/price-stats/daily${toQueryString(params)}`);
415
+ const [query, requestOptions] = splitRequestOptions(params);
416
+ return this.http.get(`/price-stats/daily${toQueryString(query)}`, requestOptions);
268
417
  }
269
418
  /** `GET /price-stats/estimated-values`: current estimated market value, filtered to matching
270
419
  * product(s). */
271
420
  estimatedValues(params = {}) {
272
- return this.http.get(`/price-stats/estimated-values${toQueryString(params)}`);
421
+ const [query, requestOptions] = splitRequestOptions(params);
422
+ return this.http.get(`/price-stats/estimated-values${toQueryString(query)}`, requestOptions);
273
423
  }
274
424
  /** `GET /price-stats/top-products`: items ranked by shop availability (how many shops carry
275
425
  * them), not by price. */
276
426
  topProducts(params = {}) {
277
- return this.http.get(`/price-stats/top-products${toQueryString(params)}`);
427
+ const [query, requestOptions] = splitRequestOptions(params);
428
+ return this.http.get(`/price-stats/top-products${toQueryString(query)}`, requestOptions);
278
429
  }
279
430
  /** `GET /price-stats/product/{id}`: daily price history, current estimate, and a variant-count
280
431
  * summary for one product. Premium. */
@@ -289,10 +440,10 @@ var PriceStatsResource = class {
289
440
  /** `GET /price-stats/product/{id}/daily`: daily price history for one product, with a
290
441
  * caller-chosen window. Premium. */
291
442
  productDaily(idOrTechnicalName, params = {}) {
292
- const [query, authToken] = splitAuthToken(params);
443
+ const [query, requestOptions] = splitRequestOptions(params);
293
444
  return this.http.get(
294
445
  `/price-stats/product/${encodeURIComponent(idOrTechnicalName)}/daily${toQueryString(query)}`,
295
- { authToken }
446
+ requestOptions
296
447
  );
297
448
  }
298
449
  /** `GET /price-stats/product/{id}/daily-last-30`: daily price history for the last 30 days
@@ -313,20 +464,20 @@ var PriceStatsResource = class {
313
464
  /** `GET /price-stats/product/{id}/by-variant`: price stats broken out per card condition/grade.
314
465
  * Premium. */
315
466
  productByVariant(idOrTechnicalName, params = {}) {
316
- const [query, authToken] = splitAuthToken(params);
467
+ const [query, requestOptions] = splitRequestOptions(params);
317
468
  return this.http.get(
318
469
  `/price-stats/product/${encodeURIComponent(idOrTechnicalName)}/by-variant${toQueryString(query)}`,
319
- { authToken }
470
+ requestOptions
320
471
  );
321
472
  }
322
473
  /** `GET /price-stats/product/{id}/daily-by-variant`: daily price history for one specific
323
474
  * condition/grade. `condition` is required for `cardType: 'loose'`; `gradingCompany` and `grade`
324
475
  * are required for `cardType: 'graded'`. Premium. */
325
476
  productDailyByVariant(idOrTechnicalName, params) {
326
- const [query, authToken] = splitAuthToken(params);
477
+ const [query, requestOptions] = splitRequestOptions(params);
327
478
  return this.http.get(
328
479
  `/price-stats/product/${encodeURIComponent(idOrTechnicalName)}/daily-by-variant${toQueryString(query)}`,
329
- { authToken }
480
+ requestOptions
330
481
  );
331
482
  }
332
483
  };
@@ -337,34 +488,48 @@ var ProductsResource = class {
337
488
  this.http = http;
338
489
  }
339
490
  http;
340
- /** `GET /product`: search or list sealed products. */
491
+ /** `GET /product`: list sealed products, newest first. No free-text search — use `search()` for
492
+ * that. */
341
493
  list(params = {}) {
342
- return this.http.get(`/product${toQueryString(params)}`);
494
+ const [query, requestOptions] = splitRequestOptions(params);
495
+ return this.http.get(`/product${toQueryString(query)}`, requestOptions);
343
496
  }
344
- /** `GET /product/{id}`: fetch one sealed product by its id or technicalName. */
345
- get(idOrTechnicalName) {
346
- return this.http.get(`/product/${encodeURIComponent(idOrTechnicalName)}`);
497
+ /** `GET /product/search`: like `list()`, but with free-text search on the product name. Premium. */
498
+ search(params = {}) {
499
+ const [query, requestOptions] = splitRequestOptions(params);
500
+ return this.http.get(`/product/search${toQueryString(query)}`, requestOptions);
501
+ }
502
+ /** `GET /product/{id}`: fetch one sealed product by its id or technicalName. Pass `brand` if two
503
+ * brands might share the same technicalName — see `GetProductParams`. */
504
+ get(idOrTechnicalName, params = {}) {
505
+ const [query, requestOptions] = splitRequestOptions(params);
506
+ return this.http.get(
507
+ `/product/${encodeURIComponent(idOrTechnicalName)}${toQueryString(query)}`,
508
+ requestOptions
509
+ );
347
510
  }
348
511
  /** `GET /product/{id}/matches`: current shop listings matched to this product (latest per shop). */
349
512
  matches(idOrTechnicalName, params = {}) {
513
+ const [query, requestOptions] = splitRequestOptions(params);
350
514
  return this.http.get(
351
- `/product/${encodeURIComponent(idOrTechnicalName)}/matches${toQueryString(params)}`
515
+ `/product/${encodeURIComponent(idOrTechnicalName)}/matches${toQueryString(query)}`,
516
+ requestOptions
352
517
  );
353
518
  }
354
519
  /** `GET /product/{id}/reference-prices`: Cardmarket/TCGplayer/Tradera price history. Premium. */
355
520
  referencePrices(idOrTechnicalName, params = {}) {
356
- const [query, authToken] = splitAuthToken(params);
521
+ const [query, requestOptions] = splitRequestOptions(params);
357
522
  return this.http.get(
358
523
  `/product/${encodeURIComponent(idOrTechnicalName)}/reference-prices${toQueryString(query)}`,
359
- { authToken }
524
+ requestOptions
360
525
  );
361
526
  }
362
527
  /** `GET /product/{id}/prices`: individual marketplace sale records. Premium. */
363
528
  prices(idOrTechnicalName, params = {}) {
364
- const [query, authToken] = splitAuthToken(params);
529
+ const [query, requestOptions] = splitRequestOptions(params);
365
530
  return this.http.get(
366
531
  `/product/${encodeURIComponent(idOrTechnicalName)}/prices${toQueryString(query)}`,
367
- { authToken }
532
+ requestOptions
368
533
  );
369
534
  }
370
535
  /** `GET /product/{id}/pricing/live`: computed fresh for this request, not read from the last
@@ -376,16 +541,38 @@ var ProductsResource = class {
376
541
  * `estimatedValue`, `lowestShopOffer`, `referencePriceSnapshotsByProvider` — refreshed once a day
377
542
  * by the nightly pricing/scraper jobs. `get()` returns content only; this is the separate,
378
543
  * shorter-cached call for the part of a product that actually changes day to day. */
379
- pricing(idOrTechnicalName) {
380
- return this.http.get(`/product/${encodeURIComponent(idOrTechnicalName)}/pricing`);
544
+ pricing(idOrTechnicalName, options = {}) {
545
+ return this.http.get(`/product/${encodeURIComponent(idOrTechnicalName)}/pricing`, options);
381
546
  }
382
547
  /** `GET /product/pricing`: pricing for up to 200 sealed products in one request, keyed by `id` —
383
548
  * the batch counterpart to `pricing()`, for a page of results (a search page, an expansion's
384
549
  * contents) that needs pricing for many items at once. Unlike `get()`/`pricing()`, this only
385
550
  * accepts `id`s, not technicalNames — pass the `id`s already on the products you fetched. Ids with
386
551
  * no match are silently omitted from the result rather than causing an error. */
387
- pricingBatch(ids) {
388
- return this.http.get(`/product/pricing?ids=${ids.map(encodeURIComponent).join(",")}`);
552
+ pricingBatch(ids, options = {}) {
553
+ return this.http.get(`/product/pricing?ids=${ids.map(encodeURIComponent).join(",")}`, options);
554
+ }
555
+ /** `GET /product/technical-names`: every sealed product's `technicalName` and `updatedAt`,
556
+ * unpaginated and with no pricing joins. The sealed counterpart to
557
+ * `client.cards.technicalNames()` — for sitemaps and incremental syncs. */
558
+ technicalNames(options = {}) {
559
+ return this.http.get("/product/technical-names", options);
560
+ }
561
+ /** `GET /product/price-stats/daily`: daily average price history, sealed products only. The same
562
+ * data as `client.priceStats.daily()`, scoped to the sealed catalog so a filter like `expansion`
563
+ * can't pull in that expansion's single cards too. */
564
+ dailyStats(params = {}) {
565
+ const [query, requestOptions] = splitRequestOptions(params);
566
+ return this.http.get(`/product/price-stats/daily${toQueryString(query)}`, requestOptions);
567
+ }
568
+ /** `GET /product/price-stats/estimated-values`: current estimated market value, sealed products
569
+ * only. The sealed-scoped counterpart to `client.priceStats.estimatedValues()`. */
570
+ estimatedValues(params = {}) {
571
+ const [query, requestOptions] = splitRequestOptions(params);
572
+ return this.http.get(
573
+ `/product/price-stats/estimated-values${toQueryString(query)}`,
574
+ requestOptions
575
+ );
389
576
  }
390
577
  };
391
578
 
@@ -397,24 +584,22 @@ var ShopMatchStatsResource = class {
397
584
  http;
398
585
  /** `GET /shop-match-stats/product/{productId}`: one product's price history, broken out per shop. */
399
586
  forProduct(productId, params = {}) {
400
- const [query, authToken] = splitAuthToken(params);
587
+ const [query, requestOptions] = splitRequestOptions(params);
401
588
  return this.http.get(
402
589
  `/shop-match-stats/product/${encodeURIComponent(productId)}${toQueryString(query)}`,
403
- { authToken }
590
+ requestOptions
404
591
  );
405
592
  }
406
593
  /** `GET /shop-match-stats/shop/{shop}`: one shop's price history, broken out per product. */
407
594
  forShop(shop, params = {}) {
408
- const [query, authToken] = splitAuthToken(params);
409
- return this.http.get(`/shop-match-stats/shop/${encodeURIComponent(shop)}${toQueryString(query)}`, {
410
- authToken
411
- });
595
+ const [query, requestOptions] = splitRequestOptions(params);
596
+ return this.http.get(`/shop-match-stats/shop/${encodeURIComponent(shop)}${toQueryString(query)}`, requestOptions);
412
597
  }
413
598
  /** `GET /shop-match-stats/compare`: one product's price at every shop that carries it, as of
414
599
  * one date (defaults to the latest). */
415
600
  compare(params) {
416
- const [query, authToken] = splitAuthToken(params);
417
- return this.http.get(`/shop-match-stats/compare${toQueryString(query)}`, { authToken });
601
+ const [query, requestOptions] = splitRequestOptions(params);
602
+ return this.http.get(`/shop-match-stats/compare${toQueryString(query)}`, requestOptions);
418
603
  }
419
604
  };
420
605
 
@@ -426,15 +611,21 @@ var ShopMatchesResource = class {
426
611
  http;
427
612
  /** `GET /shop-matches`: every current match across every shop (latest record per url+shop). */
428
613
  list(params = {}) {
429
- return this.http.get(`/shop-matches${toQueryString(params)}`);
614
+ const [query, requestOptions] = splitRequestOptions(params);
615
+ return this.http.get(`/shop-matches${toQueryString(query)}`, requestOptions);
430
616
  }
431
617
  /** `GET /shop-matches/{shop}`: every current match at one shop (latest record per url). */
432
618
  forShop(technicalName, params = {}) {
433
- return this.http.get(`/shop-matches/${encodeURIComponent(technicalName)}${toQueryString(params)}`);
619
+ const [query, requestOptions] = splitRequestOptions(params);
620
+ return this.http.get(
621
+ `/shop-matches/${encodeURIComponent(technicalName)}${toQueryString(query)}`,
622
+ requestOptions
623
+ );
434
624
  }
435
625
  /** `GET /shop-matches/shops`: match counts per shop (based on latest records only). */
436
626
  shopStats(params = {}) {
437
- return this.http.get(`/shop-matches/shops${toQueryString(params)}`);
627
+ const [query, requestOptions] = splitRequestOptions(params);
628
+ return this.http.get(`/shop-matches/shops${toQueryString(query)}`, requestOptions);
438
629
  }
439
630
  };
440
631
 
@@ -446,16 +637,16 @@ var ShopUrlsResource = class {
446
637
  http;
447
638
  /** `POST /shop-urls/submit`: submit a shop URL for scraping. */
448
639
  submit(params) {
449
- const { authToken, url, shop } = params;
450
- return this.http.post("/shop-urls/submit", { url, shop }, { authToken });
640
+ const { url, shop, ...requestOptions } = params;
641
+ return this.http.post("/shop-urls/submit", { url, shop }, requestOptions);
451
642
  }
452
643
  /** `PATCH /shop-urls/{id}/product`: manually assign (or clear) the product a shop URL resolves to. */
453
644
  assignProduct(shopUrlId, params) {
454
- const { authToken, productId } = params;
645
+ const { productId, ...requestOptions } = params;
455
646
  return this.http.patch(
456
647
  `/shop-urls/${encodeURIComponent(shopUrlId)}/product`,
457
648
  { productId },
458
- { authToken }
649
+ requestOptions
459
650
  );
460
651
  }
461
652
  };
@@ -468,12 +659,13 @@ var ShopsResource = class {
468
659
  http;
469
660
  /** `GET /shops`: every tracked shop. Unwrapped to a plain array, nothing to paginate here. */
470
661
  async list(params = {}) {
471
- const res = await this.http.get(`/shops${toQueryString(params)}`);
662
+ const [query, requestOptions] = splitRequestOptions(params);
663
+ const res = await this.http.get(`/shops${toQueryString(query)}`, requestOptions);
472
664
  return res.data;
473
665
  }
474
666
  /** `GET /shops/{id}`: fetch one shop by its id or technicalName. */
475
- get(idOrTechnicalName) {
476
- return this.http.get(`/shops/${encodeURIComponent(idOrTechnicalName)}`);
667
+ get(idOrTechnicalName, options = {}) {
668
+ return this.http.get(`/shops/${encodeURIComponent(idOrTechnicalName)}`, options);
477
669
  }
478
670
  };
479
671
 
@@ -484,8 +676,40 @@ var StatsResource = class {
484
676
  }
485
677
  http;
486
678
  /** `GET /stats`: platform-wide overview counts (shops, expansions, products, prices tracked). */
487
- platform() {
488
- return this.http.get("/stats");
679
+ platform(options = {}) {
680
+ return this.http.get("/stats", options);
681
+ }
682
+ };
683
+
684
+ // src/resources/webhooks.ts
685
+ var WebhooksResource = class {
686
+ constructor(http) {
687
+ this.http = http;
688
+ }
689
+ http;
690
+ /**
691
+ * `POST /webhooks`: register a new webhook.
692
+ *
693
+ * The returned `secret` is the only copy you will ever get — sign-verification depends on it and
694
+ * no endpoint reads it back. Persist it here, at creation, or delete the webhook and make a new
695
+ * one.
696
+ */
697
+ create(params) {
698
+ const { url, events, ...requestOptions } = params;
699
+ return this.http.post("/webhooks", { url, events }, requestOptions);
700
+ }
701
+ /** `GET /webhooks`: every webhook registered on this account. Secrets are never included. */
702
+ list(options = {}) {
703
+ return this.http.get("/webhooks", options);
704
+ }
705
+ /** `DELETE /webhooks/{id}`: revoke a webhook. Deliveries stop immediately; its secret is void. */
706
+ delete(webhookId, options = {}) {
707
+ return this.http.delete(`/webhooks/${encodeURIComponent(webhookId)}`, options);
708
+ }
709
+ /** `POST /webhooks/{id}/test`: send a sample delivery to the registered URL, so you can verify
710
+ * your endpoint and your signature check before waiting on a real event. */
711
+ test(webhookId, options = {}) {
712
+ return this.http.post(`/webhooks/${encodeURIComponent(webhookId)}/test`, void 0, options);
489
713
  }
490
714
  };
491
715
 
@@ -495,6 +719,7 @@ var TcgPriser = class {
495
719
  cards;
496
720
  products;
497
721
  expansions;
722
+ brands;
498
723
  shops;
499
724
  shopMatches;
500
725
  shopMatchStats;
@@ -503,6 +728,9 @@ var TcgPriser = class {
503
728
  bargains;
504
729
  packRates;
505
730
  stats;
731
+ webhooks;
732
+ /** Holds the `HttpClient` so `creditsRemaining` can read the running value off it. */
733
+ http;
506
734
  /**
507
735
  * @param optionsOrAuthToken A subscriber's API token (`new TcgPriser(myApiToken)`), a full
508
736
  * `TcgPriserOptions` object, or omit it entirely for an anonymous, public-only client.
@@ -516,6 +744,7 @@ var TcgPriser = class {
516
744
  );
517
745
  }
518
746
  const http = new HttpClient({
747
+ timeoutMs: advanced.timeoutMs,
519
748
  baseUrl: advanced.baseUrl ?? DEFAULT_BASE_URL,
520
749
  // Bound to globalThis: both browsers and Node's undici implement fetch as a method that
521
750
  // checks its receiver, so an unbound reference throws "Illegal invocation" the moment it's
@@ -528,6 +757,7 @@ var TcgPriser = class {
528
757
  this.cards = new CardsResource(http);
529
758
  this.products = new ProductsResource(http);
530
759
  this.expansions = new ExpansionsResource(http);
760
+ this.brands = new BrandsResource(http);
531
761
  this.shops = new ShopsResource(http);
532
762
  this.shopMatches = new ShopMatchesResource(http);
533
763
  this.shopMatchStats = new ShopMatchStatsResource(http);
@@ -536,10 +766,31 @@ var TcgPriser = class {
536
766
  this.bargains = new BargainsResource(http);
537
767
  this.packRates = new PackRatesResource(http);
538
768
  this.stats = new StatsResource(http);
769
+ this.webhooks = new WebhooksResource(http);
770
+ this.http = http;
771
+ }
772
+ /**
773
+ * Credits left in this week's allowance, as of the last charged call this client made.
774
+ *
775
+ * The API returns `X-Credits-Remaining` on every response it charges for, so this needs no extra
776
+ * request — but it is only as current as your last premium call, and it is `undefined` until you
777
+ * make one. Uncharged calls (every public method, and any call authenticated with something other
778
+ * than an API token) don't update it, because the API doesn't meter them.
779
+ *
780
+ * ```ts
781
+ * await tcgpriser.cards.livePricing('fezandipiti-ex');
782
+ * if ((tcgpriser.creditsRemaining ?? Infinity) < 100) scheduleFewerRefreshes();
783
+ * ```
784
+ *
785
+ * Reading it in a browser additionally needs the API to expose the header via CORS, which it does.
786
+ */
787
+ get creditsRemaining() {
788
+ return this.http.creditsRemaining;
539
789
  }
540
790
  };
541
791
 
542
792
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
793
+ exports.DEFAULT_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;
543
794
  exports.TcgPriser = TcgPriser;
544
795
  exports.TcgPriserError = TcgPriserError;
545
796
  //# sourceMappingURL=index.cjs.map