pesafy 0.0.2 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # pesafy
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 582d257: Restructure tests beside `src/`, add shared adapter route handlers
8
+ with parity checks, expand CLI with new Daraja commands and global flags, and
9
+ sync documentation.
10
+
11
+ ## 0.1.0
12
+
13
+ ### Minor Changes
14
+
15
+ - Restructure tests, shared adapter routes, and production CLI.
16
+ - Co-locate unit tests under `src/**/*.test.ts`; remove flat `__tests__/`
17
+ mega-suites
18
+ - Extract shared adapter handlers (`src/adapters/shared/`) with parity CI
19
+ check
20
+ - Modular CLI with new commands: tx-status, b2c-payment, b2c-disburse,
21
+ b2b-checkout, tax-remit, qr-generate, bills
22
+ - Global CLI flags: `--json`, `--env-file`, `--env`
23
+ - Update adapter and testing documentation
24
+
25
+ ## 0.0.3
26
+
27
+ ### Patch Changes
28
+
29
+ - Production hardening for supply chain and Socket scanner transparency.
30
+ - Patch dev transitive CVEs (`fast-uri` >= 3.1.2, `@ungap/structured-clone` >=
31
+ 1.3.1)
32
+ - Publish unminified `dist/` with source maps; static ESM imports in adapters
33
+ (no dynamic `require`)
34
+ - Replace placeholder domains with `example.com` / `example.org` in published
35
+ docs and CLI defaults
36
+ - Remove `prepare` lifecycle script from npm consumers; document
37
+ `pnpm exec simple-git-hooks` for contributors
38
+ - CI: `pnpm audit`, `publint`, `npm pack --dry-run`; Dependabot for npm
39
+ - Add supply-chain security documentation and fix SECURITY.md supported
40
+ versions
41
+
3
42
  ## 0.0.2
4
43
 
5
44
  ### Patch Changes
package/README.md CHANGED
@@ -73,7 +73,7 @@ const mpesa = new Mpesa({
73
73
  const response = await mpesa.stkPush({
74
74
  amount: 100,
75
75
  phoneNumber: '0712345678',
76
- callbackUrl: 'https://yourdomain.com/api/mpesa/callback',
76
+ callbackUrl: 'https://example.com/api/mpesa/callback',
77
77
  accountReference: 'INV-001',
78
78
  transactionDesc: 'Payment',
79
79
  })
@@ -114,8 +114,20 @@ npx pesafy balance --shortcode 600000 --identifier-type 4
114
114
  npx pesafy reversal <txId> Initiate a transaction reversal
115
115
  npx pesafy register-c2b-urls Register C2B Confirmation + Validation URLs
116
116
  npx pesafy simulate-c2b Simulate a C2B payment (sandbox only)
117
+ npx pesafy tx-status <txId> Query transaction status (async)
118
+ npx pesafy b2c-payment Initiate B2C payment (async)
119
+ npx pesafy b2c-disburse Initiate B2C disbursement (async)
120
+ npx pesafy b2b-checkout Initiate B2B Express checkout
121
+ npx pesafy tax-remit Initiate tax remittance (async)
122
+ npx pesafy qr-generate Generate a dynamic M-PESA QR code
123
+ npx pesafy bills <subcommand> Bill Manager (opt-in, invoice, reconcile)
117
124
  npx pesafy version Print library version
118
125
  npx pesafy help Show help
126
+
127
+ # Global flags (any command)
128
+ npx pesafy --json token Machine-readable output
129
+ npx pesafy --env-file .env.staging stk-push Load env from a custom file
130
+ npx pesafy --env production doctor Override MPESA_ENVIRONMENT for one run
119
131
  ```
120
132
 
121
133
  ### Environment variables read by the CLI
@@ -175,7 +187,7 @@ Prompts the customer to enter their M-PESA PIN on their phone.
175
187
  const push = await mpesa.stkPush({
176
188
  amount: 100, // KES — whole numbers only (1–250 000)
177
189
  phoneNumber: '0712345678', // any common Kenyan format
178
- callbackUrl: 'https://yourdomain.com/api/mpesa/callback',
190
+ callbackUrl: 'https://example.com/api/mpesa/callback',
179
191
  accountReference: 'INV-001', // max 12 chars
180
192
  transactionDesc: 'Subscription', // max 13 chars
181
193
  transactionType: 'CustomerPayBillOnline', // default; or "CustomerBuyGoodsOnline"
@@ -197,7 +209,7 @@ if (status.ResultCode === 0) {
197
209
  const result = await mpesa.stkPushSafe({
198
210
  amount: 100,
199
211
  phoneNumber: '0712345678',
200
- callbackUrl: 'https://yourdomain.com/api/mpesa/callback',
212
+ callbackUrl: 'https://example.com/api/mpesa/callback',
201
213
  accountReference: 'INV-001',
202
214
  transactionDesc: 'Payment',
203
215
  })
@@ -249,8 +261,8 @@ Register your Paybill or Till to receive M-PESA payments.
249
261
  await mpesa.registerC2BUrls({
250
262
  shortCode: '600984',
251
263
  responseType: 'Completed', // "Completed" | "Cancelled" (sentence-case required)
252
- confirmationUrl: 'https://yourdomain.com/api/mpesa/c2b/confirmation',
253
- validationUrl: 'https://yourdomain.com/api/mpesa/c2b/validation',
264
+ confirmationUrl: 'https://example.com/api/mpesa/c2b/confirmation',
265
+ validationUrl: 'https://example.com/api/mpesa/c2b/validation',
254
266
  apiVersion: 'v2', // default
255
267
  })
256
268
 
@@ -335,8 +347,8 @@ const ack = await mpesa.b2cPayment({
335
347
  partyA: '600979', // sender MMF shortcode (or set b2cPartyA in config)
336
348
  partyB: '600000', // target B2C shortcode
337
349
  accountReference: 'BATCH-2024-01',
338
- resultUrl: 'https://yourdomain.com/api/mpesa/b2c/result',
339
- queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/b2c/timeout',
350
+ resultUrl: 'https://example.com/api/mpesa/b2c/result',
351
+ queueTimeOutUrl: 'https://example.com/api/mpesa/b2c/timeout',
340
352
  remarks: 'Monthly top-up',
341
353
  })
342
354
  ```
@@ -391,8 +403,8 @@ const ack = await mpesa.b2cDisbursement({
391
403
  partyA: '600979', // sender shortcode
392
404
  partyB: '254712345678', // recipient MSISDN (2547XXXXXXXX)
393
405
  remarks: 'January salary', // required, 2–100 chars
394
- resultUrl: 'https://yourdomain.com/api/mpesa/b2c/disburse/result',
395
- queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/b2c/disburse/timeout',
406
+ resultUrl: 'https://example.com/api/mpesa/b2c/disburse/result',
407
+ queueTimeOutUrl: 'https://example.com/api/mpesa/b2c/disburse/timeout',
396
408
  occasion: 'Payroll Jan 2024', // optional
397
409
  })
398
410
  ```
@@ -452,7 +464,7 @@ const ack = await mpesa.b2bExpressCheckout({
452
464
  receiverShortCode: '000002', // vendor Paybill (credit party)
453
465
  amount: 5000,
454
466
  paymentRef: 'INV-001',
455
- callbackUrl: 'https://yourdomain.com/api/mpesa/b2b/callback',
467
+ callbackUrl: 'https://example.com/api/mpesa/b2b/callback',
456
468
  partnerName: 'Acme Supplies', // shown in merchant's USSD prompt
457
469
  requestRefId: 'unique-uuid', // auto-generated UUID if omitted
458
470
  })
@@ -520,8 +532,8 @@ const ack = await mpesa.b2bPayBill({
520
532
  partyA: '600979', // your shortcode (debit)
521
533
  partyB: '000000', // destination Paybill (credit)
522
534
  accountReference: 'ACC-353353', // max 13 chars
523
- resultUrl: 'https://yourdomain.com/api/mpesa/b2b/paybill/result',
524
- queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/b2b/paybill/timeout',
535
+ resultUrl: 'https://example.com/api/mpesa/b2b/paybill/result',
536
+ queueTimeOutUrl: 'https://example.com/api/mpesa/b2b/paybill/timeout',
525
537
  remarks: 'Supplier payment',
526
538
  requester: '254712345678', // optional — consumer MSISDN on whose behalf
527
539
  occasion: 'Q1 Invoice', // optional
@@ -572,8 +584,8 @@ const ack = await mpesa.b2bBuyGoods({
572
584
  partyA: '600979', // your shortcode (debit)
573
585
  partyB: '000000', // destination till / merchant store
574
586
  accountReference: 'PO-19008', // max 13 chars
575
- resultUrl: 'https://yourdomain.com/api/mpesa/b2b/buygoods/result',
576
- queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/b2b/buygoods/timeout',
587
+ resultUrl: 'https://example.com/api/mpesa/b2b/buygoods/result',
588
+ queueTimeOutUrl: 'https://example.com/api/mpesa/b2b/buygoods/timeout',
577
589
  remarks: 'Stock purchase',
578
590
  requester: '254712345678', // optional
579
591
  })
@@ -638,8 +650,8 @@ POSTed to your `resultUrl`.
638
650
  await mpesa.accountBalance({
639
651
  partyA: '174379',
640
652
  identifierType: '4', // "1"=MSISDN, "2"=Till, "4"=ShortCode (most common)
641
- resultUrl: 'https://yourdomain.com/api/mpesa/balance/result',
642
- queueTimeOutUrl:'https://yourdomain.com/api/mpesa/balance/timeout',
653
+ resultUrl: 'https://example.com/api/mpesa/balance/result',
654
+ queueTimeOutUrl:'https://example.com/api/mpesa/balance/timeout',
643
655
  remarks: 'Balance check',
644
656
  })
645
657
 
@@ -692,8 +704,8 @@ await mpesa.transactionStatus({
692
704
  // OR: originalConversationId: '7071-4170-...' ← when no receipt is available
693
705
  partyA: '174379',
694
706
  identifierType: '4', // "1"=MSISDN, "2"=Till, "4"=ShortCode
695
- resultUrl: 'https://yourdomain.com/api/mpesa/tx/result',
696
- queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/tx/timeout',
707
+ resultUrl: 'https://example.com/api/mpesa/tx/result',
708
+ queueTimeOutUrl: 'https://example.com/api/mpesa/tx/timeout',
697
709
  remarks: 'Check payment status',
698
710
  })
699
711
  ```
@@ -745,8 +757,8 @@ await mpesa.reverseTransaction({
745
757
  transactionId: 'OEI2AK4XXXX', // M-PESA receipt of the transaction to reverse
746
758
  receiverParty: '174379', // your shortcode
747
759
  amount: 500, // must equal the original amount
748
- resultUrl: 'https://yourdomain.com/api/mpesa/reversal/result',
749
- queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/reversal/timeout',
760
+ resultUrl: 'https://example.com/api/mpesa/reversal/result',
761
+ queueTimeOutUrl: 'https://example.com/api/mpesa/reversal/timeout',
750
762
  remarks: 'Erroneous charge', // 2–100 chars
751
763
  })
752
764
  ```
@@ -817,8 +829,8 @@ await mpesa.remitTax({
817
829
  amount: 5_000,
818
830
  partyA: '888880', // your business shortcode
819
831
  accountReference: 'PRN1234XN', // KRA Payment Registration Number (PRN)
820
- resultUrl: 'https://yourdomain.com/api/mpesa/tax/result',
821
- queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/tax/timeout',
832
+ resultUrl: 'https://example.com/api/mpesa/tax/result',
833
+ queueTimeOutUrl: 'https://example.com/api/mpesa/tax/timeout',
822
834
  remarks: 'Monthly PAYE',
823
835
  })
824
836
  ```
@@ -898,7 +910,7 @@ await mpesa.billManagerOptIn({
898
910
  officialContact: '0700000000',
899
911
  sendReminders: '1', // "1" enable | "0" disable (7-, 3-day, due-date reminders)
900
912
  logo: 'https://cdn.company.com/logo.jpg', // optional JPEG/JPG
901
- callbackUrl: 'https://yourdomain.com/api/mpesa/bills/callback',
913
+ callbackUrl: 'https://example.com/api/mpesa/bills/callback',
902
914
  })
903
915
 
904
916
  // 2. Update opt-in details
@@ -907,7 +919,7 @@ await mpesa.updateOptIn({
907
919
  email: 'new@company.com',
908
920
  officialContact: '0700000001',
909
921
  sendReminders: '1',
910
- callbackUrl: 'https://yourdomain.com/api/mpesa/bills/callback',
922
+ callbackUrl: 'https://example.com/api/mpesa/bills/callback',
911
923
  })
912
924
 
913
925
  // 3. Send a single invoice
@@ -1070,7 +1082,7 @@ createMpesaExpressRouter(router, {
1070
1082
  // STK Push
1071
1083
  lipaNaMpesaShortCode: '174379',
1072
1084
  lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
1073
- callbackUrl: 'https://yourdomain.com/api/mpesa/express/callback',
1085
+ callbackUrl: 'https://example.com/api/mpesa/express/callback',
1074
1086
 
1075
1087
  // Initiator (B2C, Tax, Reversal, Balance)
1076
1088
  initiatorName: 'testapi',
@@ -1078,13 +1090,13 @@ createMpesaExpressRouter(router, {
1078
1090
  certificatePath: './SandboxCertificate.cer',
1079
1091
 
1080
1092
  // Transaction Status
1081
- resultUrl: 'https://yourdomain.com/api/mpesa/transaction-status/result',
1082
- queueTimeOutUrl: 'https://yourdomain.com/api/mpesa/timeout',
1093
+ resultUrl: 'https://example.com/api/mpesa/transaction-status/result',
1094
+ queueTimeOutUrl: 'https://example.com/api/mpesa/timeout',
1083
1095
 
1084
1096
  // C2B
1085
1097
  c2bShortCode: '600984',
1086
- c2bConfirmationUrl: 'https://yourdomain.com/api/mpesa/c2b/confirmation',
1087
- c2bValidationUrl: 'https://yourdomain.com/api/mpesa/c2b/validation',
1098
+ c2bConfirmationUrl: 'https://example.com/api/mpesa/c2b/confirmation',
1099
+ c2bValidationUrl: 'https://example.com/api/mpesa/c2b/validation',
1088
1100
  c2bResponseType: 'Completed',
1089
1101
  c2bApiVersion: 'v2',
1090
1102
  onC2BValidation: async (payload) => {
@@ -1101,23 +1113,23 @@ createMpesaExpressRouter(router, {
1101
1113
 
1102
1114
  // Tax Remittance
1103
1115
  taxPartyA: '888880',
1104
- taxResultUrl: 'https://yourdomain.com/api/mpesa/tax/result',
1105
- taxQueueTimeOutUrl: 'https://yourdomain.com/api/mpesa/tax/timeout',
1116
+ taxResultUrl: 'https://example.com/api/mpesa/tax/result',
1117
+ taxQueueTimeOutUrl: 'https://example.com/api/mpesa/tax/timeout',
1106
1118
  onTaxRemittanceResult: async (result) => {
1107
1119
  console.log('Tax result:', result.Result.ResultCode)
1108
1120
  },
1109
1121
 
1110
1122
  // B2B Express Checkout
1111
1123
  b2bReceiverShortCode: '000002',
1112
- b2bCallbackUrl: 'https://yourdomain.com/api/mpesa/b2b/callback',
1124
+ b2bCallbackUrl: 'https://example.com/api/mpesa/b2b/callback',
1113
1125
  onB2BCheckoutCallback: async (callback) => {
1114
1126
  console.log('B2B callback:', callback.resultCode)
1115
1127
  },
1116
1128
 
1117
1129
  // B2C Account Top Up
1118
1130
  b2cPartyA: '600979',
1119
- b2cResultUrl: 'https://yourdomain.com/api/mpesa/b2c/result',
1120
- b2cQueueTimeOutUrl: 'https://yourdomain.com/api/mpesa/b2c/timeout',
1131
+ b2cResultUrl: 'https://example.com/api/mpesa/b2c/result',
1132
+ b2cQueueTimeOutUrl: 'https://example.com/api/mpesa/b2c/timeout',
1121
1133
  onB2CResult: async (result) => {
1122
1134
  console.log('B2C result:', result.Result.ResultCode)
1123
1135
  },
@@ -1167,9 +1179,9 @@ createMpesaHonoRouter(app, {
1167
1179
  environment: 'sandbox',
1168
1180
  lipaNaMpesaShortCode: '174379',
1169
1181
  lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
1170
- callbackUrl: 'https://yourdomain.com/mpesa/express/callback',
1171
- resultUrl: 'https://yourdomain.com/mpesa/result',
1172
- queueTimeOutUrl: 'https://yourdomain.com/mpesa/timeout',
1182
+ callbackUrl: 'https://example.com/mpesa/express/callback',
1183
+ resultUrl: 'https://example.com/mpesa/result',
1184
+ queueTimeOutUrl: 'https://example.com/mpesa/timeout',
1173
1185
 
1174
1186
  onStkSuccess: async ({ receiptNumber, amount, phone }) => {
1175
1187
  await db.payments.create({ receiptNumber, amount, phone })
@@ -1298,9 +1310,9 @@ await registerMpesaRoutes(app, {
1298
1310
  environment: 'sandbox',
1299
1311
  lipaNaMpesaShortCode: '174379',
1300
1312
  lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
1301
- callbackUrl: 'https://yourdomain.com/mpesa/callback',
1302
- resultUrl: 'https://yourdomain.com/mpesa/result',
1303
- queueTimeOutUrl: 'https://yourdomain.com/mpesa/timeout',
1313
+ callbackUrl: 'https://example.com/mpesa/callback',
1314
+ resultUrl: 'https://example.com/mpesa/result',
1315
+ queueTimeOutUrl: 'https://example.com/mpesa/timeout',
1304
1316
  skipIPCheck: true,
1305
1317
  onStkSuccess: async ({ receiptNumber, amount, phone }) => {
1306
1318
  app.log.info({ receiptNumber, amount, phone }, 'Payment received')
@@ -1506,6 +1518,16 @@ console.log(mpesa.environment) // "sandbox" | "production"
1506
1518
 
1507
1519
  ---
1508
1520
 
1521
+ ## Security
1522
+
1523
+ - [SECURITY.md](SECURITY.md) — supported versions and vulnerability reporting
1524
+ - [Supply chain & scanners](https://pesafy.vercel.app/guide/supply-chain) — why
1525
+ Socket/Snyk may flag network URLs, Safaricom IPs, and the CLI
1526
+ - Releases are built with npm **provenance**; verify with `pnpm pack --dry-run`
1527
+ before publishing
1528
+
1529
+ ---
1530
+
1509
1531
  ## License
1510
1532
 
1511
1533
  MIT © [Lewis Odero](https://github.com/levos-snr)
@@ -1,85 +1,8 @@
1
- import { A as TransactionStatusResult, D as MpesaConfig, T as AccountBalanceResult, b as B2BExpressCheckoutCallback, f as C2BValidationPayload, g as B2CDisbursementResult, o as ReversalResult, p as C2BValidationResponse, r as TaxRemittanceResult, s as C2BConfirmationPayload, y as B2CResult } from "../types.js";
2
- import { t as Mpesa } from "../index.js";
1
+ import { i as Mpesa, n as StkFailurePayload, r as StkSuccessPayload, t as MpesaAdapterConfig } from "../types.js";
3
2
  import { Router } from "express";
4
3
 
5
4
  //#region src/adapters/express.d.ts
6
- interface MpesaExpressConfig extends MpesaConfig {
7
- /** STK Push callback URL (required) */
8
- callbackUrl: string;
9
- /** Default ResultURL for async APIs (balance, reversal, tx-status, b2c, tax) */
10
- resultUrl?: string;
11
- /** Default QueueTimeOutURL */
12
- queueTimeoutUrl?: string;
13
- /** Skip Safaricom IP whitelist check (local dev only) */
14
- skipIPCheck?: boolean;
15
- /** Shared secret for opt-in HMAC webhook verification */
16
- webhookSecret?: string;
17
- /** Require valid HMAC when webhookSecret is set */
18
- requireHMAC?: boolean;
19
- /** Signature header name (default: x-safaricom-signature) */
20
- signatureHeader?: string;
21
- /** Prefix for all routes — default "" */
22
- routePrefix?: string;
23
- balance?: {
24
- resultUrl?: string;
25
- queueTimeoutUrl?: string;
26
- shortCode?: string;
27
- };
28
- reversal?: {
29
- resultUrl?: string;
30
- queueTimeoutUrl?: string;
31
- };
32
- txStatus?: {
33
- resultUrl?: string;
34
- queueTimeoutUrl?: string;
35
- };
36
- tax?: {
37
- resultUrl?: string;
38
- queueTimeoutUrl?: string;
39
- partyA?: string;
40
- };
41
- b2c?: {
42
- resultUrl?: string;
43
- queueTimeoutUrl?: string;
44
- partyA?: string;
45
- };
46
- c2b?: {
47
- shortCode?: string;
48
- confirmationUrl?: string;
49
- validationUrl?: string;
50
- responseType?: 'Completed' | 'Cancelled';
51
- apiVersion?: 'v1' | 'v2';
52
- };
53
- b2b?: {
54
- receiverShortCode?: string;
55
- callbackUrl?: string;
56
- };
57
- onStkSuccess?: (data: StkSuccessPayload) => Awaitable<void>;
58
- onStkFailure?: (data: StkFailurePayload) => Awaitable<void>;
59
- onC2BValidation?: (payload: C2BValidationPayload) => Awaitable<C2BValidationResponse>;
60
- onC2BConfirmation?: (payload: C2BConfirmationPayload) => Awaitable<void>;
61
- onAccountBalanceResult?: (result: AccountBalanceResult) => Awaitable<void>;
62
- onReversalResult?: (result: ReversalResult) => Awaitable<void>;
63
- onTxStatusResult?: (result: TransactionStatusResult) => Awaitable<void>;
64
- onTaxResult?: (result: TaxRemittanceResult) => Awaitable<void>;
65
- onB2BCheckoutCallback?: (callback: B2BExpressCheckoutCallback) => Awaitable<void>;
66
- onB2CResult?: (result: B2CResult) => Awaitable<void>;
67
- onB2CDisbursementResult?: (result: B2CDisbursementResult) => Awaitable<void>;
68
- }
69
- type Awaitable<T> = T | Promise<T>;
70
- interface StkSuccessPayload {
71
- receiptNumber: string | null;
72
- amount: number | null;
73
- phone: string | null;
74
- checkoutRequestId: string;
75
- merchantRequestId: string;
76
- }
77
- interface StkFailurePayload {
78
- resultCode: number;
79
- resultDesc: string;
80
- checkoutRequestId: string;
81
- merchantRequestId: string;
82
- }
5
+ type MpesaExpressConfig = MpesaAdapterConfig;
83
6
  /**
84
7
  * Creates an Express Router with all M-PESA Daraja routes mounted.
85
8
  *
@@ -93,7 +16,7 @@ interface StkFailurePayload {
93
16
  * consumerKey: process.env.MPESA_CONSUMER_KEY!,
94
17
  * consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
95
18
  * environment: 'sandbox',
96
- * callbackUrl: 'https://yourdomain.com/api/mpesa/stk/callback',
19
+ * callbackUrl: 'https://example.com/api/mpesa/stk/callback',
97
20
  * lipaNaMpesaShortCode: '174379',
98
21
  * lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
99
22
  * }))
@@ -103,4 +26,5 @@ declare function createMpesaExpressClient(config: MpesaExpressConfig): {
103
26
  mpesa: Mpesa;
104
27
  };
105
28
  //#endregion
106
- export { MpesaExpressConfig, StkFailurePayload, StkSuccessPayload, createMpesaExpressClient, createMpesaRouter as createMpesaExpressRouter, createMpesaRouter };
29
+ export { MpesaExpressConfig, type StkFailurePayload, type StkSuccessPayload, createMpesaExpressClient, createMpesaRouter as createMpesaExpressRouter, createMpesaRouter };
30
+ //# sourceMappingURL=express.d.ts.map
@@ -1 +1,127 @@
1
- import{t as e}from"../chunk.js";import{A as t,C as n,D as r,E as i,O as a,S as o,T as s,_ as c,a as l,b as u,c as d,d as f,f as p,g as m,h,i as g,j as _,k as v,l as y,m as b,n as x,o as S,p as C,r as w,s as T,t as E,u as D,v as O,w as k,x as A,y as j}from"../webhook-guard.js";function M(e){return e.headers[`x-forwarded-for`]?.split(`,`)[0]?.trim()??e.ip??``}function N(e,t){if(!e.headersSent){if(t instanceof _){e.status(t.statusCode??400).json({ok:!1,error:t.code,message:t.message});return}e.status(500).json({ok:!1,error:`INTERNAL_ERROR`,message:`Unexpected server error`})}}function P(e,t,n,r){let i=e??t??n??``;if(!i)throw new _({code:`VALIDATION_ERROR`,message:`${r} is required. Set it in config or include it in the request body.`});return i}function F(e,t){e&&e().catch(e=>console.error(`[pesafy] ${t} hook error:`,e))}function I(e){let t=e.rawBody;if(typeof t==`string`)return t;if(t instanceof Buffer)return t.toString(`utf8`);if(e.body!==void 0)return JSON.stringify(e.body)}async function L(e,t){return E(M(e),I(e),t=>{let n=e.headers[t.toLowerCase()];return Array.isArray(n)?n[0]:n},t)}function R(E,M){let{Router:N}=e(`express`),I=M??N(),R=new x(E),B=E.routePrefix??``;return I.post(`${B}/mpesa/stk/push`,z(async(e,t)=>{let{amount:n,phoneNumber:r,accountReference:i,transactionDesc:a,transactionType:o,partyB:s}=e.body;if(!n||n<=0)throw new _({code:`VALIDATION_ERROR`,message:`amount must be > 0`});if(!r)throw new _({code:`VALIDATION_ERROR`,message:`phoneNumber is required`});let c=await R.stkPush({amount:n,phoneNumber:r,callbackUrl:E.callbackUrl,accountReference:i??`REF-${Date.now().toString(36).toUpperCase()}`,transactionDesc:a??`Payment`,...o===void 0?{}:{transactionType:o},...s===void 0?{}:{partyB:s}});t.json({ok:!0,data:c})})),I.post(`${B}/mpesa/stk/query`,z(async(e,t)=>{let{checkoutRequestId:n}=e.body;if(!n)throw new _({code:`VALIDATION_ERROR`,message:`checkoutRequestId is required`});let r=await R.stkQuery({checkoutRequestId:n});t.json({ok:!0,data:r})})),I.post(`${B}/mpesa/stk/callback`,async(e,t)=>{await L(e,E);let n=e.body,r=n?.Body?.stkCallback;if(!r)return t.json({ResultCode:0,ResultDesc:`Accepted`});if(S(n)){let e={receiptNumber:l(n),amount:w(n),phone:g(n),checkoutRequestId:r.CheckoutRequestID,merchantRequestId:r.MerchantRequestID};console.info(`[pesafy] STK success:`,e),F(E.onStkSuccess?()=>Promise.resolve(E.onStkSuccess(e)):void 0,`onStkSuccess`)}else{let e={resultCode:r.ResultCode,resultDesc:r.ResultDesc,checkoutRequestId:r.CheckoutRequestID,merchantRequestId:r.MerchantRequestID};console.warn(`[pesafy] STK failure:`,e),F(E.onStkFailure?()=>Promise.resolve(E.onStkFailure(e)):void 0,`onStkFailure`)}return t.json({ResultCode:0,ResultDesc:`Accepted`})}),I.post(`${B}/mpesa/c2b/register`,z(async(e,t)=>{let{shortCode:n=E.c2b?.shortCode,confirmationUrl:r=E.c2b?.confirmationUrl,validationUrl:i=E.c2b?.validationUrl,responseType:a=E.c2b?.responseType??`Completed`,apiVersion:o=E.c2b?.apiVersion??`v2`}=e.body;if(!n)throw new _({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(!r)throw new _({code:`VALIDATION_ERROR`,message:`confirmationUrl is required`});if(!i)throw new _({code:`VALIDATION_ERROR`,message:`validationUrl is required`});let s=await R.registerC2BUrls({shortCode:n,responseType:a,confirmationUrl:r,validationUrl:i,apiVersion:o});t.json({ok:!0,data:s})})),I.post(`${B}/mpesa/c2b/simulate`,z(async(e,t)=>{let{commandId:n,amount:r,msisdn:i,billRefNumber:a,shortCode:o,apiVersion:s}=e.body;if(!n)throw new _({code:`VALIDATION_ERROR`,message:`commandId is required`});if(!r||r<=0)throw new _({code:`VALIDATION_ERROR`,message:`amount must be > 0`});if(!i)throw new _({code:`VALIDATION_ERROR`,message:`msisdn is required`});let c=await R.simulateC2B({shortCode:o??E.c2b?.shortCode??``,commandId:n,amount:r,msisdn:i,apiVersion:s??E.c2b?.apiVersion??`v2`,...a===void 0?{}:{billRefNumber:a}});t.json({ok:!0,data:c})})),I.post(`${B}/mpesa/c2b/validation`,z(async(e,t)=>{await L(e,E);let n=e.body,r=E.onC2BValidation?await E.onC2BValidation(n):b();t.json(r)})),I.post(`${B}/mpesa/c2b/confirmation`,(e,t)=>{let n=e.body;console.info(`[pesafy] C2B confirmation:`,{transactionId:n.TransID,amount:n.TransAmount,billRef:n.BillRefNumber}),F(E.onC2BConfirmation?()=>Promise.resolve(E.onC2BConfirmation(n)):void 0,`onC2BConfirmation`),t.json({ResultCode:0,ResultDesc:`Success`})}),I.post(`${B}/mpesa/balance/query`,z(async(e,t)=>{let n=e.body,r=await R.accountBalance({partyA:n.partyA??E.balance?.shortCode??``,identifierType:n.identifierType??`4`,resultUrl:P(n.resultUrl,E.balance?.resultUrl,E.resultUrl,`resultUrl`),queueTimeOutUrl:P(n.queueTimeoutUrl,E.balance?.queueTimeoutUrl,E.queueTimeoutUrl,`queueTimeoutUrl`),...n.remarks===void 0?{}:{remarks:n.remarks}});t.json({ok:!0,data:r})})),I.post(`${B}/mpesa/balance/result`,(e,n)=>{let r=e.body;if(!v(r))console.warn(`[pesafy] Account balance failed:`,r);else{let e=a(r);console.info(`[pesafy] Account balance:`,e?t(e):r)}F(E.onAccountBalanceResult?()=>Promise.resolve(E.onAccountBalanceResult(r)):void 0,`onAccountBalanceResult`),n.json({ResultCode:0,ResultDesc:`Accepted`})}),I.post(`${B}/mpesa/qr/generate`,z(async(e,t)=>{let n=await R.generateDynamicQR(e.body);t.json({ok:!0,data:n})})),I.post(`${B}/mpesa/reversal/request`,z(async(e,t)=>{let n=e.body;if(!n.transactionId)throw new _({code:`VALIDATION_ERROR`,message:`transactionId is required`});if(!n.receiverParty)throw new _({code:`VALIDATION_ERROR`,message:`receiverParty is required`});if(!n.amount||n.amount<=0)throw new _({code:`VALIDATION_ERROR`,message:`amount must be > 0`});let r=await R.reverseTransaction({transactionId:n.transactionId,receiverParty:n.receiverParty,amount:n.amount,resultUrl:P(n.resultUrl,E.reversal?.resultUrl,E.resultUrl,`resultUrl`),queueTimeOutUrl:P(n.queueTimeoutUrl,E.reversal?.queueTimeoutUrl,E.queueTimeoutUrl,`queueTimeoutUrl`),...n.remarks===void 0?{}:{remarks:n.remarks},...n.occasion===void 0?{}:{occasion:n.occasion}});t.json({ok:!0,data:r})})),I.post(`${B}/mpesa/reversal/result`,(e,t)=>{let n=e.body;p(n)&&(C(n)?console.info(`[pesafy] Reversal success:`,{txId:f(n)}):console.warn(`[pesafy] Reversal failed:`,n.Result.ResultDesc),F(E.onReversalResult?()=>Promise.resolve(E.onReversalResult(n)):void 0,`onReversalResult`)),t.json({ResultCode:0,ResultDesc:`Accepted`})}),I.post(`${B}/mpesa/tx-status/query`,z(async(e,t)=>{let n=e.body,r=await R.transactionStatus({...n.transactionId===void 0?{}:{transactionId:n.transactionId},...n.originalConversationId===void 0?{}:{originalConversationId:n.originalConversationId},partyA:n.partyA,identifierType:n.identifierType,resultUrl:P(n.resultUrl,E.txStatus?.resultUrl,E.resultUrl,`resultUrl`),queueTimeOutUrl:P(n.queueTimeoutUrl,E.txStatus?.queueTimeoutUrl,E.queueTimeoutUrl,`queueTimeoutUrl`),...n.remarks===void 0?{}:{remarks:n.remarks},...n.occasion===void 0?{}:{occasion:n.occasion}});t.json({ok:!0,data:r})})),I.post(`${B}/mpesa/tx-status/result`,(e,t)=>{let n=e.body;T(n)&&(d(n)?console.info(`[pesafy] Transaction status success:`,n.Result.TransactionID):console.warn(`[pesafy] Transaction status failed:`,n.Result.ResultDesc),F(E.onTxStatusResult?()=>Promise.resolve(E.onTxStatusResult(n)):void 0,`onTxStatusResult`)),t.json({ResultCode:0,ResultDesc:`Accepted`})}),I.post(`${B}/mpesa/tax/remit`,z(async(e,t)=>{let n=e.body;if(!n.amount||n.amount<=0)throw new _({code:`VALIDATION_ERROR`,message:`amount must be > 0`});if(!n.accountReference)throw new _({code:`VALIDATION_ERROR`,message:`accountReference (KRA PRN) is required`});let r=await R.remitTax({amount:n.amount,partyA:n.partyA??E.tax?.partyA??``,accountReference:n.accountReference,resultUrl:P(n.resultUrl,E.tax?.resultUrl,E.resultUrl,`resultUrl`),queueTimeOutUrl:P(n.queueTimeoutUrl,E.tax?.queueTimeoutUrl,E.queueTimeoutUrl,`queueTimeoutUrl`),...n.partyB===void 0?{}:{partyB:n.partyB},...n.remarks===void 0?{}:{remarks:n.remarks}});t.json({ok:!0,data:r})})),I.post(`${B}/mpesa/tax/result`,(e,t)=>{let n=e.body;y(n)&&(D(n)?console.info(`[pesafy] Tax remittance success:`,n.Result.TransactionID):console.warn(`[pesafy] Tax remittance failed:`,n.Result.ResultDesc),F(E.onTaxResult?()=>Promise.resolve(E.onTaxResult(n)):void 0,`onTaxResult`)),t.json({ResultCode:0,ResultDesc:`Accepted`})}),I.post(`${B}/mpesa/b2b/checkout`,z(async(e,t)=>{let n=e.body;if(!n.primaryShortCode)throw new _({code:`VALIDATION_ERROR`,message:`primaryShortCode is required`});if(!n.amount||n.amount<=0)throw new _({code:`VALIDATION_ERROR`,message:`amount must be > 0`});if(!n.paymentRef)throw new _({code:`VALIDATION_ERROR`,message:`paymentRef is required`});if(!n.partnerName)throw new _({code:`VALIDATION_ERROR`,message:`partnerName is required`});let r=n.receiverShortCode??E.b2b?.receiverShortCode??``;if(!r)throw new _({code:`VALIDATION_ERROR`,message:`receiverShortCode is required`});let i=n.callbackUrl??E.b2b?.callbackUrl??``;if(!i)throw new _({code:`VALIDATION_ERROR`,message:`callbackUrl is required`});let a=await R.b2bExpressCheckout({primaryShortCode:n.primaryShortCode,receiverShortCode:r,amount:n.amount,paymentRef:n.paymentRef,callbackUrl:i,partnerName:n.partnerName,...n.requestRefId===void 0?{}:{requestRefId:n.requestRefId}});t.json({ok:!0,data:a})})),I.post(`${B}/mpesa/b2b/callback`,(e,t)=>{let a=e.body;if(!s(a))return console.warn(`[pesafy] Unknown B2B callback payload`),t.json({ResultCode:0,ResultDesc:`Accepted`});let c=a;return r(c)?console.info(`[pesafy] B2B checkout success:`,{txId:k(c),conversationId:n(c),amount:o(c)}):i(c)?console.warn(`[pesafy] B2B checkout cancelled by merchant`):console.warn(`[pesafy] B2B checkout failed:`,c.resultDesc),F(E.onB2BCheckoutCallback?()=>Promise.resolve(E.onB2BCheckoutCallback(c)):void 0,`onB2BCheckoutCallback`),t.json({ResultCode:0,ResultDesc:`Accepted`})}),I.post(`${B}/mpesa/b2c/payment`,z(async(e,t)=>{let n=e.body;if(n.commandId!==`BusinessPayToBulk`)throw new _({code:`VALIDATION_ERROR`,message:`commandId must be "BusinessPayToBulk"`});if(!n.amount||n.amount<=0)throw new _({code:`VALIDATION_ERROR`,message:`amount must be > 0`});if(!n.partyB)throw new _({code:`VALIDATION_ERROR`,message:`partyB is required`});if(!n.accountReference)throw new _({code:`VALIDATION_ERROR`,message:`accountReference is required`});let r=await R.b2cPayment({commandId:`BusinessPayToBulk`,amount:n.amount,partyA:n.partyA??E.b2c?.partyA??``,partyB:n.partyB,accountReference:n.accountReference,resultUrl:P(n.resultUrl,E.b2c?.resultUrl,E.resultUrl,`resultUrl`),queueTimeOutUrl:P(n.queueTimeoutUrl,E.b2c?.queueTimeoutUrl,E.queueTimeoutUrl,`queueTimeoutUrl`),...n.requester===void 0?{}:{requester:n.requester},...n.remarks===void 0?{}:{remarks:n.remarks}});t.json({ok:!0,data:r})})),I.post(`${B}/mpesa/b2c/result`,(e,t)=>{let n=e.body;u(n)&&(A(n)?console.info(`[pesafy] B2C success:`,{txId:j(n),amount:c(n),origConvId:O(n)}):console.warn(`[pesafy] B2C failed:`,n.Result.ResultDesc),F(E.onB2CResult?()=>Promise.resolve(E.onB2CResult(n)):void 0,`onB2CResult`)),t.json({ResultCode:0,ResultDesc:`Accepted`})}),I.post(`${B}/mpesa/b2c/disburse`,z(async(e,t)=>{let n=e.body,r=await R.b2cDisbursement({originatorConversationId:n.originatorConversationId,commandId:n.commandId,amount:n.amount,partyA:n.partyA,partyB:n.partyB,remarks:n.remarks,resultUrl:P(n.resultUrl,E.b2c?.resultUrl,E.resultUrl,`resultUrl`),queueTimeOutUrl:P(n.queueTimeoutUrl,E.b2c?.queueTimeoutUrl,E.queueTimeoutUrl,`queueTimeoutUrl`),...n.occasion===void 0?{}:{occasion:n.occasion}});t.json({ok:!0,data:r})})),I.post(`${B}/mpesa/b2c/disburse/result`,(e,t)=>{let n=e.body;h(n)&&(m(n)?console.info(`[pesafy] B2C disbursement success:`,n.Result.TransactionID):console.warn(`[pesafy] B2C disbursement failed:`,n.Result.ResultDesc),F(E.onB2CDisbursementResult?()=>Promise.resolve(E.onB2CDisbursementResult(n)):void 0,`onB2CDisbursementResult`)),t.json({ResultCode:0,ResultDesc:`Accepted`})}),I.post(`${B}/mpesa/bills/optin`,z(async(e,t)=>{let n=await R.billManagerOptIn(e.body);t.json({ok:!0,data:n})})),I.patch(`${B}/mpesa/bills/optin`,z(async(e,t)=>{let n=await R.updateOptIn(e.body);t.json({ok:!0,data:n})})),I.post(`${B}/mpesa/bills/invoice`,z(async(e,t)=>{let n=await R.sendInvoice(e.body);t.json({ok:!0,data:n})})),I.post(`${B}/mpesa/bills/invoice/bulk`,z(async(e,t)=>{let n=await R.sendBulkInvoices(e.body);t.json({ok:!0,data:n})})),I.delete(`${B}/mpesa/bills/invoice`,z(async(e,t)=>{let n=await R.cancelInvoice(e.body);t.json({ok:!0,data:n})})),I.delete(`${B}/mpesa/bills/invoice/bulk`,z(async(e,t)=>{let n=await R.cancelBulkInvoices(e.body);t.json({ok:!0,data:n})})),I.post(`${B}/mpesa/bills/reconcile`,z(async(e,t)=>{let n=await R.reconcilePayment(e.body);t.json({ok:!0,data:n})})),I.get(`${B}/mpesa/health`,(e,t)=>{t.json({ok:!0,environment:R.environment,ts:new Date().toISOString()})}),I}function z(e){return(t,n,r)=>{e(t,n,r).catch(e=>{n.headersSent?r(e):N(n,e)})}}function B(e){return{mpesa:new x(e)}}export{B as createMpesaExpressClient,R as createMpesaExpressRouter,R as createMpesaRouter};
1
+ import { a as PesafyError, i as Mpesa, n as getRoutePaths, r as createRouteHandlers, t as ROUTE_DEFINITIONS } from "../route-definitions.js";
2
+ import express from "express";
3
+
4
+ //#region src/adapters/shared/mount-express.ts
5
+ /** All paths mounted by this adapter (parity anchor). */
6
+ const EXPRESS_ROUTE_PATHS = getRoutePaths();
7
+ function getIP(req) {
8
+ return req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ?? req.ip ?? "";
9
+ }
10
+ function getRawBody(req) {
11
+ const raw = req.rawBody;
12
+ if (typeof raw === "string") return raw;
13
+ if (raw instanceof Buffer) return raw.toString("utf8");
14
+ if (req.body !== void 0) return JSON.stringify(req.body);
15
+ }
16
+ function sendHandlerResult(res, result, routeId) {
17
+ if (routeId === "health") {
18
+ res.json(result.body);
19
+ return;
20
+ }
21
+ if (result.type === "daraja") {
22
+ res.json(result.body);
23
+ return;
24
+ }
25
+ res.json({
26
+ ok: true,
27
+ data: result.body
28
+ });
29
+ }
30
+ function sendError(res, error) {
31
+ if (res.headersSent) return;
32
+ if (error instanceof PesafyError) {
33
+ res.status(error.statusCode ?? 400).json({
34
+ ok: false,
35
+ error: error.code,
36
+ message: error.message
37
+ });
38
+ return;
39
+ }
40
+ res.status(500).json({
41
+ ok: false,
42
+ error: "INTERNAL_ERROR",
43
+ message: "Unexpected server error"
44
+ });
45
+ }
46
+ function buildContext(req) {
47
+ const rawBody = getRawBody(req);
48
+ return {
49
+ body: req.body,
50
+ ...rawBody !== void 0 ? { rawBody } : {},
51
+ requestIP: getIP(req),
52
+ getHeader: (name) => {
53
+ const v = req.headers[name.toLowerCase()];
54
+ return Array.isArray(v) ? v[0] : v;
55
+ }
56
+ };
57
+ }
58
+ function asyncHandler(fn) {
59
+ return (req, res, next) => {
60
+ fn(req, res, next).catch((err) => {
61
+ if (!res.headersSent) sendError(res, err);
62
+ else next(err);
63
+ });
64
+ };
65
+ }
66
+ /**
67
+ * Mounts all M-PESA routes on an Express Router using shared handlers.
68
+ */
69
+ function mountExpressRoutes(mpesa, config, router) {
70
+ const r = router ?? express.Router();
71
+ const prefix = config.routePrefix ?? "";
72
+ const handlers = createRouteHandlers(mpesa, config);
73
+ const register = (method) => (path, ...handlers) => {
74
+ r[method](path, ...handlers);
75
+ };
76
+ for (const route of ROUTE_DEFINITIONS) {
77
+ const path = `${prefix}${route.path}`;
78
+ const handler = handlers[route.id];
79
+ register(route.method.toLowerCase())(path, asyncHandler(async (req, res) => {
80
+ sendHandlerResult(res, await handler(buildContext(req)), route.id);
81
+ }));
82
+ }
83
+ return r;
84
+ }
85
+
86
+ //#endregion
87
+ //#region src/adapters/express.ts
88
+ /**
89
+ * @file src/adapters/express.ts
90
+ * Express adapter for pesafy — full M-PESA Daraja surface.
91
+ *
92
+ * Usage:
93
+ * import express from 'express'
94
+ * import { createMpesaRouter } from 'pesafy/adapters/express'
95
+ *
96
+ * const app = express()
97
+ * app.use(express.json())
98
+ * app.use(createMpesaRouter(config))
99
+ */
100
+ /**
101
+ * Creates an Express Router with all M-PESA Daraja routes mounted.
102
+ *
103
+ * @example
104
+ * import express from 'express'
105
+ * import { createMpesaRouter } from 'pesafy/adapters/express'
106
+ *
107
+ * const app = express()
108
+ * app.use(express.json())
109
+ * app.use('/api', createMpesaRouter({
110
+ * consumerKey: process.env.MPESA_CONSUMER_KEY!,
111
+ * consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
112
+ * environment: 'sandbox',
113
+ * callbackUrl: 'https://example.com/api/mpesa/stk/callback',
114
+ * lipaNaMpesaShortCode: '174379',
115
+ * lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,
116
+ * }))
117
+ */
118
+ function createMpesaRouter(config, router) {
119
+ return mountExpressRoutes(new Mpesa(config), config, router ?? express.Router());
120
+ }
121
+ function createMpesaExpressClient(config) {
122
+ return { mpesa: new Mpesa(config) };
123
+ }
124
+
125
+ //#endregion
126
+ export { createMpesaExpressClient, createMpesaRouter as createMpesaExpressRouter, createMpesaRouter };
127
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","names":[],"sources":["../../src/adapters/shared/mount-express.ts","../../src/adapters/express.ts"],"sourcesContent":["import express, {\n type NextFunction,\n type Request,\n type RequestHandler,\n type Response,\n type Router,\n} from 'express'\nimport { Mpesa } from '../../mpesa'\nimport { PesafyError } from '../../utils/errors'\nimport { createRouteHandlers } from './handlers'\nimport { ROUTE_DEFINITIONS, getRoutePaths } from './route-definitions'\n\n/** All paths mounted by this adapter (parity anchor). */\nexport const EXPRESS_ROUTE_PATHS = getRoutePaths()\nimport type { HandlerContext, HandlerResult, MpesaAdapterConfig } from './types'\n\nfunction getIP(req: Request): string {\n return (\n (req.headers['x-forwarded-for'] as string | undefined)?.split(',')[0]?.trim() ?? req.ip ?? ''\n )\n}\n\nfunction getRawBody(req: Request): string | undefined {\n const raw = (req as Request & { rawBody?: string | Buffer }).rawBody\n if (typeof raw === 'string') return raw\n if (raw instanceof Buffer) return raw.toString('utf8')\n if (req.body !== undefined) return JSON.stringify(req.body)\n return undefined\n}\n\nfunction sendHandlerResult(res: Response, result: HandlerResult, routeId: string): void {\n if (routeId === 'health') {\n res.json(result.body)\n return\n }\n if (result.type === 'daraja') {\n res.json(result.body)\n return\n }\n res.json({ ok: true, data: result.body })\n}\n\nfunction sendError(res: Response, error: unknown): void {\n if (res.headersSent) return\n if (error instanceof PesafyError) {\n res.status(error.statusCode ?? 400).json({\n ok: false,\n error: error.code,\n message: error.message,\n })\n return\n }\n res.status(500).json({ ok: false, error: 'INTERNAL_ERROR', message: 'Unexpected server error' })\n}\n\nfunction buildContext(req: Request): HandlerContext {\n const rawBody = getRawBody(req)\n return {\n body: req.body,\n ...(rawBody !== undefined ? { rawBody } : {}),\n requestIP: getIP(req),\n getHeader: (name) => {\n const v = req.headers[name.toLowerCase()]\n return Array.isArray(v) ? v[0] : v\n },\n }\n}\n\nfunction asyncHandler(\n fn: (req: Request, res: Response, next: NextFunction) => Promise<void>,\n): RequestHandler {\n return (req, res, next) => {\n fn(req, res, next).catch((err) => {\n if (!res.headersSent) sendError(res, err)\n else next(err)\n })\n }\n}\n\n/**\n * Mounts all M-PESA routes on an Express Router using shared handlers.\n */\nexport function mountExpressRoutes(\n mpesa: Mpesa,\n config: MpesaAdapterConfig,\n router?: Router,\n): Router {\n const r: Router = router ?? express.Router()\n const prefix = config.routePrefix ?? ''\n const handlers = createRouteHandlers(mpesa, config)\n\n const register =\n (method: 'get' | 'post' | 'patch' | 'delete') =>\n (path: string, ...handlers: RequestHandler[]) => {\n r[method](path, ...handlers)\n }\n\n for (const route of ROUTE_DEFINITIONS) {\n const path = `${prefix}${route.path}`\n const handler = handlers[route.id]\n const method = route.method.toLowerCase() as 'get' | 'post' | 'patch' | 'delete'\n\n register(method)(\n path,\n asyncHandler(async (req, res) => {\n const result = await handler(buildContext(req))\n sendHandlerResult(res, result, route.id)\n }),\n )\n }\n\n return r\n}\n","/**\n * @file src/adapters/express.ts\n * Express adapter for pesafy — full M-PESA Daraja surface.\n *\n * Usage:\n * import express from 'express'\n * import { createMpesaRouter } from 'pesafy/adapters/express'\n *\n * const app = express()\n * app.use(express.json())\n * app.use(createMpesaRouter(config))\n */\n\nimport express, { type Router } from 'express'\nimport { Mpesa } from '../mpesa'\nimport { mountExpressRoutes } from './shared/mount-express'\nimport type { MpesaAdapterConfig, StkFailurePayload, StkSuccessPayload } from './shared/types'\n\nexport type MpesaExpressConfig = MpesaAdapterConfig\nexport type { StkSuccessPayload, StkFailurePayload }\n\n/**\n * Creates an Express Router with all M-PESA Daraja routes mounted.\n *\n * @example\n * import express from 'express'\n * import { createMpesaRouter } from 'pesafy/adapters/express'\n *\n * const app = express()\n * app.use(express.json())\n * app.use('/api', createMpesaRouter({\n * consumerKey: process.env.MPESA_CONSUMER_KEY!,\n * consumerSecret: process.env.MPESA_CONSUMER_SECRET!,\n * environment: 'sandbox',\n * callbackUrl: 'https://example.com/api/mpesa/stk/callback',\n * lipaNaMpesaShortCode: '174379',\n * lipaNaMpesaPassKey: process.env.MPESA_PASSKEY!,\n * }))\n */\nexport function createMpesaRouter(config: MpesaExpressConfig, router?: Router): Router {\n const mpesa = new Mpesa(config)\n return mountExpressRoutes(mpesa, config, router ?? express.Router())\n}\n\nexport { createMpesaRouter as createMpesaExpressRouter }\n\nexport function createMpesaExpressClient(config: MpesaExpressConfig) {\n return { mpesa: new Mpesa(config) }\n}\n"],"mappings":";;;;;AAaA,MAAa,sBAAsB,eAAe;AAGlD,SAAS,MAAM,KAAsB;AACnC,QACG,IAAI,QAAQ,oBAA2C,MAAM,IAAI,CAAC,IAAI,MAAM,IAAI,IAAI,MAAM;;AAI/F,SAAS,WAAW,KAAkC;CACpD,MAAM,MAAO,IAAgD;AAC7D,KAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,KAAI,eAAe,OAAQ,QAAO,IAAI,SAAS,OAAO;AACtD,KAAI,IAAI,SAAS,OAAW,QAAO,KAAK,UAAU,IAAI,KAAK;;AAI7D,SAAS,kBAAkB,KAAe,QAAuB,SAAuB;AACtF,KAAI,YAAY,UAAU;AACxB,MAAI,KAAK,OAAO,KAAK;AACrB;;AAEF,KAAI,OAAO,SAAS,UAAU;AAC5B,MAAI,KAAK,OAAO,KAAK;AACrB;;AAEF,KAAI,KAAK;EAAE,IAAI;EAAM,MAAM,OAAO;EAAM,CAAC;;AAG3C,SAAS,UAAU,KAAe,OAAsB;AACtD,KAAI,IAAI,YAAa;AACrB,KAAI,iBAAiB,aAAa;AAChC,MAAI,OAAO,MAAM,cAAc,IAAI,CAAC,KAAK;GACvC,IAAI;GACJ,OAAO,MAAM;GACb,SAAS,MAAM;GAChB,CAAC;AACF;;AAEF,KAAI,OAAO,IAAI,CAAC,KAAK;EAAE,IAAI;EAAO,OAAO;EAAkB,SAAS;EAA2B,CAAC;;AAGlG,SAAS,aAAa,KAA8B;CAClD,MAAM,UAAU,WAAW,IAAI;AAC/B,QAAO;EACL,MAAM,IAAI;EACV,GAAI,YAAY,SAAY,EAAE,SAAS,GAAG,EAAE;EAC5C,WAAW,MAAM,IAAI;EACrB,YAAY,SAAS;GACnB,MAAM,IAAI,IAAI,QAAQ,KAAK,aAAa;AACxC,UAAO,MAAM,QAAQ,EAAE,GAAG,EAAE,KAAK;;EAEpC;;AAGH,SAAS,aACP,IACgB;AAChB,SAAQ,KAAK,KAAK,SAAS;AACzB,KAAG,KAAK,KAAK,KAAK,CAAC,OAAO,QAAQ;AAChC,OAAI,CAAC,IAAI,YAAa,WAAU,KAAK,IAAI;OACpC,MAAK,IAAI;IACd;;;;;;AAON,SAAgB,mBACd,OACA,QACA,QACQ;CACR,MAAM,IAAY,UAAU,QAAQ,QAAQ;CAC5C,MAAM,SAAS,OAAO,eAAe;CACrC,MAAM,WAAW,oBAAoB,OAAO,OAAO;CAEnD,MAAM,YACH,YACA,MAAc,GAAG,aAA+B;AAC/C,IAAE,QAAQ,MAAM,GAAG,SAAS;;AAGhC,MAAK,MAAM,SAAS,mBAAmB;EACrC,MAAM,OAAO,GAAG,SAAS,MAAM;EAC/B,MAAM,UAAU,SAAS,MAAM;AAG/B,WAFe,MAAM,OAAO,aAAa,CAEzB,CACd,MACA,aAAa,OAAO,KAAK,QAAQ;AAE/B,qBAAkB,KADH,MAAM,QAAQ,aAAa,IAAI,CAAC,EAChB,MAAM,GAAG;IACxC,CACH;;AAGH,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACxET,SAAgB,kBAAkB,QAA4B,QAAyB;AAErF,QAAO,mBADO,IAAI,MAAM,OAAO,EACE,QAAQ,UAAU,QAAQ,QAAQ,CAAC;;AAKtE,SAAgB,yBAAyB,QAA4B;AACnE,QAAO,EAAE,OAAO,IAAI,MAAM,OAAO,EAAE"}