@zendfi/sdk 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +389 -0
- package/dist/index.d.mts +611 -0
- package/dist/index.d.ts +611 -0
- package/dist/index.js +680 -0
- package/dist/index.mjs +640 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AuthenticationError: () => AuthenticationError,
|
|
34
|
+
ConfigLoader: () => ConfigLoader,
|
|
35
|
+
NetworkError: () => NetworkError,
|
|
36
|
+
RateLimitError: () => RateLimitError,
|
|
37
|
+
ValidationError: () => ValidationError,
|
|
38
|
+
ZendFiClient: () => ZendFiClient,
|
|
39
|
+
ZendFiError: () => ZendFiError,
|
|
40
|
+
verifyExpressWebhook: () => verifyExpressWebhook,
|
|
41
|
+
verifyNextWebhook: () => verifyNextWebhook,
|
|
42
|
+
verifyWebhookSignature: () => verifyWebhookSignature,
|
|
43
|
+
zendfi: () => zendfi
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(index_exports);
|
|
46
|
+
|
|
47
|
+
// src/client.ts
|
|
48
|
+
var import_cross_fetch = __toESM(require("cross-fetch"));
|
|
49
|
+
var import_crypto = require("crypto");
|
|
50
|
+
|
|
51
|
+
// src/types.ts
|
|
52
|
+
var ZendFiError = class extends Error {
|
|
53
|
+
constructor(message, statusCode, code, details) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.statusCode = statusCode;
|
|
56
|
+
this.code = code;
|
|
57
|
+
this.details = details;
|
|
58
|
+
this.name = "ZendFiError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var AuthenticationError = class extends ZendFiError {
|
|
62
|
+
constructor(message = "Authentication failed") {
|
|
63
|
+
super(message, 401, "AUTHENTICATION_ERROR");
|
|
64
|
+
this.name = "AuthenticationError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var ValidationError = class extends ZendFiError {
|
|
68
|
+
constructor(message, details) {
|
|
69
|
+
super(message, 400, "VALIDATION_ERROR", details);
|
|
70
|
+
this.name = "ValidationError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var NetworkError = class extends ZendFiError {
|
|
74
|
+
constructor(message) {
|
|
75
|
+
super(message, 0, "NETWORK_ERROR");
|
|
76
|
+
this.name = "NetworkError";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var RateLimitError = class extends ZendFiError {
|
|
80
|
+
constructor(message = "Rate limit exceeded") {
|
|
81
|
+
super(message, 429, "RATE_LIMIT_ERROR");
|
|
82
|
+
this.name = "RateLimitError";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/utils.ts
|
|
87
|
+
var ConfigLoader = class {
|
|
88
|
+
/**
|
|
89
|
+
* Load configuration from various sources
|
|
90
|
+
*/
|
|
91
|
+
static load(options) {
|
|
92
|
+
const environment = this.detectEnvironment();
|
|
93
|
+
const apiKey = this.loadApiKey(options?.apiKey);
|
|
94
|
+
const baseURL = this.getBaseURL(environment, options?.baseURL);
|
|
95
|
+
return {
|
|
96
|
+
apiKey,
|
|
97
|
+
baseURL,
|
|
98
|
+
environment,
|
|
99
|
+
timeout: options?.timeout ?? 3e4,
|
|
100
|
+
retries: options?.retries ?? 3,
|
|
101
|
+
idempotencyEnabled: options?.idempotencyEnabled ?? true
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Detect environment based on various signals
|
|
106
|
+
*/
|
|
107
|
+
static detectEnvironment() {
|
|
108
|
+
const envVar = process.env.ZENDFI_ENVIRONMENT || process.env.NEXT_PUBLIC_ZENDFI_ENVIRONMENT;
|
|
109
|
+
if (envVar) {
|
|
110
|
+
return envVar;
|
|
111
|
+
}
|
|
112
|
+
if (typeof process !== "undefined" && process.env) {
|
|
113
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
114
|
+
if (nodeEnv === "production") return "production";
|
|
115
|
+
if (nodeEnv === "staging") return "staging";
|
|
116
|
+
if (nodeEnv === "development" || nodeEnv === "test") return "development";
|
|
117
|
+
}
|
|
118
|
+
if (typeof window !== "undefined") {
|
|
119
|
+
const hostname = window.location.hostname;
|
|
120
|
+
if (hostname === "yourdomain.com" || hostname === "www.yourdomain.com") {
|
|
121
|
+
return "production";
|
|
122
|
+
}
|
|
123
|
+
if (hostname.includes("staging") || hostname.includes(".vercel.app")) {
|
|
124
|
+
return "staging";
|
|
125
|
+
}
|
|
126
|
+
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
127
|
+
return "development";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return "development";
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Load API key from various sources
|
|
134
|
+
*/
|
|
135
|
+
static loadApiKey(explicitKey) {
|
|
136
|
+
if (explicitKey) return explicitKey;
|
|
137
|
+
if (typeof process !== "undefined" && process.env) {
|
|
138
|
+
const envKey = process.env.ZENDFI_API_KEY || process.env.NEXT_PUBLIC_ZENDFI_API_KEY || process.env.REACT_APP_ZENDFI_API_KEY;
|
|
139
|
+
if (envKey) return envKey;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const credentials = this.loadCLICredentials();
|
|
143
|
+
if (credentials?.apiKey) return credentials.apiKey;
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
throw new Error(
|
|
147
|
+
"ZendFi API key not found. Set ZENDFI_API_KEY environment variable or pass apiKey in constructor."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get base URL for API
|
|
152
|
+
*/
|
|
153
|
+
static getBaseURL(_environment, explicitURL) {
|
|
154
|
+
if (explicitURL) return explicitURL;
|
|
155
|
+
return process.env.ZENDFI_API_URL || "https://api.zendfi.tech";
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Load credentials from CLI config file (~/.zendfi/credentials.json)
|
|
159
|
+
*/
|
|
160
|
+
static loadCLICredentials() {
|
|
161
|
+
if (typeof process === "undefined" || !process.env.HOME) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const fs = require("fs");
|
|
166
|
+
const path = require("path");
|
|
167
|
+
const credentialsPath = path.join(process.env.HOME, ".zendfi", "credentials.json");
|
|
168
|
+
if (fs.existsSync(credentialsPath)) {
|
|
169
|
+
const data = fs.readFileSync(credentialsPath, "utf-8");
|
|
170
|
+
return JSON.parse(data);
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Validate API key format
|
|
178
|
+
*/
|
|
179
|
+
static validateApiKey(apiKey) {
|
|
180
|
+
if (!apiKey.startsWith("zfi_test_") && !apiKey.startsWith("zfi_live_")) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
'Invalid API key format. ZendFi API keys should start with "zfi_test_" or "zfi_live_"'
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (apiKey.startsWith("zfi_live_")) {
|
|
186
|
+
const env = this.detectEnvironment();
|
|
187
|
+
if (env === "development") {
|
|
188
|
+
console.warn(
|
|
189
|
+
"\u26A0\uFE0F Warning: Using a live API key (zfi_live_) in development environment. This will create real transactions. Use a test key (zfi_test_) for development."
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
function parseError(response, body) {
|
|
196
|
+
const statusCode = response.status;
|
|
197
|
+
const errorMessage = body?.error || body?.message || response.statusText || "Unknown error";
|
|
198
|
+
const errorCode = body?.code;
|
|
199
|
+
const details = body?.details;
|
|
200
|
+
switch (statusCode) {
|
|
201
|
+
case 401:
|
|
202
|
+
case 403:
|
|
203
|
+
return new AuthenticationError(errorMessage);
|
|
204
|
+
case 400:
|
|
205
|
+
return new ValidationError(errorMessage, details);
|
|
206
|
+
case 429:
|
|
207
|
+
return new RateLimitError(errorMessage);
|
|
208
|
+
case 500:
|
|
209
|
+
case 502:
|
|
210
|
+
case 503:
|
|
211
|
+
case 504:
|
|
212
|
+
return new NetworkError(errorMessage);
|
|
213
|
+
default:
|
|
214
|
+
return new ZendFiError(errorMessage, statusCode, errorCode, details);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function generateIdempotencyKey() {
|
|
218
|
+
const timestamp = Date.now();
|
|
219
|
+
const random = Math.random().toString(36).substring(2, 15);
|
|
220
|
+
return `zfi_idem_${timestamp}_${random}`;
|
|
221
|
+
}
|
|
222
|
+
function sleep(ms) {
|
|
223
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/client.ts
|
|
227
|
+
var ZendFiClient = class {
|
|
228
|
+
config;
|
|
229
|
+
constructor(options) {
|
|
230
|
+
this.config = ConfigLoader.load(options);
|
|
231
|
+
ConfigLoader.validateApiKey(this.config.apiKey);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Create a new payment
|
|
235
|
+
*/
|
|
236
|
+
async createPayment(request) {
|
|
237
|
+
return this.request("POST", "/api/v1/payments", {
|
|
238
|
+
...request,
|
|
239
|
+
currency: request.currency || "USD",
|
|
240
|
+
token: request.token || "USDC"
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Get payment by ID
|
|
245
|
+
*/
|
|
246
|
+
async getPayment(paymentId) {
|
|
247
|
+
return this.request("GET", `/api/v1/payments/${paymentId}`);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* List all payments with pagination
|
|
251
|
+
*/
|
|
252
|
+
async listPayments(request) {
|
|
253
|
+
const params = new URLSearchParams();
|
|
254
|
+
if (request?.page) params.append("page", request.page.toString());
|
|
255
|
+
if (request?.limit) params.append("limit", request.limit.toString());
|
|
256
|
+
if (request?.status) params.append("status", request.status);
|
|
257
|
+
if (request?.from_date) params.append("from_date", request.from_date);
|
|
258
|
+
if (request?.to_date) params.append("to_date", request.to_date);
|
|
259
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
260
|
+
return this.request("GET", `/api/v1/payments${query}`);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Create a subscription plan
|
|
264
|
+
*/
|
|
265
|
+
async createSubscriptionPlan(request) {
|
|
266
|
+
return this.request("POST", "/api/v1/subscriptions/plans", {
|
|
267
|
+
...request,
|
|
268
|
+
currency: request.currency || "USD",
|
|
269
|
+
interval_count: request.interval_count || 1,
|
|
270
|
+
trial_days: request.trial_days || 0
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Get subscription plan by ID
|
|
275
|
+
*/
|
|
276
|
+
async getSubscriptionPlan(planId) {
|
|
277
|
+
return this.request("GET", `/api/v1/subscriptions/plans/${planId}`);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Create a subscription
|
|
281
|
+
*/
|
|
282
|
+
async createSubscription(request) {
|
|
283
|
+
return this.request("POST", "/api/v1/subscriptions", request);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Get subscription by ID
|
|
287
|
+
*/
|
|
288
|
+
async getSubscription(subscriptionId) {
|
|
289
|
+
return this.request("GET", `/api/v1/subscriptions/${subscriptionId}`);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Cancel a subscription
|
|
293
|
+
*/
|
|
294
|
+
async cancelSubscription(subscriptionId) {
|
|
295
|
+
return this.request(
|
|
296
|
+
"POST",
|
|
297
|
+
`/api/v1/subscriptions/${subscriptionId}/cancel`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Create a payment link (shareable checkout URL)
|
|
302
|
+
*/
|
|
303
|
+
async createPaymentLink(request) {
|
|
304
|
+
const response = await this.request("POST", "/api/v1/payment-links", {
|
|
305
|
+
...request,
|
|
306
|
+
currency: request.currency || "USD",
|
|
307
|
+
token: request.token || "USDC"
|
|
308
|
+
});
|
|
309
|
+
return {
|
|
310
|
+
...response,
|
|
311
|
+
url: response.hosted_page_url
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Get payment link by link code
|
|
316
|
+
*/
|
|
317
|
+
async getPaymentLink(linkCode) {
|
|
318
|
+
const response = await this.request("GET", `/api/v1/payment-links/${linkCode}`);
|
|
319
|
+
return {
|
|
320
|
+
...response,
|
|
321
|
+
url: response.hosted_page_url
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* List all payment links
|
|
326
|
+
*/
|
|
327
|
+
async listPaymentLinks() {
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
// ===================================================================
|
|
331
|
+
// INSTALLMENT PLANS - Pay over time
|
|
332
|
+
// ===================================================================
|
|
333
|
+
/**
|
|
334
|
+
* Create an installment plan
|
|
335
|
+
* Split a purchase into multiple scheduled payments
|
|
336
|
+
*/
|
|
337
|
+
async createInstallmentPlan(request) {
|
|
338
|
+
const response = await this.request(
|
|
339
|
+
"POST",
|
|
340
|
+
"/api/v1/installment-plans",
|
|
341
|
+
request
|
|
342
|
+
);
|
|
343
|
+
return {
|
|
344
|
+
id: response.plan_id,
|
|
345
|
+
plan_id: response.plan_id,
|
|
346
|
+
status: response.status
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get installment plan by ID
|
|
351
|
+
*/
|
|
352
|
+
async getInstallmentPlan(planId) {
|
|
353
|
+
return this.request("GET", `/api/v1/installment-plans/${planId}`);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* List all installment plans for merchant
|
|
357
|
+
*/
|
|
358
|
+
async listInstallmentPlans(params) {
|
|
359
|
+
const query = new URLSearchParams();
|
|
360
|
+
if (params?.limit) query.append("limit", params.limit.toString());
|
|
361
|
+
if (params?.offset) query.append("offset", params.offset.toString());
|
|
362
|
+
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
363
|
+
return this.request("GET", `/api/v1/installment-plans${queryString}`);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* List installment plans for a specific customer
|
|
367
|
+
*/
|
|
368
|
+
async listCustomerInstallmentPlans(customerWallet) {
|
|
369
|
+
return this.request(
|
|
370
|
+
"GET",
|
|
371
|
+
`/api/v1/customers/${customerWallet}/installment-plans`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Cancel an installment plan
|
|
376
|
+
*/
|
|
377
|
+
async cancelInstallmentPlan(planId) {
|
|
378
|
+
return this.request(
|
|
379
|
+
"POST",
|
|
380
|
+
`/api/v1/installment-plans/${planId}/cancel`
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
// ===================================================================
|
|
384
|
+
// ESCROW - Secure fund holding
|
|
385
|
+
// ===================================================================
|
|
386
|
+
/**
|
|
387
|
+
* Create an escrow transaction
|
|
388
|
+
* Hold funds until conditions are met
|
|
389
|
+
*/
|
|
390
|
+
async createEscrow(request) {
|
|
391
|
+
return this.request("POST", "/api/v1/escrows", {
|
|
392
|
+
...request,
|
|
393
|
+
currency: request.currency || "USD",
|
|
394
|
+
token: request.token || "USDC"
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Get escrow by ID
|
|
399
|
+
*/
|
|
400
|
+
async getEscrow(escrowId) {
|
|
401
|
+
return this.request("GET", `/api/v1/escrows/${escrowId}`);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* List all escrows for merchant
|
|
405
|
+
*/
|
|
406
|
+
async listEscrows(params) {
|
|
407
|
+
const query = new URLSearchParams();
|
|
408
|
+
if (params?.limit) query.append("limit", params.limit.toString());
|
|
409
|
+
if (params?.offset) query.append("offset", params.offset.toString());
|
|
410
|
+
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
411
|
+
return this.request("GET", `/api/v1/escrows${queryString}`);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Approve escrow release to seller
|
|
415
|
+
*/
|
|
416
|
+
async approveEscrow(escrowId, request) {
|
|
417
|
+
return this.request(
|
|
418
|
+
"POST",
|
|
419
|
+
`/api/v1/escrows/${escrowId}/approve`,
|
|
420
|
+
request
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Refund escrow to buyer
|
|
425
|
+
*/
|
|
426
|
+
async refundEscrow(escrowId, request) {
|
|
427
|
+
return this.request("POST", `/api/v1/escrows/${escrowId}/refund`, request);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Raise a dispute for an escrow
|
|
431
|
+
*/
|
|
432
|
+
async disputeEscrow(escrowId, request) {
|
|
433
|
+
return this.request("POST", `/api/v1/escrows/${escrowId}/dispute`, request);
|
|
434
|
+
}
|
|
435
|
+
// ===================================================================
|
|
436
|
+
// INVOICES - Professional billing
|
|
437
|
+
// ===================================================================
|
|
438
|
+
/**
|
|
439
|
+
* Create an invoice
|
|
440
|
+
*/
|
|
441
|
+
async createInvoice(request) {
|
|
442
|
+
return this.request("POST", "/api/v1/invoices", {
|
|
443
|
+
...request,
|
|
444
|
+
token: request.token || "USDC"
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Get invoice by ID
|
|
449
|
+
*/
|
|
450
|
+
async getInvoice(invoiceId) {
|
|
451
|
+
return this.request("GET", `/api/v1/invoices/${invoiceId}`);
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* List all invoices for merchant
|
|
455
|
+
*/
|
|
456
|
+
async listInvoices() {
|
|
457
|
+
return this.request("GET", "/api/v1/invoices");
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Send invoice to customer via email
|
|
461
|
+
*/
|
|
462
|
+
async sendInvoice(invoiceId) {
|
|
463
|
+
return this.request("POST", `/api/v1/invoices/${invoiceId}/send`);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Verify webhook signature using HMAC-SHA256
|
|
467
|
+
*
|
|
468
|
+
* @param request - Webhook verification request containing payload, signature, and secret
|
|
469
|
+
* @returns true if signature is valid, false otherwise
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* ```typescript
|
|
473
|
+
* const isValid = zendfi.verifyWebhook({
|
|
474
|
+
* payload: req.body,
|
|
475
|
+
* signature: req.headers['x-zendfi-signature'],
|
|
476
|
+
* secret: process.env.ZENDFI_WEBHOOK_SECRET
|
|
477
|
+
* });
|
|
478
|
+
*
|
|
479
|
+
* if (!isValid) {
|
|
480
|
+
* return res.status(401).json({ error: 'Invalid signature' });
|
|
481
|
+
* }
|
|
482
|
+
* ```
|
|
483
|
+
*/
|
|
484
|
+
verifyWebhook(request) {
|
|
485
|
+
try {
|
|
486
|
+
if (!request.payload || !request.signature || !request.secret) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
const parsedPayload = JSON.parse(request.payload);
|
|
490
|
+
if (!parsedPayload.event || !parsedPayload.merchant_id || !parsedPayload.timestamp) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
const computedSignature = this.computeHmacSignature(request.payload, request.secret);
|
|
494
|
+
return this.timingSafeEqual(request.signature, computedSignature);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
if (this.config.environment === "development") {
|
|
497
|
+
console.error("Webhook verification error:", error);
|
|
498
|
+
}
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Compute HMAC-SHA256 signature
|
|
504
|
+
* Works in both Node.js and browser environments
|
|
505
|
+
*/
|
|
506
|
+
computeHmacSignature(payload, secret) {
|
|
507
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
508
|
+
return (0, import_crypto.createHmac)("sha256", secret).update(payload, "utf8").digest("hex");
|
|
509
|
+
}
|
|
510
|
+
throw new Error(
|
|
511
|
+
"Webhook verification in browser is not supported. Use this method in your backend/server environment."
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Timing-safe string comparison to prevent timing attacks
|
|
516
|
+
*/
|
|
517
|
+
timingSafeEqual(a, b) {
|
|
518
|
+
if (a.length !== b.length) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
522
|
+
try {
|
|
523
|
+
const bufferA = Buffer.from(a, "utf8");
|
|
524
|
+
const bufferB = Buffer.from(b, "utf8");
|
|
525
|
+
return (0, import_crypto.timingSafeEqual)(bufferA, bufferB);
|
|
526
|
+
} catch {
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
let result = 0;
|
|
530
|
+
for (let i = 0; i < a.length; i++) {
|
|
531
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
532
|
+
}
|
|
533
|
+
return result === 0;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Make an HTTP request with retry logic
|
|
537
|
+
*/
|
|
538
|
+
async request(method, endpoint, data, options = {}) {
|
|
539
|
+
const attempt = options.attempt || 1;
|
|
540
|
+
const idempotencyKey = options.idempotencyKey || (this.config.idempotencyEnabled && method !== "GET" ? generateIdempotencyKey() : void 0);
|
|
541
|
+
try {
|
|
542
|
+
const url = `${this.config.baseURL}${endpoint}`;
|
|
543
|
+
const headers = {
|
|
544
|
+
"Content-Type": "application/json",
|
|
545
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
546
|
+
};
|
|
547
|
+
if (idempotencyKey) {
|
|
548
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
549
|
+
}
|
|
550
|
+
const controller = new AbortController();
|
|
551
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
552
|
+
const response = await (0, import_cross_fetch.default)(url, {
|
|
553
|
+
method,
|
|
554
|
+
headers,
|
|
555
|
+
body: data ? JSON.stringify(data) : void 0,
|
|
556
|
+
signal: controller.signal
|
|
557
|
+
});
|
|
558
|
+
clearTimeout(timeoutId);
|
|
559
|
+
let body;
|
|
560
|
+
try {
|
|
561
|
+
body = await response.json();
|
|
562
|
+
} catch {
|
|
563
|
+
body = null;
|
|
564
|
+
}
|
|
565
|
+
if (!response.ok) {
|
|
566
|
+
const error = parseError(response, body);
|
|
567
|
+
if (response.status >= 500 && attempt < this.config.retries) {
|
|
568
|
+
const delay = Math.pow(2, attempt) * 1e3;
|
|
569
|
+
await sleep(delay);
|
|
570
|
+
return this.request(method, endpoint, data, {
|
|
571
|
+
idempotencyKey,
|
|
572
|
+
attempt: attempt + 1
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
577
|
+
return body;
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (error.name === "AbortError") {
|
|
580
|
+
throw new Error(`Request timeout after ${this.config.timeout}ms`);
|
|
581
|
+
}
|
|
582
|
+
if (attempt < this.config.retries && error.message?.includes("fetch")) {
|
|
583
|
+
const delay = Math.pow(2, attempt) * 1e3;
|
|
584
|
+
await sleep(delay);
|
|
585
|
+
return this.request(method, endpoint, data, {
|
|
586
|
+
idempotencyKey,
|
|
587
|
+
attempt: attempt + 1
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
var zendfi = (() => {
|
|
595
|
+
try {
|
|
596
|
+
return new ZendFiClient();
|
|
597
|
+
} catch (error) {
|
|
598
|
+
if (process.env.NODE_ENV === "test" || !process.env.ZENDFI_API_KEY) {
|
|
599
|
+
return new Proxy({}, {
|
|
600
|
+
get() {
|
|
601
|
+
throw new Error(
|
|
602
|
+
'ZendFi singleton not initialized. Set ZENDFI_API_KEY environment variable or create a custom instance: new ZendFiClient({ apiKey: "..." })'
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
609
|
+
})();
|
|
610
|
+
|
|
611
|
+
// src/webhooks.ts
|
|
612
|
+
async function verifyNextWebhook(request, secret) {
|
|
613
|
+
try {
|
|
614
|
+
const payload = await request.text();
|
|
615
|
+
const signature = request.headers.get("x-zendfi-signature");
|
|
616
|
+
if (!signature) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
const webhookSecret = secret || process.env.ZENDFI_WEBHOOK_SECRET;
|
|
620
|
+
if (!webhookSecret) {
|
|
621
|
+
throw new Error("ZENDFI_WEBHOOK_SECRET not configured");
|
|
622
|
+
}
|
|
623
|
+
const isValid = zendfi.verifyWebhook({
|
|
624
|
+
payload,
|
|
625
|
+
signature,
|
|
626
|
+
secret: webhookSecret
|
|
627
|
+
});
|
|
628
|
+
if (!isValid) {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
return JSON.parse(payload);
|
|
632
|
+
} catch {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async function verifyExpressWebhook(request, secret) {
|
|
637
|
+
try {
|
|
638
|
+
const payload = request.rawBody || JSON.stringify(request.body);
|
|
639
|
+
const signature = request.headers["x-zendfi-signature"];
|
|
640
|
+
if (!signature) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
const webhookSecret = secret || process.env.ZENDFI_WEBHOOK_SECRET;
|
|
644
|
+
if (!webhookSecret) {
|
|
645
|
+
throw new Error("ZENDFI_WEBHOOK_SECRET not configured");
|
|
646
|
+
}
|
|
647
|
+
const isValid = zendfi.verifyWebhook({
|
|
648
|
+
payload,
|
|
649
|
+
signature,
|
|
650
|
+
secret: webhookSecret
|
|
651
|
+
});
|
|
652
|
+
if (!isValid) {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
return JSON.parse(payload);
|
|
656
|
+
} catch {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function verifyWebhookSignature(payload, signature, secret) {
|
|
661
|
+
return zendfi.verifyWebhook({
|
|
662
|
+
payload,
|
|
663
|
+
signature,
|
|
664
|
+
secret
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
668
|
+
0 && (module.exports = {
|
|
669
|
+
AuthenticationError,
|
|
670
|
+
ConfigLoader,
|
|
671
|
+
NetworkError,
|
|
672
|
+
RateLimitError,
|
|
673
|
+
ValidationError,
|
|
674
|
+
ZendFiClient,
|
|
675
|
+
ZendFiError,
|
|
676
|
+
verifyExpressWebhook,
|
|
677
|
+
verifyNextWebhook,
|
|
678
|
+
verifyWebhookSignature,
|
|
679
|
+
zendfi
|
|
680
|
+
});
|