@profplum700/etsy-v3-api-client 1.0.1 → 2.3.1
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/CHANGELOG.md +290 -1
- package/README.md +28 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.esm.js +1766 -7
- package/dist/browser.esm.js.map +1 -1
- package/dist/browser.umd.js +1 -1
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +1935 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1071 -5
- package/dist/index.esm.js +1893 -7
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +8517 -0
- package/dist/index.js.map +1 -0
- package/dist/index.umd.js +1180 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/index.umd.min.js +2 -0
- package/dist/index.umd.min.js.map +1 -0
- package/dist/node.cjs +1935 -6
- package/dist/node.cjs.map +1 -1
- package/dist/node.esm.js +1893 -7
- package/dist/node.esm.js.map +1 -1
- package/package.json +25 -16
package/dist/node.cjs
CHANGED
|
@@ -3,11 +3,22 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
class EtsyApiError extends Error {
|
|
6
|
-
constructor(message, _statusCode, _response) {
|
|
6
|
+
constructor(message, _statusCode, _response, _retryAfter) {
|
|
7
7
|
super(message);
|
|
8
8
|
this._statusCode = _statusCode;
|
|
9
9
|
this._response = _response;
|
|
10
|
+
this._retryAfter = _retryAfter;
|
|
10
11
|
this.name = 'EtsyApiError';
|
|
12
|
+
this.details = {
|
|
13
|
+
statusCode: _statusCode || 0,
|
|
14
|
+
retryAfter: _retryAfter
|
|
15
|
+
};
|
|
16
|
+
if (_response && typeof _response === 'object') {
|
|
17
|
+
const resp = _response;
|
|
18
|
+
this.details.errorCode = (resp.error_code || resp.code);
|
|
19
|
+
this.details.field = resp.field;
|
|
20
|
+
this.details.suggestion = (resp.suggestion || resp.message);
|
|
21
|
+
}
|
|
11
22
|
}
|
|
12
23
|
get statusCode() {
|
|
13
24
|
return this._statusCode;
|
|
@@ -15,6 +26,47 @@ class EtsyApiError extends Error {
|
|
|
15
26
|
get response() {
|
|
16
27
|
return this._response;
|
|
17
28
|
}
|
|
29
|
+
isRetryable() {
|
|
30
|
+
if (!this._statusCode) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (this._statusCode === 429) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (this._statusCode >= 500 && this._statusCode < 600) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const retryableClientErrors = [408, 409, 423, 425];
|
|
40
|
+
if (retryableClientErrors.includes(this._statusCode)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
getRetryAfter() {
|
|
46
|
+
return this._retryAfter || null;
|
|
47
|
+
}
|
|
48
|
+
getRateLimitReset() {
|
|
49
|
+
if (this._statusCode !== 429 || !this._retryAfter) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return new Date(Date.now() + this._retryAfter * 1000);
|
|
53
|
+
}
|
|
54
|
+
getUserFriendlyMessage() {
|
|
55
|
+
let message = this.message;
|
|
56
|
+
if (this.details.suggestion) {
|
|
57
|
+
message += `\nSuggestion: ${this.details.suggestion}`;
|
|
58
|
+
}
|
|
59
|
+
if (this.isRetryable()) {
|
|
60
|
+
const retryAfter = this.getRetryAfter();
|
|
61
|
+
if (retryAfter) {
|
|
62
|
+
message += `\nRetry after ${retryAfter} seconds.`;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
message += '\nThis error can be retried.';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return message;
|
|
69
|
+
}
|
|
18
70
|
}
|
|
19
71
|
class EtsyAuthError extends Error {
|
|
20
72
|
constructor(message, _code) {
|
|
@@ -106,11 +158,12 @@ class MemoryTokenStorage {
|
|
|
106
158
|
}
|
|
107
159
|
}
|
|
108
160
|
class TokenManager {
|
|
109
|
-
constructor(config, storage) {
|
|
161
|
+
constructor(config, storage, rotationConfig) {
|
|
110
162
|
this.currentTokens = null;
|
|
111
163
|
this.keystring = config.keystring;
|
|
112
164
|
this.refreshCallback = config.refreshSave;
|
|
113
165
|
this.storage = storage;
|
|
166
|
+
this.rotationConfig = rotationConfig;
|
|
114
167
|
this.currentTokens = {
|
|
115
168
|
access_token: config.accessToken,
|
|
116
169
|
refresh_token: config.refreshToken,
|
|
@@ -118,6 +171,9 @@ class TokenManager {
|
|
|
118
171
|
token_type: 'Bearer',
|
|
119
172
|
scope: ''
|
|
120
173
|
};
|
|
174
|
+
if (this.rotationConfig?.enabled && this.rotationConfig?.autoSchedule) {
|
|
175
|
+
this.startRotationScheduler();
|
|
176
|
+
}
|
|
121
177
|
}
|
|
122
178
|
async getAccessToken() {
|
|
123
179
|
if (!this.currentTokens) {
|
|
@@ -178,8 +234,7 @@ class TokenManager {
|
|
|
178
234
|
body: body.toString()
|
|
179
235
|
});
|
|
180
236
|
if (!response.ok) {
|
|
181
|
-
|
|
182
|
-
throw new EtsyAuthError(`Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`, 'TOKEN_REFRESH_FAILED');
|
|
237
|
+
throw new EtsyAuthError(`Token refresh failed: ${response.status} ${response.statusText}`, 'TOKEN_REFRESH_FAILED');
|
|
183
238
|
}
|
|
184
239
|
const tokenResponse = await response.json();
|
|
185
240
|
return {
|
|
@@ -234,6 +289,73 @@ class TokenManager {
|
|
|
234
289
|
const expiresAt = new Date(this.currentTokens.expires_at);
|
|
235
290
|
return expiresAt.getTime() - now.getTime();
|
|
236
291
|
}
|
|
292
|
+
needsProactiveRotation() {
|
|
293
|
+
if (!this.rotationConfig?.enabled || !this.currentTokens) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
const timeUntilExpiration = this.getTimeUntilExpiration();
|
|
297
|
+
if (timeUntilExpiration === null) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
const rotateBeforeExpiry = this.rotationConfig.rotateBeforeExpiry || 15 * 60 * 1000;
|
|
301
|
+
return timeUntilExpiration <= rotateBeforeExpiry;
|
|
302
|
+
}
|
|
303
|
+
async rotateToken() {
|
|
304
|
+
if (!this.currentTokens) {
|
|
305
|
+
throw new EtsyAuthError('No tokens available to rotate', 'NO_TOKENS');
|
|
306
|
+
}
|
|
307
|
+
const oldTokens = { ...this.currentTokens };
|
|
308
|
+
const newTokens = await this.refreshToken();
|
|
309
|
+
if (this.rotationConfig?.onRotation) {
|
|
310
|
+
try {
|
|
311
|
+
await Promise.resolve(this.rotationConfig.onRotation(oldTokens, newTokens));
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
console.error('Token rotation callback failed:', error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return newTokens;
|
|
318
|
+
}
|
|
319
|
+
startRotationScheduler() {
|
|
320
|
+
if (!this.rotationConfig?.enabled) {
|
|
321
|
+
throw new Error('Token rotation is not enabled');
|
|
322
|
+
}
|
|
323
|
+
this.stopRotationScheduler();
|
|
324
|
+
const checkInterval = this.rotationConfig.checkInterval || 60 * 1000;
|
|
325
|
+
this.rotationTimer = setInterval(async () => {
|
|
326
|
+
try {
|
|
327
|
+
if (this.needsProactiveRotation()) {
|
|
328
|
+
console.log('Proactively rotating token before expiration');
|
|
329
|
+
await this.rotateToken();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
console.error('Failed to proactively rotate token:', error);
|
|
334
|
+
}
|
|
335
|
+
}, checkInterval);
|
|
336
|
+
if (this.rotationTimer.unref) {
|
|
337
|
+
this.rotationTimer.unref();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
stopRotationScheduler() {
|
|
341
|
+
if (this.rotationTimer) {
|
|
342
|
+
clearInterval(this.rotationTimer);
|
|
343
|
+
this.rotationTimer = undefined;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
updateRotationConfig(config) {
|
|
347
|
+
const wasAutoScheduleEnabled = this.rotationConfig?.autoSchedule;
|
|
348
|
+
this.rotationConfig = config;
|
|
349
|
+
if (config.enabled && config.autoSchedule && !wasAutoScheduleEnabled) {
|
|
350
|
+
this.startRotationScheduler();
|
|
351
|
+
}
|
|
352
|
+
else if (!config.enabled || !config.autoSchedule) {
|
|
353
|
+
this.stopRotationScheduler();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
getRotationConfig() {
|
|
357
|
+
return this.rotationConfig ? { ...this.rotationConfig } : undefined;
|
|
358
|
+
}
|
|
237
359
|
async fetch(url, options) {
|
|
238
360
|
assertFetchSupport();
|
|
239
361
|
return fetch(url, options);
|
|
@@ -330,6 +452,7 @@ class FileTokenStorage {
|
|
|
330
452
|
try {
|
|
331
453
|
const data = JSON.stringify(tokens, null, 2);
|
|
332
454
|
await this._writeFile(this.filePath, data);
|
|
455
|
+
await this._setFilePermissions(this.filePath, 0o600);
|
|
333
456
|
}
|
|
334
457
|
catch {
|
|
335
458
|
throw new Error('Failed to save tokens to file');
|
|
@@ -367,6 +490,16 @@ class FileTokenStorage {
|
|
|
367
490
|
const fs = await import('fs');
|
|
368
491
|
await fs.promises.writeFile(filePath, data, 'utf8');
|
|
369
492
|
}
|
|
493
|
+
async _setFilePermissions(filePath, mode) {
|
|
494
|
+
if (typeof process === 'undefined')
|
|
495
|
+
return;
|
|
496
|
+
try {
|
|
497
|
+
const fs = await import('fs');
|
|
498
|
+
await fs.promises.chmod(filePath, mode);
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
}
|
|
502
|
+
}
|
|
370
503
|
async _readFile(filePath) {
|
|
371
504
|
if (typeof process === 'undefined')
|
|
372
505
|
throw new Error('Not available');
|
|
@@ -464,6 +597,407 @@ class EtsyRateLimiter {
|
|
|
464
597
|
}
|
|
465
598
|
const defaultRateLimiter = new EtsyRateLimiter();
|
|
466
599
|
|
|
600
|
+
class BulkOperationManager {
|
|
601
|
+
constructor(config = {}) {
|
|
602
|
+
this.concurrency = config.concurrency ?? 5;
|
|
603
|
+
this.stopOnError = config.stopOnError ?? false;
|
|
604
|
+
this.onProgress = config.onProgress;
|
|
605
|
+
this.onItemComplete = config.onItemComplete;
|
|
606
|
+
this.onItemError = config.onItemError;
|
|
607
|
+
}
|
|
608
|
+
async executeBulk(items, operation, getId) {
|
|
609
|
+
const startTime = Date.now();
|
|
610
|
+
const results = [];
|
|
611
|
+
const errors = [];
|
|
612
|
+
let completed = 0;
|
|
613
|
+
let successful = 0;
|
|
614
|
+
let failed = 0;
|
|
615
|
+
const queue = items.map((item, index) => ({
|
|
616
|
+
item,
|
|
617
|
+
index,
|
|
618
|
+
id: getId ? getId(item, index) : index
|
|
619
|
+
}));
|
|
620
|
+
const workers = [];
|
|
621
|
+
for (let i = 0; i < Math.min(this.concurrency, items.length); i++) {
|
|
622
|
+
workers.push(this.worker(queue, operation, results, errors, () => {
|
|
623
|
+
completed++;
|
|
624
|
+
const lastResult = results[results.length - 1];
|
|
625
|
+
if (lastResult?.success) {
|
|
626
|
+
successful++;
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
failed++;
|
|
630
|
+
}
|
|
631
|
+
if (this.onProgress) {
|
|
632
|
+
this.onProgress(completed, items.length, lastResult);
|
|
633
|
+
}
|
|
634
|
+
}));
|
|
635
|
+
}
|
|
636
|
+
await Promise.all(workers);
|
|
637
|
+
const duration = Date.now() - startTime;
|
|
638
|
+
return {
|
|
639
|
+
total: items.length,
|
|
640
|
+
successful,
|
|
641
|
+
failed,
|
|
642
|
+
results,
|
|
643
|
+
errors,
|
|
644
|
+
duration
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
async worker(queue, operation, results, errors, onComplete) {
|
|
648
|
+
while (queue.length > 0) {
|
|
649
|
+
const work = queue.shift();
|
|
650
|
+
if (!work)
|
|
651
|
+
break;
|
|
652
|
+
const { item, index, id } = work;
|
|
653
|
+
try {
|
|
654
|
+
const data = await operation(item, index);
|
|
655
|
+
const result = {
|
|
656
|
+
success: true,
|
|
657
|
+
data,
|
|
658
|
+
id,
|
|
659
|
+
index
|
|
660
|
+
};
|
|
661
|
+
results.push(result);
|
|
662
|
+
if (this.onItemComplete) {
|
|
663
|
+
this.onItemComplete(result);
|
|
664
|
+
}
|
|
665
|
+
onComplete();
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
669
|
+
const result = {
|
|
670
|
+
success: false,
|
|
671
|
+
error: err,
|
|
672
|
+
id,
|
|
673
|
+
index
|
|
674
|
+
};
|
|
675
|
+
results.push(result);
|
|
676
|
+
const bulkError = {
|
|
677
|
+
id,
|
|
678
|
+
index,
|
|
679
|
+
error: err
|
|
680
|
+
};
|
|
681
|
+
errors.push(bulkError);
|
|
682
|
+
if (this.onItemError) {
|
|
683
|
+
this.onItemError(bulkError);
|
|
684
|
+
}
|
|
685
|
+
onComplete();
|
|
686
|
+
if (this.stopOnError) {
|
|
687
|
+
queue.length = 0;
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
setConcurrency(concurrency) {
|
|
694
|
+
this.concurrency = Math.max(1, concurrency);
|
|
695
|
+
}
|
|
696
|
+
getConcurrency() {
|
|
697
|
+
return this.concurrency;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function createBulkOperationManager(config) {
|
|
701
|
+
return new BulkOperationManager(config);
|
|
702
|
+
}
|
|
703
|
+
async function executeBulkOperation(items, operation, config) {
|
|
704
|
+
const manager = new BulkOperationManager(config);
|
|
705
|
+
return manager.executeBulk(items, operation);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
class Validator {
|
|
709
|
+
constructor() {
|
|
710
|
+
this.validators = [];
|
|
711
|
+
}
|
|
712
|
+
rule(validator) {
|
|
713
|
+
this.validators.push(validator);
|
|
714
|
+
return this;
|
|
715
|
+
}
|
|
716
|
+
validate(data) {
|
|
717
|
+
const errors = [];
|
|
718
|
+
for (const validator of this.validators) {
|
|
719
|
+
const error = validator(data);
|
|
720
|
+
if (error) {
|
|
721
|
+
errors.push(error);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
valid: errors.length === 0,
|
|
726
|
+
errors
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
class FieldValidator {
|
|
731
|
+
constructor(field) {
|
|
732
|
+
this.field = field;
|
|
733
|
+
}
|
|
734
|
+
required(message) {
|
|
735
|
+
return (data) => {
|
|
736
|
+
if (typeof data !== 'object' || data === null)
|
|
737
|
+
return null;
|
|
738
|
+
const value = data[this.field];
|
|
739
|
+
if (value === undefined || value === null || value === '') {
|
|
740
|
+
return {
|
|
741
|
+
field: this.field,
|
|
742
|
+
message: message || `${this.field} is required`,
|
|
743
|
+
value
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
return null;
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
string(options) {
|
|
750
|
+
return (data) => {
|
|
751
|
+
if (typeof data !== 'object' || data === null)
|
|
752
|
+
return null;
|
|
753
|
+
const value = data[this.field];
|
|
754
|
+
if (value === undefined || value === null)
|
|
755
|
+
return null;
|
|
756
|
+
if (typeof value !== 'string') {
|
|
757
|
+
return {
|
|
758
|
+
field: this.field,
|
|
759
|
+
message: options.message || `${this.field} must be a string`,
|
|
760
|
+
value
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
if (options.min !== undefined && value.length < options.min) {
|
|
764
|
+
return {
|
|
765
|
+
field: this.field,
|
|
766
|
+
message: options.message || `${this.field} must be at least ${options.min} characters`,
|
|
767
|
+
value
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
if (options.max !== undefined && value.length > options.max) {
|
|
771
|
+
return {
|
|
772
|
+
field: this.field,
|
|
773
|
+
message: options.message || `${this.field} must be at most ${options.max} characters`,
|
|
774
|
+
value
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
if (options.pattern && !options.pattern.test(value)) {
|
|
778
|
+
return {
|
|
779
|
+
field: this.field,
|
|
780
|
+
message: options.message || `${this.field} has invalid format`,
|
|
781
|
+
value
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
return null;
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
number(options) {
|
|
788
|
+
return (data) => {
|
|
789
|
+
if (typeof data !== 'object' || data === null)
|
|
790
|
+
return null;
|
|
791
|
+
const value = data[this.field];
|
|
792
|
+
if (value === undefined || value === null)
|
|
793
|
+
return null;
|
|
794
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
795
|
+
return {
|
|
796
|
+
field: this.field,
|
|
797
|
+
message: options.message || `${this.field} must be a number`,
|
|
798
|
+
value
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
if (options.integer && !Number.isInteger(value)) {
|
|
802
|
+
return {
|
|
803
|
+
field: this.field,
|
|
804
|
+
message: options.message || `${this.field} must be an integer`,
|
|
805
|
+
value
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
if (options.positive && value <= 0) {
|
|
809
|
+
return {
|
|
810
|
+
field: this.field,
|
|
811
|
+
message: options.message || `${this.field} must be positive`,
|
|
812
|
+
value
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
if (options.min !== undefined && value < options.min) {
|
|
816
|
+
return {
|
|
817
|
+
field: this.field,
|
|
818
|
+
message: options.message || `${this.field} must be at least ${options.min}`,
|
|
819
|
+
value
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (options.max !== undefined && value > options.max) {
|
|
823
|
+
return {
|
|
824
|
+
field: this.field,
|
|
825
|
+
message: options.message || `${this.field} must be at most ${options.max}`,
|
|
826
|
+
value
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
return null;
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
enum(allowedValues, message) {
|
|
833
|
+
return (data) => {
|
|
834
|
+
if (typeof data !== 'object' || data === null)
|
|
835
|
+
return null;
|
|
836
|
+
const value = data[this.field];
|
|
837
|
+
if (value === undefined || value === null)
|
|
838
|
+
return null;
|
|
839
|
+
if (!allowedValues.includes(value)) {
|
|
840
|
+
return {
|
|
841
|
+
field: this.field,
|
|
842
|
+
message: message || `${this.field} must be one of: ${allowedValues.join(', ')}`,
|
|
843
|
+
value
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
return null;
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
array(options) {
|
|
850
|
+
return (data) => {
|
|
851
|
+
if (typeof data !== 'object' || data === null)
|
|
852
|
+
return null;
|
|
853
|
+
const value = data[this.field];
|
|
854
|
+
if (value === undefined || value === null)
|
|
855
|
+
return null;
|
|
856
|
+
if (!Array.isArray(value)) {
|
|
857
|
+
return {
|
|
858
|
+
field: this.field,
|
|
859
|
+
message: options.message || `${this.field} must be an array`,
|
|
860
|
+
value
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
if (options.min !== undefined && value.length < options.min) {
|
|
864
|
+
return {
|
|
865
|
+
field: this.field,
|
|
866
|
+
message: options.message || `${this.field} must have at least ${options.min} items`,
|
|
867
|
+
value
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
if (options.max !== undefined && value.length > options.max) {
|
|
871
|
+
return {
|
|
872
|
+
field: this.field,
|
|
873
|
+
message: options.message || `${this.field} must have at most ${options.max} items`,
|
|
874
|
+
value
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
if (options.itemValidator) {
|
|
878
|
+
for (let i = 0; i < value.length; i++) {
|
|
879
|
+
if (!options.itemValidator(value[i])) {
|
|
880
|
+
return {
|
|
881
|
+
field: this.field,
|
|
882
|
+
message: options.message || `${this.field}[${i}] has invalid value`,
|
|
883
|
+
value: value[i]
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return null;
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function field(fieldName) {
|
|
893
|
+
return new FieldValidator(fieldName);
|
|
894
|
+
}
|
|
895
|
+
const CreateListingSchema = new Validator()
|
|
896
|
+
.rule(field('quantity').required())
|
|
897
|
+
.rule(field('quantity').number({ min: 1, integer: true, message: 'quantity must be a positive integer' }))
|
|
898
|
+
.rule(field('title').required())
|
|
899
|
+
.rule(field('title').string({ min: 1, max: 140, message: 'title must be 1-140 characters' }))
|
|
900
|
+
.rule(field('description').string({ max: 65535, message: 'description must be less than 65535 characters' }))
|
|
901
|
+
.rule(field('price').required())
|
|
902
|
+
.rule(field('price').number({ min: 0.2, max: 50000, message: 'price must be between 0.20 and 50000.00' }))
|
|
903
|
+
.rule(field('who_made').enum(['i_did', 'someone_else', 'collective'], 'who_made must be one of: i_did, someone_else, collective'))
|
|
904
|
+
.rule(field('when_made').enum([
|
|
905
|
+
'made_to_order',
|
|
906
|
+
'2020_2024',
|
|
907
|
+
'2010_2019',
|
|
908
|
+
'2005_2009',
|
|
909
|
+
'2000_2004',
|
|
910
|
+
'1990s',
|
|
911
|
+
'1980s',
|
|
912
|
+
'1970s',
|
|
913
|
+
'1960s',
|
|
914
|
+
'1950s',
|
|
915
|
+
'1940s',
|
|
916
|
+
'1930s',
|
|
917
|
+
'1920s',
|
|
918
|
+
'1910s',
|
|
919
|
+
'1900s',
|
|
920
|
+
'1800s',
|
|
921
|
+
'1700s',
|
|
922
|
+
'before_1700'
|
|
923
|
+
]))
|
|
924
|
+
.rule(field('taxonomy_id').required())
|
|
925
|
+
.rule(field('taxonomy_id').number({ integer: true, positive: true, message: 'taxonomy_id must be a positive integer' }));
|
|
926
|
+
const UpdateListingSchema = new Validator()
|
|
927
|
+
.rule((data) => {
|
|
928
|
+
if (typeof data === 'object' && data !== null && 'title' in data && data.title !== undefined) {
|
|
929
|
+
return field('title').string({ min: 1, max: 140, message: 'title must be 1-140 characters' })(data);
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
})
|
|
933
|
+
.rule((data) => {
|
|
934
|
+
if (typeof data === 'object' && data !== null && 'description' in data && data.description !== undefined) {
|
|
935
|
+
return field('description').string({ max: 65535, message: 'description must be less than 65535 characters' })(data);
|
|
936
|
+
}
|
|
937
|
+
return null;
|
|
938
|
+
})
|
|
939
|
+
.rule((data) => {
|
|
940
|
+
if (data.tags !== undefined) {
|
|
941
|
+
return field('tags').array({ max: 13, message: 'tags must have at most 13 items' })(data);
|
|
942
|
+
}
|
|
943
|
+
return null;
|
|
944
|
+
})
|
|
945
|
+
.rule((data) => {
|
|
946
|
+
if (data.materials !== undefined) {
|
|
947
|
+
return field('materials').array({ max: 13, message: 'materials must have at most 13 items' })(data);
|
|
948
|
+
}
|
|
949
|
+
return null;
|
|
950
|
+
});
|
|
951
|
+
const UpdateShopSchema = new Validator()
|
|
952
|
+
.rule((data) => {
|
|
953
|
+
if (typeof data === 'object' && data !== null && 'title' in data && data.title !== undefined) {
|
|
954
|
+
return field('title').string({ min: 1, max: 55, message: 'shop title must be 1-55 characters' })(data);
|
|
955
|
+
}
|
|
956
|
+
return null;
|
|
957
|
+
})
|
|
958
|
+
.rule((data) => {
|
|
959
|
+
if (typeof data === 'object' && data !== null && 'announcement' in data && data.announcement !== undefined) {
|
|
960
|
+
return field('announcement').string({ max: 5000, message: 'announcement must be less than 5000 characters' })(data);
|
|
961
|
+
}
|
|
962
|
+
return null;
|
|
963
|
+
});
|
|
964
|
+
class ValidationException extends Error {
|
|
965
|
+
constructor(message, errors) {
|
|
966
|
+
super(message);
|
|
967
|
+
this.name = 'ValidationException';
|
|
968
|
+
this.errors = errors;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function validateOrThrow(data, schema, errorMessage = 'Validation failed') {
|
|
972
|
+
const result = schema.validate(data);
|
|
973
|
+
if (!result.valid) {
|
|
974
|
+
throw new ValidationException(errorMessage, result.errors);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
function validate(data, schema) {
|
|
978
|
+
return schema.validate(data);
|
|
979
|
+
}
|
|
980
|
+
function createValidator() {
|
|
981
|
+
return new Validator();
|
|
982
|
+
}
|
|
983
|
+
function combineValidators(...validators) {
|
|
984
|
+
return {
|
|
985
|
+
validate(data) {
|
|
986
|
+
const allErrors = [];
|
|
987
|
+
for (const validator of validators) {
|
|
988
|
+
const result = validator.validate(data);
|
|
989
|
+
if (!result.valid) {
|
|
990
|
+
allErrors.push(...result.errors);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return {
|
|
994
|
+
valid: allErrors.length === 0,
|
|
995
|
+
errors: allErrors
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
467
1001
|
class DefaultLogger {
|
|
468
1002
|
debug(message, ...args) {
|
|
469
1003
|
const isDevelopment = isNode$1
|
|
@@ -528,6 +1062,10 @@ class EtsyClient {
|
|
|
528
1062
|
this.cache = config.caching?.storage || new MemoryCache();
|
|
529
1063
|
this.cacheTtl = config.caching?.ttl || 3600;
|
|
530
1064
|
}
|
|
1065
|
+
this.bulkOperationManager = new BulkOperationManager({
|
|
1066
|
+
concurrency: 5,
|
|
1067
|
+
stopOnError: false
|
|
1068
|
+
});
|
|
531
1069
|
}
|
|
532
1070
|
async makeRequest(endpoint, options = {}, useCache = true) {
|
|
533
1071
|
const url = `${this.baseUrl}${endpoint}`;
|
|
@@ -560,6 +1098,13 @@ class EtsyClient {
|
|
|
560
1098
|
const errorText = await response.text();
|
|
561
1099
|
throw new EtsyApiError(`Etsy API error: ${response.status} ${response.statusText}`, response.status, errorText);
|
|
562
1100
|
}
|
|
1101
|
+
if (response.status === 204) {
|
|
1102
|
+
return undefined;
|
|
1103
|
+
}
|
|
1104
|
+
const contentLength = response.headers?.get?.('content-length');
|
|
1105
|
+
if (contentLength === '0') {
|
|
1106
|
+
return undefined;
|
|
1107
|
+
}
|
|
563
1108
|
const data = await response.json();
|
|
564
1109
|
if (useCache && this.cache && requestOptions.method === 'GET') {
|
|
565
1110
|
await this.cache.set(cacheKey, JSON.stringify(data), this.cacheTtl);
|
|
@@ -670,6 +1215,278 @@ class EtsyClient {
|
|
|
670
1215
|
async getListingInventory(listingId) {
|
|
671
1216
|
return this.makeRequest(`/listings/${listingId}/inventory`);
|
|
672
1217
|
}
|
|
1218
|
+
async getSellerTaxonomyNodes() {
|
|
1219
|
+
const response = await this.makeRequest('/seller-taxonomy/nodes');
|
|
1220
|
+
return response.results;
|
|
1221
|
+
}
|
|
1222
|
+
async getUserShops() {
|
|
1223
|
+
const response = await this.makeRequest('/users/me/shops');
|
|
1224
|
+
return response.results || [];
|
|
1225
|
+
}
|
|
1226
|
+
async updateShop(shopId, params, options) {
|
|
1227
|
+
if (options?.validate) {
|
|
1228
|
+
const schema = options.validateSchema || UpdateShopSchema;
|
|
1229
|
+
const throwOnError = options.throwOnValidationError !== false;
|
|
1230
|
+
if (throwOnError) {
|
|
1231
|
+
validateOrThrow(params, schema, 'Invalid shop update parameters');
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
const result = schema.validate(params);
|
|
1235
|
+
if (!result.valid) {
|
|
1236
|
+
this.logger.warn('Shop update validation failed', result.errors);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return this.makeRequest(`/shops/${shopId}`, {
|
|
1241
|
+
method: 'PUT',
|
|
1242
|
+
body: JSON.stringify(params)
|
|
1243
|
+
}, false);
|
|
1244
|
+
}
|
|
1245
|
+
async createShopSection(shopId, params) {
|
|
1246
|
+
return this.makeRequest(`/shops/${shopId}/sections`, {
|
|
1247
|
+
method: 'POST',
|
|
1248
|
+
body: JSON.stringify(params)
|
|
1249
|
+
}, false);
|
|
1250
|
+
}
|
|
1251
|
+
async updateShopSection(shopId, sectionId, params) {
|
|
1252
|
+
return this.makeRequest(`/shops/${shopId}/sections/${sectionId}`, {
|
|
1253
|
+
method: 'PUT',
|
|
1254
|
+
body: JSON.stringify(params)
|
|
1255
|
+
}, false);
|
|
1256
|
+
}
|
|
1257
|
+
async deleteShopSection(shopId, sectionId) {
|
|
1258
|
+
await this.makeRequest(`/shops/${shopId}/sections/${sectionId}`, { method: 'DELETE' }, false);
|
|
1259
|
+
}
|
|
1260
|
+
async createDraftListing(shopId, params, options) {
|
|
1261
|
+
if (options?.validate) {
|
|
1262
|
+
const schema = options.validateSchema || CreateListingSchema;
|
|
1263
|
+
const throwOnError = options.throwOnValidationError !== false;
|
|
1264
|
+
if (throwOnError) {
|
|
1265
|
+
validateOrThrow(params, schema, 'Invalid listing parameters');
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
const result = schema.validate(params);
|
|
1269
|
+
if (!result.valid) {
|
|
1270
|
+
this.logger.warn('Listing validation failed', result.errors);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return this.makeRequest(`/shops/${shopId}/listings`, {
|
|
1275
|
+
method: 'POST',
|
|
1276
|
+
body: JSON.stringify(params)
|
|
1277
|
+
}, false);
|
|
1278
|
+
}
|
|
1279
|
+
async updateListing(shopId, listingId, params, options) {
|
|
1280
|
+
if (options?.validate) {
|
|
1281
|
+
const schema = options.validateSchema || UpdateListingSchema;
|
|
1282
|
+
const throwOnError = options.throwOnValidationError !== false;
|
|
1283
|
+
if (throwOnError) {
|
|
1284
|
+
validateOrThrow(params, schema, 'Invalid listing update parameters');
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
const result = schema.validate(params);
|
|
1288
|
+
if (!result.valid) {
|
|
1289
|
+
this.logger.warn('Listing update validation failed', result.errors);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return this.makeRequest(`/shops/${shopId}/listings/${listingId}`, {
|
|
1294
|
+
method: 'PATCH',
|
|
1295
|
+
body: JSON.stringify(params)
|
|
1296
|
+
}, false);
|
|
1297
|
+
}
|
|
1298
|
+
async deleteListing(listingId) {
|
|
1299
|
+
await this.makeRequest(`/listings/${listingId}`, { method: 'DELETE' }, false);
|
|
1300
|
+
}
|
|
1301
|
+
async updateListingInventory(listingId, params) {
|
|
1302
|
+
return this.makeRequest(`/listings/${listingId}/inventory`, {
|
|
1303
|
+
method: 'PUT',
|
|
1304
|
+
body: JSON.stringify(params)
|
|
1305
|
+
}, false);
|
|
1306
|
+
}
|
|
1307
|
+
async uploadListingImage(shopId, listingId, imageData, params) {
|
|
1308
|
+
const formData = new FormData();
|
|
1309
|
+
formData.append('image', imageData);
|
|
1310
|
+
if (params?.rank !== undefined)
|
|
1311
|
+
formData.append('rank', params.rank.toString());
|
|
1312
|
+
if (params?.overwrite !== undefined)
|
|
1313
|
+
formData.append('overwrite', params.overwrite.toString());
|
|
1314
|
+
if (params?.is_watermarked !== undefined)
|
|
1315
|
+
formData.append('is_watermarked', params.is_watermarked.toString());
|
|
1316
|
+
if (params?.alt_text)
|
|
1317
|
+
formData.append('alt_text', params.alt_text);
|
|
1318
|
+
const url = `${this.baseUrl}/shops/${shopId}/listings/${listingId}/images`;
|
|
1319
|
+
await this.rateLimiter.waitForRateLimit();
|
|
1320
|
+
const accessToken = await this.tokenManager.getAccessToken();
|
|
1321
|
+
const response = await this.fetch(url, {
|
|
1322
|
+
method: 'POST',
|
|
1323
|
+
headers: {
|
|
1324
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
1325
|
+
'x-api-key': this.getApiKey(),
|
|
1326
|
+
},
|
|
1327
|
+
body: formData
|
|
1328
|
+
});
|
|
1329
|
+
if (!response.ok) {
|
|
1330
|
+
const errorText = await response.text();
|
|
1331
|
+
throw new EtsyApiError(`Failed to upload image: ${response.status} ${response.statusText}`, response.status, errorText);
|
|
1332
|
+
}
|
|
1333
|
+
return response.json();
|
|
1334
|
+
}
|
|
1335
|
+
async getListingImage(shopId, listingId, imageId) {
|
|
1336
|
+
return this.makeRequest(`/shops/${shopId}/listings/${listingId}/images/${imageId}`);
|
|
1337
|
+
}
|
|
1338
|
+
async deleteListingImage(shopId, listingId, imageId) {
|
|
1339
|
+
await this.makeRequest(`/shops/${shopId}/listings/${listingId}/images/${imageId}`, { method: 'DELETE' }, false);
|
|
1340
|
+
}
|
|
1341
|
+
async bulkUpdateListings(shopId, operations, config) {
|
|
1342
|
+
const manager = config ? new BulkOperationManager(config) : this.bulkOperationManager;
|
|
1343
|
+
return manager.executeBulk(operations, async (operation) => {
|
|
1344
|
+
return this.updateListing(shopId, operation.listingId.toString(), operation.updates);
|
|
1345
|
+
}, (operation) => operation.listingId);
|
|
1346
|
+
}
|
|
1347
|
+
async bulkUploadImages(shopId, listingId, images, config) {
|
|
1348
|
+
const manager = config ? new BulkOperationManager(config) : this.bulkOperationManager;
|
|
1349
|
+
return manager.executeBulk(images, async (image) => {
|
|
1350
|
+
return this.uploadListingImage(shopId, listingId, image.file, {
|
|
1351
|
+
rank: image.rank,
|
|
1352
|
+
alt_text: image.altText
|
|
1353
|
+
});
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
setBulkOperationConcurrency(concurrency) {
|
|
1357
|
+
this.bulkOperationManager.setConcurrency(Math.max(1, Math.min(10, concurrency)));
|
|
1358
|
+
}
|
|
1359
|
+
async getShopReceipts(shopId, params) {
|
|
1360
|
+
const searchParams = new URLSearchParams();
|
|
1361
|
+
if (params?.limit !== undefined)
|
|
1362
|
+
searchParams.set('limit', params.limit.toString());
|
|
1363
|
+
if (params?.offset !== undefined)
|
|
1364
|
+
searchParams.set('offset', params.offset.toString());
|
|
1365
|
+
if (params?.sort_on)
|
|
1366
|
+
searchParams.set('sort_on', params.sort_on);
|
|
1367
|
+
if (params?.sort_order)
|
|
1368
|
+
searchParams.set('sort_order', params.sort_order);
|
|
1369
|
+
if (params?.min_created !== undefined)
|
|
1370
|
+
searchParams.set('min_created', params.min_created.toString());
|
|
1371
|
+
if (params?.max_created !== undefined)
|
|
1372
|
+
searchParams.set('max_created', params.max_created.toString());
|
|
1373
|
+
if (params?.min_last_modified !== undefined)
|
|
1374
|
+
searchParams.set('min_last_modified', params.min_last_modified.toString());
|
|
1375
|
+
if (params?.max_last_modified !== undefined)
|
|
1376
|
+
searchParams.set('max_last_modified', params.max_last_modified.toString());
|
|
1377
|
+
if (params?.was_paid !== undefined)
|
|
1378
|
+
searchParams.set('was_paid', params.was_paid.toString());
|
|
1379
|
+
if (params?.was_shipped !== undefined)
|
|
1380
|
+
searchParams.set('was_shipped', params.was_shipped.toString());
|
|
1381
|
+
if (params?.was_delivered !== undefined)
|
|
1382
|
+
searchParams.set('was_delivered', params.was_delivered.toString());
|
|
1383
|
+
const response = await this.makeRequest(`/shops/${shopId}/receipts?${searchParams.toString()}`);
|
|
1384
|
+
return response.results;
|
|
1385
|
+
}
|
|
1386
|
+
async getShopReceipt(shopId, receiptId) {
|
|
1387
|
+
return this.makeRequest(`/shops/${shopId}/receipts/${receiptId}`);
|
|
1388
|
+
}
|
|
1389
|
+
async updateShopReceipt(shopId, receiptId, params) {
|
|
1390
|
+
return this.makeRequest(`/shops/${shopId}/receipts/${receiptId}`, {
|
|
1391
|
+
method: 'PUT',
|
|
1392
|
+
body: JSON.stringify(params)
|
|
1393
|
+
}, false);
|
|
1394
|
+
}
|
|
1395
|
+
async getShopReceiptTransactions(shopId, receiptId) {
|
|
1396
|
+
const response = await this.makeRequest(`/shops/${shopId}/receipts/${receiptId}/transactions`);
|
|
1397
|
+
return response.results;
|
|
1398
|
+
}
|
|
1399
|
+
async getShopTransaction(shopId, transactionId) {
|
|
1400
|
+
return this.makeRequest(`/shops/${shopId}/transactions/${transactionId}`);
|
|
1401
|
+
}
|
|
1402
|
+
async getShopShippingProfiles(shopId) {
|
|
1403
|
+
const response = await this.makeRequest(`/shops/${shopId}/shipping-profiles`);
|
|
1404
|
+
return response.results;
|
|
1405
|
+
}
|
|
1406
|
+
async createShopShippingProfile(shopId, params) {
|
|
1407
|
+
return this.makeRequest(`/shops/${shopId}/shipping-profiles`, {
|
|
1408
|
+
method: 'POST',
|
|
1409
|
+
body: JSON.stringify(params)
|
|
1410
|
+
}, false);
|
|
1411
|
+
}
|
|
1412
|
+
async getShopShippingProfile(shopId, profileId) {
|
|
1413
|
+
return this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}`);
|
|
1414
|
+
}
|
|
1415
|
+
async updateShopShippingProfile(shopId, profileId, params) {
|
|
1416
|
+
return this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}`, {
|
|
1417
|
+
method: 'PUT',
|
|
1418
|
+
body: JSON.stringify(params)
|
|
1419
|
+
}, false);
|
|
1420
|
+
}
|
|
1421
|
+
async deleteShopShippingProfile(shopId, profileId) {
|
|
1422
|
+
await this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}`, { method: 'DELETE' }, false);
|
|
1423
|
+
}
|
|
1424
|
+
async getShopShippingProfileDestinations(shopId, profileId) {
|
|
1425
|
+
const response = await this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}/destinations`);
|
|
1426
|
+
return response.results;
|
|
1427
|
+
}
|
|
1428
|
+
async createShopShippingProfileDestination(shopId, profileId, params) {
|
|
1429
|
+
return this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}/destinations`, {
|
|
1430
|
+
method: 'POST',
|
|
1431
|
+
body: JSON.stringify(params)
|
|
1432
|
+
}, false);
|
|
1433
|
+
}
|
|
1434
|
+
async updateShopShippingProfileDestination(shopId, profileId, destinationId, params) {
|
|
1435
|
+
return this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}/destinations/${destinationId}`, {
|
|
1436
|
+
method: 'PUT',
|
|
1437
|
+
body: JSON.stringify(params)
|
|
1438
|
+
}, false);
|
|
1439
|
+
}
|
|
1440
|
+
async deleteShopShippingProfileDestination(shopId, profileId, destinationId) {
|
|
1441
|
+
await this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}/destinations/${destinationId}`, { method: 'DELETE' }, false);
|
|
1442
|
+
}
|
|
1443
|
+
async getShopShippingProfileUpgrades(shopId, profileId) {
|
|
1444
|
+
const response = await this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}/upgrades`);
|
|
1445
|
+
return response.results;
|
|
1446
|
+
}
|
|
1447
|
+
async createReceiptShipment(shopId, receiptId, params) {
|
|
1448
|
+
return this.makeRequest(`/shops/${shopId}/receipts/${receiptId}/tracking`, {
|
|
1449
|
+
method: 'POST',
|
|
1450
|
+
body: JSON.stringify(params)
|
|
1451
|
+
}, false);
|
|
1452
|
+
}
|
|
1453
|
+
async getShopReceiptShipments(shopId, receiptId) {
|
|
1454
|
+
const response = await this.makeRequest(`/shops/${shopId}/receipts/${receiptId}/shipments`);
|
|
1455
|
+
return response.results;
|
|
1456
|
+
}
|
|
1457
|
+
async getShopPaymentAccountLedgerEntries(shopId, params) {
|
|
1458
|
+
const searchParams = new URLSearchParams();
|
|
1459
|
+
searchParams.set('min_created', params.min_created.toString());
|
|
1460
|
+
searchParams.set('max_created', params.max_created.toString());
|
|
1461
|
+
if (params.limit !== undefined)
|
|
1462
|
+
searchParams.set('limit', params.limit.toString());
|
|
1463
|
+
if (params.offset !== undefined)
|
|
1464
|
+
searchParams.set('offset', params.offset.toString());
|
|
1465
|
+
const response = await this.makeRequest(`/shops/${shopId}/payment-account/ledger-entries?${searchParams.toString()}`);
|
|
1466
|
+
return response.results;
|
|
1467
|
+
}
|
|
1468
|
+
async getShopPaymentAccountLedgerEntry(shopId, entryId) {
|
|
1469
|
+
return this.makeRequest(`/shops/${shopId}/payment-account/ledger-entries/${entryId}`);
|
|
1470
|
+
}
|
|
1471
|
+
async getShopPayment(shopId, paymentId) {
|
|
1472
|
+
return this.makeRequest(`/shops/${shopId}/payment-account/payments/${paymentId}`);
|
|
1473
|
+
}
|
|
1474
|
+
async getBuyerTaxonomyNodes() {
|
|
1475
|
+
const response = await this.makeRequest('/buyer-taxonomy/nodes');
|
|
1476
|
+
return response.results;
|
|
1477
|
+
}
|
|
1478
|
+
async getPropertiesByTaxonomyId(taxonomyId) {
|
|
1479
|
+
const response = await this.makeRequest(`/seller-taxonomy/nodes/${taxonomyId}/properties`);
|
|
1480
|
+
return response.results;
|
|
1481
|
+
}
|
|
1482
|
+
async getListingProperties(shopId, listingId) {
|
|
1483
|
+
const response = await this.makeRequest(`/shops/${shopId}/listings/${listingId}/properties`);
|
|
1484
|
+
return response.results;
|
|
1485
|
+
}
|
|
1486
|
+
async getShopProductionPartners(shopId) {
|
|
1487
|
+
const response = await this.makeRequest(`/shops/${shopId}/production-partners`);
|
|
1488
|
+
return response.results;
|
|
1489
|
+
}
|
|
673
1490
|
getRemainingRequests() {
|
|
674
1491
|
return this.rateLimiter.getRemainingRequests();
|
|
675
1492
|
}
|
|
@@ -917,6 +1734,1075 @@ const COMMON_SCOPE_COMBINATIONS = {
|
|
|
917
1734
|
]
|
|
918
1735
|
};
|
|
919
1736
|
|
|
1737
|
+
class PaginatedResults {
|
|
1738
|
+
constructor(fetcher, options = {}) {
|
|
1739
|
+
this.currentPage = [];
|
|
1740
|
+
this.totalCount = null;
|
|
1741
|
+
this.hasMore = true;
|
|
1742
|
+
this.fetcher = fetcher;
|
|
1743
|
+
this.options = {
|
|
1744
|
+
limit: options.limit || 25,
|
|
1745
|
+
offset: options.offset || 0,
|
|
1746
|
+
maxPages: options.maxPages || Infinity,
|
|
1747
|
+
maxItems: options.maxItems || Infinity
|
|
1748
|
+
};
|
|
1749
|
+
this.currentOffset = this.options.offset;
|
|
1750
|
+
}
|
|
1751
|
+
async *[Symbol.asyncIterator]() {
|
|
1752
|
+
let pageCount = 0;
|
|
1753
|
+
let itemCount = 0;
|
|
1754
|
+
while (this.hasMore && pageCount < this.options.maxPages && itemCount < this.options.maxItems) {
|
|
1755
|
+
const page = await this.fetchPage();
|
|
1756
|
+
pageCount++;
|
|
1757
|
+
for (const item of page) {
|
|
1758
|
+
if (itemCount >= this.options.maxItems) {
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
yield item;
|
|
1762
|
+
itemCount++;
|
|
1763
|
+
}
|
|
1764
|
+
if (!this.hasMore) {
|
|
1765
|
+
break;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
async getAll() {
|
|
1770
|
+
const items = [];
|
|
1771
|
+
for await (const item of this) {
|
|
1772
|
+
items.push(item);
|
|
1773
|
+
}
|
|
1774
|
+
return items;
|
|
1775
|
+
}
|
|
1776
|
+
getCurrentPage() {
|
|
1777
|
+
return this.currentPage;
|
|
1778
|
+
}
|
|
1779
|
+
hasNextPage() {
|
|
1780
|
+
return this.hasMore;
|
|
1781
|
+
}
|
|
1782
|
+
async getNextPage() {
|
|
1783
|
+
if (!this.hasMore) {
|
|
1784
|
+
return [];
|
|
1785
|
+
}
|
|
1786
|
+
return this.fetchPage();
|
|
1787
|
+
}
|
|
1788
|
+
getTotalCount() {
|
|
1789
|
+
return this.totalCount;
|
|
1790
|
+
}
|
|
1791
|
+
reset() {
|
|
1792
|
+
this.currentOffset = this.options.offset;
|
|
1793
|
+
this.currentPage = [];
|
|
1794
|
+
this.totalCount = null;
|
|
1795
|
+
this.hasMore = true;
|
|
1796
|
+
}
|
|
1797
|
+
async fetchPage() {
|
|
1798
|
+
const response = await this.fetcher(this.options.limit, this.currentOffset);
|
|
1799
|
+
this.currentPage = response.results;
|
|
1800
|
+
this.totalCount = response.count;
|
|
1801
|
+
if (response.pagination) {
|
|
1802
|
+
if (response.pagination.next_offset !== undefined) {
|
|
1803
|
+
this.hasMore = true;
|
|
1804
|
+
this.currentOffset = response.pagination.next_offset;
|
|
1805
|
+
}
|
|
1806
|
+
else {
|
|
1807
|
+
this.hasMore = false;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
else {
|
|
1811
|
+
this.hasMore = response.results.length === this.options.limit;
|
|
1812
|
+
this.currentOffset += this.options.limit;
|
|
1813
|
+
}
|
|
1814
|
+
return this.currentPage;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
function createPaginatedResults(fetcher, options) {
|
|
1818
|
+
return new PaginatedResults(fetcher, options);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
1822
|
+
maxRetries: 3,
|
|
1823
|
+
retryDelay: 1000,
|
|
1824
|
+
exponentialBackoff: true,
|
|
1825
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
1826
|
+
maxRetryDelay: 30000,
|
|
1827
|
+
jitter: 0.1
|
|
1828
|
+
};
|
|
1829
|
+
function calculateDelay(attempt, config) {
|
|
1830
|
+
let delay;
|
|
1831
|
+
if (config.exponentialBackoff) {
|
|
1832
|
+
delay = config.retryDelay * Math.pow(2, attempt - 1);
|
|
1833
|
+
}
|
|
1834
|
+
else {
|
|
1835
|
+
delay = config.retryDelay * attempt;
|
|
1836
|
+
}
|
|
1837
|
+
if (config.maxRetryDelay) {
|
|
1838
|
+
delay = Math.min(delay, config.maxRetryDelay);
|
|
1839
|
+
}
|
|
1840
|
+
if (config.jitter && config.jitter > 0) {
|
|
1841
|
+
const jitterAmount = delay * config.jitter;
|
|
1842
|
+
const randomJitter = Math.random() * jitterAmount * 2 - jitterAmount;
|
|
1843
|
+
delay += randomJitter;
|
|
1844
|
+
}
|
|
1845
|
+
return Math.max(0, Math.floor(delay));
|
|
1846
|
+
}
|
|
1847
|
+
function isRetryableError(error, config) {
|
|
1848
|
+
if (error instanceof EtsyApiError) {
|
|
1849
|
+
const statusCode = error.statusCode;
|
|
1850
|
+
if (statusCode && config.retryableStatusCodes.includes(statusCode)) {
|
|
1851
|
+
return true;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (error instanceof TypeError && error.message.includes('fetch')) {
|
|
1855
|
+
return true;
|
|
1856
|
+
}
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
1859
|
+
function getRetryAfterDelay(error) {
|
|
1860
|
+
if (error instanceof EtsyApiError && error.statusCode === 429) {
|
|
1861
|
+
const retryAfter = error.getRetryAfter();
|
|
1862
|
+
if (typeof retryAfter === 'number') {
|
|
1863
|
+
return retryAfter * 1000;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return null;
|
|
1867
|
+
}
|
|
1868
|
+
function sleep(ms) {
|
|
1869
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1870
|
+
}
|
|
1871
|
+
async function withRetry(operation, options = {}) {
|
|
1872
|
+
const config = {
|
|
1873
|
+
...DEFAULT_RETRY_CONFIG,
|
|
1874
|
+
...options
|
|
1875
|
+
};
|
|
1876
|
+
let lastError = new Error('Unknown error');
|
|
1877
|
+
let attempt = 0;
|
|
1878
|
+
while (attempt <= config.maxRetries) {
|
|
1879
|
+
try {
|
|
1880
|
+
if (options.signal?.aborted) {
|
|
1881
|
+
throw new Error('Operation aborted');
|
|
1882
|
+
}
|
|
1883
|
+
const result = await operation();
|
|
1884
|
+
return result;
|
|
1885
|
+
}
|
|
1886
|
+
catch (error) {
|
|
1887
|
+
lastError = error;
|
|
1888
|
+
attempt++;
|
|
1889
|
+
if (attempt > config.maxRetries) {
|
|
1890
|
+
throw lastError;
|
|
1891
|
+
}
|
|
1892
|
+
if (!isRetryableError(error, config)) {
|
|
1893
|
+
throw lastError;
|
|
1894
|
+
}
|
|
1895
|
+
let delay = calculateDelay(attempt, config);
|
|
1896
|
+
const retryAfterDelay = getRetryAfterDelay(error);
|
|
1897
|
+
if (retryAfterDelay !== null) {
|
|
1898
|
+
delay = retryAfterDelay;
|
|
1899
|
+
}
|
|
1900
|
+
if (config.onRetry) {
|
|
1901
|
+
config.onRetry(attempt, lastError);
|
|
1902
|
+
}
|
|
1903
|
+
await sleep(delay);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
throw lastError;
|
|
1907
|
+
}
|
|
1908
|
+
class RetryManager {
|
|
1909
|
+
constructor(config = {}) {
|
|
1910
|
+
this.config = {
|
|
1911
|
+
...DEFAULT_RETRY_CONFIG,
|
|
1912
|
+
...config
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
async execute(operation, options) {
|
|
1916
|
+
return withRetry(operation, {
|
|
1917
|
+
...this.config,
|
|
1918
|
+
...options
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
updateConfig(config) {
|
|
1922
|
+
this.config = {
|
|
1923
|
+
...this.config,
|
|
1924
|
+
...config
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
getConfig() {
|
|
1928
|
+
return { ...this.config };
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
class EtsyWebhookHandler {
|
|
1933
|
+
constructor(config) {
|
|
1934
|
+
this.config = {
|
|
1935
|
+
algorithm: 'sha256',
|
|
1936
|
+
verifySignatures: true,
|
|
1937
|
+
...config
|
|
1938
|
+
};
|
|
1939
|
+
this.handlers = new Map();
|
|
1940
|
+
if (isNode$1) {
|
|
1941
|
+
try {
|
|
1942
|
+
this.crypto = require('crypto');
|
|
1943
|
+
}
|
|
1944
|
+
catch {
|
|
1945
|
+
console.warn('crypto module not available, signature verification will not work');
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
verifySignature(payload, signature) {
|
|
1950
|
+
if (!this.config.verifySignatures) {
|
|
1951
|
+
return true;
|
|
1952
|
+
}
|
|
1953
|
+
if (!this.crypto) {
|
|
1954
|
+
throw new Error('Signature verification requires Node.js crypto module');
|
|
1955
|
+
}
|
|
1956
|
+
try {
|
|
1957
|
+
const hmac = this.crypto
|
|
1958
|
+
.createHmac(this.config.algorithm, this.config.secret)
|
|
1959
|
+
.update(payload)
|
|
1960
|
+
.digest('hex');
|
|
1961
|
+
return this.timingSafeEqual(hmac, signature);
|
|
1962
|
+
}
|
|
1963
|
+
catch (error) {
|
|
1964
|
+
console.error('Signature verification failed:', error);
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
parseEvent(payload) {
|
|
1969
|
+
const data = typeof payload === 'string' ? JSON.parse(payload) : payload;
|
|
1970
|
+
if (!data.type || !data.data) {
|
|
1971
|
+
throw new Error('Invalid webhook event format');
|
|
1972
|
+
}
|
|
1973
|
+
const event = {
|
|
1974
|
+
type: data.type,
|
|
1975
|
+
timestamp: data.timestamp || Date.now(),
|
|
1976
|
+
data: data.data,
|
|
1977
|
+
shop_id: data.shop_id,
|
|
1978
|
+
user_id: data.user_id
|
|
1979
|
+
};
|
|
1980
|
+
this.triggerHandlers(event);
|
|
1981
|
+
return event;
|
|
1982
|
+
}
|
|
1983
|
+
on(eventType, handler) {
|
|
1984
|
+
if (!this.handlers.has(eventType)) {
|
|
1985
|
+
this.handlers.set(eventType, new Set());
|
|
1986
|
+
}
|
|
1987
|
+
const handlers = this.handlers.get(eventType);
|
|
1988
|
+
if (handlers) {
|
|
1989
|
+
handlers.add(handler);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
off(eventType, handler) {
|
|
1993
|
+
const handlers = this.handlers.get(eventType);
|
|
1994
|
+
if (handlers) {
|
|
1995
|
+
handlers.delete(handler);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
removeAllListeners(eventType) {
|
|
1999
|
+
if (eventType) {
|
|
2000
|
+
this.handlers.delete(eventType);
|
|
2001
|
+
}
|
|
2002
|
+
else {
|
|
2003
|
+
this.handlers.clear();
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
async triggerHandlers(event) {
|
|
2007
|
+
const handlers = this.handlers.get(event.type);
|
|
2008
|
+
if (!handlers || handlers.size === 0) {
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
const promises = Array.from(handlers).map(handler => {
|
|
2012
|
+
try {
|
|
2013
|
+
return Promise.resolve(handler(event.data));
|
|
2014
|
+
}
|
|
2015
|
+
catch (error) {
|
|
2016
|
+
console.error(`Error in webhook handler for ${event.type}:`, error);
|
|
2017
|
+
return Promise.resolve();
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
await Promise.all(promises);
|
|
2021
|
+
}
|
|
2022
|
+
timingSafeEqual(a, b) {
|
|
2023
|
+
if (this.crypto?.timingSafeEqual) {
|
|
2024
|
+
const bufA = Buffer.from(a);
|
|
2025
|
+
const bufB = Buffer.from(b);
|
|
2026
|
+
if (bufA.length !== bufB.length) {
|
|
2027
|
+
return false;
|
|
2028
|
+
}
|
|
2029
|
+
return this.crypto.timingSafeEqual(bufA, bufB);
|
|
2030
|
+
}
|
|
2031
|
+
else {
|
|
2032
|
+
return a === b;
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
getHandlerCount(eventType) {
|
|
2036
|
+
return this.handlers.get(eventType)?.size || 0;
|
|
2037
|
+
}
|
|
2038
|
+
getRegisteredEventTypes() {
|
|
2039
|
+
return Array.from(this.handlers.keys());
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
function createWebhookHandler(config) {
|
|
2043
|
+
return new EtsyWebhookHandler(config);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
class LRUCache {
|
|
2047
|
+
constructor(config = {}) {
|
|
2048
|
+
this.cache = new Map();
|
|
2049
|
+
this.currentSize = 0;
|
|
2050
|
+
this.stats = {
|
|
2051
|
+
hits: 0,
|
|
2052
|
+
misses: 0,
|
|
2053
|
+
evictions: 0
|
|
2054
|
+
};
|
|
2055
|
+
this.maxSize = config.maxSize ?? 10485760;
|
|
2056
|
+
this.maxEntries = config.maxEntries ?? 1000;
|
|
2057
|
+
this.ttl = (config.ttl ?? 3600) * 1000;
|
|
2058
|
+
this.trackStats = config.trackStats ?? true;
|
|
2059
|
+
}
|
|
2060
|
+
async get(key) {
|
|
2061
|
+
const entry = this.cache.get(key);
|
|
2062
|
+
if (!entry) {
|
|
2063
|
+
if (this.trackStats)
|
|
2064
|
+
this.stats.misses++;
|
|
2065
|
+
return null;
|
|
2066
|
+
}
|
|
2067
|
+
if (Date.now() > entry.expires) {
|
|
2068
|
+
this.cache.delete(key);
|
|
2069
|
+
this.currentSize -= entry.size;
|
|
2070
|
+
if (this.trackStats)
|
|
2071
|
+
this.stats.misses++;
|
|
2072
|
+
return null;
|
|
2073
|
+
}
|
|
2074
|
+
entry.lastAccessed = Date.now();
|
|
2075
|
+
entry.accessCount++;
|
|
2076
|
+
this.cache.delete(key);
|
|
2077
|
+
this.cache.set(key, entry);
|
|
2078
|
+
if (this.trackStats)
|
|
2079
|
+
this.stats.hits++;
|
|
2080
|
+
return entry.value;
|
|
2081
|
+
}
|
|
2082
|
+
async set(key, value, ttl) {
|
|
2083
|
+
const size = this.estimateSize(value);
|
|
2084
|
+
const expiryTime = ttl ? ttl * 1000 : this.ttl;
|
|
2085
|
+
const now = Date.now();
|
|
2086
|
+
const existing = this.cache.get(key);
|
|
2087
|
+
if (existing) {
|
|
2088
|
+
this.currentSize -= existing.size;
|
|
2089
|
+
this.cache.delete(key);
|
|
2090
|
+
}
|
|
2091
|
+
while ((this.currentSize + size > this.maxSize || this.cache.size >= this.maxEntries) &&
|
|
2092
|
+
this.cache.size > 0) {
|
|
2093
|
+
await this.evictLRU();
|
|
2094
|
+
}
|
|
2095
|
+
const entry = {
|
|
2096
|
+
key,
|
|
2097
|
+
value,
|
|
2098
|
+
expires: now + expiryTime,
|
|
2099
|
+
size,
|
|
2100
|
+
accessCount: 0,
|
|
2101
|
+
lastAccessed: now,
|
|
2102
|
+
created: now
|
|
2103
|
+
};
|
|
2104
|
+
this.cache.set(key, entry);
|
|
2105
|
+
this.currentSize += size;
|
|
2106
|
+
}
|
|
2107
|
+
async delete(key) {
|
|
2108
|
+
const entry = this.cache.get(key);
|
|
2109
|
+
if (entry) {
|
|
2110
|
+
this.cache.delete(key);
|
|
2111
|
+
this.currentSize -= entry.size;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
async clear() {
|
|
2115
|
+
this.cache.clear();
|
|
2116
|
+
this.currentSize = 0;
|
|
2117
|
+
this.stats = { hits: 0, misses: 0, evictions: 0 };
|
|
2118
|
+
}
|
|
2119
|
+
async evictLRU() {
|
|
2120
|
+
const firstKey = this.cache.keys().next().value;
|
|
2121
|
+
if (firstKey) {
|
|
2122
|
+
const entry = this.cache.get(firstKey);
|
|
2123
|
+
if (entry) {
|
|
2124
|
+
this.currentSize -= entry.size;
|
|
2125
|
+
}
|
|
2126
|
+
this.cache.delete(firstKey);
|
|
2127
|
+
if (this.trackStats)
|
|
2128
|
+
this.stats.evictions++;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
estimateSize(str) {
|
|
2132
|
+
return str.length * 2;
|
|
2133
|
+
}
|
|
2134
|
+
getStats() {
|
|
2135
|
+
const total = this.stats.hits + this.stats.misses;
|
|
2136
|
+
return {
|
|
2137
|
+
hits: this.stats.hits,
|
|
2138
|
+
misses: this.stats.misses,
|
|
2139
|
+
hitRate: total > 0 ? this.stats.hits / total : 0,
|
|
2140
|
+
missRate: total > 0 ? this.stats.misses / total : 0,
|
|
2141
|
+
size: this.currentSize,
|
|
2142
|
+
entryCount: this.cache.size,
|
|
2143
|
+
evictions: this.stats.evictions,
|
|
2144
|
+
maxSize: this.maxSize,
|
|
2145
|
+
maxEntries: this.maxEntries
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
class LFUCache {
|
|
2150
|
+
constructor(config = {}) {
|
|
2151
|
+
this.cache = new Map();
|
|
2152
|
+
this.currentSize = 0;
|
|
2153
|
+
this.stats = {
|
|
2154
|
+
hits: 0,
|
|
2155
|
+
misses: 0,
|
|
2156
|
+
evictions: 0
|
|
2157
|
+
};
|
|
2158
|
+
this.maxSize = config.maxSize ?? 10485760;
|
|
2159
|
+
this.maxEntries = config.maxEntries ?? 1000;
|
|
2160
|
+
this.ttl = (config.ttl ?? 3600) * 1000;
|
|
2161
|
+
this.trackStats = config.trackStats ?? true;
|
|
2162
|
+
}
|
|
2163
|
+
async get(key) {
|
|
2164
|
+
const entry = this.cache.get(key);
|
|
2165
|
+
if (!entry) {
|
|
2166
|
+
if (this.trackStats)
|
|
2167
|
+
this.stats.misses++;
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
if (Date.now() > entry.expires) {
|
|
2171
|
+
this.cache.delete(key);
|
|
2172
|
+
this.currentSize -= entry.size;
|
|
2173
|
+
if (this.trackStats)
|
|
2174
|
+
this.stats.misses++;
|
|
2175
|
+
return null;
|
|
2176
|
+
}
|
|
2177
|
+
entry.accessCount++;
|
|
2178
|
+
entry.lastAccessed = Date.now();
|
|
2179
|
+
if (this.trackStats)
|
|
2180
|
+
this.stats.hits++;
|
|
2181
|
+
return entry.value;
|
|
2182
|
+
}
|
|
2183
|
+
async set(key, value, ttl) {
|
|
2184
|
+
const size = this.estimateSize(value);
|
|
2185
|
+
const expiryTime = ttl ? ttl * 1000 : this.ttl;
|
|
2186
|
+
const now = Date.now();
|
|
2187
|
+
const existing = this.cache.get(key);
|
|
2188
|
+
if (existing) {
|
|
2189
|
+
this.currentSize -= existing.size;
|
|
2190
|
+
this.cache.delete(key);
|
|
2191
|
+
}
|
|
2192
|
+
while ((this.currentSize + size > this.maxSize || this.cache.size >= this.maxEntries) &&
|
|
2193
|
+
this.cache.size > 0) {
|
|
2194
|
+
await this.evictLFU();
|
|
2195
|
+
}
|
|
2196
|
+
const entry = {
|
|
2197
|
+
key,
|
|
2198
|
+
value,
|
|
2199
|
+
expires: now + expiryTime,
|
|
2200
|
+
size,
|
|
2201
|
+
accessCount: 0,
|
|
2202
|
+
lastAccessed: now,
|
|
2203
|
+
created: now
|
|
2204
|
+
};
|
|
2205
|
+
this.cache.set(key, entry);
|
|
2206
|
+
this.currentSize += size;
|
|
2207
|
+
}
|
|
2208
|
+
async delete(key) {
|
|
2209
|
+
const entry = this.cache.get(key);
|
|
2210
|
+
if (entry) {
|
|
2211
|
+
this.cache.delete(key);
|
|
2212
|
+
this.currentSize -= entry.size;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
async clear() {
|
|
2216
|
+
this.cache.clear();
|
|
2217
|
+
this.currentSize = 0;
|
|
2218
|
+
this.stats = { hits: 0, misses: 0, evictions: 0 };
|
|
2219
|
+
}
|
|
2220
|
+
async evictLFU() {
|
|
2221
|
+
let minAccessCount = Infinity;
|
|
2222
|
+
let keyToEvict = null;
|
|
2223
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
2224
|
+
if (entry.accessCount < minAccessCount) {
|
|
2225
|
+
minAccessCount = entry.accessCount;
|
|
2226
|
+
keyToEvict = key;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
if (keyToEvict) {
|
|
2230
|
+
const entry = this.cache.get(keyToEvict);
|
|
2231
|
+
if (entry) {
|
|
2232
|
+
this.currentSize -= entry.size;
|
|
2233
|
+
}
|
|
2234
|
+
this.cache.delete(keyToEvict);
|
|
2235
|
+
if (this.trackStats)
|
|
2236
|
+
this.stats.evictions++;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
estimateSize(str) {
|
|
2240
|
+
return str.length * 2;
|
|
2241
|
+
}
|
|
2242
|
+
getStats() {
|
|
2243
|
+
const total = this.stats.hits + this.stats.misses;
|
|
2244
|
+
return {
|
|
2245
|
+
hits: this.stats.hits,
|
|
2246
|
+
misses: this.stats.misses,
|
|
2247
|
+
hitRate: total > 0 ? this.stats.hits / total : 0,
|
|
2248
|
+
missRate: total > 0 ? this.stats.misses / total : 0,
|
|
2249
|
+
size: this.currentSize,
|
|
2250
|
+
entryCount: this.cache.size,
|
|
2251
|
+
evictions: this.stats.evictions,
|
|
2252
|
+
maxSize: this.maxSize,
|
|
2253
|
+
maxEntries: this.maxEntries
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
class CacheWithInvalidation {
|
|
2258
|
+
constructor(baseCache, config = {}) {
|
|
2259
|
+
this.baseCache = baseCache;
|
|
2260
|
+
this.invalidationPatterns = new Set(config.invalidateOn?.patterns || []);
|
|
2261
|
+
this.mutationPatterns = new Map();
|
|
2262
|
+
if (config.invalidateOn?.mutations !== false) {
|
|
2263
|
+
this.mutationPatterns.set('shops/*/update', ['shops/*']);
|
|
2264
|
+
this.mutationPatterns.set('listings/*/update', ['listings/*']);
|
|
2265
|
+
this.mutationPatterns.set('receipts/*/update', ['receipts/*']);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
async get(key) {
|
|
2269
|
+
return this.baseCache.get(key);
|
|
2270
|
+
}
|
|
2271
|
+
async set(key, value, ttl) {
|
|
2272
|
+
return this.baseCache.set(key, value, ttl);
|
|
2273
|
+
}
|
|
2274
|
+
async delete(key) {
|
|
2275
|
+
return this.baseCache.delete(key);
|
|
2276
|
+
}
|
|
2277
|
+
async clear() {
|
|
2278
|
+
return this.baseCache.clear();
|
|
2279
|
+
}
|
|
2280
|
+
async invalidatePattern(_pattern) {
|
|
2281
|
+
await this.baseCache.clear();
|
|
2282
|
+
}
|
|
2283
|
+
async invalidateOnMutation(mutationType) {
|
|
2284
|
+
const patterns = this.mutationPatterns.get(mutationType);
|
|
2285
|
+
if (patterns) {
|
|
2286
|
+
for (const pattern of patterns) {
|
|
2287
|
+
await this.invalidatePattern(pattern);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
addInvalidationPattern(pattern) {
|
|
2292
|
+
this.invalidationPatterns.add(pattern);
|
|
2293
|
+
}
|
|
2294
|
+
removeInvalidationPattern(pattern) {
|
|
2295
|
+
this.invalidationPatterns.delete(pattern);
|
|
2296
|
+
}
|
|
2297
|
+
addMutationPattern(mutationType, patterns) {
|
|
2298
|
+
this.mutationPatterns.set(mutationType, patterns);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
class RedisCacheStorage {
|
|
2302
|
+
constructor(config, client) {
|
|
2303
|
+
this.config = config;
|
|
2304
|
+
this.keyPrefix = config.keyPrefix || 'etsy:';
|
|
2305
|
+
if (!client) {
|
|
2306
|
+
throw new Error('Redis client must be provided. Install a Redis client library (e.g., ioredis) and pass the client instance.');
|
|
2307
|
+
}
|
|
2308
|
+
this.client = client;
|
|
2309
|
+
}
|
|
2310
|
+
async get(key) {
|
|
2311
|
+
try {
|
|
2312
|
+
const value = await this.client.get(this.keyPrefix + key);
|
|
2313
|
+
return value;
|
|
2314
|
+
}
|
|
2315
|
+
catch (error) {
|
|
2316
|
+
console.error('Redis get error:', error);
|
|
2317
|
+
return null;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
async set(key, value, ttl = 3600) {
|
|
2321
|
+
try {
|
|
2322
|
+
await this.client.setex(this.keyPrefix + key, ttl, value);
|
|
2323
|
+
}
|
|
2324
|
+
catch (error) {
|
|
2325
|
+
console.error('Redis set error:', error);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
async delete(key) {
|
|
2329
|
+
try {
|
|
2330
|
+
await this.client.del(this.keyPrefix + key);
|
|
2331
|
+
}
|
|
2332
|
+
catch (error) {
|
|
2333
|
+
console.error('Redis delete error:', error);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
async clear() {
|
|
2337
|
+
try {
|
|
2338
|
+
const keys = await this.client.keys(this.keyPrefix + '*');
|
|
2339
|
+
if (keys.length > 0) {
|
|
2340
|
+
await this.client.del(...keys);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
catch (error) {
|
|
2344
|
+
console.error('Redis clear error:', error);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
getClient() {
|
|
2348
|
+
return this.client;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
function createCacheStorage(config = {}) {
|
|
2352
|
+
const strategy = config.strategy || 'lru';
|
|
2353
|
+
switch (strategy) {
|
|
2354
|
+
case 'lru':
|
|
2355
|
+
return new LRUCache(config);
|
|
2356
|
+
case 'lfu':
|
|
2357
|
+
return new LFUCache(config);
|
|
2358
|
+
case 'ttl':
|
|
2359
|
+
return new LRUCache(config);
|
|
2360
|
+
default:
|
|
2361
|
+
return new LRUCache(config);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
function createCacheWithInvalidation(baseCache, config = {}) {
|
|
2365
|
+
return new CacheWithInvalidation(baseCache, config);
|
|
2366
|
+
}
|
|
2367
|
+
function createRedisCacheStorage(config, client) {
|
|
2368
|
+
return new RedisCacheStorage(config, client);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
class ListingQueryBuilder {
|
|
2372
|
+
constructor(client, shopId) {
|
|
2373
|
+
this.params = {};
|
|
2374
|
+
this.includeFields = [];
|
|
2375
|
+
this.client = client;
|
|
2376
|
+
this.shopId = shopId;
|
|
2377
|
+
}
|
|
2378
|
+
where(filters) {
|
|
2379
|
+
if (filters.state) {
|
|
2380
|
+
this.params.state = filters.state;
|
|
2381
|
+
}
|
|
2382
|
+
return this;
|
|
2383
|
+
}
|
|
2384
|
+
include(fields) {
|
|
2385
|
+
this.includeFields = fields;
|
|
2386
|
+
this.params.includes = fields;
|
|
2387
|
+
return this;
|
|
2388
|
+
}
|
|
2389
|
+
limit(limit) {
|
|
2390
|
+
this.params.limit = limit;
|
|
2391
|
+
return this;
|
|
2392
|
+
}
|
|
2393
|
+
offset(offset) {
|
|
2394
|
+
this.params.offset = offset;
|
|
2395
|
+
return this;
|
|
2396
|
+
}
|
|
2397
|
+
sortBy(field, order = 'asc') {
|
|
2398
|
+
this.params.sort_on = field;
|
|
2399
|
+
this.params.sort_order = (order === 'asc' ? 'up' : 'down');
|
|
2400
|
+
return this;
|
|
2401
|
+
}
|
|
2402
|
+
async fetch() {
|
|
2403
|
+
if (!this.shopId) {
|
|
2404
|
+
throw new Error('Shop ID is required for listing queries');
|
|
2405
|
+
}
|
|
2406
|
+
return this.client.getListingsByShop(this.shopId, this.params);
|
|
2407
|
+
}
|
|
2408
|
+
async first() {
|
|
2409
|
+
this.params.limit = 1;
|
|
2410
|
+
const results = await this.fetch();
|
|
2411
|
+
return results[0] || null;
|
|
2412
|
+
}
|
|
2413
|
+
async count() {
|
|
2414
|
+
const results = await this.fetch();
|
|
2415
|
+
return results.length;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
class ReceiptQueryBuilder {
|
|
2419
|
+
constructor(client, shopId) {
|
|
2420
|
+
this.params = {};
|
|
2421
|
+
this.client = client;
|
|
2422
|
+
this.shopId = shopId;
|
|
2423
|
+
}
|
|
2424
|
+
where(filters) {
|
|
2425
|
+
Object.assign(this.params, filters);
|
|
2426
|
+
return this;
|
|
2427
|
+
}
|
|
2428
|
+
limit(limit) {
|
|
2429
|
+
this.params.limit = limit;
|
|
2430
|
+
return this;
|
|
2431
|
+
}
|
|
2432
|
+
offset(offset) {
|
|
2433
|
+
this.params.offset = offset;
|
|
2434
|
+
return this;
|
|
2435
|
+
}
|
|
2436
|
+
sortBy(field, order = 'desc') {
|
|
2437
|
+
this.params.sort_on = field;
|
|
2438
|
+
this.params.sort_order = (order === 'asc' ? 'up' : 'down');
|
|
2439
|
+
return this;
|
|
2440
|
+
}
|
|
2441
|
+
async fetch() {
|
|
2442
|
+
return this.client.getShopReceipts(this.shopId, this.params);
|
|
2443
|
+
}
|
|
2444
|
+
async first() {
|
|
2445
|
+
this.params.limit = 1;
|
|
2446
|
+
const results = await this.fetch();
|
|
2447
|
+
return results[0] || null;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
class BatchQueryExecutor {
|
|
2451
|
+
constructor(client) {
|
|
2452
|
+
this.operations = [];
|
|
2453
|
+
this.client = client;
|
|
2454
|
+
}
|
|
2455
|
+
getShop(shopId) {
|
|
2456
|
+
this.operations.push(() => this.client.getShop(shopId));
|
|
2457
|
+
return this;
|
|
2458
|
+
}
|
|
2459
|
+
getListings(shopId, params) {
|
|
2460
|
+
this.operations.push(() => this.client.getListingsByShop(shopId, params));
|
|
2461
|
+
return this;
|
|
2462
|
+
}
|
|
2463
|
+
getReceipts(shopId, params) {
|
|
2464
|
+
this.operations.push(() => this.client.getShopReceipts(shopId, params));
|
|
2465
|
+
return this;
|
|
2466
|
+
}
|
|
2467
|
+
custom(operation) {
|
|
2468
|
+
this.operations.push(operation);
|
|
2469
|
+
return this;
|
|
2470
|
+
}
|
|
2471
|
+
async execute() {
|
|
2472
|
+
return Promise.all(this.operations.map(op => op()));
|
|
2473
|
+
}
|
|
2474
|
+
async executeSequential() {
|
|
2475
|
+
const results = [];
|
|
2476
|
+
for (const operation of this.operations) {
|
|
2477
|
+
results.push(await operation());
|
|
2478
|
+
}
|
|
2479
|
+
return results;
|
|
2480
|
+
}
|
|
2481
|
+
clear() {
|
|
2482
|
+
this.operations = [];
|
|
2483
|
+
return this;
|
|
2484
|
+
}
|
|
2485
|
+
size() {
|
|
2486
|
+
return this.operations.length;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function withQueryBuilder(client) {
|
|
2490
|
+
const extendedClient = client;
|
|
2491
|
+
extendedClient.listings = function (shopId) {
|
|
2492
|
+
return new ListingQueryBuilder(this, shopId);
|
|
2493
|
+
};
|
|
2494
|
+
extendedClient.receipts = function (shopId) {
|
|
2495
|
+
return new ReceiptQueryBuilder(this, shopId);
|
|
2496
|
+
};
|
|
2497
|
+
extendedClient.batch = function () {
|
|
2498
|
+
return new BatchQueryExecutor(this);
|
|
2499
|
+
};
|
|
2500
|
+
return extendedClient;
|
|
2501
|
+
}
|
|
2502
|
+
function createListingQuery(client, shopId) {
|
|
2503
|
+
return new ListingQueryBuilder(client, shopId);
|
|
2504
|
+
}
|
|
2505
|
+
function createReceiptQuery(client, shopId) {
|
|
2506
|
+
return new ReceiptQueryBuilder(client, shopId);
|
|
2507
|
+
}
|
|
2508
|
+
function createBatchQuery(client) {
|
|
2509
|
+
return new BatchQueryExecutor(client);
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
async function encryptAES256GCM(data, key) {
|
|
2513
|
+
if (!isNode$1) {
|
|
2514
|
+
throw new Error('AES-256-GCM encryption is only available in Node.js environments');
|
|
2515
|
+
}
|
|
2516
|
+
const crypto = await import('crypto');
|
|
2517
|
+
const keyBuffer = Buffer.isBuffer(key) ? key : Buffer.from(key, 'utf8');
|
|
2518
|
+
if (keyBuffer.length !== 32) {
|
|
2519
|
+
throw new Error('Encryption key must be exactly 32 bytes for AES-256');
|
|
2520
|
+
}
|
|
2521
|
+
const iv = crypto.randomBytes(12);
|
|
2522
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv);
|
|
2523
|
+
const encrypted = Buffer.concat([
|
|
2524
|
+
cipher.update(data, 'utf8'),
|
|
2525
|
+
cipher.final()
|
|
2526
|
+
]);
|
|
2527
|
+
const authTag = cipher.getAuthTag();
|
|
2528
|
+
return {
|
|
2529
|
+
ciphertext: encrypted.toString('base64'),
|
|
2530
|
+
iv: iv.toString('base64'),
|
|
2531
|
+
authTag: authTag.toString('base64'),
|
|
2532
|
+
algorithm: 'aes-256-gcm'
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
async function decryptAES256GCM(encryptedData, key) {
|
|
2536
|
+
if (!isNode$1) {
|
|
2537
|
+
throw new Error('AES-256-GCM decryption is only available in Node.js environments');
|
|
2538
|
+
}
|
|
2539
|
+
const crypto = await import('crypto');
|
|
2540
|
+
const keyBuffer = Buffer.isBuffer(key) ? key : Buffer.from(key, 'utf8');
|
|
2541
|
+
if (keyBuffer.length !== 32) {
|
|
2542
|
+
throw new Error('Decryption key must be exactly 32 bytes for AES-256');
|
|
2543
|
+
}
|
|
2544
|
+
if (encryptedData.algorithm !== 'aes-256-gcm') {
|
|
2545
|
+
throw new Error(`Unsupported encryption algorithm: ${encryptedData.algorithm}`);
|
|
2546
|
+
}
|
|
2547
|
+
const iv = Buffer.from(encryptedData.iv, 'base64');
|
|
2548
|
+
const authTag = Buffer.from(encryptedData.authTag, 'base64');
|
|
2549
|
+
const ciphertext = Buffer.from(encryptedData.ciphertext, 'base64');
|
|
2550
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, iv);
|
|
2551
|
+
decipher.setAuthTag(authTag);
|
|
2552
|
+
const decrypted = Buffer.concat([
|
|
2553
|
+
decipher.update(ciphertext),
|
|
2554
|
+
decipher.final()
|
|
2555
|
+
]);
|
|
2556
|
+
return decrypted.toString('utf8');
|
|
2557
|
+
}
|
|
2558
|
+
async function generateEncryptionKey(length = 32) {
|
|
2559
|
+
if (!isNode$1) {
|
|
2560
|
+
throw new Error('Key generation is only available in Node.js environments');
|
|
2561
|
+
}
|
|
2562
|
+
const crypto = await import('crypto');
|
|
2563
|
+
return crypto.randomBytes(length);
|
|
2564
|
+
}
|
|
2565
|
+
async function deriveKeyFromPassword(password, salt, iterations = 100000, keyLength = 32) {
|
|
2566
|
+
if (!isNode$1) {
|
|
2567
|
+
throw new Error('Key derivation is only available in Node.js environments');
|
|
2568
|
+
}
|
|
2569
|
+
const crypto = await import('crypto');
|
|
2570
|
+
const saltBuffer = Buffer.isBuffer(salt) ? salt : Buffer.from(salt, 'utf8');
|
|
2571
|
+
return new Promise((resolve, reject) => {
|
|
2572
|
+
crypto.pbkdf2(password, saltBuffer, iterations, keyLength, 'sha256', (err, derivedKey) => {
|
|
2573
|
+
if (err) {
|
|
2574
|
+
reject(err);
|
|
2575
|
+
}
|
|
2576
|
+
else {
|
|
2577
|
+
resolve(derivedKey);
|
|
2578
|
+
}
|
|
2579
|
+
});
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
function validateEncryptionKey(key, requiredLength = 32) {
|
|
2583
|
+
const keyBuffer = Buffer.isBuffer(key) ? key : Buffer.from(key, 'utf8');
|
|
2584
|
+
if (keyBuffer.length !== requiredLength) {
|
|
2585
|
+
throw new Error(`Invalid encryption key length: expected ${requiredLength} bytes, got ${keyBuffer.length} bytes`);
|
|
2586
|
+
}
|
|
2587
|
+
return true;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
class EncryptedFileTokenStorage {
|
|
2591
|
+
constructor(config) {
|
|
2592
|
+
if (!isNode$1) {
|
|
2593
|
+
throw new Error('EncryptedFileTokenStorage is only available in Node.js environments');
|
|
2594
|
+
}
|
|
2595
|
+
this.filePath = config.filePath;
|
|
2596
|
+
this.fileMode = config.fileMode ?? 0o600;
|
|
2597
|
+
const keyBuffer = Buffer.isBuffer(config.encryptionKey)
|
|
2598
|
+
? config.encryptionKey
|
|
2599
|
+
: Buffer.from(config.encryptionKey, 'utf8');
|
|
2600
|
+
validateEncryptionKey(keyBuffer, 32);
|
|
2601
|
+
this.encryptionKey = keyBuffer;
|
|
2602
|
+
}
|
|
2603
|
+
async save(tokens) {
|
|
2604
|
+
if (!isNode$1) {
|
|
2605
|
+
throw new Error('EncryptedFileTokenStorage is only available in Node.js');
|
|
2606
|
+
}
|
|
2607
|
+
try {
|
|
2608
|
+
const tokenData = JSON.stringify(tokens);
|
|
2609
|
+
const encrypted = await encryptAES256GCM(tokenData, this.encryptionKey);
|
|
2610
|
+
const fileData = {
|
|
2611
|
+
version: 1,
|
|
2612
|
+
encrypted,
|
|
2613
|
+
timestamp: Date.now()
|
|
2614
|
+
};
|
|
2615
|
+
const fileContent = JSON.stringify(fileData, null, 2);
|
|
2616
|
+
await this._writeFile(this.filePath, fileContent);
|
|
2617
|
+
await this._setFilePermissions(this.filePath, this.fileMode);
|
|
2618
|
+
}
|
|
2619
|
+
catch (error) {
|
|
2620
|
+
throw new Error(`Failed to save encrypted tokens: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
async load() {
|
|
2624
|
+
if (!isNode$1) {
|
|
2625
|
+
return null;
|
|
2626
|
+
}
|
|
2627
|
+
try {
|
|
2628
|
+
const fileContent = await this._readFile(this.filePath);
|
|
2629
|
+
const fileData = JSON.parse(fileContent);
|
|
2630
|
+
if (fileData.version !== 1) {
|
|
2631
|
+
throw new Error(`Unsupported encrypted file format version: ${fileData.version}`);
|
|
2632
|
+
}
|
|
2633
|
+
const decryptedData = await decryptAES256GCM(fileData.encrypted, this.encryptionKey);
|
|
2634
|
+
const tokens = JSON.parse(decryptedData);
|
|
2635
|
+
if (tokens.expires_at) {
|
|
2636
|
+
tokens.expires_at = new Date(tokens.expires_at);
|
|
2637
|
+
}
|
|
2638
|
+
return tokens;
|
|
2639
|
+
}
|
|
2640
|
+
catch (error) {
|
|
2641
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
2642
|
+
return null;
|
|
2643
|
+
}
|
|
2644
|
+
console.error('Failed to load encrypted tokens:', error instanceof Error ? error.message : 'Unknown error');
|
|
2645
|
+
return null;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
async clear() {
|
|
2649
|
+
if (!isNode$1) {
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
try {
|
|
2653
|
+
await this._deleteFile(this.filePath);
|
|
2654
|
+
}
|
|
2655
|
+
catch (error) {
|
|
2656
|
+
if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
|
|
2657
|
+
console.error('Failed to clear encrypted tokens:', error instanceof Error ? error.message : 'Unknown error');
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
getFilePath() {
|
|
2662
|
+
return this.filePath;
|
|
2663
|
+
}
|
|
2664
|
+
async exists() {
|
|
2665
|
+
if (!isNode$1) {
|
|
2666
|
+
return false;
|
|
2667
|
+
}
|
|
2668
|
+
try {
|
|
2669
|
+
await this._stat(this.filePath);
|
|
2670
|
+
return true;
|
|
2671
|
+
}
|
|
2672
|
+
catch {
|
|
2673
|
+
return false;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
async _writeFile(filePath, data) {
|
|
2677
|
+
if (typeof process === 'undefined')
|
|
2678
|
+
return;
|
|
2679
|
+
const fs = await import('fs');
|
|
2680
|
+
await fs.promises.writeFile(filePath, data, 'utf8');
|
|
2681
|
+
}
|
|
2682
|
+
async _setFilePermissions(filePath, mode) {
|
|
2683
|
+
if (typeof process === 'undefined')
|
|
2684
|
+
return;
|
|
2685
|
+
try {
|
|
2686
|
+
const fs = await import('fs');
|
|
2687
|
+
await fs.promises.chmod(filePath, mode);
|
|
2688
|
+
}
|
|
2689
|
+
catch {
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
async _readFile(filePath) {
|
|
2693
|
+
if (typeof process === 'undefined')
|
|
2694
|
+
throw new Error('Not available');
|
|
2695
|
+
const fs = await import('fs');
|
|
2696
|
+
return await fs.promises.readFile(filePath, 'utf8');
|
|
2697
|
+
}
|
|
2698
|
+
async _deleteFile(filePath) {
|
|
2699
|
+
if (typeof process === 'undefined')
|
|
2700
|
+
return;
|
|
2701
|
+
const fs = await import('fs');
|
|
2702
|
+
await fs.promises.unlink(filePath);
|
|
2703
|
+
}
|
|
2704
|
+
async _stat(filePath) {
|
|
2705
|
+
if (typeof process === 'undefined')
|
|
2706
|
+
throw new Error('Not available');
|
|
2707
|
+
const fs = await import('fs');
|
|
2708
|
+
return await fs.promises.stat(filePath);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
class WebhookSecurity {
|
|
2713
|
+
constructor(config) {
|
|
2714
|
+
this.secret = config.secret;
|
|
2715
|
+
this.algorithm = config.algorithm || 'sha256';
|
|
2716
|
+
if (!this.secret) {
|
|
2717
|
+
throw new Error('Webhook secret is required');
|
|
2718
|
+
}
|
|
2719
|
+
if (isNode$1) {
|
|
2720
|
+
try {
|
|
2721
|
+
this.crypto = require('crypto');
|
|
2722
|
+
}
|
|
2723
|
+
catch {
|
|
2724
|
+
throw new Error('crypto module not available, webhook security requires Node.js');
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
else {
|
|
2728
|
+
throw new Error('WebhookSecurity is only available in Node.js environments');
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
signRequest(payload, encoding = 'hex') {
|
|
2732
|
+
if (!this.crypto) {
|
|
2733
|
+
throw new Error('Crypto module not available');
|
|
2734
|
+
}
|
|
2735
|
+
const data = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
2736
|
+
const hmac = this.crypto
|
|
2737
|
+
.createHmac(this.algorithm, this.secret)
|
|
2738
|
+
.update(data)
|
|
2739
|
+
.digest(encoding);
|
|
2740
|
+
return hmac;
|
|
2741
|
+
}
|
|
2742
|
+
verifySignature(payload, signature, encoding = 'hex') {
|
|
2743
|
+
if (!this.crypto) {
|
|
2744
|
+
throw new Error('Crypto module not available');
|
|
2745
|
+
}
|
|
2746
|
+
try {
|
|
2747
|
+
const expectedSignature = this.signRequest(payload, encoding);
|
|
2748
|
+
return this.timingSafeEqual(expectedSignature, signature);
|
|
2749
|
+
}
|
|
2750
|
+
catch (error) {
|
|
2751
|
+
console.error('Signature verification failed:', error);
|
|
2752
|
+
return false;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
signRequestWithTimestamp(payload, timestamp, encoding = 'hex') {
|
|
2756
|
+
const ts = timestamp || Math.floor(Date.now() / 1000);
|
|
2757
|
+
const data = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
2758
|
+
const signedPayload = `${ts}.${data}`;
|
|
2759
|
+
const signature = this.signRequest(signedPayload, encoding);
|
|
2760
|
+
return {
|
|
2761
|
+
timestamp: ts,
|
|
2762
|
+
signature
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
verifySignatureWithTimestamp(payload, signature, timestamp, maxAge = 300, encoding = 'hex') {
|
|
2766
|
+
const now = Math.floor(Date.now() / 1000);
|
|
2767
|
+
const age = now - timestamp;
|
|
2768
|
+
if (age < 0) {
|
|
2769
|
+
console.warn('Webhook timestamp is in the future');
|
|
2770
|
+
return false;
|
|
2771
|
+
}
|
|
2772
|
+
if (age > maxAge) {
|
|
2773
|
+
console.warn(`Webhook request is too old: ${age} seconds`);
|
|
2774
|
+
return false;
|
|
2775
|
+
}
|
|
2776
|
+
const data = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
2777
|
+
const signedPayload = `${timestamp}.${data}`;
|
|
2778
|
+
return this.verifySignature(signedPayload, signature, encoding);
|
|
2779
|
+
}
|
|
2780
|
+
timingSafeEqual(a, b) {
|
|
2781
|
+
if (!this.crypto?.timingSafeEqual) {
|
|
2782
|
+
console.warn('crypto.timingSafeEqual not available, using simple comparison');
|
|
2783
|
+
return a === b;
|
|
2784
|
+
}
|
|
2785
|
+
const bufA = Buffer.from(a);
|
|
2786
|
+
const bufB = Buffer.from(b);
|
|
2787
|
+
if (bufA.length !== bufB.length) {
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
return this.crypto.timingSafeEqual(bufA, bufB);
|
|
2791
|
+
}
|
|
2792
|
+
getAlgorithm() {
|
|
2793
|
+
return this.algorithm;
|
|
2794
|
+
}
|
|
2795
|
+
updateSecret(newSecret) {
|
|
2796
|
+
if (!newSecret) {
|
|
2797
|
+
throw new Error('Secret cannot be empty');
|
|
2798
|
+
}
|
|
2799
|
+
this.secret = newSecret;
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
function createWebhookSecurity(secret, algorithm = 'sha256') {
|
|
2803
|
+
return new WebhookSecurity({ secret, algorithm });
|
|
2804
|
+
}
|
|
2805
|
+
|
|
920
2806
|
function createEtsyClient(config) {
|
|
921
2807
|
return new EtsyClient(config);
|
|
922
2808
|
}
|
|
@@ -929,43 +2815,81 @@ function createTokenManager(config, storage) {
|
|
|
929
2815
|
function createRateLimiter(config) {
|
|
930
2816
|
return new EtsyRateLimiter(config);
|
|
931
2817
|
}
|
|
932
|
-
const VERSION = '
|
|
2818
|
+
const VERSION = '2.2.0';
|
|
933
2819
|
const LIBRARY_NAME = 'etsy-v3-api-client';
|
|
934
2820
|
function getLibraryInfo() {
|
|
935
2821
|
return {
|
|
936
2822
|
name: LIBRARY_NAME,
|
|
937
2823
|
version: VERSION,
|
|
938
2824
|
description: 'JavaScript/TypeScript client for the Etsy Open API v3 with OAuth 2.0 authentication',
|
|
939
|
-
author: '
|
|
2825
|
+
author: 'profplum700',
|
|
940
2826
|
license: 'MIT',
|
|
941
2827
|
homepage: 'https://github.com/ForestHillArtsHouse/etsy-v3-api-client#readme'
|
|
942
2828
|
};
|
|
943
2829
|
}
|
|
944
2830
|
|
|
945
2831
|
exports.AuthHelper = AuthHelper;
|
|
2832
|
+
exports.BatchQueryExecutor = BatchQueryExecutor;
|
|
2833
|
+
exports.BulkOperationManager = BulkOperationManager;
|
|
946
2834
|
exports.COMMON_SCOPE_COMBINATIONS = COMMON_SCOPE_COMBINATIONS;
|
|
2835
|
+
exports.CacheWithInvalidation = CacheWithInvalidation;
|
|
2836
|
+
exports.CreateListingSchema = CreateListingSchema;
|
|
2837
|
+
exports.DEFAULT_RETRY_CONFIG = DEFAULT_RETRY_CONFIG;
|
|
947
2838
|
exports.ETSY_SCOPES = ETSY_SCOPES;
|
|
2839
|
+
exports.EncryptedFileTokenStorage = EncryptedFileTokenStorage;
|
|
948
2840
|
exports.EtsyApiError = EtsyApiError;
|
|
949
2841
|
exports.EtsyAuthError = EtsyAuthError;
|
|
950
2842
|
exports.EtsyClient = EtsyClient;
|
|
951
2843
|
exports.EtsyRateLimitError = EtsyRateLimitError;
|
|
952
2844
|
exports.EtsyRateLimiter = EtsyRateLimiter;
|
|
2845
|
+
exports.EtsyWebhookHandler = EtsyWebhookHandler;
|
|
2846
|
+
exports.FieldValidator = FieldValidator;
|
|
953
2847
|
exports.FileTokenStorage = FileTokenStorage;
|
|
2848
|
+
exports.LFUCache = LFUCache;
|
|
954
2849
|
exports.LIBRARY_NAME = LIBRARY_NAME;
|
|
2850
|
+
exports.LRUCache = LRUCache;
|
|
2851
|
+
exports.ListingQueryBuilder = ListingQueryBuilder;
|
|
955
2852
|
exports.LocalStorageTokenStorage = LocalStorageTokenStorage;
|
|
956
2853
|
exports.MemoryTokenStorage = MemoryTokenStorage;
|
|
2854
|
+
exports.PaginatedResults = PaginatedResults;
|
|
2855
|
+
exports.ReceiptQueryBuilder = ReceiptQueryBuilder;
|
|
2856
|
+
exports.RedisCacheStorage = RedisCacheStorage;
|
|
2857
|
+
exports.RetryManager = RetryManager;
|
|
957
2858
|
exports.SessionStorageTokenStorage = SessionStorageTokenStorage;
|
|
958
2859
|
exports.TokenManager = TokenManager;
|
|
2860
|
+
exports.UpdateListingSchema = UpdateListingSchema;
|
|
2861
|
+
exports.UpdateShopSchema = UpdateShopSchema;
|
|
959
2862
|
exports.VERSION = VERSION;
|
|
2863
|
+
exports.ValidationException = ValidationException;
|
|
2864
|
+
exports.Validator = Validator;
|
|
2865
|
+
exports.WebhookSecurity = WebhookSecurity;
|
|
2866
|
+
exports.combineValidators = combineValidators;
|
|
960
2867
|
exports.createAuthHelper = createAuthHelper;
|
|
2868
|
+
exports.createBatchQuery = createBatchQuery;
|
|
2869
|
+
exports.createBulkOperationManager = createBulkOperationManager;
|
|
2870
|
+
exports.createCacheStorage = createCacheStorage;
|
|
2871
|
+
exports.createCacheWithInvalidation = createCacheWithInvalidation;
|
|
961
2872
|
exports.createCodeChallenge = createCodeChallenge;
|
|
962
2873
|
exports.createDefaultTokenStorage = createDefaultTokenStorage;
|
|
963
2874
|
exports.createEtsyClient = createEtsyClient;
|
|
2875
|
+
exports.createListingQuery = createListingQuery;
|
|
2876
|
+
exports.createPaginatedResults = createPaginatedResults;
|
|
964
2877
|
exports.createRateLimiter = createRateLimiter;
|
|
2878
|
+
exports.createReceiptQuery = createReceiptQuery;
|
|
2879
|
+
exports.createRedisCacheStorage = createRedisCacheStorage;
|
|
965
2880
|
exports.createTokenManager = createTokenManager;
|
|
2881
|
+
exports.createValidator = createValidator;
|
|
2882
|
+
exports.createWebhookHandler = createWebhookHandler;
|
|
2883
|
+
exports.createWebhookSecurity = createWebhookSecurity;
|
|
2884
|
+
exports.decryptAES256GCM = decryptAES256GCM;
|
|
966
2885
|
exports.default = EtsyClient;
|
|
967
2886
|
exports.defaultRateLimiter = defaultRateLimiter;
|
|
2887
|
+
exports.deriveKeyFromPassword = deriveKeyFromPassword;
|
|
2888
|
+
exports.encryptAES256GCM = encryptAES256GCM;
|
|
2889
|
+
exports.executeBulkOperation = executeBulkOperation;
|
|
2890
|
+
exports.field = field;
|
|
968
2891
|
exports.generateCodeVerifier = generateCodeVerifier;
|
|
2892
|
+
exports.generateEncryptionKey = generateEncryptionKey;
|
|
969
2893
|
exports.generateRandomBase64Url = generateRandomBase64Url;
|
|
970
2894
|
exports.generateState = generateState;
|
|
971
2895
|
exports.getAvailableStorage = getAvailableStorage;
|
|
@@ -977,4 +2901,9 @@ exports.isBrowser = isBrowser$1;
|
|
|
977
2901
|
exports.isNode = isNode$1;
|
|
978
2902
|
exports.sha256 = sha256;
|
|
979
2903
|
exports.sha256Base64Url = sha256Base64Url;
|
|
2904
|
+
exports.validate = validate;
|
|
2905
|
+
exports.validateEncryptionKey = validateEncryptionKey;
|
|
2906
|
+
exports.validateOrThrow = validateOrThrow;
|
|
2907
|
+
exports.withQueryBuilder = withQueryBuilder;
|
|
2908
|
+
exports.withRetry = withRetry;
|
|
980
2909
|
//# sourceMappingURL=node.cjs.map
|