pesafy 0.5.0 → 0.5.2

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +100 -16
  2. package/LICENSE +21 -0
  3. package/dist/{express/index.d.cts → adapters/express.d.ts} +23 -138
  4. package/dist/adapters/express.js +1 -0
  5. package/dist/adapters/fastify.d.ts +21 -0
  6. package/dist/adapters/fastify.js +1 -0
  7. package/dist/adapters/hono.d.ts +27 -0
  8. package/dist/adapters/hono.js +1 -0
  9. package/dist/adapters/{nextjs.d.cts → nextjs.d.ts} +3 -30
  10. package/dist/adapters/nextjs.js +1 -0
  11. package/dist/{cli/index.mjs → cli.mjs} +10 -9
  12. package/dist/encryption.mjs +22 -0
  13. package/dist/{cli/errors-DL4bkMZV.mjs → errors.mjs} +2 -1
  14. package/dist/{index.d.mts → index.d.ts} +37 -64
  15. package/dist/index.js +1 -0
  16. package/dist/{cli/phone-5wwAaQ_8.mjs → phone.mjs} +4 -2
  17. package/dist/signature-verifier.js +1 -0
  18. package/dist/{adapters/fastify.d.cts → types.d.ts} +2 -22
  19. package/dist/{cli/utils-BzEKV3nJ.mjs → utils.mjs} +4 -2
  20. package/dist/webhook-handler.js +1 -0
  21. package/package.json +119 -49
  22. package/dist/adapters/fastify.cjs +0 -1607
  23. package/dist/adapters/fastify.cjs.map +0 -1
  24. package/dist/adapters/fastify.d.mts +0 -49
  25. package/dist/adapters/fastify.mjs +0 -1606
  26. package/dist/adapters/fastify.mjs.map +0 -1
  27. package/dist/adapters/hono.cjs +0 -1651
  28. package/dist/adapters/hono.cjs.map +0 -1
  29. package/dist/adapters/hono.d.cts +0 -55
  30. package/dist/adapters/hono.d.mts +0 -55
  31. package/dist/adapters/hono.mjs +0 -1650
  32. package/dist/adapters/hono.mjs.map +0 -1
  33. package/dist/adapters/nextjs.cjs +0 -1655
  34. package/dist/adapters/nextjs.cjs.map +0 -1
  35. package/dist/adapters/nextjs.d.mts +0 -79
  36. package/dist/adapters/nextjs.mjs +0 -1651
  37. package/dist/adapters/nextjs.mjs.map +0 -1
  38. package/dist/cli/encryption-BA-_xrIW.mjs +0 -45
  39. package/dist/cli/encryption-CkSveeYj.cjs +0 -45
  40. package/dist/cli/errors-Bscvlb7X.cjs +0 -45
  41. package/dist/cli/index.cjs +0 -559
  42. package/dist/cli/phone-BD4QmEyl.cjs +0 -21
  43. package/dist/cli/utils-Dg9Gv_D3.cjs +0 -31
  44. package/dist/components/react/index.d.mts +0 -1
  45. package/dist/components/react/index.mjs +0 -1
  46. package/dist/express/index.cjs +0 -2201
  47. package/dist/express/index.cjs.map +0 -1
  48. package/dist/express/index.d.mts +0 -1322
  49. package/dist/express/index.mjs +0 -2199
  50. package/dist/express/index.mjs.map +0 -1
  51. package/dist/index.cjs +0 -2124
  52. package/dist/index.cjs.map +0 -1
  53. package/dist/index.d.cts +0 -1907
  54. package/dist/index.mjs +0 -2050
  55. package/dist/index.mjs.map +0 -1
  56. /package/dist/{components/react/index.d.cts → react/index.d.ts} +0 -0
  57. /package/dist/{components/react/index.cjs → react/index.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # pesafy
2
2
 
3
+ ## 0.5.2
4
+
5
+ ### Patch Changes
6
+
7
+ - migrate from cjs/esm to only esm
8
+
9
+ ## 0.5.1
10
+
11
+ ### Patch Changes
12
+
13
+ - reduce size increase speed
14
+
3
15
  ## 0.5.0
4
16
 
5
17
  ### Minor Changes
@@ -34,14 +46,20 @@
34
46
 
35
47
  ### Patch Changes
36
48
 
37
- - C2B URL registration panel with per-shortcode tracking (Paybill + Buy Goods), dual registration support, and session-persisted status badges"
49
+ - C2B URL registration panel with per-shortcode tracking (Paybill + Buy Goods),
50
+ dual registration support, and session-persisted status badges"
38
51
 
39
52
  ## 0.3.12
40
53
 
41
54
  ### Patch Changes
42
55
 
43
- - 047c30f: The payload was always sending BillRefNumber: request.billRefNumber ?? "". For CustomerBuyGoodsOnline, Daraja treats any BillRefNumber field — even "" — as an invalid AccountReference and rejects the request (400 or 503). The fix conditionally includes the field only for Paybill
44
- - fix error handling and URLs only registered for 600977, never for 600000. The log shows registerC2BUrls shortCode=600977 but then simulateC2B shortCode=600000
56
+ - 047c30f: The payload was always sending BillRefNumber: request.billRefNumber
57
+ ?? "". For CustomerBuyGoodsOnline, Daraja treats any BillRefNumber field
58
+ even "" — as an invalid AccountReference and rejects the request (400 or 503).
59
+ The fix conditionally includes the field only for Paybill
60
+ - fix error handling and URLs only registered for 600977, never for 600000. The
61
+ log shows registerC2BUrls shortCode=600977 but then simulateC2B
62
+ shortCode=600000
45
63
 
46
64
  ## 0.3.11
47
65
 
@@ -53,7 +71,17 @@
53
71
 
54
72
  ### Patch Changes
55
73
 
56
- - Added Dynamic QR Code support to the pesafy SDK as a new mpesa/dynamic-qr module. This introduces three new files — types.ts, generate.ts, and index.ts — under src/mpesa/dynamic-qr/, implementing the Safaricom Daraja POST /mpesa/qrcode/v1/generate endpoint. The Mpesa class gains a new generateDynamicQR() method that handles token retrieval and delegates to the internal generateDynamicQR function, which validates all inputs (merchant name, reference, amount, transaction code, CPI, and size) before dispatching the request via the existing httpRequest utility. Three new public types — DynamicQRRequest, DynamicQRResponse, and QRTransactionCode — are exported from the root src/index.ts. No breaking changes were introduced; all existing APIs remain untouched.
74
+ - Added Dynamic QR Code support to the pesafy SDK as a new mpesa/dynamic-qr
75
+ module. This introduces three new files — types.ts, generate.ts, and index.ts
76
+ — under src/mpesa/dynamic-qr/, implementing the Safaricom Daraja POST
77
+ /mpesa/qrcode/v1/generate endpoint. The Mpesa class gains a new
78
+ generateDynamicQR() method that handles token retrieval and delegates to the
79
+ internal generateDynamicQR function, which validates all inputs (merchant
80
+ name, reference, amount, transaction code, CPI, and size) before dispatching
81
+ the request via the existing httpRequest utility. Three new public types —
82
+ DynamicQRRequest, DynamicQRResponse, and QRTransactionCode — are exported from
83
+ the root src/index.ts. No breaking changes were introduced; all existing APIs
84
+ remain untouched.
57
85
 
58
86
  ## 0.3.9
59
87
 
@@ -65,7 +93,15 @@
65
93
 
66
94
  ### Patch Changes
67
95
 
68
- - Fixed four blocking TypeScript compilation errors before prepublish by extending the `ErrorCode` union to include `API_ERROR`, `NETWORK_ERROR`, and `TIMEOUT`, which resolved type assignment issues in the HTTP layer. Consolidated a duplicate `PesafyError` class (previously split between `index.ts` and `types.ts`) into a single authoritative implementation that preserves all fields, including `requestId`, `toJSON()`, and stack trace handling. Also corrected `x-forwarded-for` IP extraction in the Express webhook handler using safe optional chaining to prevent potential undefined access from the `split()` result.
96
+ - Fixed four blocking TypeScript compilation errors before prepublish by
97
+ extending the `ErrorCode` union to include `API_ERROR`, `NETWORK_ERROR`, and
98
+ `TIMEOUT`, which resolved type assignment issues in the HTTP layer.
99
+ Consolidated a duplicate `PesafyError` class (previously split between
100
+ `index.ts` and `types.ts`) into a single authoritative implementation that
101
+ preserves all fields, including `requestId`, `toJSON()`, and stack trace
102
+ handling. Also corrected `x-forwarded-for` IP extraction in the Express
103
+ webhook handler using safe optional chaining to prevent potential undefined
104
+ access from the `split()` result.
69
105
 
70
106
  ## 0.3.7
71
107
 
@@ -89,7 +125,9 @@
89
125
 
90
126
  ### Patch Changes
91
127
 
92
- - f983f64: The /v2/simulate path is the most critical bug — it will cause persistent 500.003.1001 errors in sandbox just like the /v2/registerurl issue you already fixed.
128
+ - f983f64: The /v2/simulate path is the most critical bug — it will cause
129
+ persistent 500.003.1001 errors in sandbox just like the /v2/registerurl issue
130
+ you already fixed.
93
131
 
94
132
  ## 0.3.3
95
133
 
@@ -107,21 +145,67 @@
107
145
 
108
146
  ### Patch Changes
109
147
 
110
- - **C2B module rewrite & phone utility refactor** — Fixed `simulateC2B` where `CommandID` was hardcoded to `CustomerPayBillOnline`, making `CustomerBuyGoodsOnline` (Till) payments impossible. Corrected `BillRefNumber` logic so the field is omitted entirely for BuyGoods requests rather than defaulting to the string `"default"`, which caused a `400` from Daraja. Fixed `Msisdn` being sent as a quoted string instead of a JSON number as the spec requires. Removed the phantom `ConversationID` field from both `C2BSimulateResponse` and `C2BRegisterUrlResponse` — Daraja never returns this field for C2B, only `OriginatorCoversationID`. Extracted phone formatting into a shared `src/utils/phone.ts` module with two intentionally separate formatters: `formatSafaricomPhone` (strict, Safaricom/Airtel only, for STK Push) and `formatKenyanMsisdn` (permissive, all Kenyan networks, for C2B and B2C), eliminating duplicated logic across modules. Added the `C2BCommandId` union type and full callback payload types — `C2BValidationPayload`, `C2BConfirmationPayload`, and `C2BCallbackPayload` — to enable type-safe webhook handlers.
148
+ - **C2B module rewrite & phone utility refactor** — Fixed `simulateC2B` where
149
+ `CommandID` was hardcoded to `CustomerPayBillOnline`, making
150
+ `CustomerBuyGoodsOnline` (Till) payments impossible. Corrected `BillRefNumber`
151
+ logic so the field is omitted entirely for BuyGoods requests rather than
152
+ defaulting to the string `"default"`, which caused a `400` from Daraja. Fixed
153
+ `Msisdn` being sent as a quoted string instead of a JSON number as the spec
154
+ requires. Removed the phantom `ConversationID` field from both
155
+ `C2BSimulateResponse` and `C2BRegisterUrlResponse` — Daraja never returns this
156
+ field for C2B, only `OriginatorCoversationID`. Extracted phone formatting into
157
+ a shared `src/utils/phone.ts` module with two intentionally separate
158
+ formatters: `formatSafaricomPhone` (strict, Safaricom/Airtel only, for STK
159
+ Push) and `formatKenyanMsisdn` (permissive, all Kenyan networks, for C2B and
160
+ B2C), eliminating duplicated logic across modules. Added the `C2BCommandId`
161
+ union type and full callback payload types — `C2BValidationPayload`,
162
+ `C2BConfirmationPayload`, and `C2BCallbackPayload` — to enable type-safe
163
+ webhook handlers.
111
164
  - fbfa311: fix url patch and add elemt accountref
112
- - aa9c176: **C2B module rewrite & phone utility refactor** — Fixed `simulateC2B` where `CommandID` was hardcoded to `CustomerPayBillOnline`, making `CustomerBuyGoodsOnline` (Till) payments impossible. Corrected `BillRefNumber` logic so the field is omitted entirely for BuyGoods requests rather than defaulting to the string `"default"`, which caused a `400` from Daraja. Fixed `Msisdn` being sent as a quoted string instead of a JSON number as the spec requires. Removed the phantom `ConversationID` field from both `C2BSimulateResponse` and `C2BRegisterUrlResponse` — Daraja never returns this field for C2B, only `OriginatorCoversationID`. Extracted phone formatting into a shared `src/utils/phone.ts` module with two intentionally separate formatters: `formatSafaricomPhone` (strict, Safaricom/Airtel only, for STK Push) and `formatKenyanMsisdn` (permissive, all Kenyan networks, for C2B and B2C), eliminating duplicated logic across modules. Added the `C2BCommandId` union type and full callback payload types — `C2BValidationPayload`, `C2BConfirmationPayload`, and `C2BCallbackPayload` — to enable type-safe webhook handlers.
165
+ - aa9c176: **C2B module rewrite & phone utility refactor** — Fixed `simulateC2B`
166
+ where `CommandID` was hardcoded to `CustomerPayBillOnline`, making
167
+ `CustomerBuyGoodsOnline` (Till) payments impossible. Corrected `BillRefNumber`
168
+ logic so the field is omitted entirely for BuyGoods requests rather than
169
+ defaulting to the string `"default"`, which caused a `400` from Daraja. Fixed
170
+ `Msisdn` being sent as a quoted string instead of a JSON number as the spec
171
+ requires. Removed the phantom `ConversationID` field from both
172
+ `C2BSimulateResponse` and `C2BRegisterUrlResponse` — Daraja never returns this
173
+ field for C2B, only `OriginatorCoversationID`. Extracted phone formatting into
174
+ a shared `src/utils/phone.ts` module with two intentionally separate
175
+ formatters: `formatSafaricomPhone` (strict, Safaricom/Airtel only, for STK
176
+ Push) and `formatKenyanMsisdn` (permissive, all Kenyan networks, for C2B and
177
+ B2C), eliminating duplicated logic across modules. Added the `C2BCommandId`
178
+ union type and full callback payload types — `C2BValidationPayload`,
179
+ `C2BConfirmationPayload`, and `C2BCallbackPayload` — to enable type-safe
180
+ webhook handlers.
113
181
 
114
182
  ## 0.3.0
115
183
 
116
184
  ### Minor Changes
117
185
 
118
- - fe95c2b: The C2B M-Pesa implementation in the pesafy package has been comprehensively overhauled for full Daraja C2B v2 compliance, including a complete rewrite of types.ts with all 13 callback fields (C2BCallbackPayload), validation responses, rejection codes, and command/response unions; fixes to register-url.ts ensuring ValidationURL is always sent (defaulting to confirmationUrl); enhanced simulate.ts supporting both CustomerPayBillOnline and CustomerBuyGoodsOnline with proper BillRefNumber handling, amount validation, and Msisdn casting; new c2b index exports; root index public exports; and improved http client error reporting. Dashboard updates add c2bRegisterUrls and c2bSimulate actions in mpesaActions.ts, fix http.ts webhook handlers with 6 missing fields (TransactionType, TransTime, etc.), upsert logic via createTransaction, a new getByShortCode query in businesses.ts, and a complete C2B tab in PaymentsPage.tsx featuring URL registration, simulation forms, and unified transactions—addressing prior gaps like hardcoded commands, missing UI, incomplete types, and non-upserting handlers.
186
+ - fe95c2b: The C2B M-Pesa implementation in the pesafy package has been
187
+ comprehensively overhauled for full Daraja C2B v2 compliance, including a
188
+ complete rewrite of types.ts with all 13 callback fields (C2BCallbackPayload),
189
+ validation responses, rejection codes, and command/response unions; fixes to
190
+ register-url.ts ensuring ValidationURL is always sent (defaulting to
191
+ confirmationUrl); enhanced simulate.ts supporting both CustomerPayBillOnline
192
+ and CustomerBuyGoodsOnline with proper BillRefNumber handling, amount
193
+ validation, and Msisdn casting; new c2b index exports; root index public
194
+ exports; and improved http client error reporting. Dashboard updates add
195
+ c2bRegisterUrls and c2bSimulate actions in mpesaActions.ts, fix http.ts
196
+ webhook handlers with 6 missing fields (TransactionType, TransTime, etc.),
197
+ upsert logic via createTransaction, a new getByShortCode query in
198
+ businesses.ts, and a complete C2B tab in PaymentsPage.tsx featuring URL
199
+ registration, simulation forms, and unified transactions—addressing prior gaps
200
+ like hardcoded commands, missing UI, incomplete types, and non-upserting
201
+ handlers.
119
202
 
120
203
  ## 0.2.4
121
204
 
122
205
  ### Patch Changes
123
206
 
124
- - 4a535ef: Request failed with status 500 but swallows the actual Daraja error body — it parses it into data but never includes it in the error message.
207
+ - 4a535ef: Request failed with status 500 but swallows the actual Daraja error
208
+ body — it parses it into data but never includes it in the error message.
125
209
 
126
210
  ## 0.2.3
127
211
 
@@ -146,14 +230,14 @@
146
230
  ### Minor Changes
147
231
 
148
232
  - Add M-Pesa Express (STK Push) support
149
- - Add `processStkPush` to initiate STK Push payment prompts on a
150
- customer's phone via POST /mpesa/stkpush/v1/processrequest
151
- - Add `queryStkPush` to check the status of an STK Push transaction
152
- via POST /mpesa/stkpushquery/v1/query
233
+ - Add `processStkPush` to initiate STK Push payment prompts on a customer's
234
+ phone via POST /mpesa/stkpush/v1/processrequest
235
+ - Add `queryStkPush` to check the status of an STK Push transaction via POST
236
+ /mpesa/stkpushquery/v1/query
153
237
  - Add `StkPushRequest`, `StkPushResponse`, `StkQueryRequest`,
154
238
  `StkQueryResponse` and `TransactionType` types
155
- - Add `formatPhoneNumber` utility to normalize Kenyan phone numbers
156
- to 254 format
239
+ - Add `formatPhoneNumber` utility to normalize Kenyan phone numbers to 254
240
+ format
157
241
  - Add `getStkPushPassword` to generate Base64(Shortcode+Passkey+Timestamp)
158
242
  - Add `getTimestamp` helper for Daraja-formatted timestamps
159
243
  - Export all STK Push types and utilities from the package root
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lewis Odero
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,3 +1,4 @@
1
+ import { n as MpesaConfig, t as Environment } from "../types.js";
1
2
  import { Router } from "express";
2
3
 
3
4
  //#region src/mpesa/stk-push/types.d.ts
@@ -13,7 +14,7 @@ import { Router } from "express";
13
14
  * CustomerPayBillOnline → Paybill numbers (PartyB = shortcode)
14
15
  * CustomerBuyGoodsOnline → Till numbers (PartyB = till number)
15
16
  */
16
- type TransactionType = "CustomerPayBillOnline" | "CustomerBuyGoodsOnline";
17
+ type TransactionType = 'CustomerPayBillOnline' | 'CustomerBuyGoodsOnline';
17
18
  interface StkPushRequest {
18
19
  /** Transaction amount (minimum KES 1, must round to a whole number ≥ 1) */
19
20
  amount: number;
@@ -138,7 +139,7 @@ interface TransactionStatusRequest {
138
139
  * "4" = Organisation ShortCode (Paybill / B2C) ← most common
139
140
  * Daraja field: IdentifierType
140
141
  */
141
- identifierType: "1" | "2" | "4";
142
+ identifierType: '1' | '2' | '4';
142
143
  /**
143
144
  * URL where Safaricom POSTs the final result.
144
145
  * Must be publicly accessible.
@@ -168,34 +169,6 @@ interface TransactionStatusResponse {
168
169
  OriginatorConversationID?: string;
169
170
  }
170
171
  //#endregion
171
- //#region src/mpesa/types.d.ts
172
- /**
173
- * Core M-Pesa / Daraja configuration types
174
- */
175
- type Environment = "sandbox" | "production";
176
- interface MpesaConfig {
177
- consumerKey: string;
178
- consumerSecret: string;
179
- environment: Environment;
180
- lipaNaMpesaShortCode?: string;
181
- lipaNaMpesaPassKey?: string;
182
- initiatorName?: string;
183
- initiatorPassword?: string;
184
- certificatePath?: string;
185
- certificatePem?: string;
186
- /**
187
- * Pre-computed base64 SecurityCredential — skips RSA encryption.
188
- * Use when you encrypt at startup outside the library.
189
- */
190
- securityCredential?: string;
191
- /** Override default retry count (4) for all API calls */
192
- retries?: number;
193
- /** Override default base retry delay in ms (2000) */
194
- retryDelay?: number;
195
- /** Override default per-request timeout in ms (30000) */
196
- timeout?: number;
197
- }
198
- //#endregion
199
172
  //#region src/utils/errors/index.d.ts
200
173
  /**
201
174
  * Pesafy error types — single source of truth.
@@ -288,7 +261,7 @@ interface AccountBalanceRequest {
288
261
  * "4" = Organisation ShortCode (most common — Paybill/B2C)
289
262
  * Daraja field: IdentifierType
290
263
  */
291
- identifierType: "1" | "2" | "4";
264
+ identifierType: '1' | '2' | '4';
292
265
  /**
293
266
  * URL where Safaricom POSTs the balance result.
294
267
  * Must be publicly accessible. HTTPS required in production.
@@ -447,7 +420,7 @@ type B2BExpressCheckoutCallback = B2BExpressCheckoutCallbackSuccess | B2BExpress
447
420
  * PromotionPayment — Payment of promotions/bonuses
448
421
  * BusinessPayToBulk — Load funds to a B2C shortcode for bulk disbursement
449
422
  */
450
- type B2CCommandID = "BusinessPayment" | "SalaryPayment" | "PromotionPayment" | "BusinessPayToBulk";
423
+ type B2CCommandID = 'BusinessPayment' | 'SalaryPayment' | 'PromotionPayment' | 'BusinessPayToBulk';
451
424
  interface B2CRequest {
452
425
  /**
453
426
  * The type of transaction. Use "BusinessPayToBulk" for account top-up.
@@ -485,14 +458,14 @@ interface B2CRequest {
485
458
  * Daraja field: SenderIdentifierType
486
459
  * Default: "4"
487
460
  */
488
- senderIdentifierType?: "4";
461
+ senderIdentifierType?: '4';
489
462
  /**
490
463
  * Type of the receiver (PartyB) identifier.
491
464
  * For this API, only "4" (Organisation ShortCode) is allowed.
492
465
  * Daraja field: RecieverIdentifierType
493
466
  * Default: "4"
494
467
  */
495
- receiverIdentifierType?: "4";
468
+ receiverIdentifierType?: '4';
496
469
  /**
497
470
  * A reference for this transaction (e.g. invoice number, batch reference).
498
471
  * Daraja field: AccountReference
@@ -540,7 +513,7 @@ interface B2CResponse {
540
513
  * - Any unknown future key Daraja may return is still accepted.
541
514
  * - The `no-redundant-type-constituents` ESLint rule is not triggered.
542
515
  */
543
- type B2CResultParameterKey = "DebitAccountBalance" | "Amount" | "DebitPartyAffectedAccountBalance" | "TransCompletedTime" | "DebitPartyCharges" | "ReceiverPartyPublicName" | "Currency" | "InitiatorAccountCurrentBalance" | "B2CRecipientIsRegisteredCustomer" | "B2CChargesPaidAccountAvailableFunds" | "B2CWorkingAccountAvailableFunds" | "B2CUtilityAccountAvailableFunds" | (string & {});
516
+ type B2CResultParameterKey = 'DebitAccountBalance' | 'Amount' | 'DebitPartyAffectedAccountBalance' | 'TransCompletedTime' | 'DebitPartyCharges' | 'ReceiverPartyPublicName' | 'Currency' | 'InitiatorAccountCurrentBalance' | 'B2CRecipientIsRegisteredCustomer' | 'B2CChargesPaidAccountAvailableFunds' | 'B2CWorkingAccountAvailableFunds' | 'B2CUtilityAccountAvailableFunds' | (string & {});
544
517
  interface B2CResultParameter {
545
518
  Key: B2CResultParameterKey;
546
519
  Value: string | number;
@@ -592,7 +565,7 @@ interface BillManagerOptInRequest {
592
565
  /** Your logo URL (public HTTPS link) */
593
566
  officialContact: string;
594
567
  /** Sender name shown on push notifications */
595
- sendReminders: "1" | "0";
568
+ sendReminders: '1' | '0';
596
569
  /** Logo URL */
597
570
  logo?: string;
598
571
  /** Callback URL for payment confirmations */
@@ -660,7 +633,7 @@ interface BillManagerCancelInvoiceResponse {
660
633
  * Ref: Customer To Business (C2B) — Daraja Developer Portal
661
634
  */
662
635
  /** C2B API version. v2 is recommended; v1 sends SHA256-hashed MSISDN. */
663
- type C2BApiVersion = "v1" | "v2";
636
+ type C2BApiVersion = 'v1' | 'v2';
664
637
  /**
665
638
  * What M-PESA should do if your Validation URL is unreachable or times out.
666
639
  * "Completed" — M-PESA automatically completes the transaction.
@@ -669,7 +642,7 @@ type C2BApiVersion = "v1" | "v2";
669
642
  * NOTE: Must be exactly "Completed" or "Cancelled" (sentence case, no typos).
670
643
  * Daraja docs: "the words Cancelled/Completed must be in sentence case and well-spelled."
671
644
  */
672
- type C2BResponseType = "Completed" | "Cancelled";
645
+ type C2BResponseType = 'Completed' | 'Cancelled';
673
646
  interface C2BRegisterUrlRequest {
674
647
  /**
675
648
  * Your M-PESA Paybill or Till shortcode.
@@ -716,7 +689,7 @@ interface C2BRegisterUrlResponse {
716
689
  *
717
690
  * NOTE: Simulation is ONLY supported in Sandbox, NOT in production.
718
691
  */
719
- type C2BCommandID = "CustomerPayBillOnline" | "CustomerBuyGoodsOnline";
692
+ type C2BCommandID = 'CustomerPayBillOnline' | 'CustomerBuyGoodsOnline';
720
693
  interface C2BSimulateRequest {
721
694
  /**
722
695
  * Your M-PESA Paybill or Till shortcode.
@@ -808,7 +781,7 @@ interface C2BValidationPayload {
808
781
  * "C2B00015" — Invalid Short code
809
782
  * "C2B00016" — Other Error
810
783
  */
811
- type C2BValidationResultCode = "0" | "C2B00011" | "C2B00012" | "C2B00013" | "C2B00014" | "C2B00015" | "C2B00016";
784
+ type C2BValidationResultCode = '0' | 'C2B00011' | 'C2B00012' | 'C2B00013' | 'C2B00014' | 'C2B00015' | 'C2B00016';
812
785
  interface C2BValidationResponse {
813
786
  /**
814
787
  * "0" = Accept the transaction.
@@ -822,7 +795,7 @@ interface C2BValidationResponse {
822
795
  * "Accepted" when ResultCode is "0".
823
796
  * "Rejected" when ResultCode is a non-zero error code.
824
797
  */
825
- ResultDesc: "Accepted" | "Rejected";
798
+ ResultDesc: 'Accepted' | 'Rejected';
826
799
  /**
827
800
  * Optional. If set, this value is echoed back in the Confirmation callback
828
801
  * as ThirdPartyTransID. Useful for correlating validation → confirmation.
@@ -883,7 +856,7 @@ interface C2BConfirmationPayload {
883
856
  * SM: Send Money (Mobile number)
884
857
  * SB: Sent to Business (Business number CPI in MSISDN format)
885
858
  */
886
- type QRTransactionCode = "BG" | "WA" | "PB" | "SM" | "SB";
859
+ type QRTransactionCode = 'BG' | 'WA' | 'PB' | 'SM' | 'SB';
887
860
  interface DynamicQRRequest {
888
861
  /**
889
862
  * Name of the Company / M-Pesa Merchant Name.
@@ -975,7 +948,7 @@ interface ReversalRequest {
975
948
  * "4" = Organisation ShortCode
976
949
  * Daraja field: RecieverIdentifierType
977
950
  */
978
- receiverIdentifierType: "1" | "2" | "4";
951
+ receiverIdentifierType: '1' | '2' | '4';
979
952
  /**
980
953
  * URL where Safaricom POSTs the reversal result.
981
954
  * Daraja field: ResultURL
@@ -1077,7 +1050,7 @@ interface TaxRemittanceResponse {
1077
1050
  * - Any unknown future key Daraja may return is still accepted.
1078
1051
  * - The `no-redundant-type-constituents` ESLint rule is not triggered.
1079
1052
  */
1080
- type TaxRemittanceResultParameterKey = "DebitAccountBalance" | "Amount" | "DebitPartyAffectedAccountBalance" | "TransCompletedTime" | "DebitPartyCharges" | "ReceiverPartyPublicName" | "Currency" | "InitiatorAccountCurrentBalance" | (string & {});
1053
+ type TaxRemittanceResultParameterKey = 'DebitAccountBalance' | 'Amount' | 'DebitPartyAffectedAccountBalance' | 'TransCompletedTime' | 'DebitPartyCharges' | 'ReceiverPartyPublicName' | 'Currency' | 'InitiatorAccountCurrentBalance' | (string & {});
1081
1054
  interface TaxRemittanceResultParameter {
1082
1055
  Key: TaxRemittanceResultParameterKey;
1083
1056
  Value: string | number;
@@ -1118,9 +1091,9 @@ declare class Mpesa {
1118
1091
  * Like stkPush() but returns Result<T> instead of throwing.
1119
1092
  * Ideal for application-level code that prefers not to use try/catch.
1120
1093
  */
1121
- stkPushSafe(request: Omit<StkPushRequest, "shortCode" | "passKey">): Promise<Result<Awaited<ReturnType<typeof this.stkPush>>>>;
1122
- stkPush(request: Omit<StkPushRequest, "shortCode" | "passKey">): Promise<StkPushResponse>;
1123
- stkQuery(request: Omit<StkQueryRequest, "shortCode" | "passKey">): Promise<StkQueryResponse>;
1094
+ stkPushSafe(request: Omit<StkPushRequest, 'shortCode' | 'passKey'>): Promise<Result<Awaited<ReturnType<typeof this.stkPush>>>>;
1095
+ stkPush(request: Omit<StkPushRequest, 'shortCode' | 'passKey'>): Promise<StkPushResponse>;
1096
+ stkQuery(request: Omit<StkQueryRequest, 'shortCode' | 'passKey'>): Promise<StkQueryResponse>;
1124
1097
  transactionStatus(request: TransactionStatusRequest): Promise<TransactionStatusResponse>;
1125
1098
  /**
1126
1099
  * Queries the balance of your M-PESA shortcode.
@@ -1203,25 +1176,14 @@ declare class Mpesa {
1203
1176
  //#endregion
1204
1177
  //#region src/express/index.d.ts
1205
1178
  interface MpesaExpressConfig extends MpesaConfig {
1206
- /**
1207
- * Full public URL Safaricom will POST STK Push callbacks to.
1208
- * @example "https://yourdomain.com/api/mpesa/express/callback"
1209
- */
1210
1179
  callbackUrl: string;
1211
- /**
1212
- * Full public URL Safaricom will POST Transaction Status results to.
1213
- * Required when using transactionStatus routes.
1214
- */
1215
1180
  resultUrl?: string;
1216
- /**
1217
- * Full public URL Safaricom calls on queue timeout.
1218
- */
1219
1181
  queueTimeOutUrl?: string;
1220
1182
  c2bShortCode?: string;
1221
1183
  c2bConfirmationUrl?: string;
1222
1184
  c2bValidationUrl?: string;
1223
- c2bResponseType?: "Completed" | "Cancelled";
1224
- c2bApiVersion?: "v1" | "v2";
1185
+ c2bResponseType?: 'Completed' | 'Cancelled';
1186
+ c2bApiVersion?: 'v1' | 'v2';
1225
1187
  onC2BValidation?: (payload: C2BValidationPayload) => Promise<C2BValidationResponse> | C2BValidationResponse;
1226
1188
  onC2BConfirmation?: (payload: C2BConfirmationPayload) => Promise<void> | void;
1227
1189
  taxPartyA?: string;
@@ -1231,92 +1193,15 @@ interface MpesaExpressConfig extends MpesaConfig {
1231
1193
  b2bReceiverShortCode?: string;
1232
1194
  b2bCallbackUrl?: string;
1233
1195
  onB2BCheckoutCallback?: (callback: B2BExpressCheckoutCallback) => Promise<void> | void;
1234
- /**
1235
- * Your business shortcode from which B2C money is deducted.
1236
- * Used as the default partyA for B2C payments when not in the request body.
1237
- */
1238
1196
  b2cPartyA?: string;
1239
- /**
1240
- * Full public URL Safaricom POSTs B2C results to.
1241
- * Required when using B2C routes.
1242
- * @example "https://yourdomain.com/api/mpesa/b2c/result"
1243
- */
1244
1197
  b2cResultUrl?: string;
1245
- /**
1246
- * Full public URL Safaricom calls on B2C queue timeout.
1247
- * Required when using B2C routes.
1248
- * @example "https://yourdomain.com/api/mpesa/b2c/timeout"
1249
- */
1250
1198
  b2cQueueTimeOutUrl?: string;
1251
- /**
1252
- * Optional hook called when a B2C result arrives at the result URL.
1253
- * Called for BOTH successful and failed results.
1254
- *
1255
- * Fire-and-forget — 200 response to Safaricom is sent immediately.
1256
- * Errors in this hook are logged but do NOT affect the response.
1257
- *
1258
- * @example
1259
- * onB2CResult: async (result) => {
1260
- * if (isB2CSuccess(result)) {
1261
- * await db.disbursements.markCompleted({
1262
- * transactionId: getB2CTransactionId(result),
1263
- * amount: getB2CAmount(result),
1264
- * });
1265
- * }
1266
- * }
1267
- */
1268
1199
  onB2CResult?: (result: B2CResult) => Promise<void> | void;
1269
- /**
1270
- * Skip Safaricom IP verification on callback routes.
1271
- * ONLY set true in local development — never in production.
1272
- */
1273
1200
  skipIPCheck?: boolean;
1274
1201
  }
1275
1202
  declare function createMpesaExpressClient(config: MpesaExpressConfig): {
1276
1203
  mpesa: Mpesa;
1277
1204
  };
1278
- /**
1279
- * Attaches all M-Pesa routes to the given Express Router.
1280
- *
1281
- * @example
1282
- * import express from "express";
1283
- * import {
1284
- * createMpesaExpressRouter,
1285
- * acceptC2BValidation,
1286
- * rejectC2BValidation,
1287
- * isB2CSuccess,
1288
- * getB2CTransactionId,
1289
- * getB2CAmount,
1290
- * } from "pesafy/express";
1291
- *
1292
- * const router = express.Router();
1293
- * createMpesaExpressRouter(router, {
1294
- * consumerKey: process.env.MPESA_CONSUMER_KEY!,
1295
- * consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
1296
- * environment: "sandbox",
1297
- * lipaNaMpesaShortCode: "174379",
1298
- * lipaNaMpesaPassKey: "bfb279...",
1299
- * callbackUrl: "https://yourdomain.com/mpesa/express/callback",
1300
- * initiatorName: "testapi",
1301
- * initiatorPassword: "Safaricom123!",
1302
- * certificatePath: "./SandboxCertificate.cer",
1303
- * // B2C
1304
- * b2cPartyA: "600979",
1305
- * b2cResultUrl: "https://yourdomain.com/mpesa/b2c/result",
1306
- * b2cQueueTimeOutUrl: "https://yourdomain.com/mpesa/b2c/timeout",
1307
- * onB2CResult: async (result) => {
1308
- * if (isB2CSuccess(result)) {
1309
- * await db.disbursements.markCompleted({
1310
- * transactionId: getB2CTransactionId(result),
1311
- * amount: getB2CAmount(result),
1312
- * });
1313
- * }
1314
- * },
1315
- * skipIPCheck: true, // local dev only
1316
- * });
1317
- * app.use("/api", router);
1318
- */
1319
1205
  declare function createMpesaExpressRouter(router: Router, config: MpesaExpressConfig): Router;
1320
1206
  //#endregion
1321
- export { MpesaExpressConfig, createMpesaExpressClient, createMpesaExpressRouter };
1322
- //# sourceMappingURL=index.d.cts.map
1207
+ export { MpesaExpressConfig, createMpesaExpressClient, createMpesaExpressRouter };
@@ -0,0 +1 @@
1
+ import{i as e,n as t,r as n}from"../signature-verifier.js";import{a as r,i,n as a,r as o,t as s}from"../webhook-handler.js";function c(e){return e.resultCode===`0`}function l(e){return e.resultCode===`4001`}function u(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.resultCode==`string`&&typeof t.requestId==`string`&&typeof t.amount==`string`}function d(e){return c(e)?e.transactionId??null:null}function f(e){return Number(e.amount)}function p(e){return c(e)?e.conversationID??null:null}function m(e){if(!e||typeof e!=`object`)return!1;let t=e;if(!t.Result||typeof t.Result!=`object`)return!1;let n=t.Result;return typeof n.ResultCode==`number`&&typeof n.ConversationID==`string`}function h(e){return e.Result.ResultCode===0}function g(e){return e.Result.TransactionID??null}function _(e){return e.Result.ConversationID}function v(e){return e.Result.OriginatorConversationID}function y(e){let t=b(e,`Amount`);return t===void 0?null:Number(t)}function b(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}function x(e){return{ResultCode:`0`,ResultDesc:`Accepted`,...e?{ThirdPartyTransID:e}:{}}}function S(t){if(!t.consumerKey||!t.consumerSecret)throw new e({code:`INVALID_CREDENTIALS`,message:`consumerKey and consumerSecret are required`});if(!t.lipaNaMpesaShortCode||!t.lipaNaMpesaPassKey)throw new e({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push`});if(!t.callbackUrl)throw new e({code:`VALIDATION_ERROR`,message:`callbackUrl is required for STK Push callbacks`});return{mpesa:new n(t)}}function C(t,n){if(n instanceof e){let e=n.statusCode??400;t.status(e).json({error:n.code,message:n.message,statusCode:e});return}t.status(500).json({error:`REQUEST_FAILED`,message:`An unexpected error occurred while processing the M-Pesa request`})}function w(e){return e.headers[`x-forwarded-for`]?.split(`,`)[0]?.trim()??e.ip??``}function T(n,b){let{mpesa:T}=S(b);return n.post(`/mpesa/express/stk-push`,async(t,n,r)=>{try{let r=t.body;if(!r||typeof r.amount!=`number`||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.phoneNumber)throw new e({code:`VALIDATION_ERROR`,message:`phoneNumber is required`});let i=await T.stkPush({amount:r.amount,phoneNumber:r.phoneNumber,callbackUrl:b.callbackUrl,accountReference:r.accountReference??`PESAFY-${Date.now().toString(36).toUpperCase()}`,transactionDesc:r.transactionDesc??`Payment`,...r.transactionType===void 0?{}:{transactionType:r.transactionType},...r.partyB===void 0?{}:{partyB:r.partyB}});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/express/stk-query`,async(t,n,r)=>{try{let r=t.body;if(!r?.checkoutRequestId)throw new e({code:`VALIDATION_ERROR`,message:`checkoutRequestId is required`});let i=await T.stkQuery({checkoutRequestId:r.checkoutRequestId});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/express/callback`,(e,t)=>{let n=w(e),c=i(e.body,{requestIP:n,...b.skipIPCheck===void 0?{}:{skipIPCheck:b.skipIPCheck}});if(!c.success)return console.error(`[pesafy] STK Push webhook rejected:`,c.error),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`});let l=c.data;return r(l)?console.info(`[pesafy] STK Push success:`,{receiptNumber:o(l),amount:s(l),phone:a(l)}):console.warn(`[pesafy] STK Push failed:`,{resultCode:l.Body.stkCallback.ResultCode,resultDesc:l.Body.stkCallback.ResultDesc}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/transaction-status/query`,async(t,n,r)=>{try{if(!b.resultUrl||!b.queueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`resultUrl and queueTimeOutUrl must be set in config to use transaction status routes`});let r=t.body;if(!r?.transactionId)throw new e({code:`VALIDATION_ERROR`,message:`transactionId is required`});if(!r.partyA)throw new e({code:`VALIDATION_ERROR`,message:`partyA is required`});if(!r.identifierType)throw new e({code:`VALIDATION_ERROR`,message:`identifierType is required: "1" | "2" | "4"`});let i=await T.transactionStatus({transactionId:r.transactionId,partyA:r.partyA,identifierType:r.identifierType,resultUrl:b.resultUrl,queueTimeOutUrl:b.queueTimeOutUrl,...r.remarks===void 0?{}:{remarks:r.remarks},...r.occasion===void 0?{}:{occasion:r.occasion}});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/transaction-status/result`,(e,t)=>{let n=e.body?.Result;n&&(n.ResultCode===0?console.info(`[pesafy] Transaction Status result (success):`,{transactionId:n.TransactionID,conversationId:n.ConversationID,resultDesc:n.ResultDesc}):console.warn(`[pesafy] Transaction Status result (failed):`,{resultCode:n.ResultCode,resultDesc:n.ResultDesc,transactionId:n.TransactionID})),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/c2b/register-url`,async(t,n,r)=>{try{let r=t.body,i=r.shortCode??b.c2bShortCode,a=r.confirmationUrl??b.c2bConfirmationUrl,o=r.validationUrl??b.c2bValidationUrl,s=r.responseType??b.c2bResponseType??`Completed`,c=r.apiVersion??b.c2bApiVersion??`v2`;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(!a)throw new e({code:`VALIDATION_ERROR`,message:`confirmationUrl is required`});if(!o)throw new e({code:`VALIDATION_ERROR`,message:`validationUrl is required`});let l=await T.registerC2BUrls({shortCode:i,responseType:s,confirmationUrl:a,validationUrl:o,apiVersion:c});n.status(200).json(l)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/c2b/simulate`,async(t,n,r)=>{try{let r=t.body;if(!r?.commandId)throw new e({code:`VALIDATION_ERROR`,message:`commandId is required: "CustomerPayBillOnline" | "CustomerBuyGoodsOnline"`});if(!r.amount||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.msisdn)throw new e({code:`VALIDATION_ERROR`,message:`msisdn is required`});let i=await T.simulateC2B({shortCode:r.shortCode??b.c2bShortCode??``,commandId:r.commandId,amount:r.amount,msisdn:r.msisdn,apiVersion:b.c2bApiVersion??`v2`,...r.billRefNumber===void 0?{}:{billRefNumber:r.billRefNumber}});n.status(200).json(i)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/c2b/validation`,async(e,n)=>{if(!b.skipIPCheck){let r=w(e);if(!t(r))return console.error(`[pesafy] C2B validation rejected — IP not in Safaricom whitelist:`,r),n.status(200).json({ResultCode:`0`,ResultDesc:`Accepted`})}let r=e.body;try{let e;return e=b.onC2BValidation?await b.onC2BValidation(r):x(),console.info(`[pesafy] C2B validation response:`,{transactionId:r.TransID,amount:r.TransAmount,billRef:r.BillRefNumber,resultCode:e.ResultCode}),n.status(200).json(e)}catch(e){return console.error(`[pesafy] C2B validation hook threw an error:`,e),n.status(200).json({ResultCode:`0`,ResultDesc:`Accepted`})}}),n.post(`/mpesa/c2b/confirmation`,(e,t)=>{let n=e.body;console.info(`[pesafy] C2B confirmation received:`,{transactionId:n.TransID,amount:n.TransAmount,shortCode:n.BusinessShortCode,billRef:n.BillRefNumber,transactionType:n.TransactionType,transTime:n.TransTime,balance:n.OrgAccountBalance}),b.onC2BConfirmation&&Promise.resolve(b.onC2BConfirmation(n)).catch(e=>{console.error(`[pesafy] C2B confirmation hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Success`})}),n.post(`/mpesa/tax/remit`,async(t,n,r)=>{try{if(!b.taxResultUrl||!b.taxQueueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`taxResultUrl and taxQueueTimeOutUrl must be set in config to use tax remittance routes`});let r=t.body;if(!r||typeof r.amount!=`number`||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.accountReference)throw new e({code:`VALIDATION_ERROR`,message:`accountReference is required — the KRA PRN`});let i=r.partyA??b.taxPartyA??``;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`partyA is required — set taxPartyA in config or provide in request body`});let a=await T.remitTax({amount:r.amount,partyA:i,accountReference:r.accountReference,resultUrl:b.taxResultUrl,queueTimeOutUrl:b.taxQueueTimeOutUrl,...r.remarks===void 0?{}:{remarks:r.remarks}});n.status(200).json(a)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/tax/result`,(e,t)=>{let n=e.body,r=n?.Result;r&&(r.ResultCode===0?console.info(`[pesafy] Tax Remittance result (success):`,{transactionId:r.TransactionID,conversationId:r.ConversationID,resultDesc:r.ResultDesc}):console.warn(`[pesafy] Tax Remittance result (failed):`,{resultCode:r.ResultCode,resultDesc:r.ResultDesc,transactionId:r.TransactionID})),b.onTaxRemittanceResult&&n&&Promise.resolve(b.onTaxRemittanceResult(n)).catch(e=>{console.error(`[pesafy] Tax Remittance result hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/b2b/checkout`,async(t,n,r)=>{try{let r=t.body;if(!r?.primaryShortCode)throw new e({code:`VALIDATION_ERROR`,message:`primaryShortCode is required`});if(!r.amount||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.paymentRef)throw new e({code:`VALIDATION_ERROR`,message:`paymentRef is required`});if(!r.partnerName)throw new e({code:`VALIDATION_ERROR`,message:`partnerName is required`});let i=r.receiverShortCode??b.b2bReceiverShortCode??``;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`receiverShortCode is required — set b2bReceiverShortCode in config or provide in request body`});let a=r.callbackUrl??b.b2bCallbackUrl??``;if(!a)throw new e({code:`VALIDATION_ERROR`,message:`callbackUrl is required — set b2bCallbackUrl in config or provide in request body`});let o=await T.b2bExpressCheckout({primaryShortCode:r.primaryShortCode,receiverShortCode:i,amount:r.amount,paymentRef:r.paymentRef,callbackUrl:a,partnerName:r.partnerName,...r.requestRefId===void 0?{}:{requestRefId:r.requestRefId}});n.status(200).json(o)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/b2b/callback`,(e,t)=>{let n=e.body;if(!u(n))return console.error(`[pesafy] B2B callback received unrecognised payload:`,JSON.stringify(n).slice(0,200)),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`});let r=n;return c(r)?console.info(`[pesafy] B2B Express Checkout success:`,{transactionId:d(r),conversationId:p(r),amount:f(r),requestId:r.requestId,status:r.status}):l(r)?console.warn(`[pesafy] B2B Express Checkout cancelled by merchant:`,{resultCode:r.resultCode,resultDesc:r.resultDesc,requestId:r.requestId,amount:f(r)}):console.warn(`[pesafy] B2B Express Checkout unknown result:`,{resultCode:r.resultCode,resultDesc:r.resultDesc}),b.onB2BCheckoutCallback&&Promise.resolve(b.onB2BCheckoutCallback(r)).catch(e=>{console.error(`[pesafy] B2B callback hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n.post(`/mpesa/b2c/payment`,async(t,n,r)=>{try{if(!b.b2cResultUrl||!b.b2cQueueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`b2cResultUrl and b2cQueueTimeOutUrl must be set in config to use B2C routes`});let r=t.body;if(!r?.commandId)throw new e({code:`VALIDATION_ERROR`,message:`commandId is required: "BusinessPayToBulk" | "BusinessPayment" | "SalaryPayment" | "PromotionPayment"`});if(!r.amount||r.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be a positive number`});if(!r.partyB)throw new e({code:`VALIDATION_ERROR`,message:`partyB is required — the recipient shortcode (BusinessPayToBulk) or customer MSISDN`});if(!r.accountReference)throw new e({code:`VALIDATION_ERROR`,message:`accountReference is required`});let i=r.partyA??b.b2cPartyA??``;if(!i)throw new e({code:`VALIDATION_ERROR`,message:`partyA is required — set b2cPartyA in config or provide in request body`});let a=await T.b2cPayment({commandId:r.commandId,amount:r.amount,partyA:i,partyB:r.partyB,accountReference:r.accountReference,resultUrl:r.resultUrl??b.b2cResultUrl,queueTimeOutUrl:r.queueTimeOutUrl??b.b2cQueueTimeOutUrl,...r.requester===void 0?{}:{requester:r.requester},...r.remarks===void 0?{}:{remarks:r.remarks}});n.status(200).json(a)}catch(e){if(n.headersSent)return r(e);C(n,e)}}),n.post(`/mpesa/b2c/result`,(e,t)=>{let n=e.body;if(!m(n))return console.error(`[pesafy] B2C result received unrecognised payload:`,JSON.stringify(n).slice(0,200)),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`});let r=n;return h(r)?console.info(`[pesafy] B2C payment result (success):`,{transactionId:g(r),conversationId:_(r),originatorConversationId:v(r),amount:y(r),resultDesc:r.Result.ResultDesc}):console.warn(`[pesafy] B2C payment result (failed):`,{resultCode:r.Result.ResultCode,resultDesc:r.Result.ResultDesc,transactionId:r.Result.TransactionID,conversationId:_(r),originatorConversationId:v(r)}),b.onB2CResult&&Promise.resolve(b.onB2CResult(r)).catch(e=>{console.error(`[pesafy] B2C result hook error:`,e)}),t.status(200).json({ResultCode:0,ResultDesc:`Accepted`})}),n}export{S as createMpesaExpressClient,T as createMpesaExpressRouter};
@@ -0,0 +1,21 @@
1
+ import { n as MpesaConfig } from "../types.js";
2
+ import { FastifyInstance } from "fastify";
3
+
4
+ //#region src/adapters/fastify.d.ts
5
+ interface MpesaFastifyConfig extends MpesaConfig {
6
+ callbackUrl: string;
7
+ resultUrl?: string;
8
+ queueTimeOutUrl?: string;
9
+ skipIPCheck?: boolean;
10
+ onStkSuccess?: (data: {
11
+ receiptNumber: string | null;
12
+ amount: number | null;
13
+ phone: string | null;
14
+ }) => Promise<void> | void;
15
+ }
16
+ /**
17
+ * Registers all M-PESA Fastify routes.
18
+ */
19
+ declare function registerMpesaRoutes(app: FastifyInstance, config: MpesaFastifyConfig): Promise<void>;
20
+ //#endregion
21
+ export { MpesaFastifyConfig, registerMpesaRoutes };
@@ -0,0 +1 @@
1
+ import{i as e,n as t,r as n}from"../signature-verifier.js";function r(e){let t=e.headers[`x-forwarded-for`];return typeof t==`string`?t.split(`,`)[0]?.trim()??``:e.ip??``}function i(t,n){n instanceof e?t.status(n.statusCode??400).send({error:n.code,message:n.message}):t.status(500).send({error:`INTERNAL_ERROR`})}async function a(a,o){let s=new n(o);a.post(`/mpesa/stk-push`,async(t,n)=>{try{let n=t.body;if(!n.amount||n.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be > 0`});if(!n.phoneNumber)throw new e({code:`VALIDATION_ERROR`,message:`phoneNumber is required`});return await s.stkPush({amount:n.amount,phoneNumber:n.phoneNumber,callbackUrl:o.callbackUrl,accountReference:n.accountReference??`REF-${Date.now().toString(36).toUpperCase()}`,transactionDesc:n.transactionDesc??`Payment`,...n.partyB===void 0?{}:{partyB:n.partyB}})}catch(e){i(n,e);return}}),a.post(`/mpesa/stk-query`,async(t,n)=>{try{let{checkoutRequestId:n}=t.body;if(!n)throw new e({code:`VALIDATION_ERROR`,message:`checkoutRequestId is required`});return await s.stkQuery({checkoutRequestId:n})}catch(e){i(n,e);return}}),a.post(`/mpesa/callback`,async(e,n)=>{if(!o.skipIPCheck){let n=r(e);n&&!t(n)&&e.log.warn({ip:n},`[pesafy/fastify] Callback from unknown IP`)}let i=e.body?.Body?.stkCallback;if(i&&i.ResultCode===0){let t=i.CallbackMetadata?.Item??[],n=e=>t.find(t=>t.Name===e)?.Value??null;o.onStkSuccess&&Promise.resolve(o.onStkSuccess({receiptNumber:n(`MpesaReceiptNumber`),amount:n(`Amount`),phone:n(`PhoneNumber`)})).catch(e.log.error)}return{ResultCode:0,ResultDesc:`Accepted`}}),a.post(`/mpesa/balance`,async(t,n)=>{try{if(!o.resultUrl||!o.queueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`resultUrl and queueTimeOutUrl must be set in config`});return await s.accountBalance({...t.body,resultUrl:o.resultUrl,queueTimeOutUrl:o.queueTimeOutUrl})}catch(e){i(n,e);return}}),a.post(`/mpesa/balance/result`,e=>(e.log.info(e.body,`[pesafy/fastify] Balance result`),{ResultCode:0,ResultDesc:`Accepted`})),a.post(`/mpesa/reversal`,async(t,n)=>{try{if(!o.resultUrl||!o.queueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`resultUrl and queueTimeOutUrl must be set in config`});return await s.reverseTransaction({...t.body,resultUrl:o.resultUrl,queueTimeOutUrl:o.queueTimeOutUrl})}catch(e){i(n,e);return}}),a.post(`/mpesa/reversal/result`,e=>(e.log.info(e.body,`[pesafy/fastify] Reversal result`),{ResultCode:0,ResultDesc:`Accepted`}))}export{a as registerMpesaRoutes};
@@ -0,0 +1,27 @@
1
+ import { n as MpesaConfig } from "../types.js";
2
+ import { Hono } from "hono";
3
+
4
+ //#region src/adapters/hono.d.ts
5
+ interface MpesaHonoConfig extends MpesaConfig {
6
+ callbackUrl: string;
7
+ resultUrl?: string;
8
+ queueTimeOutUrl?: string;
9
+ skipIPCheck?: boolean;
10
+ onStkSuccess?: (data: {
11
+ receiptNumber: string | null;
12
+ amount: number | null;
13
+ phone: string | null;
14
+ }) => Promise<void> | void;
15
+ onStkFailure?: (data: {
16
+ resultCode: number;
17
+ resultDesc: string;
18
+ }) => Promise<void> | void;
19
+ onAccountBalanceResult?: (body: unknown) => Promise<void> | void;
20
+ onReversalResult?: (body: unknown) => Promise<void> | void;
21
+ }
22
+ /**
23
+ * Mounts M-PESA routes onto a Hono app instance.
24
+ */
25
+ declare function createMpesaHonoRouter(app: Hono, config: MpesaHonoConfig): void;
26
+ //#endregion
27
+ export { type MpesaConfig, MpesaHonoConfig, createMpesaHonoRouter };
@@ -0,0 +1 @@
1
+ import{i as e,n as t,r as n}from"../signature-verifier.js";import{a as r,n as i,r as a,t as o}from"../webhook-handler.js";function s(e){return e.req.header(`x-forwarded-for`)?.split(`,`)[0]?.trim()??e.req.header(`cf-connecting-ip`)??e.req.header(`x-real-ip`)??``}function c(t,n){if(n instanceof e){let e=n.statusCode??400;return t.json({error:n.code,message:n.message},e)}return t.json({error:`INTERNAL_ERROR`,message:`An unexpected error occurred`},500)}function l(l,u){let d=new n(u);l.post(`/mpesa/express/stk-push`,async t=>{try{let n=await t.req.json();if(!n.amount||n.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be > 0`});if(!n.phoneNumber)throw new e({code:`VALIDATION_ERROR`,message:`phoneNumber is required`});let r=await d.stkPush({amount:n.amount,phoneNumber:n.phoneNumber,callbackUrl:u.callbackUrl,accountReference:n.accountReference??`REF-${Date.now().toString(36).toUpperCase()}`,transactionDesc:n.transactionDesc??`Payment`,...n.partyB===void 0?{}:{partyB:n.partyB}});return t.json(r)}catch(e){return c(t,e)}}),l.post(`/mpesa/express/stk-query`,async t=>{try{let{checkoutRequestId:n}=await t.req.json();if(!n)throw new e({code:`VALIDATION_ERROR`,message:`checkoutRequestId is required`});return t.json(await d.stkQuery({checkoutRequestId:n}))}catch(e){return c(t,e)}}),l.post(`/mpesa/express/callback`,async e=>{if(!u.skipIPCheck){let n=s(e);n&&!t(n)&&console.warn(`[pesafy/hono] STK callback from unknown IP:`,n)}let n=await e.req.json(),c=n?.Body?.stkCallback;if(!c)return e.json({ResultCode:0,ResultDesc:`Accepted`});if(r(n)){let e={receiptNumber:a(n),amount:o(n),phone:i(n)};console.info(`[pesafy/hono] STK success:`,e),u.onStkSuccess&&Promise.resolve(u.onStkSuccess(e)).catch(console.error)}else{let e={resultCode:c.ResultCode,resultDesc:c.ResultDesc};console.warn(`[pesafy/hono] STK failed:`,e),u.onStkFailure&&Promise.resolve(u.onStkFailure(e)).catch(console.error)}return e.json({ResultCode:0,ResultDesc:`Accepted`})}),l.post(`/mpesa/balance/query`,async t=>{try{if(!u.resultUrl||!u.queueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`resultUrl and queueTimeOutUrl are required in config for balance queries`});let n=await t.req.json();return t.json(await d.accountBalance({...n,resultUrl:u.resultUrl,queueTimeOutUrl:u.queueTimeOutUrl}))}catch(e){return c(t,e)}}),l.post(`/mpesa/balance/result`,async e=>{let t=await e.req.json();return u.onAccountBalanceResult&&Promise.resolve(u.onAccountBalanceResult(t)).catch(console.error),console.info(`[pesafy/hono] Account balance result:`,JSON.stringify(t).slice(0,300)),e.json({ResultCode:0,ResultDesc:`Accepted`})}),l.post(`/mpesa/reversal/request`,async t=>{try{if(!u.resultUrl||!u.queueTimeOutUrl)throw new e({code:`VALIDATION_ERROR`,message:`resultUrl and queueTimeOutUrl are required in config for reversals`});let n={...await t.req.json(),resultUrl:u.resultUrl,queueTimeOutUrl:u.queueTimeOutUrl};return t.json(await d.reverseTransaction(n))}catch(e){return c(t,e)}}),l.post(`/mpesa/reversal/result`,async e=>{let t=await e.req.json();return u.onReversalResult&&Promise.resolve(u.onReversalResult(t)).catch(console.error),console.info(`[pesafy/hono] Reversal result:`,JSON.stringify(t).slice(0,300)),e.json({ResultCode:0,ResultDesc:`Accepted`})})}export{l as createMpesaHonoRouter};