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 +39 -0
- package/README.md +62 -40
- package/dist/adapters/express.d.ts +5 -81
- package/dist/adapters/express.js +127 -1
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/fastify.d.ts +10 -74
- package/dist/adapters/fastify.js +85 -1
- package/dist/adapters/fastify.js.map +1 -0
- package/dist/adapters/hono.d.ts +4 -88
- package/dist/adapters/hono.js +105 -1
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/nextjs.d.ts +28 -176
- package/dist/adapters/nextjs.js +166 -1
- package/dist/adapters/nextjs.js.map +1 -0
- package/dist/cli.mjs +2387 -112
- package/dist/cli.mjs.map +1 -0
- package/dist/errors.mjs +6 -1
- package/dist/errors.mjs.map +1 -0
- package/dist/index.d.ts +4864 -136
- package/dist/index.js +4197 -1
- package/dist/index.js.map +1 -0
- package/dist/phone.mjs +5 -1
- package/dist/phone.mjs.map +1 -0
- package/dist/rolldown-runtime.mjs +18 -0
- package/dist/route-definitions.js +3292 -0
- package/dist/route-definitions.js.map +1 -0
- package/dist/types.d.ts +924 -5
- package/package.json +6 -1
- package/dist/chunk.js +0 -1
- package/dist/encryption.mjs +0 -22
- package/dist/utils.mjs +0 -32
- package/dist/webhook-guard.js +0 -1
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://
|
|
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://
|
|
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://
|
|
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://
|
|
253
|
-
validationUrl: 'https://
|
|
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://
|
|
339
|
-
queueTimeOutUrl: 'https://
|
|
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://
|
|
395
|
-
queueTimeOutUrl: 'https://
|
|
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://
|
|
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://
|
|
524
|
-
queueTimeOutUrl: 'https://
|
|
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://
|
|
576
|
-
queueTimeOutUrl: 'https://
|
|
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://
|
|
642
|
-
queueTimeOutUrl:'https://
|
|
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://
|
|
696
|
-
queueTimeOutUrl: 'https://
|
|
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://
|
|
749
|
-
queueTimeOutUrl: 'https://
|
|
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://
|
|
821
|
-
queueTimeOutUrl: 'https://
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
1082
|
-
queueTimeOutUrl: 'https://
|
|
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://
|
|
1087
|
-
c2bValidationUrl: 'https://
|
|
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://
|
|
1105
|
-
taxQueueTimeOutUrl: 'https://
|
|
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://
|
|
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://
|
|
1120
|
-
b2cQueueTimeOutUrl: 'https://
|
|
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://
|
|
1171
|
-
resultUrl: 'https://
|
|
1172
|
-
queueTimeOutUrl: 'https://
|
|
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://
|
|
1302
|
-
resultUrl: 'https://
|
|
1303
|
-
queueTimeOutUrl: 'https://
|
|
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 {
|
|
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
|
-
|
|
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://
|
|
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
|
package/dist/adapters/express.js
CHANGED
|
@@ -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"}
|