bc-api-client 0.2.2 → 1.0.0-beta.2

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 DELETED
@@ -1,741 +0,0 @@
1
- // src/net.ts
2
- import ky, { HTTPError } from "ky";
3
- var Methods = {
4
- GET: "GET",
5
- POST: "POST",
6
- PUT: "PUT",
7
- DELETE: "DELETE"
8
- };
9
- var BASE_URL = "https://api.bigcommerce.com/stores/";
10
- var CONFIG = {
11
- /** Base URL for BigCommerce API */
12
- BASE_URL,
13
- /** Default API version to use */
14
- DEFAULT_VERSION: "v3",
15
- /** Maximum delay in milliseconds for rate limit retries */
16
- DEFAULT_MAX_DELAY: 6e4,
17
- /** Maximum allowed URL length */
18
- MAX_URL_LENGTH: 2048,
19
- /** Default maximum number of retries for rate-limited requests */
20
- DEFAULT_MAX_RETRIES: 5,
21
- /** Rate limit header names */
22
- HEADERS: {
23
- /** Time window for rate limiting in milliseconds */
24
- WINDOW: "x-rate-limit-time-window-ms",
25
- /** Time to wait before retrying after rate limit in milliseconds */
26
- RETRY_AFTER: "x-rate-limit-time-reset-ms",
27
- /** Total request quota for the time window */
28
- REQUEST_QUOTA: "x-rate-limit-requests-quota",
29
- /** Number of requests remaining in the current window */
30
- REQUESTS_LEFT: "x-rate-limit-requests-left"
31
- }
32
- };
33
- var RequestError = class extends Error {
34
- constructor(status, message, data, cause) {
35
- super(message, { cause });
36
- this.status = status;
37
- this.message = message;
38
- this.data = data;
39
- this.cause = cause;
40
- }
41
- };
42
- var request = async (options) => {
43
- const {
44
- baseUrl = CONFIG.BASE_URL,
45
- maxDelay = CONFIG.DEFAULT_MAX_DELAY,
46
- maxRetries = CONFIG.DEFAULT_MAX_RETRIES,
47
- logger
48
- } = options;
49
- let retries = 0;
50
- let lastError = null;
51
- while (retries < maxRetries) {
52
- try {
53
- return await safeRequest({ ...options, baseUrl });
54
- } catch (error) {
55
- const err = error;
56
- lastError = err;
57
- if (err.status === 429 && typeof err.data === "object" && err.data !== null && "headers" in err.data) {
58
- const headers = err.data.headers;
59
- const retryAfter = Number.parseInt(headers[CONFIG.HEADERS.RETRY_AFTER]);
60
- logger?.debug(
61
- {
62
- retryAfter,
63
- retries,
64
- remaining: headers[CONFIG.HEADERS.REQUESTS_LEFT]
65
- },
66
- "Rate limit hit, retrying"
67
- );
68
- if (Number.isNaN(retryAfter)) {
69
- throw new RequestError(
70
- err.status,
71
- `Failed to parse retry after: ${headers[CONFIG.HEADERS.RETRY_AFTER]}, ${err.message}`,
72
- err.data,
73
- err.cause
74
- );
75
- }
76
- if (retryAfter > maxDelay) {
77
- logger?.warn(
78
- {
79
- retryAfter,
80
- maxDelay
81
- },
82
- "Rate limit delay exceeds maximum allowed delay"
83
- );
84
- throw new RequestError(
85
- err.status,
86
- `Rate limit exceeded: ${retryAfter}ms, ${err.message}`,
87
- err.data,
88
- err.cause
89
- );
90
- }
91
- await new Promise((resolve) => setTimeout(resolve, retryAfter));
92
- retries++;
93
- continue;
94
- }
95
- throw err;
96
- }
97
- }
98
- logger?.error(
99
- {
100
- retries,
101
- error: lastError
102
- },
103
- "Request failed after maximum retries"
104
- );
105
- throw lastError ?? new RequestError(500, "Failed to make request", "Too many retries after rate limit");
106
- };
107
- var safeRequest = async (options) => {
108
- const { logger, baseUrl = CONFIG.BASE_URL } = options;
109
- let res;
110
- try {
111
- res = await call({ ...options, baseUrl });
112
- } catch (error) {
113
- if (error instanceof RequestError) {
114
- throw error;
115
- }
116
- if (!(error instanceof HTTPError)) {
117
- logger?.error(
118
- {
119
- error: error instanceof Error ? {
120
- name: error.name,
121
- message: error.message
122
- } : error
123
- },
124
- "Unexpected error during request"
125
- );
126
- throw error;
127
- }
128
- let data;
129
- let errorMessage = error.message;
130
- try {
131
- data = await error.response.text();
132
- try {
133
- data = JSON.parse(data);
134
- if (typeof data === "object" && data !== null && "message" in data) {
135
- errorMessage = data.message;
136
- }
137
- } catch {
138
- }
139
- } catch {
140
- data = "Failed to read error response";
141
- }
142
- logger?.error(
143
- {
144
- status: error?.response?.status,
145
- errorMessage,
146
- data,
147
- endpoint: options.endpoint,
148
- query: options.query,
149
- body: options.body,
150
- headers: Object.fromEntries(error?.response?.headers?.entries() ?? [])
151
- },
152
- "HTTP error during request"
153
- );
154
- throw new RequestError(
155
- error?.response?.status ?? 500,
156
- errorMessage,
157
- {
158
- data,
159
- endpoint: options.endpoint,
160
- query: options.query,
161
- body: options.body,
162
- headers: Object.fromEntries(error?.response?.headers?.entries() ?? [])
163
- },
164
- error
165
- );
166
- }
167
- const text = await res.text();
168
- if (res.status === 204) {
169
- return void 0;
170
- }
171
- try {
172
- return JSON.parse(text);
173
- } catch (error) {
174
- logger?.error(
175
- {
176
- status: res.status,
177
- error: error instanceof Error ? {
178
- name: error.name,
179
- message: error.message
180
- } : error
181
- },
182
- "Failed to parse response"
183
- );
184
- throw new RequestError(res.status, `Failed to parse response: ${text}`, text, error);
185
- }
186
- };
187
- var call = async (options) => {
188
- const {
189
- storeHash,
190
- accessToken,
191
- endpoint,
192
- method = "GET",
193
- body,
194
- version = CONFIG.DEFAULT_VERSION,
195
- query,
196
- logger,
197
- baseUrl = CONFIG.BASE_URL,
198
- kyOptions
199
- } = options;
200
- const url = `${baseUrl}${storeHash}/${version}/${endpoint.replace(/^\//, "")}`;
201
- const searchParams = query ? new URLSearchParams(query).toString() : "";
202
- const fullUrl = searchParams ? `${url}?${searchParams}` : url;
203
- if (fullUrl.length > CONFIG.MAX_URL_LENGTH) {
204
- logger?.error(
205
- {
206
- urlLength: fullUrl.length,
207
- maxLength: CONFIG.MAX_URL_LENGTH
208
- },
209
- "URL length exceeds maximum allowed length"
210
- );
211
- throw new RequestError(
212
- 400,
213
- "URL too long",
214
- `URL length ${fullUrl.length} exceeds maximum allowed length of ${CONFIG.MAX_URL_LENGTH}`
215
- );
216
- }
217
- const request2 = {
218
- method,
219
- headers: {
220
- "Content-Type": "application/json",
221
- Accept: "application/json",
222
- "X-Auth-Token": accessToken
223
- },
224
- json: body,
225
- ...kyOptions
226
- };
227
- const response = await ky(fullUrl, request2);
228
- return response;
229
- };
230
-
231
- // src/util.ts
232
- var chunkStrLength = (items, options = {}) => {
233
- const { maxLength = 2048, chunkLength = 250, offset = 0, separatorSize = 1 } = options;
234
- const chunks = [];
235
- let currentStrLength = offset;
236
- let currentChunk = [];
237
- for (const item of items) {
238
- const itemLength = encodeURIComponent(item).length;
239
- const separatorLength = currentChunk.length > 0 ? separatorSize : 0;
240
- const totalItemLength = itemLength + separatorLength;
241
- const wouldExceedLength = currentStrLength + totalItemLength > maxLength;
242
- const wouldExceedCount = currentChunk.length >= chunkLength;
243
- if ((wouldExceedLength || wouldExceedCount) && currentChunk.length > 0) {
244
- chunks.push(currentChunk);
245
- currentChunk = [];
246
- currentStrLength = offset;
247
- }
248
- if (itemLength + offset > maxLength) {
249
- throw new Error(`Item too large: ${itemLength} exceeds maxLength ${maxLength}`);
250
- }
251
- currentChunk.push(item);
252
- currentStrLength += totalItemLength;
253
- }
254
- if (currentChunk.length > 0) {
255
- chunks.push(currentChunk);
256
- }
257
- return chunks;
258
- };
259
-
260
- // src/client.ts
261
- var MAX_PAGE_SIZE = 250;
262
- var DEFAULT_CONCURRENCY = 10;
263
- function chunkArray(array, size) {
264
- return Array.from({ length: Math.ceil(array.length / size) }, (_, i) => array.slice(i * size, i * size + size));
265
- }
266
- function rangeArray(start, end) {
267
- return Array.from({ length: end - start + 1 }, (_, i) => start + i);
268
- }
269
- var BigCommerceClient = class {
270
- /**
271
- * Creates a new BigCommerce client instance
272
- * @param config - Configuration options for the client
273
- * @param config.baseUrl - The base URL to use for the client (default: https://api.bigcommerce.com)
274
- * @param config.storeHash - The store hash to use for the client
275
- * @param config.accessToken - The API access token to use for the client
276
- * @param config.maxRetries - The maximum number of retries for rate limit errors (default: 5)
277
- * @param config.maxDelay - Maximum time to wait to retry in case of rate limit errors in milliseconds (default: 60000 - 1 minute). If `X-Rate-Limit-Time-Reset-Ms` header is higher than `maxDelay`, the request will fail immediately.
278
- * @param config.concurrency - The default concurrency for concurrent methods (default: 10)
279
- * @param config.skipErrors - Whether to skip errors during concurrent requests (default: false)
280
- * @param config.logger - Optional logger instance for debugging and error tracking
281
- */
282
- constructor(config) {
283
- this.config = config;
284
- }
285
- /**
286
- * Makes a GET request to the BigCommerce API
287
- * @param endpoint - The API endpoint to request
288
- * @param options.query - Query parameters to include in the request
289
- * @param options.version - API version to use (v2 or v3) (default: v3)
290
- * @returns Promise resolving to the response data of type `R`
291
- */
292
- async get(endpoint, options) {
293
- return request({
294
- endpoint,
295
- method: "GET",
296
- ...options,
297
- ...this.config
298
- });
299
- }
300
- /**
301
- * Makes a POST request to the BigCommerce API
302
- * @param endpoint - The API endpoint to request
303
- * @param options.query - Query parameters to include in the request
304
- * @param options.version - API version to use (v2 or v3) (default: v3)
305
- * @param options.body - Request body data of type `T`
306
- * @returns Promise resolving to the response data of type `R`
307
- */
308
- async post(endpoint, options) {
309
- return request({
310
- endpoint,
311
- method: "POST",
312
- ...options,
313
- ...this.config
314
- });
315
- }
316
- /**
317
- * Makes a PUT request to the BigCommerce API
318
- * @param endpoint - The API endpoint to request
319
- * @param options.query - Query parameters to include in the request
320
- * @param options.version - API version to use (v2 or v3) (default: v3)
321
- * @param options.body - Request body data of type `T`
322
- * @returns Promise resolving to the response data of type `R`
323
- */
324
- async put(endpoint, options) {
325
- return request({
326
- endpoint,
327
- method: "PUT",
328
- ...options,
329
- ...this.config
330
- });
331
- }
332
- /**
333
- * Makes a DELETE request to the BigCommerce API
334
- * @param endpoint - The API endpoint to delete
335
- * @param options.version - API version to use (v2 or v3) (default: v3)
336
- * @returns Promise resolving to void
337
- */
338
- async delete(endpoint, options) {
339
- await request({
340
- endpoint,
341
- method: "DELETE",
342
- ...options,
343
- ...this.config
344
- });
345
- }
346
- /**
347
- * Executes multiple requests concurrently with controlled concurrency
348
- * @param requests - Array of request options to execute
349
- * @param options.concurrency - Maximum number of concurrent requests, overrides the client's concurrency setting (default: 10)
350
- * @param options.skipErrors - Whether to skip errors and continue processing (the errors will be logged if logger is provided), overrides the client's skipErrors setting (default: false)
351
- * @returns Promise resolving to array of response data
352
- */
353
- async concurrent(requests, options) {
354
- const skipErrors = options?.skipErrors ?? this.config.skipErrors ?? false;
355
- const results = await this.concurrentSettled(requests, options);
356
- const successfulResults = [];
357
- for (const result of results) {
358
- if (result.status === "fulfilled") {
359
- successfulResults.push(result.value);
360
- } else {
361
- if (!skipErrors) {
362
- throw result.reason;
363
- } else {
364
- this.config.logger?.warn(
365
- {
366
- error: result.reason
367
- },
368
- "Error in concurrent request"
369
- );
370
- }
371
- }
372
- }
373
- return successfulResults;
374
- }
375
- /**
376
- * Lowest level concurrent request method.
377
- * This method executes requests in chunks and returns bare PromiseSettledResult objects.
378
- * Use this method if you need to handle errors in a custom way.
379
- * @param requests - Array of request options to execute
380
- * @param options.concurrency - Maximum number of concurrent requests, overrides the client's concurrency setting (default: 10)
381
- * @returns Promise resolving to array of PromiseSettledResult containing both successful and failed requests
382
- */
383
- async concurrentSettled(requests, options) {
384
- const chunkSize = options?.concurrency ?? this.config.concurrency ?? DEFAULT_CONCURRENCY;
385
- const chunks = chunkArray(requests, chunkSize);
386
- this.config.logger?.debug(
387
- {
388
- totalRequests: requests.length,
389
- chunkSize,
390
- chunks: chunks.length
391
- },
392
- "Starting concurrent requests with detailed results"
393
- );
394
- const allResults = [];
395
- for (const [index, chunk] of chunks.entries()) {
396
- const responses = await Promise.allSettled(
397
- chunk.map(
398
- (opt) => request({
399
- ...opt,
400
- ...this.config
401
- })
402
- )
403
- );
404
- this.config.logger?.debug(
405
- {
406
- chunkIndex: index,
407
- chunkSize: chunk.length,
408
- totalRequests: requests.length,
409
- totalChunks: chunks.length,
410
- responses: responses.map((response) => response.status)
411
- },
412
- "Completed chunk"
413
- );
414
- allResults.push(...responses);
415
- }
416
- return allResults;
417
- }
418
- /**
419
- * Collects all pages of data from a paginated v3 API endpoint.
420
- * This method pulls the first page and uses pagination meta to collect the remaining pages concurrently.
421
- * @param endpoint - The API endpoint to request
422
- * @param options.query - Query parameters to include in the request
423
- * @param options.concurrency - Maximum number of concurrent requests, overrides the client's concurrency setting (default: 10)
424
- * @param options.skipErrors - Whether to skip errors and continue processing (the errors will be logged if logger is provided), overrides the client's skipErrors setting (default: false)
425
- * @returns Promise resolving to array of all items across all pages
426
- */
427
- async collect(endpoint, options) {
428
- options = options ?? {};
429
- if (options.query) {
430
- if (!options.query.limit) {
431
- options.query.limit = MAX_PAGE_SIZE.toString();
432
- }
433
- } else {
434
- options.query = { limit: MAX_PAGE_SIZE.toString() };
435
- }
436
- const first = await this.get(endpoint, options);
437
- if (!Array.isArray(first.data) || !first?.meta?.pagination?.total_pages) {
438
- return first.data;
439
- }
440
- const results = [...first.data];
441
- const pages = first.meta.pagination.total_pages;
442
- if (pages > 1) {
443
- this.config.logger?.debug(
444
- {
445
- totalPages: pages,
446
- itemsPerPage: first.data.length
447
- },
448
- "Collecting remaining pages"
449
- );
450
- const pageRequests = rangeArray(2, pages).map((page) => ({
451
- endpoint,
452
- method: "GET",
453
- query: {
454
- ...options.query,
455
- page: page.toString()
456
- },
457
- kyOptions: options.kyOptions
458
- }));
459
- const remainingPages = await this.concurrent(pageRequests, options);
460
- remainingPages.forEach((page) => {
461
- if (Array.isArray(page.data)) {
462
- results.push(...page.data);
463
- }
464
- });
465
- }
466
- return results;
467
- }
468
- /**
469
- * Collects all pages of data from a paginated v2 API endpoint.
470
- * This method simply pulls all pages concurrently until a 204 is returned in a batch.
471
- * @param endpoint - The API endpoint to request
472
- * @param options.query - Query parameters to include in the request
473
- * @param options.concurrency - Maximum number of concurrent requests, overrides the client's concurrency setting (default: 10)
474
- * @param options.skipErrors - Whether to skip errors and continue processing (the errors will be logged if logger is provided), overrides the client's skipErrors setting (default: false)
475
- * @returns Promise resolving to array of all items across all pages
476
- */
477
- async collectV2(endpoint, options) {
478
- options = options ?? {};
479
- if (options.query) {
480
- if (!options.query.limit) {
481
- options.query.limit = MAX_PAGE_SIZE.toString();
482
- }
483
- } else {
484
- options.query = { limit: MAX_PAGE_SIZE.toString() };
485
- }
486
- let done = false;
487
- const results = [];
488
- let page = 1;
489
- const concurrency = options.concurrency ?? this.config.concurrency ?? DEFAULT_CONCURRENCY;
490
- while (!done) {
491
- const pages = rangeArray(page, page + concurrency);
492
- page += concurrency;
493
- const requests = pages.map((page2) => ({
494
- ...options,
495
- endpoint,
496
- version: "v2",
497
- query: { ...options.query, page: page2.toString() }
498
- }));
499
- const responses = await Promise.allSettled(requests.map((request2) => this.get(endpoint, request2)));
500
- responses.forEach((response) => {
501
- if (response.status === "fulfilled") {
502
- if (response.value) {
503
- results.push(...response.value);
504
- } else {
505
- done = true;
506
- }
507
- } else {
508
- if (response.reason instanceof RequestError && response.reason.status === 404) {
509
- done = true;
510
- } else {
511
- if (!(options.skipErrors ?? this.config.skipErrors ?? false)) {
512
- throw response.reason;
513
- } else {
514
- this.config.logger?.warn(
515
- {
516
- error: response.reason instanceof Error ? {
517
- name: response.reason.name,
518
- message: response.reason.message
519
- } : response.reason
520
- },
521
- "Error in collectV2"
522
- );
523
- }
524
- }
525
- }
526
- });
527
- }
528
- return results;
529
- }
530
- /**
531
- * Queries multiple values against a single field using the v3 API.
532
- * If the url + query params are too long, the query will be chunked. Otherwise, this method acts like `collect`.
533
- * This method does not check for uniqueness of the `values` array.
534
- *
535
- * @param endpoint - The API endpoint to request
536
- * @param options.key - The field name to query against e.g. `sku:in`
537
- * @param options.values - Array of values to query for e.g. `['123', '456', ...]`
538
- * @param options.query - Additional query parameters
539
- * @param options.concurrency - Maximum number of concurrent requests, overrides the client's concurrency setting (default: 10)
540
- * @param options.skipErrors - Whether to skip errors and continue processing (the errors will be logged if logger is provided), overrides the client's skipErrors setting (default: false)
541
- * @returns Promise resolving to array of matching items
542
- */
543
- async query(endpoint, options) {
544
- if (options.query) {
545
- if (!options.query.limit) {
546
- options.query.limit = MAX_PAGE_SIZE.toString();
547
- }
548
- } else {
549
- options.query = { limit: MAX_PAGE_SIZE.toString() };
550
- }
551
- const keySize = encodeURIComponent(options.key).length;
552
- const fullUrl = `${BASE_URL}${this.config.storeHash}/v3/${endpoint}?${new URLSearchParams(options.query).toString()}`;
553
- const offset = fullUrl.length + keySize + 1;
554
- const chunkLength = Number.parseInt(options.query?.limit) || MAX_PAGE_SIZE;
555
- const separatorSize = encodeURIComponent(",").length;
556
- const queryStr = options.values.map((value) => `${value}`);
557
- const chunks = chunkStrLength(queryStr, {
558
- separatorSize,
559
- offset,
560
- chunkLength
561
- });
562
- this.config.logger?.debug(
563
- {
564
- offset,
565
- totalValues: options.values.length,
566
- chunks: chunks.length,
567
- valuesPerChunk: chunks[0]?.length,
568
- separatorSize
569
- },
570
- "Querying with chunked values"
571
- );
572
- const requests = chunks.map((chunk) => ({
573
- ...options,
574
- endpoint,
575
- query: { ...options.query, [options.key]: chunk.join(",") }
576
- }));
577
- const responses = await this.concurrent(requests, options);
578
- return responses.flatMap((response) => response.data);
579
- }
580
- };
581
-
582
- // src/auth.ts
583
- import ky2, { HTTPError as HTTPError2 } from "ky";
584
- import * as jose from "jose";
585
- var GRANT_TYPE = "authorization_code";
586
- var TOKEN_ENDPOINT = "https://login.bigcommerce.com/oauth2/token";
587
- var ISSUER = "bc";
588
- var BigCommerceAuth = class {
589
- /**
590
- * Creates a new BigCommerceAuth instance for handling OAuth authentication
591
- * @param config - Configuration options for BigCommerce authentication
592
- * @param config.clientId - The OAuth client ID from BigCommerce
593
- * @param config.secret - The OAuth client secret from BigCommerce
594
- * @param config.redirectUri - The redirect URI registered with BigCommerce
595
- * @param config.scopes - Optional array of scopes to validate during auth callback
596
- * @param config.logger - Optional logger instance for debugging and error tracking
597
- * @throws {Error} If the redirect URI is invalid
598
- */
599
- constructor(config) {
600
- this.config = config;
601
- try {
602
- new URL(this.config.redirectUri);
603
- } catch (error) {
604
- throw new Error("Invalid redirect URI", { cause: error });
605
- }
606
- }
607
- /**
608
- * Requests an access token from BigCommerce
609
- * @param data - Either a query string, URLSearchParams, or AuthQuery object containing auth callback data
610
- * @returns Promise resolving to the token response
611
- */
612
- async requestToken(data) {
613
- const query = typeof data === "string" || data instanceof URLSearchParams ? this.parseQueryString(data) : data;
614
- this.validateScopes(query.scope);
615
- const tokenRequest = {
616
- client_id: this.config.clientId,
617
- client_secret: this.config.secret,
618
- ...query,
619
- grant_type: GRANT_TYPE,
620
- redirect_uri: this.config.redirectUri
621
- };
622
- this.config.logger?.debug(
623
- {
624
- clientId: this.config.clientId,
625
- context: query.context,
626
- scopes: query.scope
627
- },
628
- "Requesting OAuth token"
629
- );
630
- let res;
631
- try {
632
- res = await ky2(TOKEN_ENDPOINT, {
633
- method: "POST",
634
- json: tokenRequest
635
- });
636
- } catch (error) {
637
- if (error instanceof HTTPError2) {
638
- const text = await error.response.text();
639
- this.config.logger?.error({
640
- err: {
641
- name: error.name,
642
- message: error.message,
643
- text
644
- }
645
- });
646
- throw new Error(`Failed to request token. BC returned: ${text}`, { cause: error });
647
- }
648
- this.config.logger?.error({
649
- err: error instanceof Error ? {
650
- name: error.name,
651
- message: error.message
652
- } : error
653
- });
654
- throw new Error(`Failed to request token`, { cause: error });
655
- }
656
- return res.json();
657
- }
658
- /**
659
- * Verifies a JWT payload from BigCommerce
660
- * @param jwtPayload - The JWT string to verify
661
- * @param storeHash - The store hash for the BigCommerce store
662
- * @returns Promise resolving to the verified JWT claims
663
- * @throws {Error} If the JWT is invalid
664
- */
665
- async verify(jwtPayload, storeHash) {
666
- try {
667
- const secret = new TextEncoder().encode(this.config.secret);
668
- const { payload } = await jose.jwtVerify(jwtPayload, secret, {
669
- audience: this.config.clientId,
670
- issuer: ISSUER,
671
- subject: `stores/${storeHash}`
672
- });
673
- this.config.logger?.debug(
674
- {
675
- userId: payload.user?.id,
676
- storeHash: payload.sub.split("/")[1]
677
- },
678
- "JWT verified successfully"
679
- );
680
- return payload;
681
- } catch (error) {
682
- this.config.logger?.error({
683
- error: error instanceof Error ? {
684
- name: error.name,
685
- message: error.message
686
- } : error
687
- });
688
- throw new Error("Invalid JWT payload", { cause: error });
689
- }
690
- }
691
- /**
692
- * Parses and validates a query string from BigCommerce auth callback
693
- * @param queryString - The query string to parse
694
- * @returns The parsed auth query parameters
695
- * @throws {Error} If required parameters are missing or scopes are invalid
696
- */
697
- parseQueryString(queryString) {
698
- const params = typeof queryString === "string" ? new URLSearchParams(queryString) : queryString;
699
- const code = params.get("code");
700
- const scope = params.get("scope");
701
- const context = params.get("context");
702
- if (!code) {
703
- throw new Error("No code found in query string");
704
- }
705
- if (!scope) {
706
- throw new Error("No scope found in query string");
707
- } else if (this.config.scopes?.length) {
708
- this.validateScopes(scope);
709
- }
710
- if (!context) {
711
- throw new Error("No context found in query string");
712
- }
713
- return {
714
- code,
715
- scope,
716
- context
717
- };
718
- }
719
- /**
720
- * Validates that the granted scopes match the expected scopes
721
- * @param scopes - Space-separated list of granted scopes
722
- * @throws {Error} If the scopes don't match the expected scopes
723
- */
724
- validateScopes(scopes) {
725
- if (!this.config.scopes) {
726
- return;
727
- }
728
- const grantedScopes = scopes.split(" ");
729
- const requiredScopes = this.config.scopes;
730
- const missingScopes = requiredScopes.filter((scope) => !grantedScopes.includes(scope));
731
- if (missingScopes.length) {
732
- throw new Error(`Scope mismatch: ${scopes}; expected: ${this.config.scopes.join(" ")}`);
733
- }
734
- }
735
- };
736
- export {
737
- BigCommerceAuth,
738
- BigCommerceClient,
739
- Methods
740
- };
741
- //# sourceMappingURL=index.js.map