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