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