pesafy 0.1.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.

Potentially problematic release.


This version of pesafy might be problematic. Click here for more details.

package/dist/index.cjs ADDED
@@ -0,0 +1,729 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+
5
+ // Pesafy - Payment Gateway Library
6
+ // https://github.com/levos-snr/pesafy
7
+ var __defProp = Object.defineProperty;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
10
+
11
+ // src/utils/errors/error-factory.ts
12
+ var PesafyError = class _PesafyError extends Error {
13
+ constructor(options) {
14
+ super(options.message);
15
+ __publicField(this, "code");
16
+ __publicField(this, "statusCode");
17
+ __publicField(this, "response");
18
+ __publicField(this, "requestId");
19
+ __publicField(this, "cause");
20
+ Object.defineProperty(this, "name", { value: "PesafyError" });
21
+ this.code = options.code;
22
+ this.statusCode = options.statusCode;
23
+ this.response = options.response;
24
+ this.requestId = options.requestId;
25
+ this.cause = options.cause;
26
+ if (Error.captureStackTrace) {
27
+ Error.captureStackTrace(this, _PesafyError);
28
+ }
29
+ }
30
+ toJSON() {
31
+ return {
32
+ name: this.name,
33
+ code: this.code,
34
+ message: this.message,
35
+ statusCode: this.statusCode,
36
+ requestId: this.requestId
37
+ };
38
+ }
39
+ };
40
+ function createError(options) {
41
+ return new PesafyError(options);
42
+ }
43
+
44
+ // src/utils/http/client.ts
45
+ var DEFAULT_TIMEOUT = 3e4;
46
+ async function httpRequest(url, options = {}) {
47
+ const {
48
+ method = "GET",
49
+ headers = {},
50
+ body,
51
+ timeout = DEFAULT_TIMEOUT
52
+ } = options;
53
+ const controller = new AbortController();
54
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
55
+ try {
56
+ const response = await fetch(url, {
57
+ method,
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ ...headers
61
+ },
62
+ body: body ? JSON.stringify(body) : void 0,
63
+ signal: controller.signal
64
+ });
65
+ clearTimeout(timeoutId);
66
+ let data;
67
+ const text = await response.text();
68
+ try {
69
+ data = text ? JSON.parse(text) : {};
70
+ } catch {
71
+ data = { raw: text };
72
+ }
73
+ if (!response.ok) {
74
+ throw new PesafyError({
75
+ code: "API_ERROR",
76
+ message: `Request failed with status ${response.status}`,
77
+ statusCode: response.status,
78
+ response: data
79
+ });
80
+ }
81
+ return {
82
+ data,
83
+ status: response.status,
84
+ headers: response.headers
85
+ };
86
+ } catch (error) {
87
+ clearTimeout(timeoutId);
88
+ if (error instanceof PesafyError) throw error;
89
+ if (error instanceof Error) {
90
+ if (error.name === "AbortError") {
91
+ throw new PesafyError({
92
+ code: "TIMEOUT",
93
+ message: `Request timed out after ${timeout}ms`,
94
+ cause: error
95
+ });
96
+ }
97
+ throw new PesafyError({
98
+ code: "NETWORK_ERROR",
99
+ message: error.message,
100
+ cause: error
101
+ });
102
+ }
103
+ throw new PesafyError({
104
+ code: "REQUEST_FAILED",
105
+ message: "An unknown error occurred",
106
+ cause: error
107
+ });
108
+ }
109
+ }
110
+
111
+ // src/core/auth/token-manager.ts
112
+ var TOKEN_BUFFER_SECONDS = 60;
113
+ var TokenManager = class {
114
+ constructor(consumerKey, consumerSecret, baseUrl) {
115
+ __publicField(this, "consumerKey");
116
+ __publicField(this, "consumerSecret");
117
+ __publicField(this, "baseUrl");
118
+ __publicField(this, "cachedToken", null);
119
+ __publicField(this, "tokenExpiresAt", 0);
120
+ this.consumerKey = consumerKey;
121
+ this.consumerSecret = consumerSecret;
122
+ this.baseUrl = baseUrl;
123
+ }
124
+ getAuthHeader() {
125
+ const credentials = `${this.consumerKey}:${this.consumerSecret}`;
126
+ const encoded = Buffer.from(credentials, "utf-8").toString("base64");
127
+ return `Basic ${encoded}`;
128
+ }
129
+ async getAccessToken() {
130
+ const now = Date.now() / 1e3;
131
+ if (this.cachedToken && this.tokenExpiresAt > now + TOKEN_BUFFER_SECONDS) {
132
+ return this.cachedToken;
133
+ }
134
+ const url = `${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`;
135
+ const response = await httpRequest(url, {
136
+ method: "GET",
137
+ headers: {
138
+ Authorization: this.getAuthHeader()
139
+ }
140
+ });
141
+ const data = response.data;
142
+ if (!data.access_token) {
143
+ throw new PesafyError({
144
+ code: "AUTH_FAILED",
145
+ message: "Failed to obtain access token",
146
+ response: data
147
+ });
148
+ }
149
+ this.cachedToken = data.access_token;
150
+ this.tokenExpiresAt = now + (data.expires_in ?? 3600);
151
+ return this.cachedToken;
152
+ }
153
+ clearCache() {
154
+ this.cachedToken = null;
155
+ this.tokenExpiresAt = 0;
156
+ }
157
+ };
158
+ function encryptSecurityCredential(initiatorPassword, certificatePem) {
159
+ try {
160
+ const passwordBuffer = Buffer.from(initiatorPassword, "utf-8");
161
+ const encrypted = crypto.publicEncrypt(
162
+ {
163
+ key: certificatePem,
164
+ padding: 1
165
+ // RSA_PKCS1_PADDING
166
+ },
167
+ passwordBuffer
168
+ );
169
+ return encrypted.toString("base64");
170
+ } catch (error) {
171
+ throw new PesafyError({
172
+ code: "ENCRYPTION_FAILED",
173
+ message: "Failed to encrypt security credential",
174
+ cause: error
175
+ });
176
+ }
177
+ }
178
+
179
+ // src/mpesa/b2b/b2b.ts
180
+ async function processB2B(baseUrl, accessToken, securityCredential, initiatorName, request) {
181
+ const body = {
182
+ Initiator: initiatorName,
183
+ SecurityCredential: securityCredential,
184
+ CommandID: request.commandId ?? "BusinessPayBill",
185
+ SenderIdentifierType: request.senderIdentifierType ?? 4,
186
+ RecieverIdentifierType: request.receiverIdentifierType ?? 4,
187
+ Amount: Math.round(request.amount),
188
+ PartyA: request.shortCode,
189
+ PartyB: request.receiverShortCode,
190
+ AccountReference: request.accountReference ?? "",
191
+ Remarks: request.remarks ?? "B2B Payment",
192
+ QueueTimeOutURL: request.timeoutUrl,
193
+ ResultURL: request.resultUrl
194
+ };
195
+ const { data } = await httpRequest(
196
+ `${baseUrl}/mpesa/b2b/v1/paymentrequest`,
197
+ {
198
+ method: "POST",
199
+ headers: { Authorization: `Bearer ${accessToken}` },
200
+ body
201
+ }
202
+ );
203
+ return data;
204
+ }
205
+
206
+ // src/mpesa/stk-push/utils.ts
207
+ function formatPhoneNumber(phone) {
208
+ const cleaned = phone.replace(/\D/g, "");
209
+ if (cleaned.startsWith("0")) return "254" + cleaned.slice(1);
210
+ if (cleaned.startsWith("254")) return cleaned;
211
+ return "254" + cleaned;
212
+ }
213
+ function getStkPushPassword(shortCode, passKey) {
214
+ const timestamp = getTimestamp();
215
+ const password = `${shortCode}${passKey}${timestamp}`;
216
+ return Buffer.from(password, "utf-8").toString("base64");
217
+ }
218
+ function getTimestamp() {
219
+ const now = /* @__PURE__ */ new Date();
220
+ const pad = (n) => n.toString().padStart(2, "0");
221
+ return [
222
+ now.getFullYear(),
223
+ pad(now.getMonth() + 1),
224
+ pad(now.getDate()),
225
+ pad(now.getHours()),
226
+ pad(now.getMinutes()),
227
+ pad(now.getSeconds())
228
+ ].join("");
229
+ }
230
+
231
+ // src/mpesa/b2c/b2c.ts
232
+ async function processB2C(baseUrl, accessToken, securityCredential, initiatorName, request) {
233
+ const body = {
234
+ OriginatorConversationID: `AG_${Date.now()}_${Math.random().toString(36).slice(2)}`,
235
+ InitiatorName: initiatorName,
236
+ SecurityCredential: securityCredential,
237
+ CommandID: request.commandId ?? "BusinessPayment",
238
+ Amount: Math.round(request.amount),
239
+ PartyA: request.shortCode,
240
+ PartyB: formatPhoneNumber(request.phoneNumber),
241
+ Remarks: request.remarks ?? "Payment",
242
+ QueueTimeOutURL: request.timeoutUrl,
243
+ ResultURL: request.resultUrl,
244
+ Occasion: request.occasion ?? ""
245
+ };
246
+ const { data } = await httpRequest(
247
+ `${baseUrl}/mpesa/b2c/v3/paymentrequest`,
248
+ {
249
+ method: "POST",
250
+ headers: { Authorization: `Bearer ${accessToken}` },
251
+ body
252
+ }
253
+ );
254
+ return data;
255
+ }
256
+
257
+ // src/mpesa/c2b/register-url.ts
258
+ async function registerC2BUrls(baseUrl, accessToken, request) {
259
+ const body = {
260
+ ShortCode: request.shortCode,
261
+ ResponseType: request.responseType ?? "Completed",
262
+ ConfirmationURL: request.confirmationUrl,
263
+ ValidationURL: request.validationUrl
264
+ };
265
+ const { data } = await httpRequest(
266
+ `${baseUrl}/mpesa/c2b/v2/registerurl`,
267
+ {
268
+ method: "POST",
269
+ headers: { Authorization: `Bearer ${accessToken}` },
270
+ body
271
+ }
272
+ );
273
+ return data;
274
+ }
275
+
276
+ // src/mpesa/c2b/simulate.ts
277
+ async function simulateC2B(baseUrl, accessToken, request) {
278
+ const body = {
279
+ ShortCode: request.shortCode,
280
+ CommandID: "CustomerPayBillOnline",
281
+ Amount: Math.round(request.amount),
282
+ Msisdn: formatPhoneNumber(request.phoneNumber),
283
+ BillRefNumber: request.billRefNumber ?? "default"
284
+ };
285
+ const { data } = await httpRequest(
286
+ `${baseUrl}/mpesa/c2b/v2/simulate`,
287
+ {
288
+ method: "POST",
289
+ headers: { Authorization: `Bearer ${accessToken}` },
290
+ body
291
+ }
292
+ );
293
+ return data;
294
+ }
295
+
296
+ // src/mpesa/qr-code/dynamic-qr.ts
297
+ async function generateDynamicQR(baseUrl, accessToken, request) {
298
+ const body = {
299
+ MerchantName: request.merchantName,
300
+ RefNo: request.refNo,
301
+ Amount: Math.round(request.amount),
302
+ TrxCode: request.trxCode,
303
+ CPI: request.cpi,
304
+ Size: request.size ?? "300"
305
+ };
306
+ const { data } = await httpRequest(
307
+ `${baseUrl}/mpesa/qrcode/v1/generate`,
308
+ {
309
+ method: "POST",
310
+ headers: { Authorization: `Bearer ${accessToken}` },
311
+ body
312
+ }
313
+ );
314
+ return data;
315
+ }
316
+
317
+ // src/mpesa/reversal/reversal.ts
318
+ async function processReversal(baseUrl, accessToken, securityCredential, initiatorName, request) {
319
+ const body = {
320
+ Initiator: initiatorName,
321
+ SecurityCredential: securityCredential,
322
+ CommandID: "TransactionReversal",
323
+ TransactionID: request.transactionId,
324
+ Amount: Math.round(request.amount),
325
+ ReceiverParty: request.shortCode,
326
+ RecieverIdentifierType: 4,
327
+ ResultURL: request.resultUrl,
328
+ QueueTimeOutURL: request.timeoutUrl,
329
+ Remarks: request.remarks ?? "Reversal",
330
+ Occasion: request.occasion ?? "Reversal"
331
+ };
332
+ const { data } = await httpRequest(
333
+ `${baseUrl}/mpesa/reversal/v1/request`,
334
+ {
335
+ method: "POST",
336
+ headers: { Authorization: `Bearer ${accessToken}` },
337
+ body
338
+ }
339
+ );
340
+ return data;
341
+ }
342
+
343
+ // src/mpesa/stk-push/stk-push.ts
344
+ async function processStkPush(baseUrl, accessToken, request) {
345
+ const body = {
346
+ BusinessShortCode: request.shortCode,
347
+ Password: getStkPushPassword(request.shortCode, request.passKey),
348
+ Timestamp: getTimestamp(),
349
+ TransactionType: request.transactionType ?? "CustomerPayBillOnline",
350
+ Amount: Math.round(request.amount),
351
+ PartyA: formatPhoneNumber(request.phoneNumber),
352
+ PartyB: request.shortCode,
353
+ PhoneNumber: formatPhoneNumber(request.phoneNumber),
354
+ CallBackURL: request.callbackUrl,
355
+ AccountReference: request.accountReference.slice(0, 12),
356
+ TransactionDesc: request.transactionDesc.slice(0, 13)
357
+ };
358
+ const { data } = await httpRequest(
359
+ `${baseUrl}/mpesa/stkpush/v1/processrequest`,
360
+ {
361
+ method: "POST",
362
+ headers: { Authorization: `Bearer ${accessToken}` },
363
+ body
364
+ }
365
+ );
366
+ return data;
367
+ }
368
+
369
+ // src/mpesa/stk-push/stk-query.ts
370
+ async function queryStkPush(baseUrl, accessToken, request) {
371
+ const body = {
372
+ BusinessShortCode: request.shortCode,
373
+ Password: getStkPushPassword(request.shortCode, request.passKey),
374
+ Timestamp: getTimestamp(),
375
+ CheckoutRequestID: request.checkoutRequestId
376
+ };
377
+ const { data } = await httpRequest(
378
+ `${baseUrl}/mpesa/stkpushquery/v1/query`,
379
+ {
380
+ method: "POST",
381
+ headers: { Authorization: `Bearer ${accessToken}` },
382
+ body
383
+ }
384
+ );
385
+ return data;
386
+ }
387
+
388
+ // src/mpesa/transaction-status/query.ts
389
+ async function queryTransactionStatus(baseUrl, accessToken, securityCredential, initiatorName, request) {
390
+ const body = {
391
+ Initiator: initiatorName,
392
+ SecurityCredential: securityCredential,
393
+ CommandID: "TransactionStatusQuery",
394
+ TransactionID: request.transactionId,
395
+ PartyA: request.shortCode,
396
+ IdentifierType: request.identifierType ?? 4,
397
+ ResultURL: request.resultUrl,
398
+ QueueTimeOutURL: request.timeoutUrl,
399
+ Remarks: "Status",
400
+ Occasion: "Query"
401
+ };
402
+ const { data } = await httpRequest(
403
+ `${baseUrl}/mpesa/transactionstatus/v1/query`,
404
+ {
405
+ method: "POST",
406
+ headers: { Authorization: `Bearer ${accessToken}` },
407
+ body
408
+ }
409
+ );
410
+ return data;
411
+ }
412
+
413
+ // src/mpesa/types.ts
414
+ var DARAJA_BASE_URLS = {
415
+ sandbox: "https://sandbox.safaricom.co.ke",
416
+ production: "https://api.safaricom.co.ke"
417
+ };
418
+
419
+ // src/mpesa/index.ts
420
+ var Mpesa = class {
421
+ constructor(config) {
422
+ __publicField(this, "config");
423
+ __publicField(this, "tokenManager");
424
+ __publicField(this, "baseUrl");
425
+ this.config = config;
426
+ this.baseUrl = DARAJA_BASE_URLS[config.environment];
427
+ this.tokenManager = new TokenManager(
428
+ config.consumerKey,
429
+ config.consumerSecret,
430
+ this.baseUrl
431
+ );
432
+ }
433
+ async getToken() {
434
+ return this.tokenManager.getAccessToken();
435
+ }
436
+ async getSecurityCredential() {
437
+ if (this.config.securityCredential) return this.config.securityCredential;
438
+ if (!this.config.initiatorPassword) {
439
+ throw new Error(
440
+ "Security credential required: provide securityCredential or (initiatorPassword + certificatePath/certificatePem)"
441
+ );
442
+ }
443
+ let cert;
444
+ if (this.config.certificatePem) {
445
+ cert = this.config.certificatePem;
446
+ } else if (this.config.certificatePath) {
447
+ cert = await Bun.file(this.config.certificatePath).text();
448
+ } else {
449
+ throw new Error(
450
+ "certificatePath or certificatePem required for B2C/B2B/Reversal"
451
+ );
452
+ }
453
+ return encryptSecurityCredential(this.config.initiatorPassword, cert);
454
+ }
455
+ /** STK Push (M-Pesa Express) - Initiate payment on customer phone */
456
+ async stkPush(request) {
457
+ const shortCode = this.config.lipaNaMpesaShortCode ?? "";
458
+ const passKey = this.config.lipaNaMpesaPassKey ?? "";
459
+ if (!shortCode || !passKey) {
460
+ throw new Error(
461
+ "lipaNaMpesaShortCode and lipaNaMpesaPassKey required for STK Push"
462
+ );
463
+ }
464
+ const token = await this.getToken();
465
+ return processStkPush(this.baseUrl, token, {
466
+ ...request,
467
+ shortCode,
468
+ passKey
469
+ });
470
+ }
471
+ /** STK Query - Check STK Push transaction status */
472
+ async stkQuery(request) {
473
+ const shortCode = this.config.lipaNaMpesaShortCode ?? "";
474
+ const passKey = this.config.lipaNaMpesaPassKey ?? "";
475
+ if (!shortCode || !passKey) {
476
+ throw new Error(
477
+ "lipaNaMpesaShortCode and lipaNaMpesaPassKey required for STK Query"
478
+ );
479
+ }
480
+ const token = await this.getToken();
481
+ return queryStkPush(this.baseUrl, token, {
482
+ ...request,
483
+ shortCode,
484
+ passKey
485
+ });
486
+ }
487
+ /** B2C - Send money to customer */
488
+ async b2c(request) {
489
+ const initiator = this.config.initiatorName ?? "";
490
+ const securityCred = await this.getSecurityCredential();
491
+ if (!initiator) throw new Error("initiatorName required for B2C");
492
+ const token = await this.getToken();
493
+ return processB2C(this.baseUrl, token, securityCred, initiator, request);
494
+ }
495
+ /** B2B - Send money to business */
496
+ async b2b(request) {
497
+ const initiator = this.config.initiatorName ?? "";
498
+ const securityCred = await this.getSecurityCredential();
499
+ if (!initiator) throw new Error("initiatorName required for B2B");
500
+ const token = await this.getToken();
501
+ return processB2B(this.baseUrl, token, securityCred, initiator, request);
502
+ }
503
+ /** C2B - Register validation/confirmation URLs */
504
+ async c2bRegisterUrls(request) {
505
+ const token = await this.getToken();
506
+ return registerC2BUrls(this.baseUrl, token, request);
507
+ }
508
+ /** C2B - Simulate payment (sandbox only) */
509
+ async c2bSimulate(request) {
510
+ const token = await this.getToken();
511
+ return simulateC2B(this.baseUrl, token, request);
512
+ }
513
+ /** Dynamic QR - Generate LIPA NA M-PESA QR code */
514
+ async qrCode(request) {
515
+ const token = await this.getToken();
516
+ return generateDynamicQR(this.baseUrl, token, request);
517
+ }
518
+ /** Transaction Status - Query transaction status */
519
+ async transactionStatus(request) {
520
+ const initiator = this.config.initiatorName ?? "";
521
+ const securityCred = await this.getSecurityCredential();
522
+ if (!initiator)
523
+ throw new Error("initiatorName required for Transaction Status");
524
+ const token = await this.getToken();
525
+ return queryTransactionStatus(
526
+ this.baseUrl,
527
+ token,
528
+ securityCred,
529
+ initiator,
530
+ request
531
+ );
532
+ }
533
+ /** Reversal - Reverse a transaction */
534
+ async reversal(request) {
535
+ const initiator = this.config.initiatorName ?? "";
536
+ const securityCred = await this.getSecurityCredential();
537
+ if (!initiator) throw new Error("initiatorName required for Reversal");
538
+ const token = await this.getToken();
539
+ return processReversal(
540
+ this.baseUrl,
541
+ token,
542
+ securityCred,
543
+ initiator,
544
+ request
545
+ );
546
+ }
547
+ };
548
+
549
+ // src/mpesa/webhooks/retry.ts
550
+ var DEFAULT_OPTIONS = {
551
+ maxRetries: Infinity,
552
+ initialDelay: 1e3,
553
+ // 1 second
554
+ maxDelay: 36e5,
555
+ // 1 hour
556
+ backoffMultiplier: 2,
557
+ maxRetryDuration: 30 * 24 * 60 * 60 * 1e3
558
+ // 30 days
559
+ };
560
+ async function retryWithBackoff(fn, options = {}) {
561
+ const opts = { ...DEFAULT_OPTIONS, ...options };
562
+ let delay = opts.initialDelay;
563
+ let attempts = 0;
564
+ const startTime = Date.now();
565
+ while (attempts < opts.maxRetries) {
566
+ attempts++;
567
+ if (Date.now() - startTime > opts.maxRetryDuration) {
568
+ return {
569
+ success: false,
570
+ attempts,
571
+ error: new Error("Max retry duration exceeded")
572
+ };
573
+ }
574
+ try {
575
+ const data = await fn();
576
+ return { success: true, data, attempts };
577
+ } catch (error) {
578
+ const err = error instanceof Error ? error : new Error(String(error));
579
+ if (err.message.includes("4")) {
580
+ return { success: false, attempts, error: err };
581
+ }
582
+ if (attempts < opts.maxRetries) {
583
+ await new Promise((resolve) => setTimeout(resolve, delay));
584
+ delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelay);
585
+ }
586
+ }
587
+ }
588
+ return {
589
+ success: false,
590
+ attempts,
591
+ error: new Error("Max retries exceeded")
592
+ };
593
+ }
594
+
595
+ // src/mpesa/webhooks/signature-verifier.ts
596
+ var SAFARICOM_IPS = [
597
+ "196.201.214.200",
598
+ "196.201.214.206",
599
+ "196.201.213.114",
600
+ "196.201.214.207",
601
+ "196.201.214.208",
602
+ "196.201.213.44",
603
+ "196.201.212.127",
604
+ "196.201.212.138",
605
+ "196.201.212.129",
606
+ "196.201.212.136",
607
+ "196.201.212.74",
608
+ "196.201.212.69"
609
+ ];
610
+ function verifyWebhookIP(requestIP, allowedIPs = SAFARICOM_IPS) {
611
+ return allowedIPs.includes(requestIP);
612
+ }
613
+ function parseStkPushWebhook(body) {
614
+ try {
615
+ const parsed = body;
616
+ if (parsed.Body?.stkCallback) return parsed;
617
+ return null;
618
+ } catch {
619
+ return null;
620
+ }
621
+ }
622
+ function parseB2CWebhook(body) {
623
+ try {
624
+ const parsed = body;
625
+ if (parsed.Result?.ResultCode !== void 0) return parsed;
626
+ return null;
627
+ } catch {
628
+ return null;
629
+ }
630
+ }
631
+ function parseC2BWebhook(body) {
632
+ try {
633
+ const parsed = body;
634
+ if (parsed.TransID && parsed.TransAmount) return parsed;
635
+ return null;
636
+ } catch {
637
+ return null;
638
+ }
639
+ }
640
+
641
+ // src/mpesa/webhooks/webhook-handler.ts
642
+ function handleWebhook(body, options = {}) {
643
+ if (!options.skipIPCheck && options.requestIP) {
644
+ if (!verifyWebhookIP(options.requestIP, options.allowedIPs)) {
645
+ return {
646
+ success: false,
647
+ eventType: null,
648
+ data: null,
649
+ error: "IP address not whitelisted"
650
+ };
651
+ }
652
+ }
653
+ const stkPush = parseStkPushWebhook(body);
654
+ if (stkPush) {
655
+ return {
656
+ success: true,
657
+ eventType: "stk_push",
658
+ data: stkPush
659
+ };
660
+ }
661
+ const b2c = parseB2CWebhook(body);
662
+ if (b2c) {
663
+ return {
664
+ success: true,
665
+ eventType: "b2c",
666
+ data: b2c
667
+ };
668
+ }
669
+ const c2b = parseC2BWebhook(body);
670
+ if (c2b) {
671
+ return {
672
+ success: true,
673
+ eventType: "c2b",
674
+ data: c2b
675
+ };
676
+ }
677
+ return {
678
+ success: false,
679
+ eventType: null,
680
+ data: null,
681
+ error: "Unknown webhook format"
682
+ };
683
+ }
684
+ function extractTransactionId(webhook) {
685
+ if ("Body" in webhook && webhook.Body?.stkCallback) {
686
+ const items = webhook.Body.stkCallback.CallbackMetadata?.Item;
687
+ const mpesaReceipt = items?.find(
688
+ (item) => item.Name === "MpesaReceiptNumber"
689
+ );
690
+ return mpesaReceipt ? String(mpesaReceipt.Value) : null;
691
+ }
692
+ if ("Result" in webhook && webhook.Result?.TransactionID) {
693
+ return webhook.Result.TransactionID;
694
+ }
695
+ if ("TransID" in webhook) {
696
+ return webhook.TransID;
697
+ }
698
+ return null;
699
+ }
700
+ function extractAmount(webhook) {
701
+ if ("Body" in webhook && webhook.Body?.stkCallback) {
702
+ const items = webhook.Body.stkCallback.CallbackMetadata?.Item;
703
+ const amount = items?.find((item) => item.Name === "Amount");
704
+ return amount ? Number(amount.Value) : null;
705
+ }
706
+ if ("Result" in webhook && webhook.Result?.ResultParameters) {
707
+ const params = webhook.Result.ResultParameters.ResultParameter;
708
+ const amount = params?.find((p) => p.Key === "Amount");
709
+ return amount ? Number(amount.Value) : null;
710
+ }
711
+ if ("TransAmount" in webhook) {
712
+ return Number.parseFloat(webhook.TransAmount);
713
+ }
714
+ return null;
715
+ }
716
+
717
+ exports.DARAJA_BASE_URLS = DARAJA_BASE_URLS;
718
+ exports.Mpesa = Mpesa;
719
+ exports.PesafyError = PesafyError;
720
+ exports.TokenManager = TokenManager;
721
+ exports.createError = createError;
722
+ exports.encryptSecurityCredential = encryptSecurityCredential;
723
+ exports.extractAmount = extractAmount;
724
+ exports.extractTransactionId = extractTransactionId;
725
+ exports.handleWebhook = handleWebhook;
726
+ exports.retryWithBackoff = retryWithBackoff;
727
+ exports.verifyWebhookIP = verifyWebhookIP;
728
+ //# sourceMappingURL=index.cjs.map
729
+ //# sourceMappingURL=index.cjs.map