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