@profplum700/etsy-v3-api-client 1.0.2 → 2.3.5
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 +2481 -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 +2661 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1204 -5
- package/dist/index.esm.js +2608 -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 +2661 -5
- package/dist/node.cjs.map +1 -1
- package/dist/node.esm.js +2608 -6
- package/dist/node.esm.js.map +1 -1
- package/package.json +28 -7
package/dist/node.esm.js
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
class EtsyApiError extends Error {
|
|
2
|
-
constructor(message, _statusCode, _response) {
|
|
2
|
+
constructor(message, _statusCode, _response, _retryAfter, endpoint) {
|
|
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.endpoint = endpoint;
|
|
9
|
+
this.timestamp = new Date();
|
|
10
|
+
this.details = {
|
|
11
|
+
statusCode: _statusCode || 0,
|
|
12
|
+
retryAfter: _retryAfter,
|
|
13
|
+
endpoint,
|
|
14
|
+
timestamp: this.timestamp
|
|
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
|
+
if (Array.isArray(resp.errors)) {
|
|
22
|
+
this.details.validationErrors = resp.errors.map((err) => {
|
|
23
|
+
if (err && typeof err === 'object') {
|
|
24
|
+
const e = err;
|
|
25
|
+
return {
|
|
26
|
+
field: String(e.field || 'unknown'),
|
|
27
|
+
message: String(e.message || 'Validation error')
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return { field: 'unknown', message: String(err) };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
this.suggestions = this.generateSuggestions();
|
|
35
|
+
this.docsUrl = this.generateDocsUrl();
|
|
7
36
|
}
|
|
8
37
|
get statusCode() {
|
|
9
38
|
return this._statusCode;
|
|
@@ -11,6 +40,188 @@ class EtsyApiError extends Error {
|
|
|
11
40
|
get response() {
|
|
12
41
|
return this._response;
|
|
13
42
|
}
|
|
43
|
+
isRetryable() {
|
|
44
|
+
if (!this._statusCode) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (this._statusCode === 429) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (this._statusCode >= 500 && this._statusCode < 600) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
const retryableClientErrors = [408, 409, 423, 425];
|
|
54
|
+
if (retryableClientErrors.includes(this._statusCode)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
getRetryAfter() {
|
|
60
|
+
return this._retryAfter || null;
|
|
61
|
+
}
|
|
62
|
+
getRateLimitReset() {
|
|
63
|
+
if (this._statusCode !== 429 || !this._retryAfter) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return new Date(Date.now() + this._retryAfter * 1000);
|
|
67
|
+
}
|
|
68
|
+
generateSuggestions() {
|
|
69
|
+
const suggestions = [];
|
|
70
|
+
if (!this._statusCode) {
|
|
71
|
+
return ['Check your network connection and try again'];
|
|
72
|
+
}
|
|
73
|
+
switch (this._statusCode) {
|
|
74
|
+
case 400:
|
|
75
|
+
suggestions.push('Review the Etsy API documentation for this endpoint');
|
|
76
|
+
suggestions.push('Check all required parameters are provided');
|
|
77
|
+
suggestions.push('Validate parameter formats and types');
|
|
78
|
+
if (this.details.validationErrors && this.details.validationErrors.length > 0) {
|
|
79
|
+
suggestions.push('\nValidation errors:');
|
|
80
|
+
this.details.validationErrors.forEach(err => {
|
|
81
|
+
suggestions.push(` • ${err.field}: ${err.message}`);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (this.details.field) {
|
|
85
|
+
suggestions.push(`Field causing issue: ${this.details.field}`);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case 401:
|
|
89
|
+
suggestions.push('Verify your access token is valid and not expired');
|
|
90
|
+
suggestions.push('Check if you need to refresh the token');
|
|
91
|
+
suggestions.push('Ensure you completed the OAuth flow correctly');
|
|
92
|
+
if (this.endpoint?.includes('/shops/')) {
|
|
93
|
+
suggestions.push('Verify the shop_id matches the authenticated user');
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case 403:
|
|
97
|
+
suggestions.push('Check if your OAuth app has the required scopes:');
|
|
98
|
+
if (this.endpoint?.includes('/listings')) {
|
|
99
|
+
suggestions.push(' • listings_r (for reading listings)');
|
|
100
|
+
suggestions.push(' • listings_w (for creating/updating listings)');
|
|
101
|
+
}
|
|
102
|
+
if (this.endpoint?.includes('/receipts') || this.endpoint?.includes('/transactions')) {
|
|
103
|
+
suggestions.push(' • transactions_r (for reading orders)');
|
|
104
|
+
}
|
|
105
|
+
if (this.endpoint?.includes('/shops')) {
|
|
106
|
+
suggestions.push(' • shops_r (for reading shop data)');
|
|
107
|
+
suggestions.push(' • shops_w (for updating shop data)');
|
|
108
|
+
}
|
|
109
|
+
suggestions.push('Verify your app is approved for production access');
|
|
110
|
+
suggestions.push('Check if the resource belongs to the authenticated user');
|
|
111
|
+
break;
|
|
112
|
+
case 404:
|
|
113
|
+
suggestions.push('Verify the resource ID exists and is spelled correctly');
|
|
114
|
+
if (this.endpoint?.includes('/listings/')) {
|
|
115
|
+
suggestions.push('Check if the listing is active and not deleted');
|
|
116
|
+
suggestions.push('Ensure the listing belongs to the authenticated shop');
|
|
117
|
+
}
|
|
118
|
+
if (this.endpoint?.includes('/shops/')) {
|
|
119
|
+
suggestions.push('Verify the shop ID is correct');
|
|
120
|
+
}
|
|
121
|
+
if (this.endpoint?.includes('/receipts/')) {
|
|
122
|
+
suggestions.push('Check if the receipt ID is valid');
|
|
123
|
+
suggestions.push('Ensure you have access to this shop\'s receipts');
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
case 409:
|
|
127
|
+
suggestions.push('Resource state conflict detected');
|
|
128
|
+
suggestions.push('Check if the resource was modified by another process');
|
|
129
|
+
suggestions.push('Try fetching the latest resource state before updating');
|
|
130
|
+
break;
|
|
131
|
+
case 429: {
|
|
132
|
+
const resetTime = this.getRateLimitReset();
|
|
133
|
+
const resetTimeStr = resetTime
|
|
134
|
+
? resetTime.toLocaleTimeString()
|
|
135
|
+
: 'shortly';
|
|
136
|
+
suggestions.push(`Rate limit exceeded. Resets at ${resetTimeStr}`);
|
|
137
|
+
suggestions.push('Implement exponential backoff retry logic');
|
|
138
|
+
suggestions.push('Consider caching responses to reduce API calls');
|
|
139
|
+
suggestions.push('Check if you can batch multiple operations');
|
|
140
|
+
if (this._retryAfter) {
|
|
141
|
+
suggestions.push(`Wait ${this._retryAfter} seconds before retrying`);
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case 500:
|
|
146
|
+
case 502:
|
|
147
|
+
case 503:
|
|
148
|
+
case 504:
|
|
149
|
+
suggestions.push('This is an Etsy server error, not your code');
|
|
150
|
+
suggestions.push('Retry the request after a short delay (exponential backoff)');
|
|
151
|
+
suggestions.push('Check Etsy API status: https://status.etsy.com');
|
|
152
|
+
if (this.isRetryable()) {
|
|
153
|
+
suggestions.push('This error is retryable - the request can be safely retried');
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
default:
|
|
157
|
+
suggestions.push('Check the Etsy API documentation for this endpoint');
|
|
158
|
+
suggestions.push('Review your request parameters and format');
|
|
159
|
+
if (this.details.suggestion) {
|
|
160
|
+
suggestions.push(`Etsy suggestion: ${this.details.suggestion}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return suggestions;
|
|
164
|
+
}
|
|
165
|
+
generateDocsUrl() {
|
|
166
|
+
const errorCode = this.details.errorCode?.toLowerCase() || this._statusCode?.toString() || 'unknown';
|
|
167
|
+
return `https://github.com/profplum700/etsy-v3-api-client/blob/main/docs/troubleshooting/ERROR_CODES.md#${errorCode}`;
|
|
168
|
+
}
|
|
169
|
+
getUserFriendlyMessage() {
|
|
170
|
+
let message = this.message;
|
|
171
|
+
if (this.details.suggestion) {
|
|
172
|
+
message += `\nSuggestion: ${this.details.suggestion}`;
|
|
173
|
+
}
|
|
174
|
+
if (this.isRetryable()) {
|
|
175
|
+
const retryAfter = this.getRetryAfter();
|
|
176
|
+
if (retryAfter) {
|
|
177
|
+
message += `\nRetry after ${retryAfter} seconds.`;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
message += '\nThis error can be retried.';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return message;
|
|
184
|
+
}
|
|
185
|
+
toString() {
|
|
186
|
+
const parts = [
|
|
187
|
+
`EtsyApiError: ${this.message}`,
|
|
188
|
+
`Status Code: ${this._statusCode || 'Unknown'}`,
|
|
189
|
+
];
|
|
190
|
+
if (this.details.errorCode) {
|
|
191
|
+
parts.push(`Error Code: ${this.details.errorCode}`);
|
|
192
|
+
}
|
|
193
|
+
if (this.endpoint) {
|
|
194
|
+
parts.push(`Endpoint: ${this.endpoint}`);
|
|
195
|
+
}
|
|
196
|
+
parts.push(`Timestamp: ${this.timestamp.toISOString()}`);
|
|
197
|
+
if (this.suggestions.length > 0) {
|
|
198
|
+
parts.push('\nSuggestions:');
|
|
199
|
+
this.suggestions.forEach(s => {
|
|
200
|
+
if (!s.startsWith(' •')) {
|
|
201
|
+
parts.push(` • ${s}`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
parts.push(s);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
parts.push(`\nDocumentation: ${this.docsUrl}`);
|
|
209
|
+
return parts.join('\n');
|
|
210
|
+
}
|
|
211
|
+
toJSON() {
|
|
212
|
+
return {
|
|
213
|
+
name: this.name,
|
|
214
|
+
message: this.message,
|
|
215
|
+
statusCode: this._statusCode,
|
|
216
|
+
errorCode: this.details.errorCode,
|
|
217
|
+
endpoint: this.endpoint,
|
|
218
|
+
suggestions: this.suggestions,
|
|
219
|
+
docsUrl: this.docsUrl,
|
|
220
|
+
timestamp: this.timestamp.toISOString(),
|
|
221
|
+
details: this.details,
|
|
222
|
+
isRetryable: this.isRetryable(),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
14
225
|
}
|
|
15
226
|
class EtsyAuthError extends Error {
|
|
16
227
|
constructor(message, _code) {
|
|
@@ -102,11 +313,12 @@ class MemoryTokenStorage {
|
|
|
102
313
|
}
|
|
103
314
|
}
|
|
104
315
|
class TokenManager {
|
|
105
|
-
constructor(config, storage) {
|
|
316
|
+
constructor(config, storage, rotationConfig) {
|
|
106
317
|
this.currentTokens = null;
|
|
107
318
|
this.keystring = config.keystring;
|
|
108
319
|
this.refreshCallback = config.refreshSave;
|
|
109
320
|
this.storage = storage;
|
|
321
|
+
this.rotationConfig = rotationConfig;
|
|
110
322
|
this.currentTokens = {
|
|
111
323
|
access_token: config.accessToken,
|
|
112
324
|
refresh_token: config.refreshToken,
|
|
@@ -114,6 +326,9 @@ class TokenManager {
|
|
|
114
326
|
token_type: 'Bearer',
|
|
115
327
|
scope: ''
|
|
116
328
|
};
|
|
329
|
+
if (this.rotationConfig?.enabled && this.rotationConfig?.autoSchedule) {
|
|
330
|
+
this.startRotationScheduler();
|
|
331
|
+
}
|
|
117
332
|
}
|
|
118
333
|
async getAccessToken() {
|
|
119
334
|
if (!this.currentTokens) {
|
|
@@ -174,8 +389,7 @@ class TokenManager {
|
|
|
174
389
|
body: body.toString()
|
|
175
390
|
});
|
|
176
391
|
if (!response.ok) {
|
|
177
|
-
|
|
178
|
-
throw new EtsyAuthError(`Token refresh failed: ${response.status} ${response.statusText} - ${errorText}`, 'TOKEN_REFRESH_FAILED');
|
|
392
|
+
throw new EtsyAuthError(`Token refresh failed: ${response.status} ${response.statusText}`, 'TOKEN_REFRESH_FAILED');
|
|
179
393
|
}
|
|
180
394
|
const tokenResponse = await response.json();
|
|
181
395
|
return {
|
|
@@ -230,6 +444,73 @@ class TokenManager {
|
|
|
230
444
|
const expiresAt = new Date(this.currentTokens.expires_at);
|
|
231
445
|
return expiresAt.getTime() - now.getTime();
|
|
232
446
|
}
|
|
447
|
+
needsProactiveRotation() {
|
|
448
|
+
if (!this.rotationConfig?.enabled || !this.currentTokens) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
const timeUntilExpiration = this.getTimeUntilExpiration();
|
|
452
|
+
if (timeUntilExpiration === null) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
const rotateBeforeExpiry = this.rotationConfig.rotateBeforeExpiry || 15 * 60 * 1000;
|
|
456
|
+
return timeUntilExpiration <= rotateBeforeExpiry;
|
|
457
|
+
}
|
|
458
|
+
async rotateToken() {
|
|
459
|
+
if (!this.currentTokens) {
|
|
460
|
+
throw new EtsyAuthError('No tokens available to rotate', 'NO_TOKENS');
|
|
461
|
+
}
|
|
462
|
+
const oldTokens = { ...this.currentTokens };
|
|
463
|
+
const newTokens = await this.refreshToken();
|
|
464
|
+
if (this.rotationConfig?.onRotation) {
|
|
465
|
+
try {
|
|
466
|
+
await Promise.resolve(this.rotationConfig.onRotation(oldTokens, newTokens));
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
console.error('Token rotation callback failed:', error);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return newTokens;
|
|
473
|
+
}
|
|
474
|
+
startRotationScheduler() {
|
|
475
|
+
if (!this.rotationConfig?.enabled) {
|
|
476
|
+
throw new Error('Token rotation is not enabled');
|
|
477
|
+
}
|
|
478
|
+
this.stopRotationScheduler();
|
|
479
|
+
const checkInterval = this.rotationConfig.checkInterval || 60 * 1000;
|
|
480
|
+
this.rotationTimer = setInterval(async () => {
|
|
481
|
+
try {
|
|
482
|
+
if (this.needsProactiveRotation()) {
|
|
483
|
+
console.log('Proactively rotating token before expiration');
|
|
484
|
+
await this.rotateToken();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
console.error('Failed to proactively rotate token:', error);
|
|
489
|
+
}
|
|
490
|
+
}, checkInterval);
|
|
491
|
+
if (this.rotationTimer.unref) {
|
|
492
|
+
this.rotationTimer.unref();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
stopRotationScheduler() {
|
|
496
|
+
if (this.rotationTimer) {
|
|
497
|
+
clearInterval(this.rotationTimer);
|
|
498
|
+
this.rotationTimer = undefined;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
updateRotationConfig(config) {
|
|
502
|
+
const wasAutoScheduleEnabled = this.rotationConfig?.autoSchedule;
|
|
503
|
+
this.rotationConfig = config;
|
|
504
|
+
if (config.enabled && config.autoSchedule && !wasAutoScheduleEnabled) {
|
|
505
|
+
this.startRotationScheduler();
|
|
506
|
+
}
|
|
507
|
+
else if (!config.enabled || !config.autoSchedule) {
|
|
508
|
+
this.stopRotationScheduler();
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
getRotationConfig() {
|
|
512
|
+
return this.rotationConfig ? { ...this.rotationConfig } : undefined;
|
|
513
|
+
}
|
|
233
514
|
async fetch(url, options) {
|
|
234
515
|
assertFetchSupport();
|
|
235
516
|
return fetch(url, options);
|
|
@@ -326,6 +607,7 @@ class FileTokenStorage {
|
|
|
326
607
|
try {
|
|
327
608
|
const data = JSON.stringify(tokens, null, 2);
|
|
328
609
|
await this._writeFile(this.filePath, data);
|
|
610
|
+
await this._setFilePermissions(this.filePath, 0o600);
|
|
329
611
|
}
|
|
330
612
|
catch {
|
|
331
613
|
throw new Error('Failed to save tokens to file');
|
|
@@ -363,6 +645,16 @@ class FileTokenStorage {
|
|
|
363
645
|
const fs = await import('fs');
|
|
364
646
|
await fs.promises.writeFile(filePath, data, 'utf8');
|
|
365
647
|
}
|
|
648
|
+
async _setFilePermissions(filePath, mode) {
|
|
649
|
+
if (typeof process === 'undefined')
|
|
650
|
+
return;
|
|
651
|
+
try {
|
|
652
|
+
const fs = await import('fs');
|
|
653
|
+
await fs.promises.chmod(filePath, mode);
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
}
|
|
657
|
+
}
|
|
366
658
|
async _readFile(filePath) {
|
|
367
659
|
if (typeof process === 'undefined')
|
|
368
660
|
throw new Error('Not available');
|
|
@@ -460,6 +752,407 @@ class EtsyRateLimiter {
|
|
|
460
752
|
}
|
|
461
753
|
const defaultRateLimiter = new EtsyRateLimiter();
|
|
462
754
|
|
|
755
|
+
class BulkOperationManager {
|
|
756
|
+
constructor(config = {}) {
|
|
757
|
+
this.concurrency = config.concurrency ?? 5;
|
|
758
|
+
this.stopOnError = config.stopOnError ?? false;
|
|
759
|
+
this.onProgress = config.onProgress;
|
|
760
|
+
this.onItemComplete = config.onItemComplete;
|
|
761
|
+
this.onItemError = config.onItemError;
|
|
762
|
+
}
|
|
763
|
+
async executeBulk(items, operation, getId) {
|
|
764
|
+
const startTime = Date.now();
|
|
765
|
+
const results = [];
|
|
766
|
+
const errors = [];
|
|
767
|
+
let completed = 0;
|
|
768
|
+
let successful = 0;
|
|
769
|
+
let failed = 0;
|
|
770
|
+
const queue = items.map((item, index) => ({
|
|
771
|
+
item,
|
|
772
|
+
index,
|
|
773
|
+
id: getId ? getId(item, index) : index
|
|
774
|
+
}));
|
|
775
|
+
const workers = [];
|
|
776
|
+
for (let i = 0; i < Math.min(this.concurrency, items.length); i++) {
|
|
777
|
+
workers.push(this.worker(queue, operation, results, errors, () => {
|
|
778
|
+
completed++;
|
|
779
|
+
const lastResult = results[results.length - 1];
|
|
780
|
+
if (lastResult?.success) {
|
|
781
|
+
successful++;
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
failed++;
|
|
785
|
+
}
|
|
786
|
+
if (this.onProgress) {
|
|
787
|
+
this.onProgress(completed, items.length, lastResult);
|
|
788
|
+
}
|
|
789
|
+
}));
|
|
790
|
+
}
|
|
791
|
+
await Promise.all(workers);
|
|
792
|
+
const duration = Date.now() - startTime;
|
|
793
|
+
return {
|
|
794
|
+
total: items.length,
|
|
795
|
+
successful,
|
|
796
|
+
failed,
|
|
797
|
+
results,
|
|
798
|
+
errors,
|
|
799
|
+
duration
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
async worker(queue, operation, results, errors, onComplete) {
|
|
803
|
+
while (queue.length > 0) {
|
|
804
|
+
const work = queue.shift();
|
|
805
|
+
if (!work)
|
|
806
|
+
break;
|
|
807
|
+
const { item, index, id } = work;
|
|
808
|
+
try {
|
|
809
|
+
const data = await operation(item, index);
|
|
810
|
+
const result = {
|
|
811
|
+
success: true,
|
|
812
|
+
data,
|
|
813
|
+
id,
|
|
814
|
+
index
|
|
815
|
+
};
|
|
816
|
+
results.push(result);
|
|
817
|
+
if (this.onItemComplete) {
|
|
818
|
+
this.onItemComplete(result);
|
|
819
|
+
}
|
|
820
|
+
onComplete();
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
824
|
+
const result = {
|
|
825
|
+
success: false,
|
|
826
|
+
error: err,
|
|
827
|
+
id,
|
|
828
|
+
index
|
|
829
|
+
};
|
|
830
|
+
results.push(result);
|
|
831
|
+
const bulkError = {
|
|
832
|
+
id,
|
|
833
|
+
index,
|
|
834
|
+
error: err
|
|
835
|
+
};
|
|
836
|
+
errors.push(bulkError);
|
|
837
|
+
if (this.onItemError) {
|
|
838
|
+
this.onItemError(bulkError);
|
|
839
|
+
}
|
|
840
|
+
onComplete();
|
|
841
|
+
if (this.stopOnError) {
|
|
842
|
+
queue.length = 0;
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
setConcurrency(concurrency) {
|
|
849
|
+
this.concurrency = Math.max(1, concurrency);
|
|
850
|
+
}
|
|
851
|
+
getConcurrency() {
|
|
852
|
+
return this.concurrency;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function createBulkOperationManager(config) {
|
|
856
|
+
return new BulkOperationManager(config);
|
|
857
|
+
}
|
|
858
|
+
async function executeBulkOperation(items, operation, config) {
|
|
859
|
+
const manager = new BulkOperationManager(config);
|
|
860
|
+
return manager.executeBulk(items, operation);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
class Validator {
|
|
864
|
+
constructor() {
|
|
865
|
+
this.validators = [];
|
|
866
|
+
}
|
|
867
|
+
rule(validator) {
|
|
868
|
+
this.validators.push(validator);
|
|
869
|
+
return this;
|
|
870
|
+
}
|
|
871
|
+
validate(data) {
|
|
872
|
+
const errors = [];
|
|
873
|
+
for (const validator of this.validators) {
|
|
874
|
+
const error = validator(data);
|
|
875
|
+
if (error) {
|
|
876
|
+
errors.push(error);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
valid: errors.length === 0,
|
|
881
|
+
errors
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
class FieldValidator {
|
|
886
|
+
constructor(field) {
|
|
887
|
+
this.field = field;
|
|
888
|
+
}
|
|
889
|
+
required(message) {
|
|
890
|
+
return (data) => {
|
|
891
|
+
if (typeof data !== 'object' || data === null)
|
|
892
|
+
return null;
|
|
893
|
+
const value = data[this.field];
|
|
894
|
+
if (value === undefined || value === null || value === '') {
|
|
895
|
+
return {
|
|
896
|
+
field: this.field,
|
|
897
|
+
message: message || `${this.field} is required`,
|
|
898
|
+
value
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
return null;
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
string(options) {
|
|
905
|
+
return (data) => {
|
|
906
|
+
if (typeof data !== 'object' || data === null)
|
|
907
|
+
return null;
|
|
908
|
+
const value = data[this.field];
|
|
909
|
+
if (value === undefined || value === null)
|
|
910
|
+
return null;
|
|
911
|
+
if (typeof value !== 'string') {
|
|
912
|
+
return {
|
|
913
|
+
field: this.field,
|
|
914
|
+
message: options.message || `${this.field} must be a string`,
|
|
915
|
+
value
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
if (options.min !== undefined && value.length < options.min) {
|
|
919
|
+
return {
|
|
920
|
+
field: this.field,
|
|
921
|
+
message: options.message || `${this.field} must be at least ${options.min} characters`,
|
|
922
|
+
value
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
if (options.max !== undefined && value.length > options.max) {
|
|
926
|
+
return {
|
|
927
|
+
field: this.field,
|
|
928
|
+
message: options.message || `${this.field} must be at most ${options.max} characters`,
|
|
929
|
+
value
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
if (options.pattern && !options.pattern.test(value)) {
|
|
933
|
+
return {
|
|
934
|
+
field: this.field,
|
|
935
|
+
message: options.message || `${this.field} has invalid format`,
|
|
936
|
+
value
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
return null;
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
number(options) {
|
|
943
|
+
return (data) => {
|
|
944
|
+
if (typeof data !== 'object' || data === null)
|
|
945
|
+
return null;
|
|
946
|
+
const value = data[this.field];
|
|
947
|
+
if (value === undefined || value === null)
|
|
948
|
+
return null;
|
|
949
|
+
if (typeof value !== 'number' || isNaN(value)) {
|
|
950
|
+
return {
|
|
951
|
+
field: this.field,
|
|
952
|
+
message: options.message || `${this.field} must be a number`,
|
|
953
|
+
value
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
if (options.integer && !Number.isInteger(value)) {
|
|
957
|
+
return {
|
|
958
|
+
field: this.field,
|
|
959
|
+
message: options.message || `${this.field} must be an integer`,
|
|
960
|
+
value
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
if (options.positive && value <= 0) {
|
|
964
|
+
return {
|
|
965
|
+
field: this.field,
|
|
966
|
+
message: options.message || `${this.field} must be positive`,
|
|
967
|
+
value
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
if (options.min !== undefined && value < options.min) {
|
|
971
|
+
return {
|
|
972
|
+
field: this.field,
|
|
973
|
+
message: options.message || `${this.field} must be at least ${options.min}`,
|
|
974
|
+
value
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
if (options.max !== undefined && value > options.max) {
|
|
978
|
+
return {
|
|
979
|
+
field: this.field,
|
|
980
|
+
message: options.message || `${this.field} must be at most ${options.max}`,
|
|
981
|
+
value
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
return null;
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
enum(allowedValues, message) {
|
|
988
|
+
return (data) => {
|
|
989
|
+
if (typeof data !== 'object' || data === null)
|
|
990
|
+
return null;
|
|
991
|
+
const value = data[this.field];
|
|
992
|
+
if (value === undefined || value === null)
|
|
993
|
+
return null;
|
|
994
|
+
if (!allowedValues.includes(value)) {
|
|
995
|
+
return {
|
|
996
|
+
field: this.field,
|
|
997
|
+
message: message || `${this.field} must be one of: ${allowedValues.join(', ')}`,
|
|
998
|
+
value
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
return null;
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
array(options) {
|
|
1005
|
+
return (data) => {
|
|
1006
|
+
if (typeof data !== 'object' || data === null)
|
|
1007
|
+
return null;
|
|
1008
|
+
const value = data[this.field];
|
|
1009
|
+
if (value === undefined || value === null)
|
|
1010
|
+
return null;
|
|
1011
|
+
if (!Array.isArray(value)) {
|
|
1012
|
+
return {
|
|
1013
|
+
field: this.field,
|
|
1014
|
+
message: options.message || `${this.field} must be an array`,
|
|
1015
|
+
value
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
if (options.min !== undefined && value.length < options.min) {
|
|
1019
|
+
return {
|
|
1020
|
+
field: this.field,
|
|
1021
|
+
message: options.message || `${this.field} must have at least ${options.min} items`,
|
|
1022
|
+
value
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
if (options.max !== undefined && value.length > options.max) {
|
|
1026
|
+
return {
|
|
1027
|
+
field: this.field,
|
|
1028
|
+
message: options.message || `${this.field} must have at most ${options.max} items`,
|
|
1029
|
+
value
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
if (options.itemValidator) {
|
|
1033
|
+
for (let i = 0; i < value.length; i++) {
|
|
1034
|
+
if (!options.itemValidator(value[i])) {
|
|
1035
|
+
return {
|
|
1036
|
+
field: this.field,
|
|
1037
|
+
message: options.message || `${this.field}[${i}] has invalid value`,
|
|
1038
|
+
value: value[i]
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return null;
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function field(fieldName) {
|
|
1048
|
+
return new FieldValidator(fieldName);
|
|
1049
|
+
}
|
|
1050
|
+
const CreateListingSchema = new Validator()
|
|
1051
|
+
.rule(field('quantity').required())
|
|
1052
|
+
.rule(field('quantity').number({ min: 1, integer: true, message: 'quantity must be a positive integer' }))
|
|
1053
|
+
.rule(field('title').required())
|
|
1054
|
+
.rule(field('title').string({ min: 1, max: 140, message: 'title must be 1-140 characters' }))
|
|
1055
|
+
.rule(field('description').string({ max: 65535, message: 'description must be less than 65535 characters' }))
|
|
1056
|
+
.rule(field('price').required())
|
|
1057
|
+
.rule(field('price').number({ min: 0.2, max: 50000, message: 'price must be between 0.20 and 50000.00' }))
|
|
1058
|
+
.rule(field('who_made').enum(['i_did', 'someone_else', 'collective'], 'who_made must be one of: i_did, someone_else, collective'))
|
|
1059
|
+
.rule(field('when_made').enum([
|
|
1060
|
+
'made_to_order',
|
|
1061
|
+
'2020_2024',
|
|
1062
|
+
'2010_2019',
|
|
1063
|
+
'2005_2009',
|
|
1064
|
+
'2000_2004',
|
|
1065
|
+
'1990s',
|
|
1066
|
+
'1980s',
|
|
1067
|
+
'1970s',
|
|
1068
|
+
'1960s',
|
|
1069
|
+
'1950s',
|
|
1070
|
+
'1940s',
|
|
1071
|
+
'1930s',
|
|
1072
|
+
'1920s',
|
|
1073
|
+
'1910s',
|
|
1074
|
+
'1900s',
|
|
1075
|
+
'1800s',
|
|
1076
|
+
'1700s',
|
|
1077
|
+
'before_1700'
|
|
1078
|
+
]))
|
|
1079
|
+
.rule(field('taxonomy_id').required())
|
|
1080
|
+
.rule(field('taxonomy_id').number({ integer: true, positive: true, message: 'taxonomy_id must be a positive integer' }));
|
|
1081
|
+
const UpdateListingSchema = new Validator()
|
|
1082
|
+
.rule((data) => {
|
|
1083
|
+
if (typeof data === 'object' && data !== null && 'title' in data && data.title !== undefined) {
|
|
1084
|
+
return field('title').string({ min: 1, max: 140, message: 'title must be 1-140 characters' })(data);
|
|
1085
|
+
}
|
|
1086
|
+
return null;
|
|
1087
|
+
})
|
|
1088
|
+
.rule((data) => {
|
|
1089
|
+
if (typeof data === 'object' && data !== null && 'description' in data && data.description !== undefined) {
|
|
1090
|
+
return field('description').string({ max: 65535, message: 'description must be less than 65535 characters' })(data);
|
|
1091
|
+
}
|
|
1092
|
+
return null;
|
|
1093
|
+
})
|
|
1094
|
+
.rule((data) => {
|
|
1095
|
+
if (data.tags !== undefined) {
|
|
1096
|
+
return field('tags').array({ max: 13, message: 'tags must have at most 13 items' })(data);
|
|
1097
|
+
}
|
|
1098
|
+
return null;
|
|
1099
|
+
})
|
|
1100
|
+
.rule((data) => {
|
|
1101
|
+
if (data.materials !== undefined) {
|
|
1102
|
+
return field('materials').array({ max: 13, message: 'materials must have at most 13 items' })(data);
|
|
1103
|
+
}
|
|
1104
|
+
return null;
|
|
1105
|
+
});
|
|
1106
|
+
const UpdateShopSchema = new Validator()
|
|
1107
|
+
.rule((data) => {
|
|
1108
|
+
if (typeof data === 'object' && data !== null && 'title' in data && data.title !== undefined) {
|
|
1109
|
+
return field('title').string({ min: 1, max: 55, message: 'shop title must be 1-55 characters' })(data);
|
|
1110
|
+
}
|
|
1111
|
+
return null;
|
|
1112
|
+
})
|
|
1113
|
+
.rule((data) => {
|
|
1114
|
+
if (typeof data === 'object' && data !== null && 'announcement' in data && data.announcement !== undefined) {
|
|
1115
|
+
return field('announcement').string({ max: 5000, message: 'announcement must be less than 5000 characters' })(data);
|
|
1116
|
+
}
|
|
1117
|
+
return null;
|
|
1118
|
+
});
|
|
1119
|
+
class ValidationException extends Error {
|
|
1120
|
+
constructor(message, errors) {
|
|
1121
|
+
super(message);
|
|
1122
|
+
this.name = 'ValidationException';
|
|
1123
|
+
this.errors = errors;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function validateOrThrow(data, schema, errorMessage = 'Validation failed') {
|
|
1127
|
+
const result = schema.validate(data);
|
|
1128
|
+
if (!result.valid) {
|
|
1129
|
+
throw new ValidationException(errorMessage, result.errors);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function validate(data, schema) {
|
|
1133
|
+
return schema.validate(data);
|
|
1134
|
+
}
|
|
1135
|
+
function createValidator() {
|
|
1136
|
+
return new Validator();
|
|
1137
|
+
}
|
|
1138
|
+
function combineValidators(...validators) {
|
|
1139
|
+
return {
|
|
1140
|
+
validate(data) {
|
|
1141
|
+
const allErrors = [];
|
|
1142
|
+
for (const validator of validators) {
|
|
1143
|
+
const result = validator.validate(data);
|
|
1144
|
+
if (!result.valid) {
|
|
1145
|
+
allErrors.push(...result.errors);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return {
|
|
1149
|
+
valid: allErrors.length === 0,
|
|
1150
|
+
errors: allErrors
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
463
1156
|
class DefaultLogger {
|
|
464
1157
|
debug(message, ...args) {
|
|
465
1158
|
const isDevelopment = isNode$1
|
|
@@ -524,6 +1217,10 @@ class EtsyClient {
|
|
|
524
1217
|
this.cache = config.caching?.storage || new MemoryCache();
|
|
525
1218
|
this.cacheTtl = config.caching?.ttl || 3600;
|
|
526
1219
|
}
|
|
1220
|
+
this.bulkOperationManager = new BulkOperationManager({
|
|
1221
|
+
concurrency: 5,
|
|
1222
|
+
stopOnError: false
|
|
1223
|
+
});
|
|
527
1224
|
}
|
|
528
1225
|
async makeRequest(endpoint, options = {}, useCache = true) {
|
|
529
1226
|
const url = `${this.baseUrl}${endpoint}`;
|
|
@@ -556,6 +1253,13 @@ class EtsyClient {
|
|
|
556
1253
|
const errorText = await response.text();
|
|
557
1254
|
throw new EtsyApiError(`Etsy API error: ${response.status} ${response.statusText}`, response.status, errorText);
|
|
558
1255
|
}
|
|
1256
|
+
if (response.status === 204) {
|
|
1257
|
+
return undefined;
|
|
1258
|
+
}
|
|
1259
|
+
const contentLength = response.headers?.get?.('content-length');
|
|
1260
|
+
if (contentLength === '0') {
|
|
1261
|
+
return undefined;
|
|
1262
|
+
}
|
|
559
1263
|
const data = await response.json();
|
|
560
1264
|
if (useCache && this.cache && requestOptions.method === 'GET') {
|
|
561
1265
|
await this.cache.set(cacheKey, JSON.stringify(data), this.cacheTtl);
|
|
@@ -674,6 +1378,270 @@ class EtsyClient {
|
|
|
674
1378
|
const response = await this.makeRequest('/users/me/shops');
|
|
675
1379
|
return response.results || [];
|
|
676
1380
|
}
|
|
1381
|
+
async updateShop(shopId, params, options) {
|
|
1382
|
+
if (options?.validate) {
|
|
1383
|
+
const schema = options.validateSchema || UpdateShopSchema;
|
|
1384
|
+
const throwOnError = options.throwOnValidationError !== false;
|
|
1385
|
+
if (throwOnError) {
|
|
1386
|
+
validateOrThrow(params, schema, 'Invalid shop update parameters');
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
const result = schema.validate(params);
|
|
1390
|
+
if (!result.valid) {
|
|
1391
|
+
this.logger.warn('Shop update validation failed', result.errors);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return this.makeRequest(`/shops/${shopId}`, {
|
|
1396
|
+
method: 'PUT',
|
|
1397
|
+
body: JSON.stringify(params)
|
|
1398
|
+
}, false);
|
|
1399
|
+
}
|
|
1400
|
+
async createShopSection(shopId, params) {
|
|
1401
|
+
return this.makeRequest(`/shops/${shopId}/sections`, {
|
|
1402
|
+
method: 'POST',
|
|
1403
|
+
body: JSON.stringify(params)
|
|
1404
|
+
}, false);
|
|
1405
|
+
}
|
|
1406
|
+
async updateShopSection(shopId, sectionId, params) {
|
|
1407
|
+
return this.makeRequest(`/shops/${shopId}/sections/${sectionId}`, {
|
|
1408
|
+
method: 'PUT',
|
|
1409
|
+
body: JSON.stringify(params)
|
|
1410
|
+
}, false);
|
|
1411
|
+
}
|
|
1412
|
+
async deleteShopSection(shopId, sectionId) {
|
|
1413
|
+
await this.makeRequest(`/shops/${shopId}/sections/${sectionId}`, { method: 'DELETE' }, false);
|
|
1414
|
+
}
|
|
1415
|
+
async createDraftListing(shopId, params, options) {
|
|
1416
|
+
if (options?.validate) {
|
|
1417
|
+
const schema = options.validateSchema || CreateListingSchema;
|
|
1418
|
+
const throwOnError = options.throwOnValidationError !== false;
|
|
1419
|
+
if (throwOnError) {
|
|
1420
|
+
validateOrThrow(params, schema, 'Invalid listing parameters');
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
const result = schema.validate(params);
|
|
1424
|
+
if (!result.valid) {
|
|
1425
|
+
this.logger.warn('Listing validation failed', result.errors);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return this.makeRequest(`/shops/${shopId}/listings`, {
|
|
1430
|
+
method: 'POST',
|
|
1431
|
+
body: JSON.stringify(params)
|
|
1432
|
+
}, false);
|
|
1433
|
+
}
|
|
1434
|
+
async updateListing(shopId, listingId, params, options) {
|
|
1435
|
+
if (options?.validate) {
|
|
1436
|
+
const schema = options.validateSchema || UpdateListingSchema;
|
|
1437
|
+
const throwOnError = options.throwOnValidationError !== false;
|
|
1438
|
+
if (throwOnError) {
|
|
1439
|
+
validateOrThrow(params, schema, 'Invalid listing update parameters');
|
|
1440
|
+
}
|
|
1441
|
+
else {
|
|
1442
|
+
const result = schema.validate(params);
|
|
1443
|
+
if (!result.valid) {
|
|
1444
|
+
this.logger.warn('Listing update validation failed', result.errors);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return this.makeRequest(`/shops/${shopId}/listings/${listingId}`, {
|
|
1449
|
+
method: 'PATCH',
|
|
1450
|
+
body: JSON.stringify(params)
|
|
1451
|
+
}, false);
|
|
1452
|
+
}
|
|
1453
|
+
async deleteListing(listingId) {
|
|
1454
|
+
await this.makeRequest(`/listings/${listingId}`, { method: 'DELETE' }, false);
|
|
1455
|
+
}
|
|
1456
|
+
async updateListingInventory(listingId, params) {
|
|
1457
|
+
return this.makeRequest(`/listings/${listingId}/inventory`, {
|
|
1458
|
+
method: 'PUT',
|
|
1459
|
+
body: JSON.stringify(params)
|
|
1460
|
+
}, false);
|
|
1461
|
+
}
|
|
1462
|
+
async uploadListingImage(shopId, listingId, imageData, params) {
|
|
1463
|
+
const formData = new FormData();
|
|
1464
|
+
formData.append('image', imageData);
|
|
1465
|
+
if (params?.rank !== undefined)
|
|
1466
|
+
formData.append('rank', params.rank.toString());
|
|
1467
|
+
if (params?.overwrite !== undefined)
|
|
1468
|
+
formData.append('overwrite', params.overwrite.toString());
|
|
1469
|
+
if (params?.is_watermarked !== undefined)
|
|
1470
|
+
formData.append('is_watermarked', params.is_watermarked.toString());
|
|
1471
|
+
if (params?.alt_text)
|
|
1472
|
+
formData.append('alt_text', params.alt_text);
|
|
1473
|
+
const url = `${this.baseUrl}/shops/${shopId}/listings/${listingId}/images`;
|
|
1474
|
+
await this.rateLimiter.waitForRateLimit();
|
|
1475
|
+
const accessToken = await this.tokenManager.getAccessToken();
|
|
1476
|
+
const response = await this.fetch(url, {
|
|
1477
|
+
method: 'POST',
|
|
1478
|
+
headers: {
|
|
1479
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
1480
|
+
'x-api-key': this.getApiKey(),
|
|
1481
|
+
},
|
|
1482
|
+
body: formData
|
|
1483
|
+
});
|
|
1484
|
+
if (!response.ok) {
|
|
1485
|
+
const errorText = await response.text();
|
|
1486
|
+
throw new EtsyApiError(`Failed to upload image: ${response.status} ${response.statusText}`, response.status, errorText);
|
|
1487
|
+
}
|
|
1488
|
+
return response.json();
|
|
1489
|
+
}
|
|
1490
|
+
async getListingImage(shopId, listingId, imageId) {
|
|
1491
|
+
return this.makeRequest(`/shops/${shopId}/listings/${listingId}/images/${imageId}`);
|
|
1492
|
+
}
|
|
1493
|
+
async deleteListingImage(shopId, listingId, imageId) {
|
|
1494
|
+
await this.makeRequest(`/shops/${shopId}/listings/${listingId}/images/${imageId}`, { method: 'DELETE' }, false);
|
|
1495
|
+
}
|
|
1496
|
+
async bulkUpdateListings(shopId, operations, config) {
|
|
1497
|
+
const manager = config ? new BulkOperationManager(config) : this.bulkOperationManager;
|
|
1498
|
+
return manager.executeBulk(operations, async (operation) => {
|
|
1499
|
+
return this.updateListing(shopId, operation.listingId.toString(), operation.updates);
|
|
1500
|
+
}, (operation) => operation.listingId);
|
|
1501
|
+
}
|
|
1502
|
+
async bulkUploadImages(shopId, listingId, images, config) {
|
|
1503
|
+
const manager = config ? new BulkOperationManager(config) : this.bulkOperationManager;
|
|
1504
|
+
return manager.executeBulk(images, async (image) => {
|
|
1505
|
+
return this.uploadListingImage(shopId, listingId, image.file, {
|
|
1506
|
+
rank: image.rank,
|
|
1507
|
+
alt_text: image.altText
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
setBulkOperationConcurrency(concurrency) {
|
|
1512
|
+
this.bulkOperationManager.setConcurrency(Math.max(1, Math.min(10, concurrency)));
|
|
1513
|
+
}
|
|
1514
|
+
async getShopReceipts(shopId, params) {
|
|
1515
|
+
const searchParams = new URLSearchParams();
|
|
1516
|
+
if (params?.limit !== undefined)
|
|
1517
|
+
searchParams.set('limit', params.limit.toString());
|
|
1518
|
+
if (params?.offset !== undefined)
|
|
1519
|
+
searchParams.set('offset', params.offset.toString());
|
|
1520
|
+
if (params?.sort_on)
|
|
1521
|
+
searchParams.set('sort_on', params.sort_on);
|
|
1522
|
+
if (params?.sort_order)
|
|
1523
|
+
searchParams.set('sort_order', params.sort_order);
|
|
1524
|
+
if (params?.min_created !== undefined)
|
|
1525
|
+
searchParams.set('min_created', params.min_created.toString());
|
|
1526
|
+
if (params?.max_created !== undefined)
|
|
1527
|
+
searchParams.set('max_created', params.max_created.toString());
|
|
1528
|
+
if (params?.min_last_modified !== undefined)
|
|
1529
|
+
searchParams.set('min_last_modified', params.min_last_modified.toString());
|
|
1530
|
+
if (params?.max_last_modified !== undefined)
|
|
1531
|
+
searchParams.set('max_last_modified', params.max_last_modified.toString());
|
|
1532
|
+
if (params?.was_paid !== undefined)
|
|
1533
|
+
searchParams.set('was_paid', params.was_paid.toString());
|
|
1534
|
+
if (params?.was_shipped !== undefined)
|
|
1535
|
+
searchParams.set('was_shipped', params.was_shipped.toString());
|
|
1536
|
+
if (params?.was_delivered !== undefined)
|
|
1537
|
+
searchParams.set('was_delivered', params.was_delivered.toString());
|
|
1538
|
+
const response = await this.makeRequest(`/shops/${shopId}/receipts?${searchParams.toString()}`);
|
|
1539
|
+
return response.results;
|
|
1540
|
+
}
|
|
1541
|
+
async getShopReceipt(shopId, receiptId) {
|
|
1542
|
+
return this.makeRequest(`/shops/${shopId}/receipts/${receiptId}`);
|
|
1543
|
+
}
|
|
1544
|
+
async updateShopReceipt(shopId, receiptId, params) {
|
|
1545
|
+
return this.makeRequest(`/shops/${shopId}/receipts/${receiptId}`, {
|
|
1546
|
+
method: 'PUT',
|
|
1547
|
+
body: JSON.stringify(params)
|
|
1548
|
+
}, false);
|
|
1549
|
+
}
|
|
1550
|
+
async getShopReceiptTransactions(shopId, receiptId) {
|
|
1551
|
+
const response = await this.makeRequest(`/shops/${shopId}/receipts/${receiptId}/transactions`);
|
|
1552
|
+
return response.results;
|
|
1553
|
+
}
|
|
1554
|
+
async getShopTransaction(shopId, transactionId) {
|
|
1555
|
+
return this.makeRequest(`/shops/${shopId}/transactions/${transactionId}`);
|
|
1556
|
+
}
|
|
1557
|
+
async getShopShippingProfiles(shopId) {
|
|
1558
|
+
const response = await this.makeRequest(`/shops/${shopId}/shipping-profiles`);
|
|
1559
|
+
return response.results;
|
|
1560
|
+
}
|
|
1561
|
+
async createShopShippingProfile(shopId, params) {
|
|
1562
|
+
return this.makeRequest(`/shops/${shopId}/shipping-profiles`, {
|
|
1563
|
+
method: 'POST',
|
|
1564
|
+
body: JSON.stringify(params)
|
|
1565
|
+
}, false);
|
|
1566
|
+
}
|
|
1567
|
+
async getShopShippingProfile(shopId, profileId) {
|
|
1568
|
+
return this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}`);
|
|
1569
|
+
}
|
|
1570
|
+
async updateShopShippingProfile(shopId, profileId, params) {
|
|
1571
|
+
return this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}`, {
|
|
1572
|
+
method: 'PUT',
|
|
1573
|
+
body: JSON.stringify(params)
|
|
1574
|
+
}, false);
|
|
1575
|
+
}
|
|
1576
|
+
async deleteShopShippingProfile(shopId, profileId) {
|
|
1577
|
+
await this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}`, { method: 'DELETE' }, false);
|
|
1578
|
+
}
|
|
1579
|
+
async getShopShippingProfileDestinations(shopId, profileId) {
|
|
1580
|
+
const response = await this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}/destinations`);
|
|
1581
|
+
return response.results;
|
|
1582
|
+
}
|
|
1583
|
+
async createShopShippingProfileDestination(shopId, profileId, params) {
|
|
1584
|
+
return this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}/destinations`, {
|
|
1585
|
+
method: 'POST',
|
|
1586
|
+
body: JSON.stringify(params)
|
|
1587
|
+
}, false);
|
|
1588
|
+
}
|
|
1589
|
+
async updateShopShippingProfileDestination(shopId, profileId, destinationId, params) {
|
|
1590
|
+
return this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}/destinations/${destinationId}`, {
|
|
1591
|
+
method: 'PUT',
|
|
1592
|
+
body: JSON.stringify(params)
|
|
1593
|
+
}, false);
|
|
1594
|
+
}
|
|
1595
|
+
async deleteShopShippingProfileDestination(shopId, profileId, destinationId) {
|
|
1596
|
+
await this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}/destinations/${destinationId}`, { method: 'DELETE' }, false);
|
|
1597
|
+
}
|
|
1598
|
+
async getShopShippingProfileUpgrades(shopId, profileId) {
|
|
1599
|
+
const response = await this.makeRequest(`/shops/${shopId}/shipping-profiles/${profileId}/upgrades`);
|
|
1600
|
+
return response.results;
|
|
1601
|
+
}
|
|
1602
|
+
async createReceiptShipment(shopId, receiptId, params) {
|
|
1603
|
+
return this.makeRequest(`/shops/${shopId}/receipts/${receiptId}/tracking`, {
|
|
1604
|
+
method: 'POST',
|
|
1605
|
+
body: JSON.stringify(params)
|
|
1606
|
+
}, false);
|
|
1607
|
+
}
|
|
1608
|
+
async getShopReceiptShipments(shopId, receiptId) {
|
|
1609
|
+
const response = await this.makeRequest(`/shops/${shopId}/receipts/${receiptId}/shipments`);
|
|
1610
|
+
return response.results;
|
|
1611
|
+
}
|
|
1612
|
+
async getShopPaymentAccountLedgerEntries(shopId, params) {
|
|
1613
|
+
const searchParams = new URLSearchParams();
|
|
1614
|
+
searchParams.set('min_created', params.min_created.toString());
|
|
1615
|
+
searchParams.set('max_created', params.max_created.toString());
|
|
1616
|
+
if (params.limit !== undefined)
|
|
1617
|
+
searchParams.set('limit', params.limit.toString());
|
|
1618
|
+
if (params.offset !== undefined)
|
|
1619
|
+
searchParams.set('offset', params.offset.toString());
|
|
1620
|
+
const response = await this.makeRequest(`/shops/${shopId}/payment-account/ledger-entries?${searchParams.toString()}`);
|
|
1621
|
+
return response.results;
|
|
1622
|
+
}
|
|
1623
|
+
async getShopPaymentAccountLedgerEntry(shopId, entryId) {
|
|
1624
|
+
return this.makeRequest(`/shops/${shopId}/payment-account/ledger-entries/${entryId}`);
|
|
1625
|
+
}
|
|
1626
|
+
async getShopPayment(shopId, paymentId) {
|
|
1627
|
+
return this.makeRequest(`/shops/${shopId}/payment-account/payments/${paymentId}`);
|
|
1628
|
+
}
|
|
1629
|
+
async getBuyerTaxonomyNodes() {
|
|
1630
|
+
const response = await this.makeRequest('/buyer-taxonomy/nodes');
|
|
1631
|
+
return response.results;
|
|
1632
|
+
}
|
|
1633
|
+
async getPropertiesByTaxonomyId(taxonomyId) {
|
|
1634
|
+
const response = await this.makeRequest(`/seller-taxonomy/nodes/${taxonomyId}/properties`);
|
|
1635
|
+
return response.results;
|
|
1636
|
+
}
|
|
1637
|
+
async getListingProperties(shopId, listingId) {
|
|
1638
|
+
const response = await this.makeRequest(`/shops/${shopId}/listings/${listingId}/properties`);
|
|
1639
|
+
return response.results;
|
|
1640
|
+
}
|
|
1641
|
+
async getShopProductionPartners(shopId) {
|
|
1642
|
+
const response = await this.makeRequest(`/shops/${shopId}/production-partners`);
|
|
1643
|
+
return response.results;
|
|
1644
|
+
}
|
|
677
1645
|
getRemainingRequests() {
|
|
678
1646
|
return this.rateLimiter.getRemainingRequests();
|
|
679
1647
|
}
|
|
@@ -921,6 +1889,1640 @@ const COMMON_SCOPE_COMBINATIONS = {
|
|
|
921
1889
|
]
|
|
922
1890
|
};
|
|
923
1891
|
|
|
1892
|
+
class GlobalRequestQueue {
|
|
1893
|
+
constructor() {
|
|
1894
|
+
this.queue = [];
|
|
1895
|
+
this.processing = false;
|
|
1896
|
+
this.rateLimits = new Map();
|
|
1897
|
+
this.requestCount = 0;
|
|
1898
|
+
this.dailyReset = new Date();
|
|
1899
|
+
this.lastRequestTime = 0;
|
|
1900
|
+
this.maxRequestsPerDay = 10000;
|
|
1901
|
+
this.maxRequestsPerSecond = 10;
|
|
1902
|
+
this.minRequestInterval = 100;
|
|
1903
|
+
this.setNextDailyReset();
|
|
1904
|
+
}
|
|
1905
|
+
static getInstance() {
|
|
1906
|
+
if (!this.instance) {
|
|
1907
|
+
this.instance = new GlobalRequestQueue();
|
|
1908
|
+
}
|
|
1909
|
+
return this.instance;
|
|
1910
|
+
}
|
|
1911
|
+
static resetInstance() {
|
|
1912
|
+
this.instance = null;
|
|
1913
|
+
}
|
|
1914
|
+
async enqueue(request, options = {}) {
|
|
1915
|
+
return new Promise((resolve, reject) => {
|
|
1916
|
+
const queuedRequest = {
|
|
1917
|
+
id: this.generateId(),
|
|
1918
|
+
request,
|
|
1919
|
+
resolve: resolve,
|
|
1920
|
+
reject,
|
|
1921
|
+
priority: options.priority ?? 'normal',
|
|
1922
|
+
addedAt: Date.now(),
|
|
1923
|
+
timeout: options.timeout ?? 30000,
|
|
1924
|
+
endpoint: options.endpoint,
|
|
1925
|
+
};
|
|
1926
|
+
this.queue.push(queuedRequest);
|
|
1927
|
+
if (!this.processing) {
|
|
1928
|
+
this.processQueue().catch(error => {
|
|
1929
|
+
console.error('Queue processing error:', error);
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
getStatus() {
|
|
1935
|
+
return {
|
|
1936
|
+
queueLength: this.queue.length,
|
|
1937
|
+
processing: this.processing,
|
|
1938
|
+
remainingRequests: Math.max(0, this.maxRequestsPerDay - this.requestCount),
|
|
1939
|
+
resetTime: this.dailyReset,
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
clear() {
|
|
1943
|
+
this.queue.forEach(item => {
|
|
1944
|
+
item.reject(new Error('Queue cleared'));
|
|
1945
|
+
});
|
|
1946
|
+
this.queue = [];
|
|
1947
|
+
}
|
|
1948
|
+
async processQueue() {
|
|
1949
|
+
if (this.processing) {
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
this.processing = true;
|
|
1953
|
+
try {
|
|
1954
|
+
while (this.queue.length > 0) {
|
|
1955
|
+
if (Date.now() >= this.dailyReset.getTime()) {
|
|
1956
|
+
this.requestCount = 0;
|
|
1957
|
+
this.setNextDailyReset();
|
|
1958
|
+
}
|
|
1959
|
+
if (this.requestCount >= this.maxRequestsPerDay) {
|
|
1960
|
+
const timeUntilReset = this.dailyReset.getTime() - Date.now();
|
|
1961
|
+
console.warn(`Daily rate limit reached. Waiting ${Math.ceil(timeUntilReset / 1000 / 60)} minutes until reset.`);
|
|
1962
|
+
await this.delay(timeUntilReset);
|
|
1963
|
+
this.requestCount = 0;
|
|
1964
|
+
this.setNextDailyReset();
|
|
1965
|
+
}
|
|
1966
|
+
await this.waitForRateLimit();
|
|
1967
|
+
this.queue.sort((a, b) => {
|
|
1968
|
+
const priorityOrder = {
|
|
1969
|
+
high: 0,
|
|
1970
|
+
normal: 1,
|
|
1971
|
+
low: 2,
|
|
1972
|
+
};
|
|
1973
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
1974
|
+
});
|
|
1975
|
+
const item = this.queue.shift();
|
|
1976
|
+
if (!item) {
|
|
1977
|
+
break;
|
|
1978
|
+
}
|
|
1979
|
+
const elapsed = Date.now() - item.addedAt;
|
|
1980
|
+
if (item.timeout && elapsed > item.timeout) {
|
|
1981
|
+
item.reject(new Error(`Request timeout after ${elapsed}ms (exceeded while waiting in queue)`));
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
try {
|
|
1985
|
+
let result;
|
|
1986
|
+
if (item.timeout) {
|
|
1987
|
+
const remainingTimeout = item.timeout - elapsed;
|
|
1988
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1989
|
+
setTimeout(() => {
|
|
1990
|
+
reject(new Error(`Request timeout after ${item.timeout}ms (exceeded during execution)`));
|
|
1991
|
+
}, remainingTimeout);
|
|
1992
|
+
});
|
|
1993
|
+
result = await Promise.race([item.request(), timeoutPromise]);
|
|
1994
|
+
}
|
|
1995
|
+
else {
|
|
1996
|
+
result = await item.request();
|
|
1997
|
+
}
|
|
1998
|
+
item.resolve(result);
|
|
1999
|
+
this.requestCount++;
|
|
2000
|
+
this.lastRequestTime = Date.now();
|
|
2001
|
+
}
|
|
2002
|
+
catch (error) {
|
|
2003
|
+
if (error instanceof EtsyRateLimitError) {
|
|
2004
|
+
this.updateRateLimitInfo(error);
|
|
2005
|
+
}
|
|
2006
|
+
item.reject(error);
|
|
2007
|
+
}
|
|
2008
|
+
await this.delay(this.minRequestInterval);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
finally {
|
|
2012
|
+
this.processing = false;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
async waitForRateLimit() {
|
|
2016
|
+
const now = Date.now();
|
|
2017
|
+
const globalRateLimit = this.rateLimits.get('global');
|
|
2018
|
+
if (globalRateLimit && now < globalRateLimit.resetAt) {
|
|
2019
|
+
const waitTime = globalRateLimit.resetAt - now;
|
|
2020
|
+
console.log(`Global rate limit active. Waiting ${waitTime}ms`);
|
|
2021
|
+
await this.delay(waitTime);
|
|
2022
|
+
}
|
|
2023
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
2024
|
+
if (timeSinceLastRequest < this.minRequestInterval) {
|
|
2025
|
+
const waitTime = this.minRequestInterval - timeSinceLastRequest;
|
|
2026
|
+
await this.delay(waitTime);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
updateRateLimitInfo(error) {
|
|
2030
|
+
const retryAfter = error.retryAfter;
|
|
2031
|
+
if (retryAfter) {
|
|
2032
|
+
this.rateLimits.set('global', {
|
|
2033
|
+
remaining: 0,
|
|
2034
|
+
resetAt: Date.now() + retryAfter * 1000,
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
setNextDailyReset() {
|
|
2039
|
+
const now = new Date();
|
|
2040
|
+
this.dailyReset = new Date(now);
|
|
2041
|
+
this.dailyReset.setUTCDate(this.dailyReset.getUTCDate() + 1);
|
|
2042
|
+
this.dailyReset.setUTCHours(0, 0, 0, 0);
|
|
2043
|
+
}
|
|
2044
|
+
generateId() {
|
|
2045
|
+
return `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
2046
|
+
}
|
|
2047
|
+
delay(ms) {
|
|
2048
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
GlobalRequestQueue.instance = null;
|
|
2052
|
+
function getGlobalQueue() {
|
|
2053
|
+
return GlobalRequestQueue.getInstance();
|
|
2054
|
+
}
|
|
2055
|
+
function withQueue(request, options) {
|
|
2056
|
+
const queue = GlobalRequestQueue.getInstance();
|
|
2057
|
+
return queue.enqueue(request, options);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
class PaginatedResults {
|
|
2061
|
+
constructor(fetcher, options = {}) {
|
|
2062
|
+
this.currentPage = [];
|
|
2063
|
+
this.totalCount = null;
|
|
2064
|
+
this.hasMore = true;
|
|
2065
|
+
this.fetcher = fetcher;
|
|
2066
|
+
this.options = {
|
|
2067
|
+
limit: options.limit || 25,
|
|
2068
|
+
offset: options.offset || 0,
|
|
2069
|
+
maxPages: options.maxPages || Infinity,
|
|
2070
|
+
maxItems: options.maxItems || Infinity
|
|
2071
|
+
};
|
|
2072
|
+
this.currentOffset = this.options.offset;
|
|
2073
|
+
}
|
|
2074
|
+
async *[Symbol.asyncIterator]() {
|
|
2075
|
+
let pageCount = 0;
|
|
2076
|
+
let itemCount = 0;
|
|
2077
|
+
while (this.hasMore && pageCount < this.options.maxPages && itemCount < this.options.maxItems) {
|
|
2078
|
+
const page = await this.fetchPage();
|
|
2079
|
+
pageCount++;
|
|
2080
|
+
for (const item of page) {
|
|
2081
|
+
if (itemCount >= this.options.maxItems) {
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
yield item;
|
|
2085
|
+
itemCount++;
|
|
2086
|
+
}
|
|
2087
|
+
if (!this.hasMore) {
|
|
2088
|
+
break;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
async getAll() {
|
|
2093
|
+
const items = [];
|
|
2094
|
+
for await (const item of this) {
|
|
2095
|
+
items.push(item);
|
|
2096
|
+
}
|
|
2097
|
+
return items;
|
|
2098
|
+
}
|
|
2099
|
+
getCurrentPage() {
|
|
2100
|
+
return this.currentPage;
|
|
2101
|
+
}
|
|
2102
|
+
hasNextPage() {
|
|
2103
|
+
return this.hasMore;
|
|
2104
|
+
}
|
|
2105
|
+
async getNextPage() {
|
|
2106
|
+
if (!this.hasMore) {
|
|
2107
|
+
return [];
|
|
2108
|
+
}
|
|
2109
|
+
return this.fetchPage();
|
|
2110
|
+
}
|
|
2111
|
+
getTotalCount() {
|
|
2112
|
+
return this.totalCount;
|
|
2113
|
+
}
|
|
2114
|
+
reset() {
|
|
2115
|
+
this.currentOffset = this.options.offset;
|
|
2116
|
+
this.currentPage = [];
|
|
2117
|
+
this.totalCount = null;
|
|
2118
|
+
this.hasMore = true;
|
|
2119
|
+
}
|
|
2120
|
+
async fetchPage() {
|
|
2121
|
+
const response = await this.fetcher(this.options.limit, this.currentOffset);
|
|
2122
|
+
this.currentPage = response.results;
|
|
2123
|
+
this.totalCount = response.count;
|
|
2124
|
+
if (response.pagination) {
|
|
2125
|
+
if (response.pagination.next_offset !== undefined) {
|
|
2126
|
+
this.hasMore = true;
|
|
2127
|
+
this.currentOffset = response.pagination.next_offset;
|
|
2128
|
+
}
|
|
2129
|
+
else {
|
|
2130
|
+
this.hasMore = false;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
else {
|
|
2134
|
+
this.hasMore = response.results.length === this.options.limit;
|
|
2135
|
+
this.currentOffset += this.options.limit;
|
|
2136
|
+
}
|
|
2137
|
+
return this.currentPage;
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
function createPaginatedResults(fetcher, options) {
|
|
2141
|
+
return new PaginatedResults(fetcher, options);
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
2145
|
+
maxRetries: 3,
|
|
2146
|
+
retryDelay: 1000,
|
|
2147
|
+
exponentialBackoff: true,
|
|
2148
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
2149
|
+
maxRetryDelay: 30000,
|
|
2150
|
+
jitter: 0.1
|
|
2151
|
+
};
|
|
2152
|
+
function calculateDelay(attempt, config) {
|
|
2153
|
+
let delay;
|
|
2154
|
+
if (config.exponentialBackoff) {
|
|
2155
|
+
delay = config.retryDelay * Math.pow(2, attempt - 1);
|
|
2156
|
+
}
|
|
2157
|
+
else {
|
|
2158
|
+
delay = config.retryDelay * attempt;
|
|
2159
|
+
}
|
|
2160
|
+
if (config.maxRetryDelay) {
|
|
2161
|
+
delay = Math.min(delay, config.maxRetryDelay);
|
|
2162
|
+
}
|
|
2163
|
+
if (config.jitter && config.jitter > 0) {
|
|
2164
|
+
const jitterAmount = delay * config.jitter;
|
|
2165
|
+
const randomJitter = Math.random() * jitterAmount * 2 - jitterAmount;
|
|
2166
|
+
delay += randomJitter;
|
|
2167
|
+
}
|
|
2168
|
+
return Math.max(0, Math.floor(delay));
|
|
2169
|
+
}
|
|
2170
|
+
function isRetryableError(error, config) {
|
|
2171
|
+
if (error instanceof EtsyApiError) {
|
|
2172
|
+
const statusCode = error.statusCode;
|
|
2173
|
+
if (statusCode && config.retryableStatusCodes.includes(statusCode)) {
|
|
2174
|
+
return true;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
if (error instanceof TypeError && error.message.includes('fetch')) {
|
|
2178
|
+
return true;
|
|
2179
|
+
}
|
|
2180
|
+
return false;
|
|
2181
|
+
}
|
|
2182
|
+
function getRetryAfterDelay(error) {
|
|
2183
|
+
if (error instanceof EtsyApiError && error.statusCode === 429) {
|
|
2184
|
+
const retryAfter = error.getRetryAfter();
|
|
2185
|
+
if (typeof retryAfter === 'number') {
|
|
2186
|
+
return retryAfter * 1000;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return null;
|
|
2190
|
+
}
|
|
2191
|
+
function sleep(ms) {
|
|
2192
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
2193
|
+
}
|
|
2194
|
+
async function withRetry(operation, options = {}) {
|
|
2195
|
+
const config = {
|
|
2196
|
+
...DEFAULT_RETRY_CONFIG,
|
|
2197
|
+
...options
|
|
2198
|
+
};
|
|
2199
|
+
let lastError = new Error('Unknown error');
|
|
2200
|
+
let attempt = 0;
|
|
2201
|
+
while (attempt <= config.maxRetries) {
|
|
2202
|
+
try {
|
|
2203
|
+
if (options.signal?.aborted) {
|
|
2204
|
+
throw new Error('Operation aborted');
|
|
2205
|
+
}
|
|
2206
|
+
const result = await operation();
|
|
2207
|
+
return result;
|
|
2208
|
+
}
|
|
2209
|
+
catch (error) {
|
|
2210
|
+
lastError = error;
|
|
2211
|
+
attempt++;
|
|
2212
|
+
if (attempt > config.maxRetries) {
|
|
2213
|
+
throw lastError;
|
|
2214
|
+
}
|
|
2215
|
+
if (!isRetryableError(error, config)) {
|
|
2216
|
+
throw lastError;
|
|
2217
|
+
}
|
|
2218
|
+
let delay = calculateDelay(attempt, config);
|
|
2219
|
+
const retryAfterDelay = getRetryAfterDelay(error);
|
|
2220
|
+
if (retryAfterDelay !== null) {
|
|
2221
|
+
delay = retryAfterDelay;
|
|
2222
|
+
}
|
|
2223
|
+
if (config.onRetry) {
|
|
2224
|
+
config.onRetry(attempt, lastError);
|
|
2225
|
+
}
|
|
2226
|
+
await sleep(delay);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
throw lastError;
|
|
2230
|
+
}
|
|
2231
|
+
class RetryManager {
|
|
2232
|
+
constructor(config = {}) {
|
|
2233
|
+
this.config = {
|
|
2234
|
+
...DEFAULT_RETRY_CONFIG,
|
|
2235
|
+
...config
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
async execute(operation, options) {
|
|
2239
|
+
return withRetry(operation, {
|
|
2240
|
+
...this.config,
|
|
2241
|
+
...options
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
updateConfig(config) {
|
|
2245
|
+
this.config = {
|
|
2246
|
+
...this.config,
|
|
2247
|
+
...config
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
getConfig() {
|
|
2251
|
+
return { ...this.config };
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
class EtsyWebhookHandler {
|
|
2256
|
+
constructor(config) {
|
|
2257
|
+
this.config = {
|
|
2258
|
+
algorithm: 'sha256',
|
|
2259
|
+
verifySignatures: true,
|
|
2260
|
+
...config
|
|
2261
|
+
};
|
|
2262
|
+
this.handlers = new Map();
|
|
2263
|
+
if (isNode$1) {
|
|
2264
|
+
try {
|
|
2265
|
+
this.crypto = require('crypto');
|
|
2266
|
+
}
|
|
2267
|
+
catch {
|
|
2268
|
+
console.warn('crypto module not available, signature verification will not work');
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
verifySignature(payload, signature) {
|
|
2273
|
+
if (!this.config.verifySignatures) {
|
|
2274
|
+
return true;
|
|
2275
|
+
}
|
|
2276
|
+
if (!this.crypto) {
|
|
2277
|
+
throw new Error('Signature verification requires Node.js crypto module');
|
|
2278
|
+
}
|
|
2279
|
+
try {
|
|
2280
|
+
const hmac = this.crypto
|
|
2281
|
+
.createHmac(this.config.algorithm, this.config.secret)
|
|
2282
|
+
.update(payload)
|
|
2283
|
+
.digest('hex');
|
|
2284
|
+
return this.timingSafeEqual(hmac, signature);
|
|
2285
|
+
}
|
|
2286
|
+
catch (error) {
|
|
2287
|
+
console.error('Signature verification failed:', error);
|
|
2288
|
+
return false;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
parseEvent(payload) {
|
|
2292
|
+
const data = typeof payload === 'string' ? JSON.parse(payload) : payload;
|
|
2293
|
+
if (!data.type || !data.data) {
|
|
2294
|
+
throw new Error('Invalid webhook event format');
|
|
2295
|
+
}
|
|
2296
|
+
const event = {
|
|
2297
|
+
type: data.type,
|
|
2298
|
+
timestamp: data.timestamp || Date.now(),
|
|
2299
|
+
data: data.data,
|
|
2300
|
+
shop_id: data.shop_id,
|
|
2301
|
+
user_id: data.user_id
|
|
2302
|
+
};
|
|
2303
|
+
this.triggerHandlers(event);
|
|
2304
|
+
return event;
|
|
2305
|
+
}
|
|
2306
|
+
on(eventType, handler) {
|
|
2307
|
+
if (!this.handlers.has(eventType)) {
|
|
2308
|
+
this.handlers.set(eventType, new Set());
|
|
2309
|
+
}
|
|
2310
|
+
const handlers = this.handlers.get(eventType);
|
|
2311
|
+
if (handlers) {
|
|
2312
|
+
handlers.add(handler);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
off(eventType, handler) {
|
|
2316
|
+
const handlers = this.handlers.get(eventType);
|
|
2317
|
+
if (handlers) {
|
|
2318
|
+
handlers.delete(handler);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
removeAllListeners(eventType) {
|
|
2322
|
+
if (eventType) {
|
|
2323
|
+
this.handlers.delete(eventType);
|
|
2324
|
+
}
|
|
2325
|
+
else {
|
|
2326
|
+
this.handlers.clear();
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
async triggerHandlers(event) {
|
|
2330
|
+
const handlers = this.handlers.get(event.type);
|
|
2331
|
+
if (!handlers || handlers.size === 0) {
|
|
2332
|
+
return;
|
|
2333
|
+
}
|
|
2334
|
+
const promises = Array.from(handlers).map(handler => {
|
|
2335
|
+
try {
|
|
2336
|
+
return Promise.resolve(handler(event.data));
|
|
2337
|
+
}
|
|
2338
|
+
catch (error) {
|
|
2339
|
+
console.error(`Error in webhook handler for ${event.type}:`, error);
|
|
2340
|
+
return Promise.resolve();
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
await Promise.all(promises);
|
|
2344
|
+
}
|
|
2345
|
+
timingSafeEqual(a, b) {
|
|
2346
|
+
if (this.crypto?.timingSafeEqual) {
|
|
2347
|
+
const bufA = Buffer.from(a);
|
|
2348
|
+
const bufB = Buffer.from(b);
|
|
2349
|
+
if (bufA.length !== bufB.length) {
|
|
2350
|
+
return false;
|
|
2351
|
+
}
|
|
2352
|
+
return this.crypto.timingSafeEqual(bufA, bufB);
|
|
2353
|
+
}
|
|
2354
|
+
else {
|
|
2355
|
+
return a === b;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
getHandlerCount(eventType) {
|
|
2359
|
+
return this.handlers.get(eventType)?.size || 0;
|
|
2360
|
+
}
|
|
2361
|
+
getRegisteredEventTypes() {
|
|
2362
|
+
return Array.from(this.handlers.keys());
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
function createWebhookHandler(config) {
|
|
2366
|
+
return new EtsyWebhookHandler(config);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
class LRUCache {
|
|
2370
|
+
constructor(config = {}) {
|
|
2371
|
+
this.cache = new Map();
|
|
2372
|
+
this.currentSize = 0;
|
|
2373
|
+
this.stats = {
|
|
2374
|
+
hits: 0,
|
|
2375
|
+
misses: 0,
|
|
2376
|
+
evictions: 0
|
|
2377
|
+
};
|
|
2378
|
+
this.maxSize = config.maxSize ?? 10485760;
|
|
2379
|
+
this.maxEntries = config.maxEntries ?? 1000;
|
|
2380
|
+
this.ttl = (config.ttl ?? 3600) * 1000;
|
|
2381
|
+
this.trackStats = config.trackStats ?? true;
|
|
2382
|
+
}
|
|
2383
|
+
async get(key) {
|
|
2384
|
+
const entry = this.cache.get(key);
|
|
2385
|
+
if (!entry) {
|
|
2386
|
+
if (this.trackStats)
|
|
2387
|
+
this.stats.misses++;
|
|
2388
|
+
return null;
|
|
2389
|
+
}
|
|
2390
|
+
if (Date.now() > entry.expires) {
|
|
2391
|
+
this.cache.delete(key);
|
|
2392
|
+
this.currentSize -= entry.size;
|
|
2393
|
+
if (this.trackStats)
|
|
2394
|
+
this.stats.misses++;
|
|
2395
|
+
return null;
|
|
2396
|
+
}
|
|
2397
|
+
entry.lastAccessed = Date.now();
|
|
2398
|
+
entry.accessCount++;
|
|
2399
|
+
this.cache.delete(key);
|
|
2400
|
+
this.cache.set(key, entry);
|
|
2401
|
+
if (this.trackStats)
|
|
2402
|
+
this.stats.hits++;
|
|
2403
|
+
return entry.value;
|
|
2404
|
+
}
|
|
2405
|
+
async set(key, value, ttl) {
|
|
2406
|
+
const size = this.estimateSize(value);
|
|
2407
|
+
const expiryTime = ttl ? ttl * 1000 : this.ttl;
|
|
2408
|
+
const now = Date.now();
|
|
2409
|
+
const existing = this.cache.get(key);
|
|
2410
|
+
if (existing) {
|
|
2411
|
+
this.currentSize -= existing.size;
|
|
2412
|
+
this.cache.delete(key);
|
|
2413
|
+
}
|
|
2414
|
+
while ((this.currentSize + size > this.maxSize || this.cache.size >= this.maxEntries) &&
|
|
2415
|
+
this.cache.size > 0) {
|
|
2416
|
+
await this.evictLRU();
|
|
2417
|
+
}
|
|
2418
|
+
const entry = {
|
|
2419
|
+
key,
|
|
2420
|
+
value,
|
|
2421
|
+
expires: now + expiryTime,
|
|
2422
|
+
size,
|
|
2423
|
+
accessCount: 0,
|
|
2424
|
+
lastAccessed: now,
|
|
2425
|
+
created: now
|
|
2426
|
+
};
|
|
2427
|
+
this.cache.set(key, entry);
|
|
2428
|
+
this.currentSize += size;
|
|
2429
|
+
}
|
|
2430
|
+
async delete(key) {
|
|
2431
|
+
const entry = this.cache.get(key);
|
|
2432
|
+
if (entry) {
|
|
2433
|
+
this.cache.delete(key);
|
|
2434
|
+
this.currentSize -= entry.size;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
async clear() {
|
|
2438
|
+
this.cache.clear();
|
|
2439
|
+
this.currentSize = 0;
|
|
2440
|
+
this.stats = { hits: 0, misses: 0, evictions: 0 };
|
|
2441
|
+
}
|
|
2442
|
+
async evictLRU() {
|
|
2443
|
+
const firstKey = this.cache.keys().next().value;
|
|
2444
|
+
if (firstKey) {
|
|
2445
|
+
const entry = this.cache.get(firstKey);
|
|
2446
|
+
if (entry) {
|
|
2447
|
+
this.currentSize -= entry.size;
|
|
2448
|
+
}
|
|
2449
|
+
this.cache.delete(firstKey);
|
|
2450
|
+
if (this.trackStats)
|
|
2451
|
+
this.stats.evictions++;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
estimateSize(str) {
|
|
2455
|
+
return str.length * 2;
|
|
2456
|
+
}
|
|
2457
|
+
getStats() {
|
|
2458
|
+
const total = this.stats.hits + this.stats.misses;
|
|
2459
|
+
return {
|
|
2460
|
+
hits: this.stats.hits,
|
|
2461
|
+
misses: this.stats.misses,
|
|
2462
|
+
hitRate: total > 0 ? this.stats.hits / total : 0,
|
|
2463
|
+
missRate: total > 0 ? this.stats.misses / total : 0,
|
|
2464
|
+
size: this.currentSize,
|
|
2465
|
+
entryCount: this.cache.size,
|
|
2466
|
+
evictions: this.stats.evictions,
|
|
2467
|
+
maxSize: this.maxSize,
|
|
2468
|
+
maxEntries: this.maxEntries
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
class LFUCache {
|
|
2473
|
+
constructor(config = {}) {
|
|
2474
|
+
this.cache = new Map();
|
|
2475
|
+
this.currentSize = 0;
|
|
2476
|
+
this.stats = {
|
|
2477
|
+
hits: 0,
|
|
2478
|
+
misses: 0,
|
|
2479
|
+
evictions: 0
|
|
2480
|
+
};
|
|
2481
|
+
this.maxSize = config.maxSize ?? 10485760;
|
|
2482
|
+
this.maxEntries = config.maxEntries ?? 1000;
|
|
2483
|
+
this.ttl = (config.ttl ?? 3600) * 1000;
|
|
2484
|
+
this.trackStats = config.trackStats ?? true;
|
|
2485
|
+
}
|
|
2486
|
+
async get(key) {
|
|
2487
|
+
const entry = this.cache.get(key);
|
|
2488
|
+
if (!entry) {
|
|
2489
|
+
if (this.trackStats)
|
|
2490
|
+
this.stats.misses++;
|
|
2491
|
+
return null;
|
|
2492
|
+
}
|
|
2493
|
+
if (Date.now() > entry.expires) {
|
|
2494
|
+
this.cache.delete(key);
|
|
2495
|
+
this.currentSize -= entry.size;
|
|
2496
|
+
if (this.trackStats)
|
|
2497
|
+
this.stats.misses++;
|
|
2498
|
+
return null;
|
|
2499
|
+
}
|
|
2500
|
+
entry.accessCount++;
|
|
2501
|
+
entry.lastAccessed = Date.now();
|
|
2502
|
+
if (this.trackStats)
|
|
2503
|
+
this.stats.hits++;
|
|
2504
|
+
return entry.value;
|
|
2505
|
+
}
|
|
2506
|
+
async set(key, value, ttl) {
|
|
2507
|
+
const size = this.estimateSize(value);
|
|
2508
|
+
const expiryTime = ttl ? ttl * 1000 : this.ttl;
|
|
2509
|
+
const now = Date.now();
|
|
2510
|
+
const existing = this.cache.get(key);
|
|
2511
|
+
if (existing) {
|
|
2512
|
+
this.currentSize -= existing.size;
|
|
2513
|
+
this.cache.delete(key);
|
|
2514
|
+
}
|
|
2515
|
+
while ((this.currentSize + size > this.maxSize || this.cache.size >= this.maxEntries) &&
|
|
2516
|
+
this.cache.size > 0) {
|
|
2517
|
+
await this.evictLFU();
|
|
2518
|
+
}
|
|
2519
|
+
const entry = {
|
|
2520
|
+
key,
|
|
2521
|
+
value,
|
|
2522
|
+
expires: now + expiryTime,
|
|
2523
|
+
size,
|
|
2524
|
+
accessCount: 0,
|
|
2525
|
+
lastAccessed: now,
|
|
2526
|
+
created: now
|
|
2527
|
+
};
|
|
2528
|
+
this.cache.set(key, entry);
|
|
2529
|
+
this.currentSize += size;
|
|
2530
|
+
}
|
|
2531
|
+
async delete(key) {
|
|
2532
|
+
const entry = this.cache.get(key);
|
|
2533
|
+
if (entry) {
|
|
2534
|
+
this.cache.delete(key);
|
|
2535
|
+
this.currentSize -= entry.size;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
async clear() {
|
|
2539
|
+
this.cache.clear();
|
|
2540
|
+
this.currentSize = 0;
|
|
2541
|
+
this.stats = { hits: 0, misses: 0, evictions: 0 };
|
|
2542
|
+
}
|
|
2543
|
+
async evictLFU() {
|
|
2544
|
+
let minAccessCount = Infinity;
|
|
2545
|
+
let keyToEvict = null;
|
|
2546
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
2547
|
+
if (entry.accessCount < minAccessCount) {
|
|
2548
|
+
minAccessCount = entry.accessCount;
|
|
2549
|
+
keyToEvict = key;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
if (keyToEvict) {
|
|
2553
|
+
const entry = this.cache.get(keyToEvict);
|
|
2554
|
+
if (entry) {
|
|
2555
|
+
this.currentSize -= entry.size;
|
|
2556
|
+
}
|
|
2557
|
+
this.cache.delete(keyToEvict);
|
|
2558
|
+
if (this.trackStats)
|
|
2559
|
+
this.stats.evictions++;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
estimateSize(str) {
|
|
2563
|
+
return str.length * 2;
|
|
2564
|
+
}
|
|
2565
|
+
getStats() {
|
|
2566
|
+
const total = this.stats.hits + this.stats.misses;
|
|
2567
|
+
return {
|
|
2568
|
+
hits: this.stats.hits,
|
|
2569
|
+
misses: this.stats.misses,
|
|
2570
|
+
hitRate: total > 0 ? this.stats.hits / total : 0,
|
|
2571
|
+
missRate: total > 0 ? this.stats.misses / total : 0,
|
|
2572
|
+
size: this.currentSize,
|
|
2573
|
+
entryCount: this.cache.size,
|
|
2574
|
+
evictions: this.stats.evictions,
|
|
2575
|
+
maxSize: this.maxSize,
|
|
2576
|
+
maxEntries: this.maxEntries
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
class CacheWithInvalidation {
|
|
2581
|
+
constructor(baseCache, config = {}) {
|
|
2582
|
+
this.baseCache = baseCache;
|
|
2583
|
+
this.invalidationPatterns = new Set(config.invalidateOn?.patterns || []);
|
|
2584
|
+
this.mutationPatterns = new Map();
|
|
2585
|
+
if (config.invalidateOn?.mutations !== false) {
|
|
2586
|
+
this.mutationPatterns.set('shops/*/update', ['shops/*']);
|
|
2587
|
+
this.mutationPatterns.set('listings/*/update', ['listings/*']);
|
|
2588
|
+
this.mutationPatterns.set('receipts/*/update', ['receipts/*']);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
async get(key) {
|
|
2592
|
+
return this.baseCache.get(key);
|
|
2593
|
+
}
|
|
2594
|
+
async set(key, value, ttl) {
|
|
2595
|
+
return this.baseCache.set(key, value, ttl);
|
|
2596
|
+
}
|
|
2597
|
+
async delete(key) {
|
|
2598
|
+
return this.baseCache.delete(key);
|
|
2599
|
+
}
|
|
2600
|
+
async clear() {
|
|
2601
|
+
return this.baseCache.clear();
|
|
2602
|
+
}
|
|
2603
|
+
async invalidatePattern(_pattern) {
|
|
2604
|
+
await this.baseCache.clear();
|
|
2605
|
+
}
|
|
2606
|
+
async invalidateOnMutation(mutationType) {
|
|
2607
|
+
const patterns = this.mutationPatterns.get(mutationType);
|
|
2608
|
+
if (patterns) {
|
|
2609
|
+
for (const pattern of patterns) {
|
|
2610
|
+
await this.invalidatePattern(pattern);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
addInvalidationPattern(pattern) {
|
|
2615
|
+
this.invalidationPatterns.add(pattern);
|
|
2616
|
+
}
|
|
2617
|
+
removeInvalidationPattern(pattern) {
|
|
2618
|
+
this.invalidationPatterns.delete(pattern);
|
|
2619
|
+
}
|
|
2620
|
+
addMutationPattern(mutationType, patterns) {
|
|
2621
|
+
this.mutationPatterns.set(mutationType, patterns);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
class RedisCacheStorage {
|
|
2625
|
+
constructor(config, client) {
|
|
2626
|
+
this.config = config;
|
|
2627
|
+
this.keyPrefix = config.keyPrefix || 'etsy:';
|
|
2628
|
+
if (!client) {
|
|
2629
|
+
throw new Error('Redis client must be provided. Install a Redis client library (e.g., ioredis) and pass the client instance.');
|
|
2630
|
+
}
|
|
2631
|
+
this.client = client;
|
|
2632
|
+
}
|
|
2633
|
+
async get(key) {
|
|
2634
|
+
try {
|
|
2635
|
+
const value = await this.client.get(this.keyPrefix + key);
|
|
2636
|
+
return value;
|
|
2637
|
+
}
|
|
2638
|
+
catch (error) {
|
|
2639
|
+
console.error('Redis get error:', error);
|
|
2640
|
+
return null;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
async set(key, value, ttl = 3600) {
|
|
2644
|
+
try {
|
|
2645
|
+
await this.client.setex(this.keyPrefix + key, ttl, value);
|
|
2646
|
+
}
|
|
2647
|
+
catch (error) {
|
|
2648
|
+
console.error('Redis set error:', error);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
async delete(key) {
|
|
2652
|
+
try {
|
|
2653
|
+
await this.client.del(this.keyPrefix + key);
|
|
2654
|
+
}
|
|
2655
|
+
catch (error) {
|
|
2656
|
+
console.error('Redis delete error:', error);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
async clear() {
|
|
2660
|
+
try {
|
|
2661
|
+
const keys = await this.client.keys(this.keyPrefix + '*');
|
|
2662
|
+
if (keys.length > 0) {
|
|
2663
|
+
await this.client.del(...keys);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
catch (error) {
|
|
2667
|
+
console.error('Redis clear error:', error);
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
getClient() {
|
|
2671
|
+
return this.client;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
function createCacheStorage(config = {}) {
|
|
2675
|
+
const strategy = config.strategy || 'lru';
|
|
2676
|
+
switch (strategy) {
|
|
2677
|
+
case 'lru':
|
|
2678
|
+
return new LRUCache(config);
|
|
2679
|
+
case 'lfu':
|
|
2680
|
+
return new LFUCache(config);
|
|
2681
|
+
case 'ttl':
|
|
2682
|
+
return new LRUCache(config);
|
|
2683
|
+
default:
|
|
2684
|
+
return new LRUCache(config);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
function createCacheWithInvalidation(baseCache, config = {}) {
|
|
2688
|
+
return new CacheWithInvalidation(baseCache, config);
|
|
2689
|
+
}
|
|
2690
|
+
function createRedisCacheStorage(config, client) {
|
|
2691
|
+
return new RedisCacheStorage(config, client);
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
class ListingQueryBuilder {
|
|
2695
|
+
constructor(client, shopId) {
|
|
2696
|
+
this.params = {};
|
|
2697
|
+
this.includeFields = [];
|
|
2698
|
+
this.client = client;
|
|
2699
|
+
this.shopId = shopId;
|
|
2700
|
+
}
|
|
2701
|
+
where(filters) {
|
|
2702
|
+
if (filters.state) {
|
|
2703
|
+
this.params.state = filters.state;
|
|
2704
|
+
}
|
|
2705
|
+
return this;
|
|
2706
|
+
}
|
|
2707
|
+
include(fields) {
|
|
2708
|
+
this.includeFields = fields;
|
|
2709
|
+
this.params.includes = fields;
|
|
2710
|
+
return this;
|
|
2711
|
+
}
|
|
2712
|
+
limit(limit) {
|
|
2713
|
+
this.params.limit = limit;
|
|
2714
|
+
return this;
|
|
2715
|
+
}
|
|
2716
|
+
offset(offset) {
|
|
2717
|
+
this.params.offset = offset;
|
|
2718
|
+
return this;
|
|
2719
|
+
}
|
|
2720
|
+
sortBy(field, order = 'asc') {
|
|
2721
|
+
this.params.sort_on = field;
|
|
2722
|
+
this.params.sort_order = (order === 'asc' ? 'up' : 'down');
|
|
2723
|
+
return this;
|
|
2724
|
+
}
|
|
2725
|
+
async fetch() {
|
|
2726
|
+
if (!this.shopId) {
|
|
2727
|
+
throw new Error('Shop ID is required for listing queries');
|
|
2728
|
+
}
|
|
2729
|
+
return this.client.getListingsByShop(this.shopId, this.params);
|
|
2730
|
+
}
|
|
2731
|
+
async first() {
|
|
2732
|
+
this.params.limit = 1;
|
|
2733
|
+
const results = await this.fetch();
|
|
2734
|
+
return results[0] || null;
|
|
2735
|
+
}
|
|
2736
|
+
async count() {
|
|
2737
|
+
const results = await this.fetch();
|
|
2738
|
+
return results.length;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
class ReceiptQueryBuilder {
|
|
2742
|
+
constructor(client, shopId) {
|
|
2743
|
+
this.params = {};
|
|
2744
|
+
this.client = client;
|
|
2745
|
+
this.shopId = shopId;
|
|
2746
|
+
}
|
|
2747
|
+
where(filters) {
|
|
2748
|
+
Object.assign(this.params, filters);
|
|
2749
|
+
return this;
|
|
2750
|
+
}
|
|
2751
|
+
limit(limit) {
|
|
2752
|
+
this.params.limit = limit;
|
|
2753
|
+
return this;
|
|
2754
|
+
}
|
|
2755
|
+
offset(offset) {
|
|
2756
|
+
this.params.offset = offset;
|
|
2757
|
+
return this;
|
|
2758
|
+
}
|
|
2759
|
+
sortBy(field, order = 'desc') {
|
|
2760
|
+
this.params.sort_on = field;
|
|
2761
|
+
this.params.sort_order = (order === 'asc' ? 'up' : 'down');
|
|
2762
|
+
return this;
|
|
2763
|
+
}
|
|
2764
|
+
async fetch() {
|
|
2765
|
+
return this.client.getShopReceipts(this.shopId, this.params);
|
|
2766
|
+
}
|
|
2767
|
+
async first() {
|
|
2768
|
+
this.params.limit = 1;
|
|
2769
|
+
const results = await this.fetch();
|
|
2770
|
+
return results[0] || null;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
class BatchQueryExecutor {
|
|
2774
|
+
constructor(client) {
|
|
2775
|
+
this.operations = [];
|
|
2776
|
+
this.client = client;
|
|
2777
|
+
}
|
|
2778
|
+
getShop(shopId) {
|
|
2779
|
+
this.operations.push(() => this.client.getShop(shopId));
|
|
2780
|
+
return this;
|
|
2781
|
+
}
|
|
2782
|
+
getListings(shopId, params) {
|
|
2783
|
+
this.operations.push(() => this.client.getListingsByShop(shopId, params));
|
|
2784
|
+
return this;
|
|
2785
|
+
}
|
|
2786
|
+
getReceipts(shopId, params) {
|
|
2787
|
+
this.operations.push(() => this.client.getShopReceipts(shopId, params));
|
|
2788
|
+
return this;
|
|
2789
|
+
}
|
|
2790
|
+
custom(operation) {
|
|
2791
|
+
this.operations.push(operation);
|
|
2792
|
+
return this;
|
|
2793
|
+
}
|
|
2794
|
+
async execute() {
|
|
2795
|
+
return Promise.all(this.operations.map(op => op()));
|
|
2796
|
+
}
|
|
2797
|
+
async executeSequential() {
|
|
2798
|
+
const results = [];
|
|
2799
|
+
for (const operation of this.operations) {
|
|
2800
|
+
results.push(await operation());
|
|
2801
|
+
}
|
|
2802
|
+
return results;
|
|
2803
|
+
}
|
|
2804
|
+
clear() {
|
|
2805
|
+
this.operations = [];
|
|
2806
|
+
return this;
|
|
2807
|
+
}
|
|
2808
|
+
size() {
|
|
2809
|
+
return this.operations.length;
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
function withQueryBuilder(client) {
|
|
2813
|
+
const extendedClient = client;
|
|
2814
|
+
extendedClient.listings = function (shopId) {
|
|
2815
|
+
return new ListingQueryBuilder(this, shopId);
|
|
2816
|
+
};
|
|
2817
|
+
extendedClient.receipts = function (shopId) {
|
|
2818
|
+
return new ReceiptQueryBuilder(this, shopId);
|
|
2819
|
+
};
|
|
2820
|
+
extendedClient.batch = function () {
|
|
2821
|
+
return new BatchQueryExecutor(this);
|
|
2822
|
+
};
|
|
2823
|
+
return extendedClient;
|
|
2824
|
+
}
|
|
2825
|
+
function createListingQuery(client, shopId) {
|
|
2826
|
+
return new ListingQueryBuilder(client, shopId);
|
|
2827
|
+
}
|
|
2828
|
+
function createReceiptQuery(client, shopId) {
|
|
2829
|
+
return new ReceiptQueryBuilder(client, shopId);
|
|
2830
|
+
}
|
|
2831
|
+
function createBatchQuery(client) {
|
|
2832
|
+
return new BatchQueryExecutor(client);
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
async function encryptAES256GCM(data, key) {
|
|
2836
|
+
if (!isNode$1) {
|
|
2837
|
+
throw new Error('AES-256-GCM encryption is only available in Node.js environments');
|
|
2838
|
+
}
|
|
2839
|
+
const crypto = await import('crypto');
|
|
2840
|
+
const keyBuffer = Buffer.isBuffer(key) ? key : Buffer.from(key, 'utf8');
|
|
2841
|
+
if (keyBuffer.length !== 32) {
|
|
2842
|
+
throw new Error('Encryption key must be exactly 32 bytes for AES-256');
|
|
2843
|
+
}
|
|
2844
|
+
const iv = crypto.randomBytes(12);
|
|
2845
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', keyBuffer, iv);
|
|
2846
|
+
const encrypted = Buffer.concat([
|
|
2847
|
+
cipher.update(data, 'utf8'),
|
|
2848
|
+
cipher.final()
|
|
2849
|
+
]);
|
|
2850
|
+
const authTag = cipher.getAuthTag();
|
|
2851
|
+
return {
|
|
2852
|
+
ciphertext: encrypted.toString('base64'),
|
|
2853
|
+
iv: iv.toString('base64'),
|
|
2854
|
+
authTag: authTag.toString('base64'),
|
|
2855
|
+
algorithm: 'aes-256-gcm'
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
async function decryptAES256GCM(encryptedData, key) {
|
|
2859
|
+
if (!isNode$1) {
|
|
2860
|
+
throw new Error('AES-256-GCM decryption is only available in Node.js environments');
|
|
2861
|
+
}
|
|
2862
|
+
const crypto = await import('crypto');
|
|
2863
|
+
const keyBuffer = Buffer.isBuffer(key) ? key : Buffer.from(key, 'utf8');
|
|
2864
|
+
if (keyBuffer.length !== 32) {
|
|
2865
|
+
throw new Error('Decryption key must be exactly 32 bytes for AES-256');
|
|
2866
|
+
}
|
|
2867
|
+
if (encryptedData.algorithm !== 'aes-256-gcm') {
|
|
2868
|
+
throw new Error(`Unsupported encryption algorithm: ${encryptedData.algorithm}`);
|
|
2869
|
+
}
|
|
2870
|
+
const iv = Buffer.from(encryptedData.iv, 'base64');
|
|
2871
|
+
const authTag = Buffer.from(encryptedData.authTag, 'base64');
|
|
2872
|
+
const ciphertext = Buffer.from(encryptedData.ciphertext, 'base64');
|
|
2873
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuffer, iv);
|
|
2874
|
+
decipher.setAuthTag(authTag);
|
|
2875
|
+
const decrypted = Buffer.concat([
|
|
2876
|
+
decipher.update(ciphertext),
|
|
2877
|
+
decipher.final()
|
|
2878
|
+
]);
|
|
2879
|
+
return decrypted.toString('utf8');
|
|
2880
|
+
}
|
|
2881
|
+
async function generateEncryptionKey(length = 32) {
|
|
2882
|
+
if (!isNode$1) {
|
|
2883
|
+
throw new Error('Key generation is only available in Node.js environments');
|
|
2884
|
+
}
|
|
2885
|
+
const crypto = await import('crypto');
|
|
2886
|
+
return crypto.randomBytes(length);
|
|
2887
|
+
}
|
|
2888
|
+
async function deriveKeyFromPassword(password, salt, iterations = 100000, keyLength = 32) {
|
|
2889
|
+
if (!isNode$1) {
|
|
2890
|
+
throw new Error('Key derivation is only available in Node.js environments');
|
|
2891
|
+
}
|
|
2892
|
+
const crypto = await import('crypto');
|
|
2893
|
+
const saltBuffer = Buffer.isBuffer(salt) ? salt : Buffer.from(salt, 'utf8');
|
|
2894
|
+
return new Promise((resolve, reject) => {
|
|
2895
|
+
crypto.pbkdf2(password, saltBuffer, iterations, keyLength, 'sha256', (err, derivedKey) => {
|
|
2896
|
+
if (err) {
|
|
2897
|
+
reject(err);
|
|
2898
|
+
}
|
|
2899
|
+
else {
|
|
2900
|
+
resolve(derivedKey);
|
|
2901
|
+
}
|
|
2902
|
+
});
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
function validateEncryptionKey(key, requiredLength = 32) {
|
|
2906
|
+
const keyBuffer = Buffer.isBuffer(key) ? key : Buffer.from(key, 'utf8');
|
|
2907
|
+
if (keyBuffer.length !== requiredLength) {
|
|
2908
|
+
throw new Error(`Invalid encryption key length: expected ${requiredLength} bytes, got ${keyBuffer.length} bytes`);
|
|
2909
|
+
}
|
|
2910
|
+
return true;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
class EncryptedFileTokenStorage {
|
|
2914
|
+
constructor(config) {
|
|
2915
|
+
if (!isNode$1) {
|
|
2916
|
+
throw new Error('EncryptedFileTokenStorage is only available in Node.js environments');
|
|
2917
|
+
}
|
|
2918
|
+
this.filePath = config.filePath;
|
|
2919
|
+
this.fileMode = config.fileMode ?? 0o600;
|
|
2920
|
+
const keyBuffer = Buffer.isBuffer(config.encryptionKey)
|
|
2921
|
+
? config.encryptionKey
|
|
2922
|
+
: Buffer.from(config.encryptionKey, 'utf8');
|
|
2923
|
+
validateEncryptionKey(keyBuffer, 32);
|
|
2924
|
+
this.encryptionKey = keyBuffer;
|
|
2925
|
+
}
|
|
2926
|
+
async save(tokens) {
|
|
2927
|
+
if (!isNode$1) {
|
|
2928
|
+
throw new Error('EncryptedFileTokenStorage is only available in Node.js');
|
|
2929
|
+
}
|
|
2930
|
+
try {
|
|
2931
|
+
const tokenData = JSON.stringify(tokens);
|
|
2932
|
+
const encrypted = await encryptAES256GCM(tokenData, this.encryptionKey);
|
|
2933
|
+
const fileData = {
|
|
2934
|
+
version: 1,
|
|
2935
|
+
encrypted,
|
|
2936
|
+
timestamp: Date.now()
|
|
2937
|
+
};
|
|
2938
|
+
const fileContent = JSON.stringify(fileData, null, 2);
|
|
2939
|
+
await this._writeFile(this.filePath, fileContent);
|
|
2940
|
+
await this._setFilePermissions(this.filePath, this.fileMode);
|
|
2941
|
+
}
|
|
2942
|
+
catch (error) {
|
|
2943
|
+
throw new Error(`Failed to save encrypted tokens: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
async load() {
|
|
2947
|
+
if (!isNode$1) {
|
|
2948
|
+
return null;
|
|
2949
|
+
}
|
|
2950
|
+
try {
|
|
2951
|
+
const fileContent = await this._readFile(this.filePath);
|
|
2952
|
+
const fileData = JSON.parse(fileContent);
|
|
2953
|
+
if (fileData.version !== 1) {
|
|
2954
|
+
throw new Error(`Unsupported encrypted file format version: ${fileData.version}`);
|
|
2955
|
+
}
|
|
2956
|
+
const decryptedData = await decryptAES256GCM(fileData.encrypted, this.encryptionKey);
|
|
2957
|
+
const tokens = JSON.parse(decryptedData);
|
|
2958
|
+
if (tokens.expires_at) {
|
|
2959
|
+
tokens.expires_at = new Date(tokens.expires_at);
|
|
2960
|
+
}
|
|
2961
|
+
return tokens;
|
|
2962
|
+
}
|
|
2963
|
+
catch (error) {
|
|
2964
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
console.error('Failed to load encrypted tokens:', error instanceof Error ? error.message : 'Unknown error');
|
|
2968
|
+
return null;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
async clear() {
|
|
2972
|
+
if (!isNode$1) {
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
try {
|
|
2976
|
+
await this._deleteFile(this.filePath);
|
|
2977
|
+
}
|
|
2978
|
+
catch (error) {
|
|
2979
|
+
if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') {
|
|
2980
|
+
console.error('Failed to clear encrypted tokens:', error instanceof Error ? error.message : 'Unknown error');
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
getFilePath() {
|
|
2985
|
+
return this.filePath;
|
|
2986
|
+
}
|
|
2987
|
+
async exists() {
|
|
2988
|
+
if (!isNode$1) {
|
|
2989
|
+
return false;
|
|
2990
|
+
}
|
|
2991
|
+
try {
|
|
2992
|
+
await this._stat(this.filePath);
|
|
2993
|
+
return true;
|
|
2994
|
+
}
|
|
2995
|
+
catch {
|
|
2996
|
+
return false;
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
async _writeFile(filePath, data) {
|
|
3000
|
+
if (typeof process === 'undefined')
|
|
3001
|
+
return;
|
|
3002
|
+
const fs = await import('fs');
|
|
3003
|
+
await fs.promises.writeFile(filePath, data, 'utf8');
|
|
3004
|
+
}
|
|
3005
|
+
async _setFilePermissions(filePath, mode) {
|
|
3006
|
+
if (typeof process === 'undefined')
|
|
3007
|
+
return;
|
|
3008
|
+
try {
|
|
3009
|
+
const fs = await import('fs');
|
|
3010
|
+
await fs.promises.chmod(filePath, mode);
|
|
3011
|
+
}
|
|
3012
|
+
catch {
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
async _readFile(filePath) {
|
|
3016
|
+
if (typeof process === 'undefined')
|
|
3017
|
+
throw new Error('Not available');
|
|
3018
|
+
const fs = await import('fs');
|
|
3019
|
+
return await fs.promises.readFile(filePath, 'utf8');
|
|
3020
|
+
}
|
|
3021
|
+
async _deleteFile(filePath) {
|
|
3022
|
+
if (typeof process === 'undefined')
|
|
3023
|
+
return;
|
|
3024
|
+
const fs = await import('fs');
|
|
3025
|
+
await fs.promises.unlink(filePath);
|
|
3026
|
+
}
|
|
3027
|
+
async _stat(filePath) {
|
|
3028
|
+
if (typeof process === 'undefined')
|
|
3029
|
+
throw new Error('Not available');
|
|
3030
|
+
const fs = await import('fs');
|
|
3031
|
+
return await fs.promises.stat(filePath);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
class WebhookSecurity {
|
|
3036
|
+
constructor(config) {
|
|
3037
|
+
this.secret = config.secret;
|
|
3038
|
+
this.algorithm = config.algorithm || 'sha256';
|
|
3039
|
+
if (!this.secret) {
|
|
3040
|
+
throw new Error('Webhook secret is required');
|
|
3041
|
+
}
|
|
3042
|
+
if (isNode$1) {
|
|
3043
|
+
try {
|
|
3044
|
+
this.crypto = require('crypto');
|
|
3045
|
+
}
|
|
3046
|
+
catch {
|
|
3047
|
+
throw new Error('crypto module not available, webhook security requires Node.js');
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
else {
|
|
3051
|
+
throw new Error('WebhookSecurity is only available in Node.js environments');
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
signRequest(payload, encoding = 'hex') {
|
|
3055
|
+
if (!this.crypto) {
|
|
3056
|
+
throw new Error('Crypto module not available');
|
|
3057
|
+
}
|
|
3058
|
+
const data = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
3059
|
+
const hmac = this.crypto
|
|
3060
|
+
.createHmac(this.algorithm, this.secret)
|
|
3061
|
+
.update(data)
|
|
3062
|
+
.digest(encoding);
|
|
3063
|
+
return hmac;
|
|
3064
|
+
}
|
|
3065
|
+
verifySignature(payload, signature, encoding = 'hex') {
|
|
3066
|
+
if (!this.crypto) {
|
|
3067
|
+
throw new Error('Crypto module not available');
|
|
3068
|
+
}
|
|
3069
|
+
try {
|
|
3070
|
+
const expectedSignature = this.signRequest(payload, encoding);
|
|
3071
|
+
return this.timingSafeEqual(expectedSignature, signature);
|
|
3072
|
+
}
|
|
3073
|
+
catch (error) {
|
|
3074
|
+
console.error('Signature verification failed:', error);
|
|
3075
|
+
return false;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
signRequestWithTimestamp(payload, timestamp, encoding = 'hex') {
|
|
3079
|
+
const ts = timestamp || Math.floor(Date.now() / 1000);
|
|
3080
|
+
const data = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
3081
|
+
const signedPayload = `${ts}.${data}`;
|
|
3082
|
+
const signature = this.signRequest(signedPayload, encoding);
|
|
3083
|
+
return {
|
|
3084
|
+
timestamp: ts,
|
|
3085
|
+
signature
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
verifySignatureWithTimestamp(payload, signature, timestamp, maxAge = 300, encoding = 'hex') {
|
|
3089
|
+
const now = Math.floor(Date.now() / 1000);
|
|
3090
|
+
const age = now - timestamp;
|
|
3091
|
+
if (age < 0) {
|
|
3092
|
+
console.warn('Webhook timestamp is in the future');
|
|
3093
|
+
return false;
|
|
3094
|
+
}
|
|
3095
|
+
if (age > maxAge) {
|
|
3096
|
+
console.warn(`Webhook request is too old: ${age} seconds`);
|
|
3097
|
+
return false;
|
|
3098
|
+
}
|
|
3099
|
+
const data = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
3100
|
+
const signedPayload = `${timestamp}.${data}`;
|
|
3101
|
+
return this.verifySignature(signedPayload, signature, encoding);
|
|
3102
|
+
}
|
|
3103
|
+
timingSafeEqual(a, b) {
|
|
3104
|
+
if (!this.crypto?.timingSafeEqual) {
|
|
3105
|
+
console.warn('crypto.timingSafeEqual not available, using simple comparison');
|
|
3106
|
+
return a === b;
|
|
3107
|
+
}
|
|
3108
|
+
const bufA = Buffer.from(a);
|
|
3109
|
+
const bufB = Buffer.from(b);
|
|
3110
|
+
if (bufA.length !== bufB.length) {
|
|
3111
|
+
return false;
|
|
3112
|
+
}
|
|
3113
|
+
return this.crypto.timingSafeEqual(bufA, bufB);
|
|
3114
|
+
}
|
|
3115
|
+
getAlgorithm() {
|
|
3116
|
+
return this.algorithm;
|
|
3117
|
+
}
|
|
3118
|
+
updateSecret(newSecret) {
|
|
3119
|
+
if (!newSecret) {
|
|
3120
|
+
throw new Error('Secret cannot be empty');
|
|
3121
|
+
}
|
|
3122
|
+
this.secret = newSecret;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
function createWebhookSecurity(secret, algorithm = 'sha256') {
|
|
3126
|
+
return new WebhookSecurity({ secret, algorithm });
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
class SecureTokenStorage {
|
|
3130
|
+
constructor(config = {}) {
|
|
3131
|
+
this.encryptionKey = null;
|
|
3132
|
+
if (typeof window === 'undefined') {
|
|
3133
|
+
throw new Error('SecureTokenStorage is only available in browser environments. ' +
|
|
3134
|
+
'For Node.js, use EncryptedFileTokenStorage instead.');
|
|
3135
|
+
}
|
|
3136
|
+
if (!window.crypto || !window.crypto.subtle) {
|
|
3137
|
+
throw new Error('Web Crypto API is not supported in this browser. ' +
|
|
3138
|
+
'Please use a modern browser (Chrome 37+, Firefox 34+, Safari 11+, Edge 79+).');
|
|
3139
|
+
}
|
|
3140
|
+
this.keyPrefix = config.keyPrefix || 'etsy_token';
|
|
3141
|
+
this.derivationInput = config.derivationInput || this.getDefaultDerivationInput();
|
|
3142
|
+
this.storage = config.useSessionStorage ? sessionStorage : localStorage;
|
|
3143
|
+
}
|
|
3144
|
+
async save(tokens) {
|
|
3145
|
+
const key = await this.getEncryptionKey();
|
|
3146
|
+
const tokenJson = JSON.stringify(tokens);
|
|
3147
|
+
const tokenData = new TextEncoder().encode(tokenJson);
|
|
3148
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
3149
|
+
const encrypted = await window.crypto.subtle.encrypt({
|
|
3150
|
+
name: 'AES-GCM',
|
|
3151
|
+
iv,
|
|
3152
|
+
}, key, tokenData);
|
|
3153
|
+
const integrity = await this.generateIntegrity(encrypted, iv);
|
|
3154
|
+
const stored = {
|
|
3155
|
+
version: 1,
|
|
3156
|
+
encrypted: this.arrayBufferToBase64(encrypted),
|
|
3157
|
+
iv: this.arrayBufferToBase64(iv),
|
|
3158
|
+
integrity: this.arrayBufferToBase64(integrity),
|
|
3159
|
+
expiresAt: tokens.expires_at.getTime(),
|
|
3160
|
+
timestamp: Date.now(),
|
|
3161
|
+
};
|
|
3162
|
+
this.storage.setItem(this.keyPrefix, JSON.stringify(stored));
|
|
3163
|
+
}
|
|
3164
|
+
async load() {
|
|
3165
|
+
const stored = this.storage.getItem(this.keyPrefix);
|
|
3166
|
+
if (!stored) {
|
|
3167
|
+
return null;
|
|
3168
|
+
}
|
|
3169
|
+
try {
|
|
3170
|
+
const data = JSON.parse(stored);
|
|
3171
|
+
if (data.version !== 1) {
|
|
3172
|
+
console.warn('Unsupported token storage version. Clearing storage.');
|
|
3173
|
+
await this.clear();
|
|
3174
|
+
return null;
|
|
3175
|
+
}
|
|
3176
|
+
if (Date.now() > data.expiresAt) {
|
|
3177
|
+
console.info('Stored tokens have expired. Clearing storage.');
|
|
3178
|
+
await this.clear();
|
|
3179
|
+
return null;
|
|
3180
|
+
}
|
|
3181
|
+
const encrypted = this.base64ToArrayBuffer(data.encrypted);
|
|
3182
|
+
const iv = this.base64ToArrayBuffer(data.iv);
|
|
3183
|
+
const storedIntegrity = this.base64ToArrayBuffer(data.integrity);
|
|
3184
|
+
const computedIntegrity = await this.generateIntegrity(encrypted, iv);
|
|
3185
|
+
if (!this.compareArrayBuffers(storedIntegrity, computedIntegrity)) {
|
|
3186
|
+
console.warn('Token integrity check failed. Data may have been tampered with. Clearing storage.');
|
|
3187
|
+
await this.clear();
|
|
3188
|
+
return null;
|
|
3189
|
+
}
|
|
3190
|
+
const key = await this.getEncryptionKey();
|
|
3191
|
+
const decrypted = await window.crypto.subtle.decrypt({
|
|
3192
|
+
name: 'AES-GCM',
|
|
3193
|
+
iv,
|
|
3194
|
+
}, key, encrypted);
|
|
3195
|
+
const tokenJson = new TextDecoder().decode(decrypted);
|
|
3196
|
+
const tokens = JSON.parse(tokenJson);
|
|
3197
|
+
tokens.expires_at = new Date(tokens.expires_at);
|
|
3198
|
+
return tokens;
|
|
3199
|
+
}
|
|
3200
|
+
catch (error) {
|
|
3201
|
+
console.error('Failed to load tokens from storage:', error);
|
|
3202
|
+
await this.clear();
|
|
3203
|
+
return null;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
async clear() {
|
|
3207
|
+
this.storage.removeItem(this.keyPrefix);
|
|
3208
|
+
}
|
|
3209
|
+
async getEncryptionKey() {
|
|
3210
|
+
if (this.encryptionKey) {
|
|
3211
|
+
return this.encryptionKey;
|
|
3212
|
+
}
|
|
3213
|
+
const keyMaterial = await this.deriveKeyMaterial();
|
|
3214
|
+
this.encryptionKey = await window.crypto.subtle.deriveKey({
|
|
3215
|
+
name: 'PBKDF2',
|
|
3216
|
+
salt: this.stringToArrayBuffer('etsy-v3-api-client-salt'),
|
|
3217
|
+
iterations: 100000,
|
|
3218
|
+
hash: 'SHA-256',
|
|
3219
|
+
}, keyMaterial, {
|
|
3220
|
+
name: 'AES-GCM',
|
|
3221
|
+
length: 256,
|
|
3222
|
+
}, false, ['encrypt', 'decrypt']);
|
|
3223
|
+
return this.encryptionKey;
|
|
3224
|
+
}
|
|
3225
|
+
async deriveKeyMaterial() {
|
|
3226
|
+
const keyData = this.stringToArrayBuffer(this.derivationInput);
|
|
3227
|
+
return window.crypto.subtle.importKey('raw', keyData, 'PBKDF2', false, ['deriveKey']);
|
|
3228
|
+
}
|
|
3229
|
+
async generateIntegrity(encrypted, iv) {
|
|
3230
|
+
const combined = new Uint8Array(encrypted.byteLength + iv.byteLength);
|
|
3231
|
+
combined.set(new Uint8Array(encrypted), 0);
|
|
3232
|
+
combined.set(new Uint8Array(iv), encrypted.byteLength);
|
|
3233
|
+
const keyMaterial = await this.deriveKeyMaterial();
|
|
3234
|
+
const hmacKey = await window.crypto.subtle.deriveKey({
|
|
3235
|
+
name: 'PBKDF2',
|
|
3236
|
+
salt: this.stringToArrayBuffer('etsy-integrity-salt'),
|
|
3237
|
+
iterations: 100000,
|
|
3238
|
+
hash: 'SHA-256',
|
|
3239
|
+
}, keyMaterial, {
|
|
3240
|
+
name: 'HMAC',
|
|
3241
|
+
hash: 'SHA-256',
|
|
3242
|
+
}, false, ['sign']);
|
|
3243
|
+
return window.crypto.subtle.sign('HMAC', hmacKey, combined);
|
|
3244
|
+
}
|
|
3245
|
+
getDefaultDerivationInput() {
|
|
3246
|
+
const domain = window.location.hostname;
|
|
3247
|
+
const userAgent = navigator.userAgent;
|
|
3248
|
+
return `${domain}:${userAgent}`;
|
|
3249
|
+
}
|
|
3250
|
+
stringToArrayBuffer(str) {
|
|
3251
|
+
return new TextEncoder().encode(str);
|
|
3252
|
+
}
|
|
3253
|
+
arrayBufferToBase64(buffer) {
|
|
3254
|
+
const bytes = new Uint8Array(buffer);
|
|
3255
|
+
let binary = '';
|
|
3256
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
3257
|
+
const byte = bytes[i];
|
|
3258
|
+
if (byte !== undefined) {
|
|
3259
|
+
binary += String.fromCharCode(byte);
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
return btoa(binary);
|
|
3263
|
+
}
|
|
3264
|
+
base64ToArrayBuffer(base64) {
|
|
3265
|
+
const binary = atob(base64);
|
|
3266
|
+
const bytes = new Uint8Array(binary.length);
|
|
3267
|
+
for (let i = 0; i < binary.length; i++) {
|
|
3268
|
+
bytes[i] = binary.charCodeAt(i);
|
|
3269
|
+
}
|
|
3270
|
+
return bytes.buffer;
|
|
3271
|
+
}
|
|
3272
|
+
compareArrayBuffers(a, b) {
|
|
3273
|
+
if (a.byteLength !== b.byteLength) {
|
|
3274
|
+
return false;
|
|
3275
|
+
}
|
|
3276
|
+
const viewA = new Uint8Array(a);
|
|
3277
|
+
const viewB = new Uint8Array(b);
|
|
3278
|
+
for (let i = 0; i < viewA.length; i++) {
|
|
3279
|
+
if (viewA[i] !== viewB[i]) {
|
|
3280
|
+
return false;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
return true;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
function isSecureStorageSupported() {
|
|
3287
|
+
if (typeof window === 'undefined') {
|
|
3288
|
+
return false;
|
|
3289
|
+
}
|
|
3290
|
+
return !!(window.crypto &&
|
|
3291
|
+
window.crypto.subtle &&
|
|
3292
|
+
typeof window.crypto.subtle.encrypt === 'function' &&
|
|
3293
|
+
typeof window.crypto.subtle.decrypt === 'function' &&
|
|
3294
|
+
typeof window.crypto.subtle.deriveKey === 'function');
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
class PluginManager {
|
|
3298
|
+
constructor() {
|
|
3299
|
+
this.plugins = [];
|
|
3300
|
+
}
|
|
3301
|
+
async register(plugin) {
|
|
3302
|
+
if (this.plugins.some(p => p.name === plugin.name)) {
|
|
3303
|
+
throw new Error(`Plugin with name "${plugin.name}" is already registered`);
|
|
3304
|
+
}
|
|
3305
|
+
this.plugins.push(plugin);
|
|
3306
|
+
if (plugin.onInit) {
|
|
3307
|
+
await plugin.onInit();
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
async unregister(pluginName) {
|
|
3311
|
+
const index = this.plugins.findIndex(p => p.name === pluginName);
|
|
3312
|
+
if (index === -1) {
|
|
3313
|
+
return false;
|
|
3314
|
+
}
|
|
3315
|
+
const plugin = this.plugins[index];
|
|
3316
|
+
if (!plugin) {
|
|
3317
|
+
return false;
|
|
3318
|
+
}
|
|
3319
|
+
if (plugin.onDestroy) {
|
|
3320
|
+
await plugin.onDestroy();
|
|
3321
|
+
}
|
|
3322
|
+
this.plugins.splice(index, 1);
|
|
3323
|
+
return true;
|
|
3324
|
+
}
|
|
3325
|
+
getPlugins() {
|
|
3326
|
+
return this.plugins;
|
|
3327
|
+
}
|
|
3328
|
+
getPlugin(name) {
|
|
3329
|
+
return this.plugins.find(p => p.name === name);
|
|
3330
|
+
}
|
|
3331
|
+
async executeBeforeRequest(config) {
|
|
3332
|
+
let currentConfig = config;
|
|
3333
|
+
for (const plugin of this.plugins) {
|
|
3334
|
+
if (plugin.onBeforeRequest) {
|
|
3335
|
+
currentConfig = await plugin.onBeforeRequest(currentConfig);
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
return currentConfig;
|
|
3339
|
+
}
|
|
3340
|
+
async executeAfterResponse(response) {
|
|
3341
|
+
let currentResponse = response;
|
|
3342
|
+
for (const plugin of this.plugins) {
|
|
3343
|
+
if (plugin.onAfterResponse) {
|
|
3344
|
+
currentResponse = await plugin.onAfterResponse(currentResponse);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
return currentResponse;
|
|
3348
|
+
}
|
|
3349
|
+
async executeOnError(error) {
|
|
3350
|
+
for (const plugin of this.plugins) {
|
|
3351
|
+
if (plugin.onError) {
|
|
3352
|
+
await plugin.onError(error);
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
async clear() {
|
|
3357
|
+
for (const plugin of this.plugins) {
|
|
3358
|
+
if (plugin.onDestroy) {
|
|
3359
|
+
await plugin.onDestroy();
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
this.plugins = [];
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
function createAnalyticsPlugin(config) {
|
|
3366
|
+
const requestTimes = new Map();
|
|
3367
|
+
return {
|
|
3368
|
+
name: 'analytics',
|
|
3369
|
+
version: '1.0.0',
|
|
3370
|
+
onBeforeRequest(requestConfig) {
|
|
3371
|
+
const requestId = `${requestConfig.method}:${requestConfig.endpoint}:${Date.now()}`;
|
|
3372
|
+
requestTimes.set(requestId, Date.now());
|
|
3373
|
+
return requestConfig;
|
|
3374
|
+
},
|
|
3375
|
+
onAfterResponse(response) {
|
|
3376
|
+
if (config.trackEndpoint) {
|
|
3377
|
+
const requestId = Array.from(requestTimes.keys()).pop();
|
|
3378
|
+
if (requestId) {
|
|
3379
|
+
const startTime = requestTimes.get(requestId);
|
|
3380
|
+
if (startTime) {
|
|
3381
|
+
const duration = Date.now() - startTime;
|
|
3382
|
+
const parts = requestId.split(':');
|
|
3383
|
+
const method = parts[0];
|
|
3384
|
+
const endpoint = parts[1];
|
|
3385
|
+
if (endpoint && method) {
|
|
3386
|
+
config.trackEndpoint(endpoint, method, duration);
|
|
3387
|
+
}
|
|
3388
|
+
requestTimes.delete(requestId);
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
return response;
|
|
3393
|
+
},
|
|
3394
|
+
onError(error) {
|
|
3395
|
+
if (config.trackError) {
|
|
3396
|
+
config.trackError(error);
|
|
3397
|
+
}
|
|
3398
|
+
},
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
function createRetryPlugin(config = {}) {
|
|
3402
|
+
const { retryableStatusCodes = [408, 429, 500, 502, 503, 504], } = config;
|
|
3403
|
+
return {
|
|
3404
|
+
name: 'retry',
|
|
3405
|
+
version: '1.0.0',
|
|
3406
|
+
async onError(error) {
|
|
3407
|
+
if (error instanceof EtsyApiError) {
|
|
3408
|
+
const statusCode = error.statusCode;
|
|
3409
|
+
if (statusCode && retryableStatusCodes.includes(statusCode)) {
|
|
3410
|
+
if (config.onRetry) {
|
|
3411
|
+
config.onRetry(1, error);
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
},
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
function createLoggingPlugin(config = {}) {
|
|
3419
|
+
const logger = config.logger || console;
|
|
3420
|
+
const logLevel = config.logLevel || 'info';
|
|
3421
|
+
const shouldLog = (level) => {
|
|
3422
|
+
const levels = ['debug', 'info', 'warn', 'error'];
|
|
3423
|
+
const currentLevelIndex = levels.indexOf(logLevel);
|
|
3424
|
+
const messageLevelIndex = levels.indexOf(level);
|
|
3425
|
+
return messageLevelIndex >= currentLevelIndex;
|
|
3426
|
+
};
|
|
3427
|
+
return {
|
|
3428
|
+
name: 'logging',
|
|
3429
|
+
version: '1.0.0',
|
|
3430
|
+
onBeforeRequest(requestConfig) {
|
|
3431
|
+
if (shouldLog('debug')) {
|
|
3432
|
+
logger.debug('[Etsy API] Request:', {
|
|
3433
|
+
method: requestConfig.method,
|
|
3434
|
+
endpoint: requestConfig.endpoint,
|
|
3435
|
+
params: requestConfig.params,
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
return requestConfig;
|
|
3439
|
+
},
|
|
3440
|
+
onAfterResponse(response) {
|
|
3441
|
+
if (shouldLog('debug')) {
|
|
3442
|
+
logger.debug('[Etsy API] Response:', {
|
|
3443
|
+
status: response.status,
|
|
3444
|
+
data: response.data,
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
return response;
|
|
3448
|
+
},
|
|
3449
|
+
onError(error) {
|
|
3450
|
+
if (shouldLog('error')) {
|
|
3451
|
+
logger.error('[Etsy API] Error:', error);
|
|
3452
|
+
if (error instanceof EtsyApiError) {
|
|
3453
|
+
logger.error('[Etsy API] Error Details:', {
|
|
3454
|
+
statusCode: error.statusCode,
|
|
3455
|
+
endpoint: error.endpoint,
|
|
3456
|
+
suggestions: error.suggestions,
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
},
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
function createCachingPlugin(config = {}) {
|
|
3464
|
+
const cache = new Map();
|
|
3465
|
+
const ttl = (config.ttl || 300) * 1000;
|
|
3466
|
+
const generateKey = (requestConfig) => {
|
|
3467
|
+
if (config.keyGenerator) {
|
|
3468
|
+
return config.keyGenerator(requestConfig);
|
|
3469
|
+
}
|
|
3470
|
+
return `${requestConfig.method}:${requestConfig.endpoint}:${JSON.stringify(requestConfig.params || {})}`;
|
|
3471
|
+
};
|
|
3472
|
+
return {
|
|
3473
|
+
name: 'caching',
|
|
3474
|
+
version: '1.0.0',
|
|
3475
|
+
onBeforeRequest(requestConfig) {
|
|
3476
|
+
if (requestConfig.method.toUpperCase() === 'GET') {
|
|
3477
|
+
const key = generateKey(requestConfig);
|
|
3478
|
+
const cached = cache.get(key);
|
|
3479
|
+
if (cached && Date.now() < cached.expiresAt) ;
|
|
3480
|
+
}
|
|
3481
|
+
return requestConfig;
|
|
3482
|
+
},
|
|
3483
|
+
onAfterResponse(response) {
|
|
3484
|
+
const key = `GET:${response.status}`;
|
|
3485
|
+
cache.set(key, {
|
|
3486
|
+
data: response.data,
|
|
3487
|
+
expiresAt: Date.now() + ttl,
|
|
3488
|
+
});
|
|
3489
|
+
return response;
|
|
3490
|
+
},
|
|
3491
|
+
onDestroy() {
|
|
3492
|
+
cache.clear();
|
|
3493
|
+
},
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
function createRateLimitPlugin(config = {}) {
|
|
3497
|
+
const maxRequestsPerSecond = config.maxRequestsPerSecond || 10;
|
|
3498
|
+
const requests = [];
|
|
3499
|
+
return {
|
|
3500
|
+
name: 'rateLimit',
|
|
3501
|
+
version: '1.0.0',
|
|
3502
|
+
async onBeforeRequest(requestConfig) {
|
|
3503
|
+
const now = Date.now();
|
|
3504
|
+
const oneSecondAgo = now - 1000;
|
|
3505
|
+
while (requests.length > 0 && requests[0] !== undefined && requests[0] < oneSecondAgo) {
|
|
3506
|
+
requests.shift();
|
|
3507
|
+
}
|
|
3508
|
+
if (requests.length >= maxRequestsPerSecond) {
|
|
3509
|
+
if (config.onRateLimitExceeded) {
|
|
3510
|
+
config.onRateLimitExceeded();
|
|
3511
|
+
}
|
|
3512
|
+
const oldestRequest = requests[0];
|
|
3513
|
+
if (oldestRequest !== undefined) {
|
|
3514
|
+
const waitTime = oldestRequest + 1000 - now;
|
|
3515
|
+
if (waitTime > 0) {
|
|
3516
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
requests.push(now);
|
|
3521
|
+
return requestConfig;
|
|
3522
|
+
},
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
|
|
924
3526
|
function createEtsyClient(config) {
|
|
925
3527
|
return new EtsyClient(config);
|
|
926
3528
|
}
|
|
@@ -933,7 +3535,7 @@ function createTokenManager(config, storage) {
|
|
|
933
3535
|
function createRateLimiter(config) {
|
|
934
3536
|
return new EtsyRateLimiter(config);
|
|
935
3537
|
}
|
|
936
|
-
const VERSION = '
|
|
3538
|
+
const VERSION = '2.2.0';
|
|
937
3539
|
const LIBRARY_NAME = 'etsy-v3-api-client';
|
|
938
3540
|
function getLibraryInfo() {
|
|
939
3541
|
return {
|
|
@@ -946,5 +3548,5 @@ function getLibraryInfo() {
|
|
|
946
3548
|
};
|
|
947
3549
|
}
|
|
948
3550
|
|
|
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 };
|
|
3551
|
+
export { AuthHelper, BatchQueryExecutor, BulkOperationManager, COMMON_SCOPE_COMBINATIONS, CacheWithInvalidation, CreateListingSchema, DEFAULT_RETRY_CONFIG, ETSY_SCOPES, EncryptedFileTokenStorage, EtsyApiError, EtsyAuthError, EtsyClient, EtsyRateLimitError, EtsyRateLimiter, EtsyWebhookHandler, FieldValidator, FileTokenStorage, GlobalRequestQueue, LFUCache, LIBRARY_NAME, LRUCache, ListingQueryBuilder, LocalStorageTokenStorage, MemoryTokenStorage, PaginatedResults, PluginManager, ReceiptQueryBuilder, RedisCacheStorage, RetryManager, SecureTokenStorage, SessionStorageTokenStorage, TokenManager, UpdateListingSchema, UpdateShopSchema, VERSION, ValidationException, Validator, WebhookSecurity, combineValidators, createAnalyticsPlugin, createAuthHelper, createBatchQuery, createBulkOperationManager, createCacheStorage, createCacheWithInvalidation, createCachingPlugin, createCodeChallenge, createDefaultTokenStorage, createEtsyClient, createListingQuery, createLoggingPlugin, createPaginatedResults, createRateLimitPlugin, createRateLimiter, createReceiptQuery, createRedisCacheStorage, createRetryPlugin, createTokenManager, createValidator, createWebhookHandler, createWebhookSecurity, decryptAES256GCM, EtsyClient as default, defaultRateLimiter, deriveKeyFromPassword, encryptAES256GCM, executeBulkOperation, field, generateCodeVerifier, generateEncryptionKey, generateRandomBase64Url, generateState, getAvailableStorage, getEnvironmentInfo, getGlobalQueue, getLibraryInfo, hasLocalStorage, hasSessionStorage, isBrowser$1 as isBrowser, isNode$1 as isNode, isSecureStorageSupported, sha256, sha256Base64Url, validate, validateEncryptionKey, validateOrThrow, withQueryBuilder, withQueue, withRetry };
|
|
950
3552
|
//# sourceMappingURL=node.esm.js.map
|