tcgpriser 0.7.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",
@@ -44,8 +59,16 @@ var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set([
44
59
  "readOnlyField",
45
60
  "rateLimited",
46
61
  "premiumRequired",
62
+ "businessRequired",
63
+ "creditsExhausted",
47
64
  "internalError"
48
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
+ }
49
72
  async function toApiError(res, url) {
50
73
  const body = await res.text();
51
74
  let code = "unknown";
@@ -60,18 +83,63 @@ async function toApiError(res, url) {
60
83
  details = parsed.error?.details;
61
84
  } catch {
62
85
  }
63
- 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
+ };
64
125
  }
65
126
  var HttpClient = class {
66
127
  baseUrl;
67
128
  fetchImpl;
68
129
  defaultHeaders;
69
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;
70
137
  constructor(options) {
71
138
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
72
139
  this.fetchImpl = options.fetch;
73
140
  this.defaultHeaders = options.headers ?? {};
74
141
  this.defaultAuthToken = options.authToken;
142
+ this.defaultTimeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
75
143
  }
76
144
  get(path, requestOptions) {
77
145
  return this.request("GET", path, void 0, requestOptions);
@@ -82,17 +150,42 @@ var HttpClient = class {
82
150
  patch(path, body, requestOptions) {
83
151
  return this.request("PATCH", path, body, requestOptions);
84
152
  }
153
+ delete(path, requestOptions) {
154
+ return this.request("DELETE", path, void 0, requestOptions);
155
+ }
85
156
  async request(method, path, body, requestOptions) {
86
157
  const url = `${this.baseUrl}${path}`;
87
158
  const authToken = requestOptions && "authToken" in requestOptions ? requestOptions.authToken : this.defaultAuthToken;
88
159
  const headers = { Accept: "application/json", ...this.defaultHeaders };
89
160
  if (authToken) headers.Authorization = `Bearer ${authToken}`;
90
161
  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
- });
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;
96
189
  if (!res.ok) throw await toApiError(res, url);
97
190
  if (res.status === 204) return void 0;
98
191
  return nullsToUndefined(await res.json());
@@ -119,13 +212,14 @@ var BargainsResource = class {
119
212
  * count is fixed by the API (no `limit`/`skip` on the public tier); `pagination.hasMore` tells
120
213
  * you if more exist. */
121
214
  list(params = {}) {
122
- return this.http.get(`/bargains${toQueryString(params)}`);
215
+ const [query, requestOptions] = splitRequestOptions(params);
216
+ return this.http.get(`/bargains${toQueryString(query)}`, requestOptions);
123
217
  }
124
218
  /** `GET /bargains/search`: like `list()`, but with real pagination and filters (shop, discount
125
219
  * threshold, card condition/grade, free-text search). Premium. */
126
220
  search(params = {}) {
127
- const [query, authToken] = splitAuthToken(params);
128
- 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);
129
223
  }
130
224
  };
131
225
 
@@ -135,34 +229,40 @@ var CardsResource = class {
135
229
  this.http = http;
136
230
  }
137
231
  http;
138
- /** `GET /cards`: search or list cards. */
232
+ /** `GET /cards`: list cards, newest first. No free-text search use `search()` for that. */
139
233
  list(params = {}) {
140
- 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);
141
241
  }
142
242
  /** `GET /cards/{id}`: fetch one card by its id or technicalName. */
143
- get(idOrTechnicalName) {
144
- return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}`);
243
+ get(idOrTechnicalName, options = {}) {
244
+ return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}`, options);
145
245
  }
146
246
  /** `GET /cards/{id}/matches`: current shop listings matched to this card (latest per shop). */
147
247
  matches(idOrTechnicalName, params = {}) {
248
+ const [query, requestOptions] = splitRequestOptions(params);
148
249
  return this.http.get(
149
- `/cards/${encodeURIComponent(idOrTechnicalName)}/matches${toQueryString(params)}`
250
+ `/cards/${encodeURIComponent(idOrTechnicalName)}/matches${toQueryString(query)}`,
251
+ requestOptions
150
252
  );
151
253
  }
152
254
  /** `GET /cards/{id}/reference-prices`: Cardmarket/TCGplayer/eBay/Tradera price history. Premium. */
153
255
  referencePrices(idOrTechnicalName, params = {}) {
154
- const [query, authToken] = splitAuthToken(params);
256
+ const [query, requestOptions] = splitRequestOptions(params);
155
257
  return this.http.get(
156
258
  `/cards/${encodeURIComponent(idOrTechnicalName)}/reference-prices${toQueryString(query)}`,
157
- { authToken }
259
+ requestOptions
158
260
  );
159
261
  }
160
262
  /** `GET /cards/{id}/prices`: individual marketplace sale records. Premium. */
161
263
  prices(idOrTechnicalName, params = {}) {
162
- const [query, authToken] = splitAuthToken(params);
163
- return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}/prices${toQueryString(query)}`, {
164
- authToken
165
- });
264
+ const [query, requestOptions] = splitRequestOptions(params);
265
+ return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}/prices${toQueryString(query)}`, requestOptions);
166
266
  }
167
267
  /** `GET /cards/{id}/pricing/live`: computed fresh for this request, not read from the last
168
268
  * stats job. Premium. */
@@ -173,16 +273,39 @@ var CardsResource = class {
173
273
  * `estimatedValue`, `lowestShopOffer`, `referencePriceSnapshotsByProvider` — refreshed once a day
174
274
  * by the nightly pricing/scraper jobs. `get()` returns content only; this is the separate,
175
275
  * 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`);
276
+ pricing(idOrTechnicalName, options = {}) {
277
+ return this.http.get(`/cards/${encodeURIComponent(idOrTechnicalName)}/pricing`, options);
178
278
  }
179
279
  /** `GET /cards/pricing`: pricing for up to 200 cards in one request, keyed by `id` — the batch
180
280
  * counterpart to `pricing()`, for a page of results (a search page, an expansion's contents) that
181
281
  * needs pricing for many items at once. Unlike `get()`/`pricing()`, this only accepts `id`s, not
182
282
  * technicalNames — pass the `id`s already on the cards you fetched. Ids with no match are
183
283
  * 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(",")}`);
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
+ );
186
309
  }
187
310
  };
188
311
 
@@ -193,21 +316,42 @@ var ExpansionsResource = class {
193
316
  }
194
317
  http;
195
318
  /** `GET /expansions`: every expansion. Unwrapped to a plain array, nothing to paginate here. */
196
- async list() {
197
- const res = await this.http.get("/expansions");
319
+ async list(options = {}) {
320
+ const res = await this.http.get("/expansions", options);
198
321
  return res.data;
199
322
  }
200
- /** `GET /expansions/{technicalName}/products`: every card and sealed product in one
201
- * expansion, kept as separate `cards`/`sealed` groups. Content only, no pricing fields pass the
202
- * `id`s from the result to `client.cards.pricingBatch()` / `client.products.pricingBatch()` if you
203
- * need pricing too. This mirrors the API 1:1 rather than fetching pricing for you, since pricing
204
- * for every item in an expansion is a second, separately-cached call the caller may not want. */
205
- products(technicalName) {
206
- return this.http.get(`/expansions/${encodeURIComponent(technicalName)}/products?grouped=true`);
207
- }
208
- /** `GET /expansions/{technicalName}/products/live-pricing`: computed fresh for every item in
209
- * this expansion, not read from the last stats job. Premium. */
210
- livePricing(technicalName, options = {}) {
323
+ /** `GET /expansions/{technicalName}`: metadata only no cards or sealed products. Returns the
324
+ * smaller `ExpansionRef`, not the full `Expansion`: this is a plain lookup by technicalName, not
325
+ * the aggregation `list()` runs, so `sealedCount`/`cardCount`/`productCount` aren't available
326
+ * here. See `cards()` and `sealedProducts()` for this expansion's contents. */
327
+ get(technicalName, options = {}) {
328
+ return this.http.get(`/expansions/${encodeURIComponent(technicalName)}`, options);
329
+ }
330
+ /** `GET /expansions/{technicalName}/cards`: every card in this expansion. Content only, no
331
+ * pricing fields — pass the `id`s from the result to `client.cards.pricingBatch()` if you need
332
+ * pricing too. Sealed products are a separate call see `sealedProducts()` — never merged into
333
+ * this one. */
334
+ cards(technicalName, options = {}) {
335
+ return this.http.get(`/expansions/${encodeURIComponent(technicalName)}/cards`, options);
336
+ }
337
+ /** `GET /expansions/{technicalName}/products`: every sealed product in this expansion. Content
338
+ * only, no pricing fields — pass the `id`s from the result to `client.products.pricingBatch()`
339
+ * if you need pricing too. Cards are a separate call — see `cards()` — never merged into this
340
+ * one. */
341
+ sealedProducts(technicalName, options = {}) {
342
+ return this.http.get(`/expansions/${encodeURIComponent(technicalName)}/products`, options);
343
+ }
344
+ /** `GET /expansions/{technicalName}/cards/live-pricing`: computed fresh for every card in this
345
+ * expansion, not read from the last stats job. Premium. */
346
+ cardsLivePricing(technicalName, options = {}) {
347
+ return this.http.get(
348
+ `/expansions/${encodeURIComponent(technicalName)}/cards/live-pricing`,
349
+ options
350
+ );
351
+ }
352
+ /** `GET /expansions/{technicalName}/products/live-pricing`: computed fresh for every sealed
353
+ * product in this expansion, not read from the last stats job. Premium. */
354
+ productsLivePricing(technicalName, options = {}) {
211
355
  return this.http.get(
212
356
  `/expansions/${encodeURIComponent(technicalName)}/products/live-pricing`,
213
357
  options
@@ -223,13 +367,13 @@ var PackRatesResource = class {
223
367
  http;
224
368
  /** `GET /pack-rates`: pull-rate odds for every expansion that has them. Unwrapped to a plain
225
369
  * array, nothing to paginate here. */
226
- async list() {
227
- const res = await this.http.get("/pack-rates");
370
+ async list(options = {}) {
371
+ const res = await this.http.get("/pack-rates", options);
228
372
  return res.data;
229
373
  }
230
374
  /** `GET /pack-rates/{expansionId}`: pull-rate odds for one expansion. */
231
- get(expansionId) {
232
- return this.http.get(`/pack-rates/${encodeURIComponent(expansionId)}`);
375
+ get(expansionId, options = {}) {
376
+ return this.http.get(`/pack-rates/${encodeURIComponent(expansionId)}`, options);
233
377
  }
234
378
  };
235
379
 
@@ -241,17 +385,20 @@ var PriceStatsResource = class {
241
385
  http;
242
386
  /** `GET /price-stats/daily`: daily average price history, filtered to matching product(s). */
243
387
  daily(params = {}) {
244
- 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);
245
390
  }
246
391
  /** `GET /price-stats/estimated-values`: current estimated market value, filtered to matching
247
392
  * product(s). */
248
393
  estimatedValues(params = {}) {
249
- 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);
250
396
  }
251
397
  /** `GET /price-stats/top-products`: items ranked by shop availability (how many shops carry
252
398
  * them), not by price. */
253
399
  topProducts(params = {}) {
254
- 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);
255
402
  }
256
403
  /** `GET /price-stats/product/{id}`: daily price history, current estimate, and a variant-count
257
404
  * summary for one product. Premium. */
@@ -266,10 +413,10 @@ var PriceStatsResource = class {
266
413
  /** `GET /price-stats/product/{id}/daily`: daily price history for one product, with a
267
414
  * caller-chosen window. Premium. */
268
415
  productDaily(idOrTechnicalName, params = {}) {
269
- const [query, authToken] = splitAuthToken(params);
416
+ const [query, requestOptions] = splitRequestOptions(params);
270
417
  return this.http.get(
271
418
  `/price-stats/product/${encodeURIComponent(idOrTechnicalName)}/daily${toQueryString(query)}`,
272
- { authToken }
419
+ requestOptions
273
420
  );
274
421
  }
275
422
  /** `GET /price-stats/product/{id}/daily-last-30`: daily price history for the last 30 days
@@ -290,20 +437,20 @@ var PriceStatsResource = class {
290
437
  /** `GET /price-stats/product/{id}/by-variant`: price stats broken out per card condition/grade.
291
438
  * Premium. */
292
439
  productByVariant(idOrTechnicalName, params = {}) {
293
- const [query, authToken] = splitAuthToken(params);
440
+ const [query, requestOptions] = splitRequestOptions(params);
294
441
  return this.http.get(
295
442
  `/price-stats/product/${encodeURIComponent(idOrTechnicalName)}/by-variant${toQueryString(query)}`,
296
- { authToken }
443
+ requestOptions
297
444
  );
298
445
  }
299
446
  /** `GET /price-stats/product/{id}/daily-by-variant`: daily price history for one specific
300
447
  * condition/grade. `condition` is required for `cardType: 'loose'`; `gradingCompany` and `grade`
301
448
  * are required for `cardType: 'graded'`. Premium. */
302
449
  productDailyByVariant(idOrTechnicalName, params) {
303
- const [query, authToken] = splitAuthToken(params);
450
+ const [query, requestOptions] = splitRequestOptions(params);
304
451
  return this.http.get(
305
452
  `/price-stats/product/${encodeURIComponent(idOrTechnicalName)}/daily-by-variant${toQueryString(query)}`,
306
- { authToken }
453
+ requestOptions
307
454
  );
308
455
  }
309
456
  };
@@ -314,34 +461,43 @@ var ProductsResource = class {
314
461
  this.http = http;
315
462
  }
316
463
  http;
317
- /** `GET /product`: search or list sealed products. */
464
+ /** `GET /product`: list sealed products, newest first. No free-text search — use `search()` for
465
+ * that. */
318
466
  list(params = {}) {
319
- 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);
320
474
  }
321
475
  /** `GET /product/{id}`: fetch one sealed product by its id or technicalName. */
322
- get(idOrTechnicalName) {
323
- return this.http.get(`/product/${encodeURIComponent(idOrTechnicalName)}`);
476
+ get(idOrTechnicalName, options = {}) {
477
+ return this.http.get(`/product/${encodeURIComponent(idOrTechnicalName)}`, options);
324
478
  }
325
479
  /** `GET /product/{id}/matches`: current shop listings matched to this product (latest per shop). */
326
480
  matches(idOrTechnicalName, params = {}) {
481
+ const [query, requestOptions] = splitRequestOptions(params);
327
482
  return this.http.get(
328
- `/product/${encodeURIComponent(idOrTechnicalName)}/matches${toQueryString(params)}`
483
+ `/product/${encodeURIComponent(idOrTechnicalName)}/matches${toQueryString(query)}`,
484
+ requestOptions
329
485
  );
330
486
  }
331
487
  /** `GET /product/{id}/reference-prices`: Cardmarket/TCGplayer/Tradera price history. Premium. */
332
488
  referencePrices(idOrTechnicalName, params = {}) {
333
- const [query, authToken] = splitAuthToken(params);
489
+ const [query, requestOptions] = splitRequestOptions(params);
334
490
  return this.http.get(
335
491
  `/product/${encodeURIComponent(idOrTechnicalName)}/reference-prices${toQueryString(query)}`,
336
- { authToken }
492
+ requestOptions
337
493
  );
338
494
  }
339
495
  /** `GET /product/{id}/prices`: individual marketplace sale records. Premium. */
340
496
  prices(idOrTechnicalName, params = {}) {
341
- const [query, authToken] = splitAuthToken(params);
497
+ const [query, requestOptions] = splitRequestOptions(params);
342
498
  return this.http.get(
343
499
  `/product/${encodeURIComponent(idOrTechnicalName)}/prices${toQueryString(query)}`,
344
- { authToken }
500
+ requestOptions
345
501
  );
346
502
  }
347
503
  /** `GET /product/{id}/pricing/live`: computed fresh for this request, not read from the last
@@ -353,16 +509,38 @@ var ProductsResource = class {
353
509
  * `estimatedValue`, `lowestShopOffer`, `referencePriceSnapshotsByProvider` — refreshed once a day
354
510
  * by the nightly pricing/scraper jobs. `get()` returns content only; this is the separate,
355
511
  * shorter-cached call for the part of a product that actually changes day to day. */
356
- pricing(idOrTechnicalName) {
357
- return this.http.get(`/product/${encodeURIComponent(idOrTechnicalName)}/pricing`);
512
+ pricing(idOrTechnicalName, options = {}) {
513
+ return this.http.get(`/product/${encodeURIComponent(idOrTechnicalName)}/pricing`, options);
358
514
  }
359
515
  /** `GET /product/pricing`: pricing for up to 200 sealed products in one request, keyed by `id` —
360
516
  * the batch counterpart to `pricing()`, for a page of results (a search page, an expansion's
361
517
  * contents) that needs pricing for many items at once. Unlike `get()`/`pricing()`, this only
362
518
  * accepts `id`s, not technicalNames — pass the `id`s already on the products you fetched. Ids with
363
519
  * no match are silently omitted from the result rather than causing an error. */
364
- pricingBatch(ids) {
365
- 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
+ );
366
544
  }
367
545
  };
368
546
 
@@ -374,24 +552,22 @@ var ShopMatchStatsResource = class {
374
552
  http;
375
553
  /** `GET /shop-match-stats/product/{productId}`: one product's price history, broken out per shop. */
376
554
  forProduct(productId, params = {}) {
377
- const [query, authToken] = splitAuthToken(params);
555
+ const [query, requestOptions] = splitRequestOptions(params);
378
556
  return this.http.get(
379
557
  `/shop-match-stats/product/${encodeURIComponent(productId)}${toQueryString(query)}`,
380
- { authToken }
558
+ requestOptions
381
559
  );
382
560
  }
383
561
  /** `GET /shop-match-stats/shop/{shop}`: one shop's price history, broken out per product. */
384
562
  forShop(shop, params = {}) {
385
- const [query, authToken] = splitAuthToken(params);
386
- return this.http.get(`/shop-match-stats/shop/${encodeURIComponent(shop)}${toQueryString(query)}`, {
387
- authToken
388
- });
563
+ const [query, requestOptions] = splitRequestOptions(params);
564
+ return this.http.get(`/shop-match-stats/shop/${encodeURIComponent(shop)}${toQueryString(query)}`, requestOptions);
389
565
  }
390
566
  /** `GET /shop-match-stats/compare`: one product's price at every shop that carries it, as of
391
567
  * one date (defaults to the latest). */
392
568
  compare(params) {
393
- const [query, authToken] = splitAuthToken(params);
394
- 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);
395
571
  }
396
572
  };
397
573
 
@@ -403,15 +579,21 @@ var ShopMatchesResource = class {
403
579
  http;
404
580
  /** `GET /shop-matches`: every current match across every shop (latest record per url+shop). */
405
581
  list(params = {}) {
406
- return this.http.get(`/shop-matches${toQueryString(params)}`);
582
+ const [query, requestOptions] = splitRequestOptions(params);
583
+ return this.http.get(`/shop-matches${toQueryString(query)}`, requestOptions);
407
584
  }
408
585
  /** `GET /shop-matches/{shop}`: every current match at one shop (latest record per url). */
409
586
  forShop(technicalName, params = {}) {
410
- 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
+ );
411
592
  }
412
593
  /** `GET /shop-matches/shops`: match counts per shop (based on latest records only). */
413
594
  shopStats(params = {}) {
414
- 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);
415
597
  }
416
598
  };
417
599
 
@@ -423,16 +605,16 @@ var ShopUrlsResource = class {
423
605
  http;
424
606
  /** `POST /shop-urls/submit`: submit a shop URL for scraping. */
425
607
  submit(params) {
426
- const { authToken, url, shop } = params;
427
- 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);
428
610
  }
429
611
  /** `PATCH /shop-urls/{id}/product`: manually assign (or clear) the product a shop URL resolves to. */
430
612
  assignProduct(shopUrlId, params) {
431
- const { authToken, productId } = params;
613
+ const { productId, ...requestOptions } = params;
432
614
  return this.http.patch(
433
615
  `/shop-urls/${encodeURIComponent(shopUrlId)}/product`,
434
616
  { productId },
435
- { authToken }
617
+ requestOptions
436
618
  );
437
619
  }
438
620
  };
@@ -445,12 +627,13 @@ var ShopsResource = class {
445
627
  http;
446
628
  /** `GET /shops`: every tracked shop. Unwrapped to a plain array, nothing to paginate here. */
447
629
  async list(params = {}) {
448
- 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);
449
632
  return res.data;
450
633
  }
451
634
  /** `GET /shops/{id}`: fetch one shop by its id or technicalName. */
452
- get(idOrTechnicalName) {
453
- return this.http.get(`/shops/${encodeURIComponent(idOrTechnicalName)}`);
635
+ get(idOrTechnicalName, options = {}) {
636
+ return this.http.get(`/shops/${encodeURIComponent(idOrTechnicalName)}`, options);
454
637
  }
455
638
  };
456
639
 
@@ -461,8 +644,40 @@ var StatsResource = class {
461
644
  }
462
645
  http;
463
646
  /** `GET /stats`: platform-wide overview counts (shops, expansions, products, prices tracked). */
464
- platform() {
465
- 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);
466
681
  }
467
682
  };
468
683
 
@@ -480,6 +695,9 @@ var TcgPriser = class {
480
695
  bargains;
481
696
  packRates;
482
697
  stats;
698
+ webhooks;
699
+ /** Holds the `HttpClient` so `creditsRemaining` can read the running value off it. */
700
+ http;
483
701
  /**
484
702
  * @param optionsOrAuthToken A subscriber's API token (`new TcgPriser(myApiToken)`), a full
485
703
  * `TcgPriserOptions` object, or omit it entirely for an anonymous, public-only client.
@@ -493,6 +711,7 @@ var TcgPriser = class {
493
711
  );
494
712
  }
495
713
  const http = new HttpClient({
714
+ timeoutMs: advanced.timeoutMs,
496
715
  baseUrl: advanced.baseUrl ?? DEFAULT_BASE_URL,
497
716
  // Bound to globalThis: both browsers and Node's undici implement fetch as a method that
498
717
  // checks its receiver, so an unbound reference throws "Illegal invocation" the moment it's
@@ -513,10 +732,31 @@ var TcgPriser = class {
513
732
  this.bargains = new BargainsResource(http);
514
733
  this.packRates = new PackRatesResource(http);
515
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;
516
755
  }
517
756
  };
518
757
 
519
758
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
759
+ exports.DEFAULT_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;
520
760
  exports.TcgPriser = TcgPriser;
521
761
  exports.TcgPriserError = TcgPriserError;
522
762
  //# sourceMappingURL=index.cjs.map