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