tcgpriser 0.10.0 → 0.13.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,14 @@ 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);
131
223
  }
132
224
  };
133
225
 
@@ -137,34 +229,40 @@ var CardsResource = class {
137
229
  this.http = http;
138
230
  }
139
231
  http;
140
- /** `GET /cards`: search or list cards. */
232
+ /** `GET /cards`: list cards, newest first. No free-text search — use `search()` for that. */
141
233
  list(params = {}) {
142
- return this.http.get(`/cards${toQueryString(params)}`);
234
+ const [query, requestOptions] = splitRequestOptions(params);
235
+ return this.http.get(`/cards${toQueryString(query)}`, requestOptions);
236
+ }
237
+ /** `GET /cards/search`: like `list()`, but with free-text search on card and set names. Premium. */
238
+ search(params = {}) {
239
+ const [query, requestOptions] = splitRequestOptions(params);
240
+ return this.http.get(`/cards/search${toQueryString(query)}`, requestOptions);
143
241
  }
144
242
  /** `GET /cards/{id}`: fetch one card by its id or technicalName. */
145
- get(idOrTechnicalName) {
146
- return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}`);
243
+ get(idOrTechnicalName, options = {}) {
244
+ return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}`, options);
147
245
  }
148
246
  /** `GET /cards/{id}/matches`: current shop listings matched to this card (latest per shop). */
149
247
  matches(idOrTechnicalName, params = {}) {
248
+ const [query, requestOptions] = splitRequestOptions(params);
150
249
  return this.http.get(
151
- `/cards/${encodeURIComponent(idOrTechnicalName)}/matches${toQueryString(params)}`
250
+ `/cards/${encodeURIComponent(idOrTechnicalName)}/matches${toQueryString(query)}`,
251
+ requestOptions
152
252
  );
153
253
  }
154
254
  /** `GET /cards/{id}/reference-prices`: Cardmarket/TCGplayer/eBay/Tradera price history. Premium. */
155
255
  referencePrices(idOrTechnicalName, params = {}) {
156
- const [query, authToken] = splitAuthToken(params);
256
+ const [query, requestOptions] = splitRequestOptions(params);
157
257
  return this.http.get(
158
258
  `/cards/${encodeURIComponent(idOrTechnicalName)}/reference-prices${toQueryString(query)}`,
159
- { authToken }
259
+ requestOptions
160
260
  );
161
261
  }
162
262
  /** `GET /cards/{id}/prices`: individual marketplace sale records. Premium. */
163
263
  prices(idOrTechnicalName, params = {}) {
164
- const [query, authToken] = splitAuthToken(params);
165
- return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}/prices${toQueryString(query)}`, {
166
- authToken
167
- });
264
+ const [query, requestOptions] = splitRequestOptions(params);
265
+ return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}/prices${toQueryString(query)}`, requestOptions);
168
266
  }
169
267
  /** `GET /cards/{id}/pricing/live`: computed fresh for this request, not read from the last
170
268
  * stats job. Premium. */
@@ -175,16 +273,39 @@ var CardsResource = class {
175
273
  * `estimatedValue`, `lowestShopOffer`, `referencePriceSnapshotsByProvider` — refreshed once a day
176
274
  * by the nightly pricing/scraper jobs. `get()` returns content only; this is the separate,
177
275
  * 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`);
276
+ pricing(idOrTechnicalName, options = {}) {
277
+ return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}/pricing`, options);
180
278
  }
181
279
  /** `GET /cards/pricing`: pricing for up to 200 cards in one request, keyed by `id` — the batch
182
280
  * counterpart to `pricing()`, for a page of results (a search page, an expansion's contents) that
183
281
  * needs pricing for many items at once. Unlike `get()`/`pricing()`, this only accepts `id`s, not
184
282
  * technicalNames — pass the `id`s already on the cards you fetched. Ids with no match are
185
283
  * 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(",")}`);
284
+ pricingBatch(ids, options = {}) {
285
+ return this.http.get(`/cards/pricing?ids=${ids.map(encodeURIComponent).join(",")}`, options);
286
+ }
287
+ /** `GET /cards/technical-names`: every card's `technicalName` and `updatedAt`, unpaginated and
288
+ * with no pricing joins. Built for enumerating the whole catalog cheaply — a sitemap, or working
289
+ * out which items changed since your last sync — where `list()` would make you page through full
290
+ * card documents to learn the same two fields. */
291
+ technicalNames(options = {}) {
292
+ return this.http.get("/cards/technical-names", options);
293
+ }
294
+ /** `GET /cards/price-stats/daily`: daily average price history, cards only. The same data as
295
+ * `client.priceStats.daily()`, scoped to the card catalog so a filter like `expansion` can't pull
296
+ * in that expansion's sealed products too. */
297
+ dailyStats(params = {}) {
298
+ const [query, requestOptions] = splitRequestOptions(params);
299
+ return this.http.get(`/cards/price-stats/daily${toQueryString(query)}`, requestOptions);
300
+ }
301
+ /** `GET /cards/price-stats/estimated-values`: current estimated market value, cards only. The
302
+ * card-scoped counterpart to `client.priceStats.estimatedValues()`. */
303
+ estimatedValues(params = {}) {
304
+ const [query, requestOptions] = splitRequestOptions(params);
305
+ return this.http.get(
306
+ `/cards/price-stats/estimated-values${toQueryString(query)}`,
307
+ requestOptions
308
+ );
188
309
  }
189
310
  };
190
311
 
@@ -195,30 +316,30 @@ var ExpansionsResource = class {
195
316
  }
196
317
  http;
197
318
  /** `GET /expansions`: every expansion. Unwrapped to a plain array, nothing to paginate here. */
198
- async list() {
199
- const res = await this.http.get("/expansions");
319
+ async list(options = {}) {
320
+ const res = await this.http.get("/expansions", options);
200
321
  return res.data;
201
322
  }
202
323
  /** `GET /expansions/{technicalName}`: metadata only — no cards or sealed products. Returns the
203
324
  * smaller `ExpansionRef`, not the full `Expansion`: this is a plain lookup by technicalName, not
204
325
  * the aggregation `list()` runs, so `sealedCount`/`cardCount`/`productCount` aren't available
205
326
  * here. See `cards()` and `sealedProducts()` for this expansion's contents. */
206
- get(technicalName) {
207
- return this.http.get(`/expansions/${encodeURIComponent(technicalName)}`);
327
+ get(technicalName, options = {}) {
328
+ return this.http.get(`/expansions/${encodeURIComponent(technicalName)}`, options);
208
329
  }
209
330
  /** `GET /expansions/{technicalName}/cards`: every card in this expansion. Content only, no
210
331
  * pricing fields — pass the `id`s from the result to `client.cards.pricingBatch()` if you need
211
332
  * pricing too. Sealed products are a separate call — see `sealedProducts()` — never merged into
212
333
  * this one. */
213
- cards(technicalName) {
214
- return this.http.get(`/expansions/${encodeURIComponent(technicalName)}/cards`);
334
+ cards(technicalName, options = {}) {
335
+ return this.http.get(`/expansions/${encodeURIComponent(technicalName)}/cards`, options);
215
336
  }
216
337
  /** `GET /expansions/{technicalName}/products`: every sealed product in this expansion. Content
217
338
  * only, no pricing fields — pass the `id`s from the result to `client.products.pricingBatch()`
218
339
  * if you need pricing too. Cards are a separate call — see `cards()` — never merged into this
219
340
  * one. */
220
- sealedProducts(technicalName) {
221
- return this.http.get(`/expansions/${encodeURIComponent(technicalName)}/products`);
341
+ sealedProducts(technicalName, options = {}) {
342
+ return this.http.get(`/expansions/${encodeURIComponent(technicalName)}/products`, options);
222
343
  }
223
344
  /** `GET /expansions/{technicalName}/cards/live-pricing`: computed fresh for every card in this
224
345
  * expansion, not read from the last stats job. Premium. */
@@ -246,13 +367,13 @@ var PackRatesResource = class {
246
367
  http;
247
368
  /** `GET /pack-rates`: pull-rate odds for every expansion that has them. Unwrapped to a plain
248
369
  * array, nothing to paginate here. */
249
- async list() {
250
- const res = await this.http.get("/pack-rates");
370
+ async list(options = {}) {
371
+ const res = await this.http.get("/pack-rates", options);
251
372
  return res.data;
252
373
  }
253
374
  /** `GET /pack-rates/{expansionId}`: pull-rate odds for one expansion. */
254
- get(expansionId) {
255
- return this.http.get(`/pack-rates/${encodeURIComponent(expansionId)}`);
375
+ get(expansionId, options = {}) {
376
+ return this.http.get(`/pack-rates/${encodeURIComponent(expansionId)}`, options);
256
377
  }
257
378
  };
258
379
 
@@ -264,17 +385,20 @@ var PriceStatsResource = class {
264
385
  http;
265
386
  /** `GET /price-stats/daily`: daily average price history, filtered to matching product(s). */
266
387
  daily(params = {}) {
267
- return this.http.get(`/price-stats/daily${toQueryString(params)}`);
388
+ const [query, requestOptions] = splitRequestOptions(params);
389
+ return this.http.get(`/price-stats/daily${toQueryString(query)}`, requestOptions);
268
390
  }
269
391
  /** `GET /price-stats/estimated-values`: current estimated market value, filtered to matching
270
392
  * product(s). */
271
393
  estimatedValues(params = {}) {
272
- return this.http.get(`/price-stats/estimated-values${toQueryString(params)}`);
394
+ const [query, requestOptions] = splitRequestOptions(params);
395
+ return this.http.get(`/price-stats/estimated-values${toQueryString(query)}`, requestOptions);
273
396
  }
274
397
  /** `GET /price-stats/top-products`: items ranked by shop availability (how many shops carry
275
398
  * them), not by price. */
276
399
  topProducts(params = {}) {
277
- return this.http.get(`/price-stats/top-products${toQueryString(params)}`);
400
+ const [query, requestOptions] = splitRequestOptions(params);
401
+ return this.http.get(`/price-stats/top-products${toQueryString(query)}`, requestOptions);
278
402
  }
279
403
  /** `GET /price-stats/product/{id}`: daily price history, current estimate, and a variant-count
280
404
  * summary for one product. Premium. */
@@ -289,10 +413,10 @@ var PriceStatsResource = class {
289
413
  /** `GET /price-stats/product/{id}/daily`: daily price history for one product, with a
290
414
  * caller-chosen window. Premium. */
291
415
  productDaily(idOrTechnicalName, params = {}) {
292
- const [query, authToken] = splitAuthToken(params);
416
+ const [query, requestOptions] = splitRequestOptions(params);
293
417
  return this.http.get(
294
418
  `/price-stats/product/${encodeURIComponent(idOrTechnicalName)}/daily${toQueryString(query)}`,
295
- { authToken }
419
+ requestOptions
296
420
  );
297
421
  }
298
422
  /** `GET /price-stats/product/{id}/daily-last-30`: daily price history for the last 30 days
@@ -313,20 +437,20 @@ var PriceStatsResource = class {
313
437
  /** `GET /price-stats/product/{id}/by-variant`: price stats broken out per card condition/grade.
314
438
  * Premium. */
315
439
  productByVariant(idOrTechnicalName, params = {}) {
316
- const [query, authToken] = splitAuthToken(params);
440
+ const [query, requestOptions] = splitRequestOptions(params);
317
441
  return this.http.get(
318
442
  `/price-stats/product/${encodeURIComponent(idOrTechnicalName)}/by-variant${toQueryString(query)}`,
319
- { authToken }
443
+ requestOptions
320
444
  );
321
445
  }
322
446
  /** `GET /price-stats/product/{id}/daily-by-variant`: daily price history for one specific
323
447
  * condition/grade. `condition` is required for `cardType: 'loose'`; `gradingCompany` and `grade`
324
448
  * are required for `cardType: 'graded'`. Premium. */
325
449
  productDailyByVariant(idOrTechnicalName, params) {
326
- const [query, authToken] = splitAuthToken(params);
450
+ const [query, requestOptions] = splitRequestOptions(params);
327
451
  return this.http.get(
328
452
  `/price-stats/product/${encodeURIComponent(idOrTechnicalName)}/daily-by-variant${toQueryString(query)}`,
329
- { authToken }
453
+ requestOptions
330
454
  );
331
455
  }
332
456
  };
@@ -337,34 +461,43 @@ var ProductsResource = class {
337
461
  this.http = http;
338
462
  }
339
463
  http;
340
- /** `GET /product`: search or list sealed products. */
464
+ /** `GET /product`: list sealed products, newest first. No free-text search — use `search()` for
465
+ * that. */
341
466
  list(params = {}) {
342
- return this.http.get(`/product${toQueryString(params)}`);
467
+ const [query, requestOptions] = splitRequestOptions(params);
468
+ return this.http.get(`/product${toQueryString(query)}`, requestOptions);
469
+ }
470
+ /** `GET /product/search`: like `list()`, but with free-text search on the product name. Premium. */
471
+ search(params = {}) {
472
+ const [query, requestOptions] = splitRequestOptions(params);
473
+ return this.http.get(`/product/search${toQueryString(query)}`, requestOptions);
343
474
  }
344
475
  /** `GET /product/{id}`: fetch one sealed product by its id or technicalName. */
345
- get(idOrTechnicalName) {
346
- return this.http.get(`/product/${encodeURIComponent(idOrTechnicalName)}`);
476
+ get(idOrTechnicalName, options = {}) {
477
+ return this.http.get(`/product/${encodeURIComponent(idOrTechnicalName)}`, options);
347
478
  }
348
479
  /** `GET /product/{id}/matches`: current shop listings matched to this product (latest per shop). */
349
480
  matches(idOrTechnicalName, params = {}) {
481
+ const [query, requestOptions] = splitRequestOptions(params);
350
482
  return this.http.get(
351
- `/product/${encodeURIComponent(idOrTechnicalName)}/matches${toQueryString(params)}`
483
+ `/product/${encodeURIComponent(idOrTechnicalName)}/matches${toQueryString(query)}`,
484
+ requestOptions
352
485
  );
353
486
  }
354
487
  /** `GET /product/{id}/reference-prices`: Cardmarket/TCGplayer/Tradera price history. Premium. */
355
488
  referencePrices(idOrTechnicalName, params = {}) {
356
- const [query, authToken] = splitAuthToken(params);
489
+ const [query, requestOptions] = splitRequestOptions(params);
357
490
  return this.http.get(
358
491
  `/product/${encodeURIComponent(idOrTechnicalName)}/reference-prices${toQueryString(query)}`,
359
- { authToken }
492
+ requestOptions
360
493
  );
361
494
  }
362
495
  /** `GET /product/{id}/prices`: individual marketplace sale records. Premium. */
363
496
  prices(idOrTechnicalName, params = {}) {
364
- const [query, authToken] = splitAuthToken(params);
497
+ const [query, requestOptions] = splitRequestOptions(params);
365
498
  return this.http.get(
366
499
  `/product/${encodeURIComponent(idOrTechnicalName)}/prices${toQueryString(query)}`,
367
- { authToken }
500
+ requestOptions
368
501
  );
369
502
  }
370
503
  /** `GET /product/{id}/pricing/live`: computed fresh for this request, not read from the last
@@ -376,16 +509,38 @@ var ProductsResource = class {
376
509
  * `estimatedValue`, `lowestShopOffer`, `referencePriceSnapshotsByProvider` — refreshed once a day
377
510
  * by the nightly pricing/scraper jobs. `get()` returns content only; this is the separate,
378
511
  * 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`);
512
+ pricing(idOrTechnicalName, options = {}) {
513
+ return this.http.get(`/product/${encodeURIComponent(idOrTechnicalName)}/pricing`, options);
381
514
  }
382
515
  /** `GET /product/pricing`: pricing for up to 200 sealed products in one request, keyed by `id` —
383
516
  * the batch counterpart to `pricing()`, for a page of results (a search page, an expansion's
384
517
  * contents) that needs pricing for many items at once. Unlike `get()`/`pricing()`, this only
385
518
  * accepts `id`s, not technicalNames — pass the `id`s already on the products you fetched. Ids with
386
519
  * 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(",")}`);
520
+ pricingBatch(ids, options = {}) {
521
+ return this.http.get(`/product/pricing?ids=${ids.map(encodeURIComponent).join(",")}`, options);
522
+ }
523
+ /** `GET /product/technical-names`: every sealed product's `technicalName` and `updatedAt`,
524
+ * unpaginated and with no pricing joins. The sealed counterpart to
525
+ * `client.cards.technicalNames()` — for sitemaps and incremental syncs. */
526
+ technicalNames(options = {}) {
527
+ return this.http.get("/product/technical-names", options);
528
+ }
529
+ /** `GET /product/price-stats/daily`: daily average price history, sealed products only. The same
530
+ * data as `client.priceStats.daily()`, scoped to the sealed catalog so a filter like `expansion`
531
+ * can't pull in that expansion's single cards too. */
532
+ dailyStats(params = {}) {
533
+ const [query, requestOptions] = splitRequestOptions(params);
534
+ return this.http.get(`/product/price-stats/daily${toQueryString(query)}`, requestOptions);
535
+ }
536
+ /** `GET /product/price-stats/estimated-values`: current estimated market value, sealed products
537
+ * only. The sealed-scoped counterpart to `client.priceStats.estimatedValues()`. */
538
+ estimatedValues(params = {}) {
539
+ const [query, requestOptions] = splitRequestOptions(params);
540
+ return this.http.get(
541
+ `/product/price-stats/estimated-values${toQueryString(query)}`,
542
+ requestOptions
543
+ );
389
544
  }
390
545
  };
391
546
 
@@ -397,24 +552,22 @@ var ShopMatchStatsResource = class {
397
552
  http;
398
553
  /** `GET /shop-match-stats/product/{productId}`: one product's price history, broken out per shop. */
399
554
  forProduct(productId, params = {}) {
400
- const [query, authToken] = splitAuthToken(params);
555
+ const [query, requestOptions] = splitRequestOptions(params);
401
556
  return this.http.get(
402
557
  `/shop-match-stats/product/${encodeURIComponent(productId)}${toQueryString(query)}`,
403
- { authToken }
558
+ requestOptions
404
559
  );
405
560
  }
406
561
  /** `GET /shop-match-stats/shop/{shop}`: one shop's price history, broken out per product. */
407
562
  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
- });
563
+ const [query, requestOptions] = splitRequestOptions(params);
564
+ return this.http.get(`/shop-match-stats/shop/${encodeURIComponent(shop)}${toQueryString(query)}`, requestOptions);
412
565
  }
413
566
  /** `GET /shop-match-stats/compare`: one product's price at every shop that carries it, as of
414
567
  * one date (defaults to the latest). */
415
568
  compare(params) {
416
- const [query, authToken] = splitAuthToken(params);
417
- return this.http.get(`/shop-match-stats/compare${toQueryString(query)}`, { authToken });
569
+ const [query, requestOptions] = splitRequestOptions(params);
570
+ return this.http.get(`/shop-match-stats/compare${toQueryString(query)}`, requestOptions);
418
571
  }
419
572
  };
420
573
 
@@ -426,15 +579,21 @@ var ShopMatchesResource = class {
426
579
  http;
427
580
  /** `GET /shop-matches`: every current match across every shop (latest record per url+shop). */
428
581
  list(params = {}) {
429
- return this.http.get(`/shop-matches${toQueryString(params)}`);
582
+ const [query, requestOptions] = splitRequestOptions(params);
583
+ return this.http.get(`/shop-matches${toQueryString(query)}`, requestOptions);
430
584
  }
431
585
  /** `GET /shop-matches/{shop}`: every current match at one shop (latest record per url). */
432
586
  forShop(technicalName, params = {}) {
433
- return this.http.get(`/shop-matches/${encodeURIComponent(technicalName)}${toQueryString(params)}`);
587
+ const [query, requestOptions] = splitRequestOptions(params);
588
+ return this.http.get(
589
+ `/shop-matches/${encodeURIComponent(technicalName)}${toQueryString(query)}`,
590
+ requestOptions
591
+ );
434
592
  }
435
593
  /** `GET /shop-matches/shops`: match counts per shop (based on latest records only). */
436
594
  shopStats(params = {}) {
437
- return this.http.get(`/shop-matches/shops${toQueryString(params)}`);
595
+ const [query, requestOptions] = splitRequestOptions(params);
596
+ return this.http.get(`/shop-matches/shops${toQueryString(query)}`, requestOptions);
438
597
  }
439
598
  };
440
599
 
@@ -446,16 +605,16 @@ var ShopUrlsResource = class {
446
605
  http;
447
606
  /** `POST /shop-urls/submit`: submit a shop URL for scraping. */
448
607
  submit(params) {
449
- const { authToken, url, shop } = params;
450
- return this.http.post("/shop-urls/submit", { url, shop }, { authToken });
608
+ const { url, shop, ...requestOptions } = params;
609
+ return this.http.post("/shop-urls/submit", { url, shop }, requestOptions);
451
610
  }
452
611
  /** `PATCH /shop-urls/{id}/product`: manually assign (or clear) the product a shop URL resolves to. */
453
612
  assignProduct(shopUrlId, params) {
454
- const { authToken, productId } = params;
613
+ const { productId, ...requestOptions } = params;
455
614
  return this.http.patch(
456
615
  `/shop-urls/${encodeURIComponent(shopUrlId)}/product`,
457
616
  { productId },
458
- { authToken }
617
+ requestOptions
459
618
  );
460
619
  }
461
620
  };
@@ -468,12 +627,13 @@ var ShopsResource = class {
468
627
  http;
469
628
  /** `GET /shops`: every tracked shop. Unwrapped to a plain array, nothing to paginate here. */
470
629
  async list(params = {}) {
471
- const res = await this.http.get(`/shops${toQueryString(params)}`);
630
+ const [query, requestOptions] = splitRequestOptions(params);
631
+ const res = await this.http.get(`/shops${toQueryString(query)}`, requestOptions);
472
632
  return res.data;
473
633
  }
474
634
  /** `GET /shops/{id}`: fetch one shop by its id or technicalName. */
475
- get(idOrTechnicalName) {
476
- return this.http.get(`/shops/${encodeURIComponent(idOrTechnicalName)}`);
635
+ get(idOrTechnicalName, options = {}) {
636
+ return this.http.get(`/shops/${encodeURIComponent(idOrTechnicalName)}`, options);
477
637
  }
478
638
  };
479
639
 
@@ -484,8 +644,40 @@ var StatsResource = class {
484
644
  }
485
645
  http;
486
646
  /** `GET /stats`: platform-wide overview counts (shops, expansions, products, prices tracked). */
487
- platform() {
488
- return this.http.get("/stats");
647
+ platform(options = {}) {
648
+ return this.http.get("/stats", options);
649
+ }
650
+ };
651
+
652
+ // src/resources/webhooks.ts
653
+ var WebhooksResource = class {
654
+ constructor(http) {
655
+ this.http = http;
656
+ }
657
+ http;
658
+ /**
659
+ * `POST /webhooks`: register a new webhook.
660
+ *
661
+ * The returned `secret` is the only copy you will ever get — sign-verification depends on it and
662
+ * no endpoint reads it back. Persist it here, at creation, or delete the webhook and make a new
663
+ * one.
664
+ */
665
+ create(params) {
666
+ const { url, events, ...requestOptions } = params;
667
+ return this.http.post("/webhooks", { url, events }, requestOptions);
668
+ }
669
+ /** `GET /webhooks`: every webhook registered on this account. Secrets are never included. */
670
+ list(options = {}) {
671
+ return this.http.get("/webhooks", options);
672
+ }
673
+ /** `DELETE /webhooks/{id}`: revoke a webhook. Deliveries stop immediately; its secret is void. */
674
+ delete(webhookId, options = {}) {
675
+ return this.http.delete(`/webhooks/${encodeURIComponent(webhookId)}`, options);
676
+ }
677
+ /** `POST /webhooks/{id}/test`: send a sample delivery to the registered URL, so you can verify
678
+ * your endpoint and your signature check before waiting on a real event. */
679
+ test(webhookId, options = {}) {
680
+ return this.http.post(`/webhooks/${encodeURIComponent(webhookId)}/test`, void 0, options);
489
681
  }
490
682
  };
491
683
 
@@ -503,6 +695,9 @@ var TcgPriser = class {
503
695
  bargains;
504
696
  packRates;
505
697
  stats;
698
+ webhooks;
699
+ /** Holds the `HttpClient` so `creditsRemaining` can read the running value off it. */
700
+ http;
506
701
  /**
507
702
  * @param optionsOrAuthToken A subscriber's API token (`new TcgPriser(myApiToken)`), a full
508
703
  * `TcgPriserOptions` object, or omit it entirely for an anonymous, public-only client.
@@ -516,6 +711,7 @@ var TcgPriser = class {
516
711
  );
517
712
  }
518
713
  const http = new HttpClient({
714
+ timeoutMs: advanced.timeoutMs,
519
715
  baseUrl: advanced.baseUrl ?? DEFAULT_BASE_URL,
520
716
  // Bound to globalThis: both browsers and Node's undici implement fetch as a method that
521
717
  // checks its receiver, so an unbound reference throws "Illegal invocation" the moment it's
@@ -536,10 +732,31 @@ var TcgPriser = class {
536
732
  this.bargains = new BargainsResource(http);
537
733
  this.packRates = new PackRatesResource(http);
538
734
  this.stats = new StatsResource(http);
735
+ this.webhooks = new WebhooksResource(http);
736
+ this.http = http;
737
+ }
738
+ /**
739
+ * Credits left in this week's allowance, as of the last charged call this client made.
740
+ *
741
+ * The API returns `X-Credits-Remaining` on every response it charges for, so this needs no extra
742
+ * request — but it is only as current as your last premium call, and it is `undefined` until you
743
+ * make one. Uncharged calls (every public method, and any call authenticated with something other
744
+ * than an API token) don't update it, because the API doesn't meter them.
745
+ *
746
+ * ```ts
747
+ * await tcgpriser.cards.livePricing('fezandipiti-ex');
748
+ * if ((tcgpriser.creditsRemaining ?? Infinity) < 100) scheduleFewerRefreshes();
749
+ * ```
750
+ *
751
+ * Reading it in a browser additionally needs the API to expose the header via CORS, which it does.
752
+ */
753
+ get creditsRemaining() {
754
+ return this.http.creditsRemaining;
539
755
  }
540
756
  };
541
757
 
542
758
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
759
+ exports.DEFAULT_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;
543
760
  exports.TcgPriser = TcgPriser;
544
761
  exports.TcgPriserError = TcgPriserError;
545
762
  //# sourceMappingURL=index.cjs.map