@profplum700/etsy-v3-api-client 2.3.1 → 2.3.9
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 +21 -13
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.esm.js +727 -3
- 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 +737 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +146 -3
- package/dist/index.esm.js +727 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/node.cjs +737 -2
- package/dist/node.cjs.map +1 -1
- package/dist/node.esm.js +727 -3
- package/dist/node.esm.js.map +1 -1
- package/package.json +14 -4
package/dist/node.cjs
CHANGED
|
@@ -3,22 +3,40 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
class EtsyApiError extends Error {
|
|
6
|
-
constructor(message, _statusCode, _response, _retryAfter) {
|
|
6
|
+
constructor(message, _statusCode, _response, _retryAfter, endpoint) {
|
|
7
7
|
super(message);
|
|
8
8
|
this._statusCode = _statusCode;
|
|
9
9
|
this._response = _response;
|
|
10
10
|
this._retryAfter = _retryAfter;
|
|
11
11
|
this.name = 'EtsyApiError';
|
|
12
|
+
this.endpoint = endpoint;
|
|
13
|
+
this.timestamp = new Date();
|
|
12
14
|
this.details = {
|
|
13
15
|
statusCode: _statusCode || 0,
|
|
14
|
-
retryAfter: _retryAfter
|
|
16
|
+
retryAfter: _retryAfter,
|
|
17
|
+
endpoint,
|
|
18
|
+
timestamp: this.timestamp
|
|
15
19
|
};
|
|
16
20
|
if (_response && typeof _response === 'object') {
|
|
17
21
|
const resp = _response;
|
|
18
22
|
this.details.errorCode = (resp.error_code || resp.code);
|
|
19
23
|
this.details.field = resp.field;
|
|
20
24
|
this.details.suggestion = (resp.suggestion || resp.message);
|
|
25
|
+
if (Array.isArray(resp.errors)) {
|
|
26
|
+
this.details.validationErrors = resp.errors.map((err) => {
|
|
27
|
+
if (err && typeof err === 'object') {
|
|
28
|
+
const e = err;
|
|
29
|
+
return {
|
|
30
|
+
field: String(e.field || 'unknown'),
|
|
31
|
+
message: String(e.message || 'Validation error')
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { field: 'unknown', message: String(err) };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
21
37
|
}
|
|
38
|
+
this.suggestions = this.generateSuggestions();
|
|
39
|
+
this.docsUrl = this.generateDocsUrl();
|
|
22
40
|
}
|
|
23
41
|
get statusCode() {
|
|
24
42
|
return this._statusCode;
|
|
@@ -51,6 +69,107 @@ class EtsyApiError extends Error {
|
|
|
51
69
|
}
|
|
52
70
|
return new Date(Date.now() + this._retryAfter * 1000);
|
|
53
71
|
}
|
|
72
|
+
generateSuggestions() {
|
|
73
|
+
const suggestions = [];
|
|
74
|
+
if (!this._statusCode) {
|
|
75
|
+
return ['Check your network connection and try again'];
|
|
76
|
+
}
|
|
77
|
+
switch (this._statusCode) {
|
|
78
|
+
case 400:
|
|
79
|
+
suggestions.push('Review the Etsy API documentation for this endpoint');
|
|
80
|
+
suggestions.push('Check all required parameters are provided');
|
|
81
|
+
suggestions.push('Validate parameter formats and types');
|
|
82
|
+
if (this.details.validationErrors && this.details.validationErrors.length > 0) {
|
|
83
|
+
suggestions.push('\nValidation errors:');
|
|
84
|
+
this.details.validationErrors.forEach(err => {
|
|
85
|
+
suggestions.push(` • ${err.field}: ${err.message}`);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (this.details.field) {
|
|
89
|
+
suggestions.push(`Field causing issue: ${this.details.field}`);
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case 401:
|
|
93
|
+
suggestions.push('Verify your access token is valid and not expired');
|
|
94
|
+
suggestions.push('Check if you need to refresh the token');
|
|
95
|
+
suggestions.push('Ensure you completed the OAuth flow correctly');
|
|
96
|
+
if (this.endpoint?.includes('/shops/')) {
|
|
97
|
+
suggestions.push('Verify the shop_id matches the authenticated user');
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case 403:
|
|
101
|
+
suggestions.push('Check if your OAuth app has the required scopes:');
|
|
102
|
+
if (this.endpoint?.includes('/listings')) {
|
|
103
|
+
suggestions.push(' • listings_r (for reading listings)');
|
|
104
|
+
suggestions.push(' • listings_w (for creating/updating listings)');
|
|
105
|
+
}
|
|
106
|
+
if (this.endpoint?.includes('/receipts') || this.endpoint?.includes('/transactions')) {
|
|
107
|
+
suggestions.push(' • transactions_r (for reading orders)');
|
|
108
|
+
}
|
|
109
|
+
if (this.endpoint?.includes('/shops')) {
|
|
110
|
+
suggestions.push(' • shops_r (for reading shop data)');
|
|
111
|
+
suggestions.push(' • shops_w (for updating shop data)');
|
|
112
|
+
}
|
|
113
|
+
suggestions.push('Verify your app is approved for production access');
|
|
114
|
+
suggestions.push('Check if the resource belongs to the authenticated user');
|
|
115
|
+
break;
|
|
116
|
+
case 404:
|
|
117
|
+
suggestions.push('Verify the resource ID exists and is spelled correctly');
|
|
118
|
+
if (this.endpoint?.includes('/listings/')) {
|
|
119
|
+
suggestions.push('Check if the listing is active and not deleted');
|
|
120
|
+
suggestions.push('Ensure the listing belongs to the authenticated shop');
|
|
121
|
+
}
|
|
122
|
+
if (this.endpoint?.includes('/shops/')) {
|
|
123
|
+
suggestions.push('Verify the shop ID is correct');
|
|
124
|
+
}
|
|
125
|
+
if (this.endpoint?.includes('/receipts/')) {
|
|
126
|
+
suggestions.push('Check if the receipt ID is valid');
|
|
127
|
+
suggestions.push('Ensure you have access to this shop\'s receipts');
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
case 409:
|
|
131
|
+
suggestions.push('Resource state conflict detected');
|
|
132
|
+
suggestions.push('Check if the resource was modified by another process');
|
|
133
|
+
suggestions.push('Try fetching the latest resource state before updating');
|
|
134
|
+
break;
|
|
135
|
+
case 429: {
|
|
136
|
+
const resetTime = this.getRateLimitReset();
|
|
137
|
+
const resetTimeStr = resetTime
|
|
138
|
+
? resetTime.toLocaleTimeString()
|
|
139
|
+
: 'shortly';
|
|
140
|
+
suggestions.push(`Rate limit exceeded. Resets at ${resetTimeStr}`);
|
|
141
|
+
suggestions.push('Implement exponential backoff retry logic');
|
|
142
|
+
suggestions.push('Consider caching responses to reduce API calls');
|
|
143
|
+
suggestions.push('Check if you can batch multiple operations');
|
|
144
|
+
if (this._retryAfter) {
|
|
145
|
+
suggestions.push(`Wait ${this._retryAfter} seconds before retrying`);
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 500:
|
|
150
|
+
case 502:
|
|
151
|
+
case 503:
|
|
152
|
+
case 504:
|
|
153
|
+
suggestions.push('This is an Etsy server error, not your code');
|
|
154
|
+
suggestions.push('Retry the request after a short delay (exponential backoff)');
|
|
155
|
+
suggestions.push('Check Etsy API status: https://status.etsy.com');
|
|
156
|
+
if (this.isRetryable()) {
|
|
157
|
+
suggestions.push('This error is retryable - the request can be safely retried');
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
default:
|
|
161
|
+
suggestions.push('Check the Etsy API documentation for this endpoint');
|
|
162
|
+
suggestions.push('Review your request parameters and format');
|
|
163
|
+
if (this.details.suggestion) {
|
|
164
|
+
suggestions.push(`Etsy suggestion: ${this.details.suggestion}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return suggestions;
|
|
168
|
+
}
|
|
169
|
+
generateDocsUrl() {
|
|
170
|
+
const errorCode = this.details.errorCode?.toLowerCase() || this._statusCode?.toString() || 'unknown';
|
|
171
|
+
return `https://github.com/profplum700/etsy-v3-api-client/blob/main/docs/troubleshooting/ERROR_CODES.md#${errorCode}`;
|
|
172
|
+
}
|
|
54
173
|
getUserFriendlyMessage() {
|
|
55
174
|
let message = this.message;
|
|
56
175
|
if (this.details.suggestion) {
|
|
@@ -67,6 +186,46 @@ class EtsyApiError extends Error {
|
|
|
67
186
|
}
|
|
68
187
|
return message;
|
|
69
188
|
}
|
|
189
|
+
toString() {
|
|
190
|
+
const parts = [
|
|
191
|
+
`EtsyApiError: ${this.message}`,
|
|
192
|
+
`Status Code: ${this._statusCode || 'Unknown'}`,
|
|
193
|
+
];
|
|
194
|
+
if (this.details.errorCode) {
|
|
195
|
+
parts.push(`Error Code: ${this.details.errorCode}`);
|
|
196
|
+
}
|
|
197
|
+
if (this.endpoint) {
|
|
198
|
+
parts.push(`Endpoint: ${this.endpoint}`);
|
|
199
|
+
}
|
|
200
|
+
parts.push(`Timestamp: ${this.timestamp.toISOString()}`);
|
|
201
|
+
if (this.suggestions.length > 0) {
|
|
202
|
+
parts.push('\nSuggestions:');
|
|
203
|
+
this.suggestions.forEach(s => {
|
|
204
|
+
if (!s.startsWith(' •')) {
|
|
205
|
+
parts.push(` • ${s}`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
parts.push(s);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
parts.push(`\nDocumentation: ${this.docsUrl}`);
|
|
213
|
+
return parts.join('\n');
|
|
214
|
+
}
|
|
215
|
+
toJSON() {
|
|
216
|
+
return {
|
|
217
|
+
name: this.name,
|
|
218
|
+
message: this.message,
|
|
219
|
+
statusCode: this._statusCode,
|
|
220
|
+
errorCode: this.details.errorCode,
|
|
221
|
+
endpoint: this.endpoint,
|
|
222
|
+
suggestions: this.suggestions,
|
|
223
|
+
docsUrl: this.docsUrl,
|
|
224
|
+
timestamp: this.timestamp.toISOString(),
|
|
225
|
+
details: this.details,
|
|
226
|
+
isRetryable: this.isRetryable(),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
70
229
|
}
|
|
71
230
|
class EtsyAuthError extends Error {
|
|
72
231
|
constructor(message, _code) {
|
|
@@ -1734,6 +1893,174 @@ const COMMON_SCOPE_COMBINATIONS = {
|
|
|
1734
1893
|
]
|
|
1735
1894
|
};
|
|
1736
1895
|
|
|
1896
|
+
class GlobalRequestQueue {
|
|
1897
|
+
constructor() {
|
|
1898
|
+
this.queue = [];
|
|
1899
|
+
this.processing = false;
|
|
1900
|
+
this.rateLimits = new Map();
|
|
1901
|
+
this.requestCount = 0;
|
|
1902
|
+
this.dailyReset = new Date();
|
|
1903
|
+
this.lastRequestTime = 0;
|
|
1904
|
+
this.maxRequestsPerDay = 10000;
|
|
1905
|
+
this.maxRequestsPerSecond = 10;
|
|
1906
|
+
this.minRequestInterval = 100;
|
|
1907
|
+
this.setNextDailyReset();
|
|
1908
|
+
}
|
|
1909
|
+
static getInstance() {
|
|
1910
|
+
if (!this.instance) {
|
|
1911
|
+
this.instance = new GlobalRequestQueue();
|
|
1912
|
+
}
|
|
1913
|
+
return this.instance;
|
|
1914
|
+
}
|
|
1915
|
+
static resetInstance() {
|
|
1916
|
+
this.instance = null;
|
|
1917
|
+
}
|
|
1918
|
+
async enqueue(request, options = {}) {
|
|
1919
|
+
return new Promise((resolve, reject) => {
|
|
1920
|
+
const queuedRequest = {
|
|
1921
|
+
id: this.generateId(),
|
|
1922
|
+
request,
|
|
1923
|
+
resolve: resolve,
|
|
1924
|
+
reject,
|
|
1925
|
+
priority: options.priority ?? 'normal',
|
|
1926
|
+
addedAt: Date.now(),
|
|
1927
|
+
timeout: options.timeout ?? 30000,
|
|
1928
|
+
endpoint: options.endpoint,
|
|
1929
|
+
};
|
|
1930
|
+
this.queue.push(queuedRequest);
|
|
1931
|
+
if (!this.processing) {
|
|
1932
|
+
this.processQueue().catch(error => {
|
|
1933
|
+
console.error('Queue processing error:', error);
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
getStatus() {
|
|
1939
|
+
return {
|
|
1940
|
+
queueLength: this.queue.length,
|
|
1941
|
+
processing: this.processing,
|
|
1942
|
+
remainingRequests: Math.max(0, this.maxRequestsPerDay - this.requestCount),
|
|
1943
|
+
resetTime: this.dailyReset,
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
clear() {
|
|
1947
|
+
this.queue.forEach(item => {
|
|
1948
|
+
item.reject(new Error('Queue cleared'));
|
|
1949
|
+
});
|
|
1950
|
+
this.queue = [];
|
|
1951
|
+
}
|
|
1952
|
+
async processQueue() {
|
|
1953
|
+
if (this.processing) {
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
this.processing = true;
|
|
1957
|
+
try {
|
|
1958
|
+
while (this.queue.length > 0) {
|
|
1959
|
+
if (Date.now() >= this.dailyReset.getTime()) {
|
|
1960
|
+
this.requestCount = 0;
|
|
1961
|
+
this.setNextDailyReset();
|
|
1962
|
+
}
|
|
1963
|
+
if (this.requestCount >= this.maxRequestsPerDay) {
|
|
1964
|
+
const timeUntilReset = this.dailyReset.getTime() - Date.now();
|
|
1965
|
+
console.warn(`Daily rate limit reached. Waiting ${Math.ceil(timeUntilReset / 1000 / 60)} minutes until reset.`);
|
|
1966
|
+
await this.delay(timeUntilReset);
|
|
1967
|
+
this.requestCount = 0;
|
|
1968
|
+
this.setNextDailyReset();
|
|
1969
|
+
}
|
|
1970
|
+
await this.waitForRateLimit();
|
|
1971
|
+
this.queue.sort((a, b) => {
|
|
1972
|
+
const priorityOrder = {
|
|
1973
|
+
high: 0,
|
|
1974
|
+
normal: 1,
|
|
1975
|
+
low: 2,
|
|
1976
|
+
};
|
|
1977
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
1978
|
+
});
|
|
1979
|
+
const item = this.queue.shift();
|
|
1980
|
+
if (!item) {
|
|
1981
|
+
break;
|
|
1982
|
+
}
|
|
1983
|
+
const elapsed = Date.now() - item.addedAt;
|
|
1984
|
+
if (item.timeout && elapsed > item.timeout) {
|
|
1985
|
+
item.reject(new Error(`Request timeout after ${elapsed}ms (exceeded while waiting in queue)`));
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
try {
|
|
1989
|
+
let result;
|
|
1990
|
+
if (item.timeout) {
|
|
1991
|
+
const remainingTimeout = item.timeout - elapsed;
|
|
1992
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1993
|
+
setTimeout(() => {
|
|
1994
|
+
reject(new Error(`Request timeout after ${item.timeout}ms (exceeded during execution)`));
|
|
1995
|
+
}, remainingTimeout);
|
|
1996
|
+
});
|
|
1997
|
+
result = await Promise.race([item.request(), timeoutPromise]);
|
|
1998
|
+
}
|
|
1999
|
+
else {
|
|
2000
|
+
result = await item.request();
|
|
2001
|
+
}
|
|
2002
|
+
item.resolve(result);
|
|
2003
|
+
this.requestCount++;
|
|
2004
|
+
this.lastRequestTime = Date.now();
|
|
2005
|
+
}
|
|
2006
|
+
catch (error) {
|
|
2007
|
+
if (error instanceof EtsyRateLimitError) {
|
|
2008
|
+
this.updateRateLimitInfo(error);
|
|
2009
|
+
}
|
|
2010
|
+
item.reject(error);
|
|
2011
|
+
}
|
|
2012
|
+
await this.delay(this.minRequestInterval);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
finally {
|
|
2016
|
+
this.processing = false;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
async waitForRateLimit() {
|
|
2020
|
+
const now = Date.now();
|
|
2021
|
+
const globalRateLimit = this.rateLimits.get('global');
|
|
2022
|
+
if (globalRateLimit && now < globalRateLimit.resetAt) {
|
|
2023
|
+
const waitTime = globalRateLimit.resetAt - now;
|
|
2024
|
+
console.log(`Global rate limit active. Waiting ${waitTime}ms`);
|
|
2025
|
+
await this.delay(waitTime);
|
|
2026
|
+
}
|
|
2027
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
2028
|
+
if (timeSinceLastRequest < this.minRequestInterval) {
|
|
2029
|
+
const waitTime = this.minRequestInterval - timeSinceLastRequest;
|
|
2030
|
+
await this.delay(waitTime);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
updateRateLimitInfo(error) {
|
|
2034
|
+
const retryAfter = error.retryAfter;
|
|
2035
|
+
if (retryAfter) {
|
|
2036
|
+
this.rateLimits.set('global', {
|
|
2037
|
+
remaining: 0,
|
|
2038
|
+
resetAt: Date.now() + retryAfter * 1000,
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
setNextDailyReset() {
|
|
2043
|
+
const now = new Date();
|
|
2044
|
+
this.dailyReset = new Date(now);
|
|
2045
|
+
this.dailyReset.setUTCDate(this.dailyReset.getUTCDate() + 1);
|
|
2046
|
+
this.dailyReset.setUTCHours(0, 0, 0, 0);
|
|
2047
|
+
}
|
|
2048
|
+
generateId() {
|
|
2049
|
+
return `${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
2050
|
+
}
|
|
2051
|
+
delay(ms) {
|
|
2052
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
GlobalRequestQueue.instance = null;
|
|
2056
|
+
function getGlobalQueue() {
|
|
2057
|
+
return GlobalRequestQueue.getInstance();
|
|
2058
|
+
}
|
|
2059
|
+
function withQueue(request, options) {
|
|
2060
|
+
const queue = GlobalRequestQueue.getInstance();
|
|
2061
|
+
return queue.enqueue(request, options);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1737
2064
|
class PaginatedResults {
|
|
1738
2065
|
constructor(fetcher, options = {}) {
|
|
1739
2066
|
this.currentPage = [];
|
|
@@ -2803,6 +3130,403 @@ function createWebhookSecurity(secret, algorithm = 'sha256') {
|
|
|
2803
3130
|
return new WebhookSecurity({ secret, algorithm });
|
|
2804
3131
|
}
|
|
2805
3132
|
|
|
3133
|
+
class SecureTokenStorage {
|
|
3134
|
+
constructor(config = {}) {
|
|
3135
|
+
this.encryptionKey = null;
|
|
3136
|
+
if (typeof window === 'undefined') {
|
|
3137
|
+
throw new Error('SecureTokenStorage is only available in browser environments. ' +
|
|
3138
|
+
'For Node.js, use EncryptedFileTokenStorage instead.');
|
|
3139
|
+
}
|
|
3140
|
+
if (!window.crypto || !window.crypto.subtle) {
|
|
3141
|
+
throw new Error('Web Crypto API is not supported in this browser. ' +
|
|
3142
|
+
'Please use a modern browser (Chrome 37+, Firefox 34+, Safari 11+, Edge 79+).');
|
|
3143
|
+
}
|
|
3144
|
+
this.keyPrefix = config.keyPrefix || 'etsy_token';
|
|
3145
|
+
this.derivationInput = config.derivationInput || this.getDefaultDerivationInput();
|
|
3146
|
+
this.storage = config.useSessionStorage ? sessionStorage : localStorage;
|
|
3147
|
+
}
|
|
3148
|
+
async save(tokens) {
|
|
3149
|
+
const key = await this.getEncryptionKey();
|
|
3150
|
+
const tokenJson = JSON.stringify(tokens);
|
|
3151
|
+
const tokenData = new TextEncoder().encode(tokenJson);
|
|
3152
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
3153
|
+
const encrypted = await window.crypto.subtle.encrypt({
|
|
3154
|
+
name: 'AES-GCM',
|
|
3155
|
+
iv,
|
|
3156
|
+
}, key, tokenData);
|
|
3157
|
+
const integrity = await this.generateIntegrity(encrypted, iv);
|
|
3158
|
+
const stored = {
|
|
3159
|
+
version: 1,
|
|
3160
|
+
encrypted: this.arrayBufferToBase64(encrypted),
|
|
3161
|
+
iv: this.arrayBufferToBase64(iv),
|
|
3162
|
+
integrity: this.arrayBufferToBase64(integrity),
|
|
3163
|
+
expiresAt: tokens.expires_at.getTime(),
|
|
3164
|
+
timestamp: Date.now(),
|
|
3165
|
+
};
|
|
3166
|
+
this.storage.setItem(this.keyPrefix, JSON.stringify(stored));
|
|
3167
|
+
}
|
|
3168
|
+
async load() {
|
|
3169
|
+
const stored = this.storage.getItem(this.keyPrefix);
|
|
3170
|
+
if (!stored) {
|
|
3171
|
+
return null;
|
|
3172
|
+
}
|
|
3173
|
+
try {
|
|
3174
|
+
const data = JSON.parse(stored);
|
|
3175
|
+
if (data.version !== 1) {
|
|
3176
|
+
console.warn('Unsupported token storage version. Clearing storage.');
|
|
3177
|
+
await this.clear();
|
|
3178
|
+
return null;
|
|
3179
|
+
}
|
|
3180
|
+
if (Date.now() > data.expiresAt) {
|
|
3181
|
+
console.info('Stored tokens have expired. Clearing storage.');
|
|
3182
|
+
await this.clear();
|
|
3183
|
+
return null;
|
|
3184
|
+
}
|
|
3185
|
+
const encrypted = this.base64ToArrayBuffer(data.encrypted);
|
|
3186
|
+
const iv = this.base64ToArrayBuffer(data.iv);
|
|
3187
|
+
const storedIntegrity = this.base64ToArrayBuffer(data.integrity);
|
|
3188
|
+
const computedIntegrity = await this.generateIntegrity(encrypted, iv);
|
|
3189
|
+
if (!this.compareArrayBuffers(storedIntegrity, computedIntegrity)) {
|
|
3190
|
+
console.warn('Token integrity check failed. Data may have been tampered with. Clearing storage.');
|
|
3191
|
+
await this.clear();
|
|
3192
|
+
return null;
|
|
3193
|
+
}
|
|
3194
|
+
const key = await this.getEncryptionKey();
|
|
3195
|
+
const decrypted = await window.crypto.subtle.decrypt({
|
|
3196
|
+
name: 'AES-GCM',
|
|
3197
|
+
iv,
|
|
3198
|
+
}, key, encrypted);
|
|
3199
|
+
const tokenJson = new TextDecoder().decode(decrypted);
|
|
3200
|
+
const tokens = JSON.parse(tokenJson);
|
|
3201
|
+
tokens.expires_at = new Date(tokens.expires_at);
|
|
3202
|
+
return tokens;
|
|
3203
|
+
}
|
|
3204
|
+
catch (error) {
|
|
3205
|
+
console.error('Failed to load tokens from storage:', error);
|
|
3206
|
+
await this.clear();
|
|
3207
|
+
return null;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
async clear() {
|
|
3211
|
+
this.storage.removeItem(this.keyPrefix);
|
|
3212
|
+
}
|
|
3213
|
+
async getEncryptionKey() {
|
|
3214
|
+
if (this.encryptionKey) {
|
|
3215
|
+
return this.encryptionKey;
|
|
3216
|
+
}
|
|
3217
|
+
const keyMaterial = await this.deriveKeyMaterial();
|
|
3218
|
+
this.encryptionKey = await window.crypto.subtle.deriveKey({
|
|
3219
|
+
name: 'PBKDF2',
|
|
3220
|
+
salt: this.stringToArrayBuffer('etsy-v3-api-client-salt'),
|
|
3221
|
+
iterations: 100000,
|
|
3222
|
+
hash: 'SHA-256',
|
|
3223
|
+
}, keyMaterial, {
|
|
3224
|
+
name: 'AES-GCM',
|
|
3225
|
+
length: 256,
|
|
3226
|
+
}, false, ['encrypt', 'decrypt']);
|
|
3227
|
+
return this.encryptionKey;
|
|
3228
|
+
}
|
|
3229
|
+
async deriveKeyMaterial() {
|
|
3230
|
+
const keyData = this.stringToArrayBuffer(this.derivationInput);
|
|
3231
|
+
return window.crypto.subtle.importKey('raw', keyData, 'PBKDF2', false, ['deriveKey']);
|
|
3232
|
+
}
|
|
3233
|
+
async generateIntegrity(encrypted, iv) {
|
|
3234
|
+
const combined = new Uint8Array(encrypted.byteLength + iv.byteLength);
|
|
3235
|
+
combined.set(new Uint8Array(encrypted), 0);
|
|
3236
|
+
combined.set(new Uint8Array(iv), encrypted.byteLength);
|
|
3237
|
+
const keyMaterial = await this.deriveKeyMaterial();
|
|
3238
|
+
const hmacKey = await window.crypto.subtle.deriveKey({
|
|
3239
|
+
name: 'PBKDF2',
|
|
3240
|
+
salt: this.stringToArrayBuffer('etsy-integrity-salt'),
|
|
3241
|
+
iterations: 100000,
|
|
3242
|
+
hash: 'SHA-256',
|
|
3243
|
+
}, keyMaterial, {
|
|
3244
|
+
name: 'HMAC',
|
|
3245
|
+
hash: 'SHA-256',
|
|
3246
|
+
}, false, ['sign']);
|
|
3247
|
+
return window.crypto.subtle.sign('HMAC', hmacKey, combined);
|
|
3248
|
+
}
|
|
3249
|
+
getDefaultDerivationInput() {
|
|
3250
|
+
const domain = window.location.hostname;
|
|
3251
|
+
const userAgent = navigator.userAgent;
|
|
3252
|
+
return `${domain}:${userAgent}`;
|
|
3253
|
+
}
|
|
3254
|
+
stringToArrayBuffer(str) {
|
|
3255
|
+
return new TextEncoder().encode(str);
|
|
3256
|
+
}
|
|
3257
|
+
arrayBufferToBase64(buffer) {
|
|
3258
|
+
const bytes = new Uint8Array(buffer);
|
|
3259
|
+
let binary = '';
|
|
3260
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
3261
|
+
const byte = bytes[i];
|
|
3262
|
+
if (byte !== undefined) {
|
|
3263
|
+
binary += String.fromCharCode(byte);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
return btoa(binary);
|
|
3267
|
+
}
|
|
3268
|
+
base64ToArrayBuffer(base64) {
|
|
3269
|
+
const binary = atob(base64);
|
|
3270
|
+
const bytes = new Uint8Array(binary.length);
|
|
3271
|
+
for (let i = 0; i < binary.length; i++) {
|
|
3272
|
+
bytes[i] = binary.charCodeAt(i);
|
|
3273
|
+
}
|
|
3274
|
+
return bytes.buffer;
|
|
3275
|
+
}
|
|
3276
|
+
compareArrayBuffers(a, b) {
|
|
3277
|
+
if (a.byteLength !== b.byteLength) {
|
|
3278
|
+
return false;
|
|
3279
|
+
}
|
|
3280
|
+
const viewA = new Uint8Array(a);
|
|
3281
|
+
const viewB = new Uint8Array(b);
|
|
3282
|
+
for (let i = 0; i < viewA.length; i++) {
|
|
3283
|
+
if (viewA[i] !== viewB[i]) {
|
|
3284
|
+
return false;
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
return true;
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
function isSecureStorageSupported() {
|
|
3291
|
+
if (typeof window === 'undefined') {
|
|
3292
|
+
return false;
|
|
3293
|
+
}
|
|
3294
|
+
return !!(window.crypto &&
|
|
3295
|
+
window.crypto.subtle &&
|
|
3296
|
+
typeof window.crypto.subtle.encrypt === 'function' &&
|
|
3297
|
+
typeof window.crypto.subtle.decrypt === 'function' &&
|
|
3298
|
+
typeof window.crypto.subtle.deriveKey === 'function');
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
class PluginManager {
|
|
3302
|
+
constructor() {
|
|
3303
|
+
this.plugins = [];
|
|
3304
|
+
}
|
|
3305
|
+
async register(plugin) {
|
|
3306
|
+
if (this.plugins.some(p => p.name === plugin.name)) {
|
|
3307
|
+
throw new Error(`Plugin with name "${plugin.name}" is already registered`);
|
|
3308
|
+
}
|
|
3309
|
+
this.plugins.push(plugin);
|
|
3310
|
+
if (plugin.onInit) {
|
|
3311
|
+
await plugin.onInit();
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
async unregister(pluginName) {
|
|
3315
|
+
const index = this.plugins.findIndex(p => p.name === pluginName);
|
|
3316
|
+
if (index === -1) {
|
|
3317
|
+
return false;
|
|
3318
|
+
}
|
|
3319
|
+
const plugin = this.plugins[index];
|
|
3320
|
+
if (!plugin) {
|
|
3321
|
+
return false;
|
|
3322
|
+
}
|
|
3323
|
+
if (plugin.onDestroy) {
|
|
3324
|
+
await plugin.onDestroy();
|
|
3325
|
+
}
|
|
3326
|
+
this.plugins.splice(index, 1);
|
|
3327
|
+
return true;
|
|
3328
|
+
}
|
|
3329
|
+
getPlugins() {
|
|
3330
|
+
return this.plugins;
|
|
3331
|
+
}
|
|
3332
|
+
getPlugin(name) {
|
|
3333
|
+
return this.plugins.find(p => p.name === name);
|
|
3334
|
+
}
|
|
3335
|
+
async executeBeforeRequest(config) {
|
|
3336
|
+
let currentConfig = config;
|
|
3337
|
+
for (const plugin of this.plugins) {
|
|
3338
|
+
if (plugin.onBeforeRequest) {
|
|
3339
|
+
currentConfig = await plugin.onBeforeRequest(currentConfig);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
return currentConfig;
|
|
3343
|
+
}
|
|
3344
|
+
async executeAfterResponse(response) {
|
|
3345
|
+
let currentResponse = response;
|
|
3346
|
+
for (const plugin of this.plugins) {
|
|
3347
|
+
if (plugin.onAfterResponse) {
|
|
3348
|
+
currentResponse = await plugin.onAfterResponse(currentResponse);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
return currentResponse;
|
|
3352
|
+
}
|
|
3353
|
+
async executeOnError(error) {
|
|
3354
|
+
for (const plugin of this.plugins) {
|
|
3355
|
+
if (plugin.onError) {
|
|
3356
|
+
await plugin.onError(error);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
async clear() {
|
|
3361
|
+
for (const plugin of this.plugins) {
|
|
3362
|
+
if (plugin.onDestroy) {
|
|
3363
|
+
await plugin.onDestroy();
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
this.plugins = [];
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
function createAnalyticsPlugin(config) {
|
|
3370
|
+
const requestTimes = new Map();
|
|
3371
|
+
return {
|
|
3372
|
+
name: 'analytics',
|
|
3373
|
+
version: '1.0.0',
|
|
3374
|
+
onBeforeRequest(requestConfig) {
|
|
3375
|
+
const requestId = `${requestConfig.method}:${requestConfig.endpoint}:${Date.now()}`;
|
|
3376
|
+
requestTimes.set(requestId, Date.now());
|
|
3377
|
+
return requestConfig;
|
|
3378
|
+
},
|
|
3379
|
+
onAfterResponse(response) {
|
|
3380
|
+
if (config.trackEndpoint) {
|
|
3381
|
+
const requestId = Array.from(requestTimes.keys()).pop();
|
|
3382
|
+
if (requestId) {
|
|
3383
|
+
const startTime = requestTimes.get(requestId);
|
|
3384
|
+
if (startTime) {
|
|
3385
|
+
const duration = Date.now() - startTime;
|
|
3386
|
+
const parts = requestId.split(':');
|
|
3387
|
+
const method = parts[0];
|
|
3388
|
+
const endpoint = parts[1];
|
|
3389
|
+
if (endpoint && method) {
|
|
3390
|
+
config.trackEndpoint(endpoint, method, duration);
|
|
3391
|
+
}
|
|
3392
|
+
requestTimes.delete(requestId);
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
return response;
|
|
3397
|
+
},
|
|
3398
|
+
onError(error) {
|
|
3399
|
+
if (config.trackError) {
|
|
3400
|
+
config.trackError(error);
|
|
3401
|
+
}
|
|
3402
|
+
},
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
function createRetryPlugin(config = {}) {
|
|
3406
|
+
const { retryableStatusCodes = [408, 429, 500, 502, 503, 504], } = config;
|
|
3407
|
+
return {
|
|
3408
|
+
name: 'retry',
|
|
3409
|
+
version: '1.0.0',
|
|
3410
|
+
async onError(error) {
|
|
3411
|
+
if (error instanceof EtsyApiError) {
|
|
3412
|
+
const statusCode = error.statusCode;
|
|
3413
|
+
if (statusCode && retryableStatusCodes.includes(statusCode)) {
|
|
3414
|
+
if (config.onRetry) {
|
|
3415
|
+
config.onRetry(1, error);
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
},
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
function createLoggingPlugin(config = {}) {
|
|
3423
|
+
const logger = config.logger || console;
|
|
3424
|
+
const logLevel = config.logLevel || 'info';
|
|
3425
|
+
const shouldLog = (level) => {
|
|
3426
|
+
const levels = ['debug', 'info', 'warn', 'error'];
|
|
3427
|
+
const currentLevelIndex = levels.indexOf(logLevel);
|
|
3428
|
+
const messageLevelIndex = levels.indexOf(level);
|
|
3429
|
+
return messageLevelIndex >= currentLevelIndex;
|
|
3430
|
+
};
|
|
3431
|
+
return {
|
|
3432
|
+
name: 'logging',
|
|
3433
|
+
version: '1.0.0',
|
|
3434
|
+
onBeforeRequest(requestConfig) {
|
|
3435
|
+
if (shouldLog('debug')) {
|
|
3436
|
+
logger.debug('[Etsy API] Request:', {
|
|
3437
|
+
method: requestConfig.method,
|
|
3438
|
+
endpoint: requestConfig.endpoint,
|
|
3439
|
+
params: requestConfig.params,
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
return requestConfig;
|
|
3443
|
+
},
|
|
3444
|
+
onAfterResponse(response) {
|
|
3445
|
+
if (shouldLog('debug')) {
|
|
3446
|
+
logger.debug('[Etsy API] Response:', {
|
|
3447
|
+
status: response.status,
|
|
3448
|
+
data: response.data,
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
return response;
|
|
3452
|
+
},
|
|
3453
|
+
onError(error) {
|
|
3454
|
+
if (shouldLog('error')) {
|
|
3455
|
+
logger.error('[Etsy API] Error:', error);
|
|
3456
|
+
if (error instanceof EtsyApiError) {
|
|
3457
|
+
logger.error('[Etsy API] Error Details:', {
|
|
3458
|
+
statusCode: error.statusCode,
|
|
3459
|
+
endpoint: error.endpoint,
|
|
3460
|
+
suggestions: error.suggestions,
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
},
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
function createCachingPlugin(config = {}) {
|
|
3468
|
+
const cache = new Map();
|
|
3469
|
+
const ttl = (config.ttl || 300) * 1000;
|
|
3470
|
+
const generateKey = (requestConfig) => {
|
|
3471
|
+
if (config.keyGenerator) {
|
|
3472
|
+
return config.keyGenerator(requestConfig);
|
|
3473
|
+
}
|
|
3474
|
+
return `${requestConfig.method}:${requestConfig.endpoint}:${JSON.stringify(requestConfig.params || {})}`;
|
|
3475
|
+
};
|
|
3476
|
+
return {
|
|
3477
|
+
name: 'caching',
|
|
3478
|
+
version: '1.0.0',
|
|
3479
|
+
onBeforeRequest(requestConfig) {
|
|
3480
|
+
if (requestConfig.method.toUpperCase() === 'GET') {
|
|
3481
|
+
const key = generateKey(requestConfig);
|
|
3482
|
+
const cached = cache.get(key);
|
|
3483
|
+
if (cached && Date.now() < cached.expiresAt) ;
|
|
3484
|
+
}
|
|
3485
|
+
return requestConfig;
|
|
3486
|
+
},
|
|
3487
|
+
onAfterResponse(response) {
|
|
3488
|
+
const key = `GET:${response.status}`;
|
|
3489
|
+
cache.set(key, {
|
|
3490
|
+
data: response.data,
|
|
3491
|
+
expiresAt: Date.now() + ttl,
|
|
3492
|
+
});
|
|
3493
|
+
return response;
|
|
3494
|
+
},
|
|
3495
|
+
onDestroy() {
|
|
3496
|
+
cache.clear();
|
|
3497
|
+
},
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
function createRateLimitPlugin(config = {}) {
|
|
3501
|
+
const maxRequestsPerSecond = config.maxRequestsPerSecond || 10;
|
|
3502
|
+
const requests = [];
|
|
3503
|
+
return {
|
|
3504
|
+
name: 'rateLimit',
|
|
3505
|
+
version: '1.0.0',
|
|
3506
|
+
async onBeforeRequest(requestConfig) {
|
|
3507
|
+
const now = Date.now();
|
|
3508
|
+
const oneSecondAgo = now - 1000;
|
|
3509
|
+
while (requests.length > 0 && requests[0] !== undefined && requests[0] < oneSecondAgo) {
|
|
3510
|
+
requests.shift();
|
|
3511
|
+
}
|
|
3512
|
+
if (requests.length >= maxRequestsPerSecond) {
|
|
3513
|
+
if (config.onRateLimitExceeded) {
|
|
3514
|
+
config.onRateLimitExceeded();
|
|
3515
|
+
}
|
|
3516
|
+
const oldestRequest = requests[0];
|
|
3517
|
+
if (oldestRequest !== undefined) {
|
|
3518
|
+
const waitTime = oldestRequest + 1000 - now;
|
|
3519
|
+
if (waitTime > 0) {
|
|
3520
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
requests.push(now);
|
|
3525
|
+
return requestConfig;
|
|
3526
|
+
},
|
|
3527
|
+
};
|
|
3528
|
+
}
|
|
3529
|
+
|
|
2806
3530
|
function createEtsyClient(config) {
|
|
2807
3531
|
return new EtsyClient(config);
|
|
2808
3532
|
}
|
|
@@ -2845,6 +3569,7 @@ exports.EtsyRateLimiter = EtsyRateLimiter;
|
|
|
2845
3569
|
exports.EtsyWebhookHandler = EtsyWebhookHandler;
|
|
2846
3570
|
exports.FieldValidator = FieldValidator;
|
|
2847
3571
|
exports.FileTokenStorage = FileTokenStorage;
|
|
3572
|
+
exports.GlobalRequestQueue = GlobalRequestQueue;
|
|
2848
3573
|
exports.LFUCache = LFUCache;
|
|
2849
3574
|
exports.LIBRARY_NAME = LIBRARY_NAME;
|
|
2850
3575
|
exports.LRUCache = LRUCache;
|
|
@@ -2852,9 +3577,11 @@ exports.ListingQueryBuilder = ListingQueryBuilder;
|
|
|
2852
3577
|
exports.LocalStorageTokenStorage = LocalStorageTokenStorage;
|
|
2853
3578
|
exports.MemoryTokenStorage = MemoryTokenStorage;
|
|
2854
3579
|
exports.PaginatedResults = PaginatedResults;
|
|
3580
|
+
exports.PluginManager = PluginManager;
|
|
2855
3581
|
exports.ReceiptQueryBuilder = ReceiptQueryBuilder;
|
|
2856
3582
|
exports.RedisCacheStorage = RedisCacheStorage;
|
|
2857
3583
|
exports.RetryManager = RetryManager;
|
|
3584
|
+
exports.SecureTokenStorage = SecureTokenStorage;
|
|
2858
3585
|
exports.SessionStorageTokenStorage = SessionStorageTokenStorage;
|
|
2859
3586
|
exports.TokenManager = TokenManager;
|
|
2860
3587
|
exports.UpdateListingSchema = UpdateListingSchema;
|
|
@@ -2864,19 +3591,24 @@ exports.ValidationException = ValidationException;
|
|
|
2864
3591
|
exports.Validator = Validator;
|
|
2865
3592
|
exports.WebhookSecurity = WebhookSecurity;
|
|
2866
3593
|
exports.combineValidators = combineValidators;
|
|
3594
|
+
exports.createAnalyticsPlugin = createAnalyticsPlugin;
|
|
2867
3595
|
exports.createAuthHelper = createAuthHelper;
|
|
2868
3596
|
exports.createBatchQuery = createBatchQuery;
|
|
2869
3597
|
exports.createBulkOperationManager = createBulkOperationManager;
|
|
2870
3598
|
exports.createCacheStorage = createCacheStorage;
|
|
2871
3599
|
exports.createCacheWithInvalidation = createCacheWithInvalidation;
|
|
3600
|
+
exports.createCachingPlugin = createCachingPlugin;
|
|
2872
3601
|
exports.createCodeChallenge = createCodeChallenge;
|
|
2873
3602
|
exports.createDefaultTokenStorage = createDefaultTokenStorage;
|
|
2874
3603
|
exports.createEtsyClient = createEtsyClient;
|
|
2875
3604
|
exports.createListingQuery = createListingQuery;
|
|
3605
|
+
exports.createLoggingPlugin = createLoggingPlugin;
|
|
2876
3606
|
exports.createPaginatedResults = createPaginatedResults;
|
|
3607
|
+
exports.createRateLimitPlugin = createRateLimitPlugin;
|
|
2877
3608
|
exports.createRateLimiter = createRateLimiter;
|
|
2878
3609
|
exports.createReceiptQuery = createReceiptQuery;
|
|
2879
3610
|
exports.createRedisCacheStorage = createRedisCacheStorage;
|
|
3611
|
+
exports.createRetryPlugin = createRetryPlugin;
|
|
2880
3612
|
exports.createTokenManager = createTokenManager;
|
|
2881
3613
|
exports.createValidator = createValidator;
|
|
2882
3614
|
exports.createWebhookHandler = createWebhookHandler;
|
|
@@ -2894,16 +3626,19 @@ exports.generateRandomBase64Url = generateRandomBase64Url;
|
|
|
2894
3626
|
exports.generateState = generateState;
|
|
2895
3627
|
exports.getAvailableStorage = getAvailableStorage;
|
|
2896
3628
|
exports.getEnvironmentInfo = getEnvironmentInfo;
|
|
3629
|
+
exports.getGlobalQueue = getGlobalQueue;
|
|
2897
3630
|
exports.getLibraryInfo = getLibraryInfo;
|
|
2898
3631
|
exports.hasLocalStorage = hasLocalStorage;
|
|
2899
3632
|
exports.hasSessionStorage = hasSessionStorage;
|
|
2900
3633
|
exports.isBrowser = isBrowser$1;
|
|
2901
3634
|
exports.isNode = isNode$1;
|
|
3635
|
+
exports.isSecureStorageSupported = isSecureStorageSupported;
|
|
2902
3636
|
exports.sha256 = sha256;
|
|
2903
3637
|
exports.sha256Base64Url = sha256Base64Url;
|
|
2904
3638
|
exports.validate = validate;
|
|
2905
3639
|
exports.validateEncryptionKey = validateEncryptionKey;
|
|
2906
3640
|
exports.validateOrThrow = validateOrThrow;
|
|
2907
3641
|
exports.withQueryBuilder = withQueryBuilder;
|
|
3642
|
+
exports.withQueue = withQueue;
|
|
2908
3643
|
exports.withRetry = withRetry;
|
|
2909
3644
|
//# sourceMappingURL=node.cjs.map
|