swiftshopr-payments 1.0.0
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/LICENSE +21 -0
- package/README.md +339 -0
- package/package.json +54 -0
- package/src/branding.js +127 -0
- package/src/client.js +111 -0
- package/src/config.js +273 -0
- package/src/dashboard.js +265 -0
- package/src/index.js +79 -0
- package/src/payments.js +319 -0
- package/src/refunds.js +209 -0
- package/src/types/index.d.ts +460 -0
- package/src/utils/http.js +217 -0
- package/src/webhooks.js +163 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript definitions for swiftshopr-payments
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface SwiftShoprConfig {
|
|
6
|
+
/** Your SwiftShopr API key */
|
|
7
|
+
apiKey: string;
|
|
8
|
+
/** Webhook secret for signature verification */
|
|
9
|
+
webhookSecret?: string;
|
|
10
|
+
/** API base URL (default: production) */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
13
|
+
timeout?: number;
|
|
14
|
+
/** Number of retries for failed requests (default: 3) */
|
|
15
|
+
retries?: number;
|
|
16
|
+
/** Hook called before each request */
|
|
17
|
+
onRequest?: (info: RequestInfo) => void;
|
|
18
|
+
/** Hook called after each response */
|
|
19
|
+
onResponse?: (info: ResponseInfo) => void;
|
|
20
|
+
/** Hook called on errors */
|
|
21
|
+
onError?: (error: Error) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RequestInfo {
|
|
25
|
+
method: string;
|
|
26
|
+
url: string;
|
|
27
|
+
headers: Record<string, string>;
|
|
28
|
+
body?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ResponseInfo {
|
|
32
|
+
status: number;
|
|
33
|
+
data: unknown;
|
|
34
|
+
headers: Headers;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Payment Types
|
|
38
|
+
export interface CreateSessionParams {
|
|
39
|
+
/** Amount in USD */
|
|
40
|
+
amount: number;
|
|
41
|
+
/** Your order reference */
|
|
42
|
+
orderId?: string;
|
|
43
|
+
/** Store ID (resolves wallet automatically) */
|
|
44
|
+
storeId?: string;
|
|
45
|
+
/** Retailer wallet address */
|
|
46
|
+
destinationAddress?: string;
|
|
47
|
+
/** Payment method (ACH_BANK_ACCOUNT, CARD, etc.) */
|
|
48
|
+
paymentMethod?: string;
|
|
49
|
+
/** Custom metadata */
|
|
50
|
+
metadata?: Record<string, unknown>;
|
|
51
|
+
/** Idempotency key for safe retries */
|
|
52
|
+
idempotencyKey?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CreateTransferParams {
|
|
56
|
+
/** Amount in USD */
|
|
57
|
+
amount: number;
|
|
58
|
+
/** User's wallet address */
|
|
59
|
+
userWalletAddress: string;
|
|
60
|
+
/** Your order reference */
|
|
61
|
+
orderId?: string;
|
|
62
|
+
/** Store ID */
|
|
63
|
+
storeId?: string;
|
|
64
|
+
/** Retailer wallet address */
|
|
65
|
+
destinationAddress?: string;
|
|
66
|
+
/** Custom metadata */
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
/** Idempotency key */
|
|
69
|
+
idempotencyKey?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface PaymentSession {
|
|
73
|
+
intentId: string;
|
|
74
|
+
sessionId: string;
|
|
75
|
+
orderId: string | null;
|
|
76
|
+
quoteId: string | null;
|
|
77
|
+
onrampUrl: string;
|
|
78
|
+
expiresAt: string;
|
|
79
|
+
branding: Branding | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface TransferResult {
|
|
83
|
+
intentId: string;
|
|
84
|
+
orderId: string | null;
|
|
85
|
+
status: string;
|
|
86
|
+
transfer: {
|
|
87
|
+
to: string;
|
|
88
|
+
amount: string;
|
|
89
|
+
asset: string;
|
|
90
|
+
network: string;
|
|
91
|
+
chainId: number;
|
|
92
|
+
};
|
|
93
|
+
expiresAt: string;
|
|
94
|
+
branding: Branding | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface PaymentStatus {
|
|
98
|
+
intentId: string;
|
|
99
|
+
orderId: string | null;
|
|
100
|
+
status: 'pending' | 'processing' | 'completed' | 'failed' | 'canceled' | 'expired';
|
|
101
|
+
amount: number;
|
|
102
|
+
storeId: string | null;
|
|
103
|
+
destinationAddress: string;
|
|
104
|
+
txHash: string | null;
|
|
105
|
+
explorerUrl: string | null;
|
|
106
|
+
confirmedAt: string | null;
|
|
107
|
+
createdAt: string;
|
|
108
|
+
expiresAt: string;
|
|
109
|
+
isExpired: boolean;
|
|
110
|
+
refund: RefundInfo | null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface RefundInfo {
|
|
114
|
+
status: string;
|
|
115
|
+
amount: number;
|
|
116
|
+
txHash: string | null;
|
|
117
|
+
refundedAt: string | null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface WaitOptions {
|
|
121
|
+
/** Max wait time in ms (default: 300000 for payments, 600000 for refunds) */
|
|
122
|
+
timeout?: number;
|
|
123
|
+
/** Poll interval in ms (default: 2000 for payments, 5000 for refunds) */
|
|
124
|
+
interval?: number;
|
|
125
|
+
/** Callback on status change */
|
|
126
|
+
onStatusChange?: (status: PaymentStatus | RefundStatus) => void;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Refund Types
|
|
130
|
+
export interface CreateRefundParams {
|
|
131
|
+
/** Original payment intent ID */
|
|
132
|
+
intentId: string;
|
|
133
|
+
/** Refund amount (optional, defaults to full refund) */
|
|
134
|
+
amount?: number;
|
|
135
|
+
/** Reason for refund */
|
|
136
|
+
reason?: string;
|
|
137
|
+
/** Idempotency key */
|
|
138
|
+
idempotencyKey?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface RefundResult {
|
|
142
|
+
id: string;
|
|
143
|
+
intentId: string;
|
|
144
|
+
orderId: string | null;
|
|
145
|
+
status: 'requested' | 'pending' | 'completed' | 'failed';
|
|
146
|
+
amount: number;
|
|
147
|
+
originalAmount: number;
|
|
148
|
+
isPartial: boolean;
|
|
149
|
+
reason: string;
|
|
150
|
+
createdAt: string;
|
|
151
|
+
instructions: {
|
|
152
|
+
message: string;
|
|
153
|
+
transfer: {
|
|
154
|
+
to: string;
|
|
155
|
+
amount: string;
|
|
156
|
+
asset: string;
|
|
157
|
+
network: string;
|
|
158
|
+
chainId: number;
|
|
159
|
+
};
|
|
160
|
+
fromWallet: string;
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface RefundStatus {
|
|
165
|
+
id: string;
|
|
166
|
+
intentId: string;
|
|
167
|
+
orderId: string | null;
|
|
168
|
+
amount: number;
|
|
169
|
+
reason: string;
|
|
170
|
+
status: string;
|
|
171
|
+
txHash: string | null;
|
|
172
|
+
explorerUrl: string | null;
|
|
173
|
+
fromAddress: string;
|
|
174
|
+
toAddress: string;
|
|
175
|
+
createdAt: string;
|
|
176
|
+
completedAt: string | null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Dashboard Types
|
|
180
|
+
export interface DashboardSummary {
|
|
181
|
+
today: {
|
|
182
|
+
revenue: number;
|
|
183
|
+
transactionCount: number;
|
|
184
|
+
completed: number;
|
|
185
|
+
pending: number;
|
|
186
|
+
failed: number;
|
|
187
|
+
revenueTrendPercent: number;
|
|
188
|
+
};
|
|
189
|
+
week: {
|
|
190
|
+
revenue: number;
|
|
191
|
+
transactionCount: number;
|
|
192
|
+
completed: number;
|
|
193
|
+
};
|
|
194
|
+
month: {
|
|
195
|
+
revenue: number;
|
|
196
|
+
transactionCount: number;
|
|
197
|
+
completed: number;
|
|
198
|
+
avgTransaction: number;
|
|
199
|
+
estimatedCardSavings: number;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface TransactionListParams {
|
|
204
|
+
status?: string;
|
|
205
|
+
storeId?: string;
|
|
206
|
+
startDate?: string;
|
|
207
|
+
endDate?: string;
|
|
208
|
+
limit?: number;
|
|
209
|
+
offset?: number;
|
|
210
|
+
sort?: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface Transaction {
|
|
214
|
+
id: string;
|
|
215
|
+
orderId: string | null;
|
|
216
|
+
status: string;
|
|
217
|
+
amount: number;
|
|
218
|
+
storeId: string | null;
|
|
219
|
+
txHash: string | null;
|
|
220
|
+
explorerUrl: string | null;
|
|
221
|
+
createdAt: string;
|
|
222
|
+
confirmedAt: string | null;
|
|
223
|
+
refund: RefundInfo | null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface TransactionList {
|
|
227
|
+
transactions: Transaction[];
|
|
228
|
+
pagination: {
|
|
229
|
+
total: number;
|
|
230
|
+
limit: number;
|
|
231
|
+
offset: number;
|
|
232
|
+
hasMore: boolean;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface StoreMetrics {
|
|
237
|
+
storeId: string;
|
|
238
|
+
storeName: string | null;
|
|
239
|
+
revenue: number;
|
|
240
|
+
transactionCount: number;
|
|
241
|
+
completed: number;
|
|
242
|
+
pending: number;
|
|
243
|
+
failed: number;
|
|
244
|
+
successRate: number;
|
|
245
|
+
avgTransaction: number;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface DailyMetrics {
|
|
249
|
+
date: string;
|
|
250
|
+
revenue: number;
|
|
251
|
+
transactionCount: number;
|
|
252
|
+
completed: number;
|
|
253
|
+
failed: number;
|
|
254
|
+
avgTransaction: number;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Branding Types
|
|
258
|
+
export interface Branding {
|
|
259
|
+
storeId: string;
|
|
260
|
+
branding: {
|
|
261
|
+
name: string | null;
|
|
262
|
+
logoUrl: string | null;
|
|
263
|
+
theme: {
|
|
264
|
+
primaryColor: string;
|
|
265
|
+
secondaryColor: string;
|
|
266
|
+
backgroundColor: string;
|
|
267
|
+
textColor: string;
|
|
268
|
+
accentColor: string;
|
|
269
|
+
mode: 'light' | 'dark';
|
|
270
|
+
fontFamily: string | null;
|
|
271
|
+
};
|
|
272
|
+
cdpTheme: Record<string, string> | null;
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Webhook Types
|
|
277
|
+
export interface WebhookEvent {
|
|
278
|
+
type: string;
|
|
279
|
+
data: {
|
|
280
|
+
intentId: string;
|
|
281
|
+
orderId: string | null;
|
|
282
|
+
txHash: string | null;
|
|
283
|
+
amount: string;
|
|
284
|
+
status: string;
|
|
285
|
+
currency: string;
|
|
286
|
+
network: string;
|
|
287
|
+
explorerUrl: string | null;
|
|
288
|
+
storeId: string | null;
|
|
289
|
+
timestamp: string;
|
|
290
|
+
refundId?: string;
|
|
291
|
+
refundAmount?: string;
|
|
292
|
+
refundReason?: string;
|
|
293
|
+
};
|
|
294
|
+
receivedAt: string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface WebhooksHelper {
|
|
298
|
+
verify(payload: string | object, signature: string, timestamp: string): boolean;
|
|
299
|
+
constructEvent(payload: string | object, headers: Record<string, string>): WebhookEvent;
|
|
300
|
+
events: typeof WebhookEvents;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export declare const WebhookEvents: {
|
|
304
|
+
PAYMENT_COMPLETED: 'payment.completed';
|
|
305
|
+
PAYMENT_FAILED: 'payment.failed';
|
|
306
|
+
PAYMENT_EXPIRED: 'payment.expired';
|
|
307
|
+
REFUND_REQUESTED: 'refund.requested';
|
|
308
|
+
REFUND_COMPLETED: 'refund.completed';
|
|
309
|
+
REFUND_FAILED: 'refund.failed';
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Error Types
|
|
313
|
+
export declare class HttpError extends Error {
|
|
314
|
+
statusCode: number;
|
|
315
|
+
response: unknown;
|
|
316
|
+
requestId: string | null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export declare class TimeoutError extends Error {
|
|
320
|
+
timeout: number;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export declare class SwiftShoprError extends Error {
|
|
324
|
+
code: string;
|
|
325
|
+
details: Record<string, unknown>;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// API Modules
|
|
329
|
+
export interface PaymentsAPI {
|
|
330
|
+
createSession(params: CreateSessionParams): Promise<PaymentSession>;
|
|
331
|
+
createTransfer(params: CreateTransferParams): Promise<TransferResult>;
|
|
332
|
+
getStatus(intentId: string): Promise<PaymentStatus>;
|
|
333
|
+
getStatusByOrderId(orderId: string): Promise<PaymentStatus>;
|
|
334
|
+
waitForCompletion(intentId: string, options?: WaitOptions): Promise<PaymentStatus>;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface RefundsAPI {
|
|
338
|
+
create(params: CreateRefundParams): Promise<RefundResult>;
|
|
339
|
+
get(refundId: string): Promise<RefundStatus>;
|
|
340
|
+
listForPayment(intentId: string): Promise<RefundStatus[]>;
|
|
341
|
+
waitForCompletion(refundId: string, options?: WaitOptions): Promise<RefundStatus>;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export interface DashboardAPI {
|
|
345
|
+
getSummary(): Promise<DashboardSummary>;
|
|
346
|
+
getTransactions(params?: TransactionListParams): Promise<TransactionList>;
|
|
347
|
+
getAllTransactions(params?: TransactionListParams & { maxResults?: number }): Promise<Transaction[]>;
|
|
348
|
+
getStores(): Promise<StoreMetrics[]>;
|
|
349
|
+
getDaily(params?: { days?: number; storeId?: string }): Promise<DailyMetrics[]>;
|
|
350
|
+
export(params?: { startDate?: string; endDate?: string; status?: string; storeId?: string }): Promise<string>;
|
|
351
|
+
getRevenueAnalytics(params?: { period?: 'day' | 'week' | 'month' | 'year'; periods?: number }): Promise<{
|
|
352
|
+
totalRevenue: number;
|
|
353
|
+
totalTransactions: number;
|
|
354
|
+
averageDailyRevenue: number;
|
|
355
|
+
averageTransactionValue: number;
|
|
356
|
+
dataPoints: DailyMetrics[];
|
|
357
|
+
}>;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export interface BrandingAPI {
|
|
361
|
+
get(storeId: string): Promise<Branding>;
|
|
362
|
+
toCssVariables(branding: Branding): string;
|
|
363
|
+
toStyleObject(branding: Branding): Record<string, string>;
|
|
364
|
+
toCdpTheme(branding: Branding): Record<string, string> | null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Config Types
|
|
368
|
+
export interface RetailerConfig {
|
|
369
|
+
chainId: string;
|
|
370
|
+
label: string | null;
|
|
371
|
+
webhook: {
|
|
372
|
+
url: string | null;
|
|
373
|
+
hasSecret: boolean;
|
|
374
|
+
};
|
|
375
|
+
permissions: Record<string, boolean>;
|
|
376
|
+
rateLimit: number;
|
|
377
|
+
allowedIps: string[] | null;
|
|
378
|
+
createdAt: string;
|
|
379
|
+
lastUsedAt: string | null;
|
|
380
|
+
stores: RegisteredStore[];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export interface RegisteredStore {
|
|
384
|
+
id: string;
|
|
385
|
+
storeId: string;
|
|
386
|
+
storeName: string | null;
|
|
387
|
+
walletAddress: string;
|
|
388
|
+
webhook: {
|
|
389
|
+
url: string | null;
|
|
390
|
+
hasSecret: boolean;
|
|
391
|
+
} | null;
|
|
392
|
+
branding: Record<string, unknown>;
|
|
393
|
+
isActive: boolean;
|
|
394
|
+
createdAt: string;
|
|
395
|
+
updatedAt?: string;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export interface WebhookConfig {
|
|
399
|
+
url: string | null;
|
|
400
|
+
hasSecret: boolean;
|
|
401
|
+
secret?: string;
|
|
402
|
+
message?: string;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export interface RegisterStoreParams {
|
|
406
|
+
storeId: string;
|
|
407
|
+
walletAddress: string;
|
|
408
|
+
storeName?: string;
|
|
409
|
+
webhookUrl?: string;
|
|
410
|
+
branding?: Record<string, unknown>;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export interface UpdateStoreParams {
|
|
414
|
+
storeName?: string;
|
|
415
|
+
walletAddress?: string;
|
|
416
|
+
webhookUrl?: string;
|
|
417
|
+
branding?: Record<string, unknown>;
|
|
418
|
+
isActive?: boolean;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export interface ConfigAPI {
|
|
422
|
+
get(): Promise<RetailerConfig>;
|
|
423
|
+
setWebhook(params?: { url?: string; generateSecret?: boolean }): Promise<WebhookConfig>;
|
|
424
|
+
rotateWebhookSecret(): Promise<WebhookConfig>;
|
|
425
|
+
getStores(): Promise<RegisteredStore[]>;
|
|
426
|
+
registerStore(params: RegisterStoreParams): Promise<RegisteredStore>;
|
|
427
|
+
updateStore(storeId: string, updates: UpdateStoreParams): Promise<RegisteredStore>;
|
|
428
|
+
deactivateStore(storeId: string): Promise<{ message: string }>;
|
|
429
|
+
rotateStoreWebhookSecret(storeId: string): Promise<WebhookConfig>;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Main Client
|
|
433
|
+
export declare class SwiftShoprClient {
|
|
434
|
+
constructor(config: SwiftShoprConfig);
|
|
435
|
+
|
|
436
|
+
static readonly VERSION: string;
|
|
437
|
+
|
|
438
|
+
payments: PaymentsAPI;
|
|
439
|
+
refunds: RefundsAPI;
|
|
440
|
+
dashboard: DashboardAPI;
|
|
441
|
+
branding: BrandingAPI;
|
|
442
|
+
config: ConfigAPI;
|
|
443
|
+
webhooks: WebhooksHelper | { configure(secret: string): WebhooksHelper };
|
|
444
|
+
|
|
445
|
+
getConfig(): {
|
|
446
|
+
baseUrl: string;
|
|
447
|
+
timeout: number;
|
|
448
|
+
retries: number;
|
|
449
|
+
hasApiKey: boolean;
|
|
450
|
+
hasWebhookSecret: boolean;
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Utility exports
|
|
455
|
+
export declare function generateIdempotencyKey(): string;
|
|
456
|
+
export declare function createWebhooksHelper(secret: string): WebhooksHelper;
|
|
457
|
+
export declare function webhookMiddleware(secret: string): (req: any, res: any, next: any) => void;
|
|
458
|
+
|
|
459
|
+
// Default export
|
|
460
|
+
export default SwiftShoprClient;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Client for SwiftShopr API
|
|
3
|
+
* Zero dependencies - uses native fetch
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEFAULT_BASE_URL = 'https://shopr-scanner-backend.onrender.com';
|
|
7
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
8
|
+
const DEFAULT_RETRIES = 3;
|
|
9
|
+
const RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504];
|
|
10
|
+
|
|
11
|
+
class HttpError extends Error {
|
|
12
|
+
constructor(message, statusCode, response, requestId) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'HttpError';
|
|
15
|
+
this.statusCode = statusCode;
|
|
16
|
+
this.response = response;
|
|
17
|
+
this.requestId = requestId;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class TimeoutError extends Error {
|
|
22
|
+
constructor(message, timeout) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'TimeoutError';
|
|
25
|
+
this.timeout = timeout;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class SwiftShoprError extends Error {
|
|
30
|
+
constructor(code, message, details = {}) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'SwiftShoprError';
|
|
33
|
+
this.code = code;
|
|
34
|
+
this.details = details;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sleep for specified milliseconds
|
|
40
|
+
*/
|
|
41
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate idempotency key
|
|
45
|
+
*/
|
|
46
|
+
const generateIdempotencyKey = () => {
|
|
47
|
+
const timestamp = Date.now().toString(36);
|
|
48
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
49
|
+
return `idem_${timestamp}_${random}`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create HTTP client with configuration
|
|
54
|
+
*/
|
|
55
|
+
function createHttpClient(config = {}) {
|
|
56
|
+
const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
|
|
57
|
+
const { apiKey } = config;
|
|
58
|
+
const timeout = config.timeout || DEFAULT_TIMEOUT;
|
|
59
|
+
const maxRetries = config.retries ?? DEFAULT_RETRIES;
|
|
60
|
+
const onRequest = config.onRequest || null;
|
|
61
|
+
const onResponse = config.onResponse || null;
|
|
62
|
+
const onError = config.onError || null;
|
|
63
|
+
|
|
64
|
+
if (!apiKey) {
|
|
65
|
+
throw new SwiftShoprError('CONFIG_ERROR', 'API key is required');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Make HTTP request with retries and timeout
|
|
70
|
+
*/
|
|
71
|
+
async function request(method, path, options = {}) {
|
|
72
|
+
const url = `${baseUrl}${path}`;
|
|
73
|
+
const idempotencyKey =
|
|
74
|
+
options.idempotencyKey ||
|
|
75
|
+
(['POST', 'PUT', 'PATCH'].includes(method)
|
|
76
|
+
? generateIdempotencyKey()
|
|
77
|
+
: null);
|
|
78
|
+
|
|
79
|
+
const headers = {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
'x-swiftshopr-key': apiKey,
|
|
82
|
+
...(idempotencyKey && { 'Idempotency-Key': idempotencyKey }),
|
|
83
|
+
...options.headers,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const fetchOptions = {
|
|
87
|
+
method,
|
|
88
|
+
headers,
|
|
89
|
+
...(options.body && { body: JSON.stringify(options.body) }),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Call onRequest hook
|
|
93
|
+
if (onRequest) {
|
|
94
|
+
onRequest({ method, url, headers, body: options.body });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let lastError;
|
|
98
|
+
let attempt = 0;
|
|
99
|
+
|
|
100
|
+
while (attempt <= maxRetries) {
|
|
101
|
+
attempt++;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Create abort controller for timeout
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
107
|
+
|
|
108
|
+
const response = await fetch(url, {
|
|
109
|
+
...fetchOptions,
|
|
110
|
+
signal: controller.signal,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
clearTimeout(timeoutId);
|
|
114
|
+
|
|
115
|
+
// Parse response
|
|
116
|
+
const contentType = response.headers.get('content-type');
|
|
117
|
+
let data;
|
|
118
|
+
|
|
119
|
+
if (contentType && contentType.includes('application/json')) {
|
|
120
|
+
data = await response.json();
|
|
121
|
+
} else {
|
|
122
|
+
data = await response.text();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Call onResponse hook
|
|
126
|
+
if (onResponse) {
|
|
127
|
+
onResponse({
|
|
128
|
+
status: response.status,
|
|
129
|
+
data,
|
|
130
|
+
headers: response.headers,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Handle non-2xx responses
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
const error = new HttpError(
|
|
137
|
+
data?.message || data?.error || `HTTP ${response.status}`,
|
|
138
|
+
response.status,
|
|
139
|
+
data,
|
|
140
|
+
data?.requestId,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Retry on specific status codes
|
|
144
|
+
if (
|
|
145
|
+
RETRY_STATUS_CODES.includes(response.status) &&
|
|
146
|
+
attempt <= maxRetries
|
|
147
|
+
) {
|
|
148
|
+
lastError = error;
|
|
149
|
+
const backoff = Math.min(1000 * 2 ** (attempt - 1), 10000);
|
|
150
|
+
await sleep(backoff);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (onError) {
|
|
155
|
+
onError(error);
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return data;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// Handle abort (timeout)
|
|
163
|
+
if (error.name === 'AbortError') {
|
|
164
|
+
const timeoutError = new TimeoutError(
|
|
165
|
+
`Request timed out after ${timeout}ms`,
|
|
166
|
+
timeout,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (attempt <= maxRetries) {
|
|
170
|
+
lastError = timeoutError;
|
|
171
|
+
const backoff = Math.min(1000 * 2 ** (attempt - 1), 10000);
|
|
172
|
+
await sleep(backoff);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (onError) {
|
|
177
|
+
onError(timeoutError);
|
|
178
|
+
}
|
|
179
|
+
throw timeoutError;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Network errors - retry
|
|
183
|
+
if (error.name === 'TypeError' && attempt <= maxRetries) {
|
|
184
|
+
lastError = error;
|
|
185
|
+
const backoff = Math.min(1000 * 2 ** (attempt - 1), 10000);
|
|
186
|
+
await sleep(backoff);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (onError) {
|
|
191
|
+
onError(error);
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// All retries exhausted
|
|
198
|
+
throw lastError;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
get: (path, options) => request('GET', path, options),
|
|
203
|
+
post: (path, body, options) => request('POST', path, { ...options, body }),
|
|
204
|
+
put: (path, body, options) => request('PUT', path, { ...options, body }),
|
|
205
|
+
patch: (path, body, options) =>
|
|
206
|
+
request('PATCH', path, { ...options, body }),
|
|
207
|
+
delete: (path, options) => request('DELETE', path, options),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = {
|
|
212
|
+
createHttpClient,
|
|
213
|
+
HttpError,
|
|
214
|
+
TimeoutError,
|
|
215
|
+
SwiftShoprError,
|
|
216
|
+
generateIdempotencyKey,
|
|
217
|
+
};
|