pesafy 0.5.1 → 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.
- package/CHANGELOG.md +94 -16
- package/dist/adapters/express.d.ts +0 -87
- package/dist/adapters/express.js +1 -1
- package/dist/adapters/fastify.js +1 -1
- package/dist/adapters/hono.js +1 -1
- package/dist/adapters/nextjs.js +1 -1
- package/dist/{cli.js → cli.mjs} +7 -7
- package/dist/{encryption.js → encryption.mjs} +1 -1
- package/dist/index.js +1 -1
- package/dist/{phone.js → phone.mjs} +1 -1
- package/dist/signature-verifier.js +1 -1
- package/dist/{utils.js → utils.mjs} +1 -1
- package/package.json +79 -54
- package/dist/adapters/express.cjs +0 -1
- package/dist/adapters/express.d.cts +0 -1294
- package/dist/adapters/fastify.cjs +0 -1
- package/dist/adapters/fastify.d.cts +0 -21
- package/dist/adapters/hono.cjs +0 -1
- package/dist/adapters/hono.d.cts +0 -27
- package/dist/adapters/nextjs.cjs +0 -1
- package/dist/adapters/nextjs.d.cts +0 -52
- package/dist/index.cjs +0 -1
- package/dist/index.d.cts +0 -1880
- package/dist/signature-verifier.cjs +0 -1
- package/dist/types.d.cts +0 -29
- package/dist/webhook-handler.cjs +0 -1
- /package/dist/{errors.js → errors.mjs} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# pesafy
|
|
2
2
|
|
|
3
|
+
## 0.5.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- migrate from cjs/esm to only esm
|
|
8
|
+
|
|
3
9
|
## 0.5.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -40,14 +46,20 @@
|
|
|
40
46
|
|
|
41
47
|
### Patch Changes
|
|
42
48
|
|
|
43
|
-
- C2B URL registration panel with per-shortcode tracking (Paybill + Buy Goods),
|
|
49
|
+
- C2B URL registration panel with per-shortcode tracking (Paybill + Buy Goods),
|
|
50
|
+
dual registration support, and session-persisted status badges"
|
|
44
51
|
|
|
45
52
|
## 0.3.12
|
|
46
53
|
|
|
47
54
|
### Patch Changes
|
|
48
55
|
|
|
49
|
-
- 047c30f: The payload was always sending BillRefNumber: request.billRefNumber
|
|
50
|
-
|
|
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
|
|
51
63
|
|
|
52
64
|
## 0.3.11
|
|
53
65
|
|
|
@@ -59,7 +71,17 @@
|
|
|
59
71
|
|
|
60
72
|
### Patch Changes
|
|
61
73
|
|
|
62
|
-
- Added Dynamic QR Code support to the pesafy SDK as a new mpesa/dynamic-qr
|
|
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.
|
|
63
85
|
|
|
64
86
|
## 0.3.9
|
|
65
87
|
|
|
@@ -71,7 +93,15 @@
|
|
|
71
93
|
|
|
72
94
|
### Patch Changes
|
|
73
95
|
|
|
74
|
-
- Fixed four blocking TypeScript compilation errors before prepublish by
|
|
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.
|
|
75
105
|
|
|
76
106
|
## 0.3.7
|
|
77
107
|
|
|
@@ -95,7 +125,9 @@
|
|
|
95
125
|
|
|
96
126
|
### Patch Changes
|
|
97
127
|
|
|
98
|
-
- f983f64: The /v2/simulate path is the most critical bug — it will cause
|
|
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.
|
|
99
131
|
|
|
100
132
|
## 0.3.3
|
|
101
133
|
|
|
@@ -113,21 +145,67 @@
|
|
|
113
145
|
|
|
114
146
|
### Patch Changes
|
|
115
147
|
|
|
116
|
-
- **C2B module rewrite & phone utility refactor** — Fixed `simulateC2B` where
|
|
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.
|
|
117
164
|
- fbfa311: fix url patch and add elemt accountref
|
|
118
|
-
- aa9c176: **C2B module rewrite & phone utility refactor** — Fixed `simulateC2B`
|
|
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.
|
|
119
181
|
|
|
120
182
|
## 0.3.0
|
|
121
183
|
|
|
122
184
|
### Minor Changes
|
|
123
185
|
|
|
124
|
-
- fe95c2b: The C2B M-Pesa implementation in the pesafy package has been
|
|
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.
|
|
125
202
|
|
|
126
203
|
## 0.2.4
|
|
127
204
|
|
|
128
205
|
### Patch Changes
|
|
129
206
|
|
|
130
|
-
- 4a535ef: Request failed with status 500 but swallows the actual Daraja error
|
|
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.
|
|
131
209
|
|
|
132
210
|
## 0.2.3
|
|
133
211
|
|
|
@@ -152,14 +230,14 @@
|
|
|
152
230
|
### Minor Changes
|
|
153
231
|
|
|
154
232
|
- Add M-Pesa Express (STK Push) support
|
|
155
|
-
- Add `processStkPush` to initiate STK Push payment prompts on a
|
|
156
|
-
|
|
157
|
-
- Add `queryStkPush` to check the status of an STK Push transaction
|
|
158
|
-
|
|
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
|
|
159
237
|
- Add `StkPushRequest`, `StkPushResponse`, `StkQueryRequest`,
|
|
160
238
|
`StkQueryResponse` and `TransactionType` types
|
|
161
|
-
- Add `formatPhoneNumber` utility to normalize Kenyan phone numbers
|
|
162
|
-
|
|
239
|
+
- Add `formatPhoneNumber` utility to normalize Kenyan phone numbers to 254
|
|
240
|
+
format
|
|
163
241
|
- Add `getStkPushPassword` to generate Base64(Shortcode+Passkey+Timestamp)
|
|
164
242
|
- Add `getTimestamp` helper for Daraja-formatted timestamps
|
|
165
243
|
- Export all STK Push types and utilities from the package root
|
|
@@ -1176,19 +1176,8 @@ declare class Mpesa {
|
|
|
1176
1176
|
//#endregion
|
|
1177
1177
|
//#region src/express/index.d.ts
|
|
1178
1178
|
interface MpesaExpressConfig extends MpesaConfig {
|
|
1179
|
-
/**
|
|
1180
|
-
* Full public URL Safaricom will POST STK Push callbacks to.
|
|
1181
|
-
* @example "https://yourdomain.com/api/mpesa/express/callback"
|
|
1182
|
-
*/
|
|
1183
1179
|
callbackUrl: string;
|
|
1184
|
-
/**
|
|
1185
|
-
* Full public URL Safaricom will POST Transaction Status results to.
|
|
1186
|
-
* Required when using transactionStatus routes.
|
|
1187
|
-
*/
|
|
1188
1180
|
resultUrl?: string;
|
|
1189
|
-
/**
|
|
1190
|
-
* Full public URL Safaricom calls on queue timeout.
|
|
1191
|
-
*/
|
|
1192
1181
|
queueTimeOutUrl?: string;
|
|
1193
1182
|
c2bShortCode?: string;
|
|
1194
1183
|
c2bConfirmationUrl?: string;
|
|
@@ -1204,91 +1193,15 @@ interface MpesaExpressConfig extends MpesaConfig {
|
|
|
1204
1193
|
b2bReceiverShortCode?: string;
|
|
1205
1194
|
b2bCallbackUrl?: string;
|
|
1206
1195
|
onB2BCheckoutCallback?: (callback: B2BExpressCheckoutCallback) => Promise<void> | void;
|
|
1207
|
-
/**
|
|
1208
|
-
* Your business shortcode from which B2C money is deducted.
|
|
1209
|
-
* Used as the default partyA for B2C payments when not in the request body.
|
|
1210
|
-
*/
|
|
1211
1196
|
b2cPartyA?: string;
|
|
1212
|
-
/**
|
|
1213
|
-
* Full public URL Safaricom POSTs B2C results to.
|
|
1214
|
-
* Required when using B2C routes.
|
|
1215
|
-
* @example "https://yourdomain.com/api/mpesa/b2c/result"
|
|
1216
|
-
*/
|
|
1217
1197
|
b2cResultUrl?: string;
|
|
1218
|
-
/**
|
|
1219
|
-
* Full public URL Safaricom calls on B2C queue timeout.
|
|
1220
|
-
* Required when using B2C routes.
|
|
1221
|
-
* @example "https://yourdomain.com/api/mpesa/b2c/timeout"
|
|
1222
|
-
*/
|
|
1223
1198
|
b2cQueueTimeOutUrl?: string;
|
|
1224
|
-
/**
|
|
1225
|
-
* Optional hook called when a B2C result arrives at the result URL.
|
|
1226
|
-
* Called for BOTH successful and failed results.
|
|
1227
|
-
*
|
|
1228
|
-
* Fire-and-forget — 200 response to Safaricom is sent immediately.
|
|
1229
|
-
* Errors in this hook are logged but do NOT affect the response.
|
|
1230
|
-
*
|
|
1231
|
-
* @example
|
|
1232
|
-
* onB2CResult: async (result) => {
|
|
1233
|
-
* if (isB2CSuccess(result)) {
|
|
1234
|
-
* await db.disbursements.markCompleted({
|
|
1235
|
-
* transactionId: getB2CTransactionId(result),
|
|
1236
|
-
* amount: getB2CAmount(result),
|
|
1237
|
-
* });
|
|
1238
|
-
* }
|
|
1239
|
-
* }
|
|
1240
|
-
*/
|
|
1241
1199
|
onB2CResult?: (result: B2CResult) => Promise<void> | void;
|
|
1242
|
-
/**
|
|
1243
|
-
* Skip Safaricom IP verification on callback routes.
|
|
1244
|
-
* ONLY set true in local development — never in production.
|
|
1245
|
-
*/
|
|
1246
1200
|
skipIPCheck?: boolean;
|
|
1247
1201
|
}
|
|
1248
1202
|
declare function createMpesaExpressClient(config: MpesaExpressConfig): {
|
|
1249
1203
|
mpesa: Mpesa;
|
|
1250
1204
|
};
|
|
1251
|
-
/**
|
|
1252
|
-
* Attaches all M-Pesa routes to the given Express Router.
|
|
1253
|
-
*
|
|
1254
|
-
* @example
|
|
1255
|
-
* import express from "express";
|
|
1256
|
-
* import {
|
|
1257
|
-
* createMpesaExpressRouter,
|
|
1258
|
-
* acceptC2BValidation,
|
|
1259
|
-
* rejectC2BValidation,
|
|
1260
|
-
* isB2CSuccess,
|
|
1261
|
-
* getB2CTransactionId,
|
|
1262
|
-
* getB2CAmount,
|
|
1263
|
-
* } from "pesafy/express";
|
|
1264
|
-
*
|
|
1265
|
-
* const router = express.Router();
|
|
1266
|
-
* createMpesaExpressRouter(router, {
|
|
1267
|
-
* consumerKey: process.env.MPESA_CONSUMER_KEY!,
|
|
1268
|
-
* consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
|
|
1269
|
-
* environment: "sandbox",
|
|
1270
|
-
* lipaNaMpesaShortCode: "174379",
|
|
1271
|
-
* lipaNaMpesaPassKey: "bfb279...",
|
|
1272
|
-
* callbackUrl: "https://yourdomain.com/mpesa/express/callback",
|
|
1273
|
-
* initiatorName: "testapi",
|
|
1274
|
-
* initiatorPassword: "Safaricom123!",
|
|
1275
|
-
* certificatePath: "./SandboxCertificate.cer",
|
|
1276
|
-
* // B2C
|
|
1277
|
-
* b2cPartyA: "600979",
|
|
1278
|
-
* b2cResultUrl: "https://yourdomain.com/mpesa/b2c/result",
|
|
1279
|
-
* b2cQueueTimeOutUrl: "https://yourdomain.com/mpesa/b2c/timeout",
|
|
1280
|
-
* onB2CResult: async (result) => {
|
|
1281
|
-
* if (isB2CSuccess(result)) {
|
|
1282
|
-
* await db.disbursements.markCompleted({
|
|
1283
|
-
* transactionId: getB2CTransactionId(result),
|
|
1284
|
-
* amount: getB2CAmount(result),
|
|
1285
|
-
* });
|
|
1286
|
-
* }
|
|
1287
|
-
* },
|
|
1288
|
-
* skipIPCheck: true, // local dev only
|
|
1289
|
-
* });
|
|
1290
|
-
* app.use("/api", router);
|
|
1291
|
-
*/
|
|
1292
1205
|
declare function createMpesaExpressRouter(router: Router, config: MpesaExpressConfig): Router;
|
|
1293
1206
|
//#endregion
|
|
1294
1207
|
export { MpesaExpressConfig, createMpesaExpressClient, createMpesaExpressRouter };
|
package/dist/adapters/express.js
CHANGED
|
@@ -1 +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`,transactionType:r.transactionType,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,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;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,remarks:r.remarks,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,billRefNumber:r.billRefNumber,apiVersion:b.c2bApiVersion??`v2`});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,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,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;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,requester:r.requester,remarks:r.remarks,resultUrl:r.resultUrl??b.b2cResultUrl,queueTimeOutUrl:r.queueTimeOutUrl??b.b2cQueueTimeOutUrl});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;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};
|
|
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};
|
package/dist/adapters/fastify.js
CHANGED
|
@@ -1 +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
|
|
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};
|
package/dist/adapters/hono.js
CHANGED
|
@@ -1 +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
|
|
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};
|
package/dist/adapters/nextjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{i as e,n as t,r as n}from"../signature-verifier.js";function r(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function i(e){return e.headers.get(`x-forwarded-for`)?.split(`,`)[0]?.trim()??e.headers.get(`x-real-ip`)??``}function a(t){let i=new n(t);return async function(n){try{let a=await n.json();if(!a.amount||a.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be > 0`});if(!a.phoneNumber)throw new e({code:`VALIDATION_ERROR`,message:`phoneNumber is required`});return r(await i.stkPush({amount:a.amount,phoneNumber:a.phoneNumber,callbackUrl:t.callbackUrl,accountReference:a.accountReference??`REF-${Date.now().toString(36).toUpperCase()}`,transactionDesc:a.transactionDesc??`Payment
|
|
1
|
+
import{i as e,n as t,r as n}from"../signature-verifier.js";function r(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function i(e){return e.headers.get(`x-forwarded-for`)?.split(`,`)[0]?.trim()??e.headers.get(`x-real-ip`)??``}function a(t){let i=new n(t);return async function(n){try{let a=await n.json();if(!a.amount||a.amount<=0)throw new e({code:`VALIDATION_ERROR`,message:`amount must be > 0`});if(!a.phoneNumber)throw new e({code:`VALIDATION_ERROR`,message:`phoneNumber is required`});return r(await i.stkPush({amount:a.amount,phoneNumber:a.phoneNumber,callbackUrl:t.callbackUrl,accountReference:a.accountReference??`REF-${Date.now().toString(36).toUpperCase()}`,transactionDesc:a.transactionDesc??`Payment`,...a.partyB===void 0?{}:{partyB:a.partyB}}))}catch(t){return t instanceof e?r({error:t.code,message:t.message},t.statusCode??400):r({error:`INTERNAL_ERROR`,message:`Unexpected error`},500)}}}function o(t){let i=new n(t);return async function(t){try{let{checkoutRequestId:n}=await t.json();if(!n)throw new e({code:`VALIDATION_ERROR`,message:`checkoutRequestId is required`});return r(await i.stkQuery({checkoutRequestId:n}))}catch(t){return t instanceof e?r({error:t.code,message:t.message},t.statusCode??400):r({error:`INTERNAL_ERROR`},500)}}}function s(e){return async function(n){if(!e.skipIPCheck){let e=i(n);e&&!t(e)&&console.warn(`[pesafy/nextjs] Callback from unknown IP:`,e)}let a=(await n.json())?.Body?.stkCallback;if(!a)return r({ResultCode:0,ResultDesc:`Accepted`});let o=a.ResultCode===0,s=a.CallbackMetadata?.Item,c=e=>s?.find(t=>t.Name===e)?.Value;return o&&e.onSuccess?Promise.resolve(e.onSuccess({receiptNumber:c(`MpesaReceiptNumber`),amount:c(`Amount`),phone:c(`PhoneNumber`)})).catch(console.error):!o&&e.onFailure&&Promise.resolve(e.onFailure({resultCode:a.ResultCode,resultDesc:a.ResultDesc})).catch(console.error),r({ResultCode:0,ResultDesc:`Accepted`})}}function c(t){let i=a(t),c=o(t),l=s(t),u=new n(t);return{async POST(n){let a=n.nextUrl?.pathname??``;if(a.endsWith(`stk-push`))return i(n);if(a.endsWith(`stk-query`))return c(n);if(a.endsWith(`callback`))return l(n);if(a.endsWith(`balance`))try{let e=await n.json();return r(await u.accountBalance({...e,resultUrl:t.resultUrl??``,queueTimeOutUrl:t.queueTimeOutUrl??``}))}catch(t){return t instanceof e?r({error:t.code,message:t.message},400):r({error:`INTERNAL_ERROR`},500)}return r({error:`NOT_FOUND`,message:`No handler for ${a}`},404)}}}export{c as createMpesaNextHandlers,s as createStkCallbackHandler,a as createStkPushHandler,o as createStkQueryHandler};
|
package/dist/{cli.js → cli.mjs}
RENAMED
|
@@ -283,7 +283,7 @@ async function cmdEncrypt(args) {
|
|
|
283
283
|
process.exit(1);
|
|
284
284
|
}
|
|
285
285
|
try {
|
|
286
|
-
const { encryptSecurityCredential } = await import("./encryption.
|
|
286
|
+
const { encryptSecurityCredential } = await import("./encryption.mjs");
|
|
287
287
|
const { readFile } = await import("node:fs/promises");
|
|
288
288
|
const pem = await readFile(resolve(process.cwd(), certPath), "utf-8");
|
|
289
289
|
const credential = encryptSecurityCredential(password, pem);
|
|
@@ -298,7 +298,7 @@ async function cmdValidatePhone(args) {
|
|
|
298
298
|
let phone = args[0];
|
|
299
299
|
if (!phone) phone = await prompt("Phone number to validate");
|
|
300
300
|
try {
|
|
301
|
-
const { formatSafaricomPhone } = await import("./phone.
|
|
301
|
+
const { formatSafaricomPhone } = await import("./phone.mjs");
|
|
302
302
|
const normalised = formatSafaricomPhone(phone);
|
|
303
303
|
console.log(`\n${g("✔")} ${b(phone)} → ${c(normalised)}\n`);
|
|
304
304
|
} catch (e) {
|
|
@@ -323,9 +323,9 @@ async function cmdStkPush(args) {
|
|
|
323
323
|
const baseUrl = env["MPESA_ENVIRONMENT"] === "production" ? "https://api.safaricom.co.ke" : "https://sandbox.safaricom.co.ke";
|
|
324
324
|
console.log(dim("\nFetching token…"));
|
|
325
325
|
const token = await getToken(env["MPESA_CONSUMER_KEY"], env["MPESA_CONSUMER_SECRET"], baseUrl);
|
|
326
|
-
const { formatSafaricomPhone } = await import("./phone.
|
|
326
|
+
const { formatSafaricomPhone } = await import("./phone.mjs");
|
|
327
327
|
const msisdn = formatSafaricomPhone(phone);
|
|
328
|
-
const { getStkPushPassword, getTimestamp } = await import("./utils.
|
|
328
|
+
const { getStkPushPassword, getTimestamp } = await import("./utils.mjs");
|
|
329
329
|
const timestamp = getTimestamp();
|
|
330
330
|
const password = getStkPushPassword(env["MPESA_SHORTCODE"], env["MPESA_PASSKEY"], timestamp);
|
|
331
331
|
console.log(dim("Sending STK Push…\n"));
|
|
@@ -365,7 +365,7 @@ async function cmdStkQuery(args) {
|
|
|
365
365
|
if (!checkoutId) checkoutId = await prompt("CheckoutRequestID");
|
|
366
366
|
const baseUrl = env["MPESA_ENVIRONMENT"] === "production" ? "https://api.safaricom.co.ke" : "https://sandbox.safaricom.co.ke";
|
|
367
367
|
const token = await getToken(env["MPESA_CONSUMER_KEY"], env["MPESA_CONSUMER_SECRET"], baseUrl);
|
|
368
|
-
const { getStkPushPassword, getTimestamp } = await import("./utils.
|
|
368
|
+
const { getStkPushPassword, getTimestamp } = await import("./utils.mjs");
|
|
369
369
|
const timestamp = getTimestamp();
|
|
370
370
|
const password = getStkPushPassword(env["MPESA_SHORTCODE"], env["MPESA_PASSKEY"], timestamp);
|
|
371
371
|
try {
|
|
@@ -394,7 +394,7 @@ async function cmdBalance(args) {
|
|
|
394
394
|
const baseUrl = env["MPESA_ENVIRONMENT"] === "production" ? "https://api.safaricom.co.ke" : "https://sandbox.safaricom.co.ke";
|
|
395
395
|
console.log(dim("\nFetching token…"));
|
|
396
396
|
const token = await getToken(env["MPESA_CONSUMER_KEY"], env["MPESA_CONSUMER_SECRET"], baseUrl);
|
|
397
|
-
const { encryptSecurityCredential } = await import("./encryption.
|
|
397
|
+
const { encryptSecurityCredential } = await import("./encryption.mjs");
|
|
398
398
|
const { readFile } = await import("node:fs/promises");
|
|
399
399
|
const certPath = env["MPESA_CERTIFICATE_PATH"] ?? "./SandboxCertificate.cer";
|
|
400
400
|
const pem = await readFile(resolve(process.cwd(), certPath), "utf-8");
|
|
@@ -490,7 +490,7 @@ async function cmdReversal(args) {
|
|
|
490
490
|
const queueTimeoutUrl = env["MPESA_QUEUE_TIMEOUT_URL"] ?? await prompt("Queue Timeout URL");
|
|
491
491
|
const baseUrl = env["MPESA_ENVIRONMENT"] === "production" ? "https://api.safaricom.co.ke" : "https://sandbox.safaricom.co.ke";
|
|
492
492
|
const token = await getToken(env["MPESA_CONSUMER_KEY"], env["MPESA_CONSUMER_SECRET"], baseUrl);
|
|
493
|
-
const { encryptSecurityCredential } = await import("./encryption.
|
|
493
|
+
const { encryptSecurityCredential } = await import("./encryption.mjs");
|
|
494
494
|
const { readFile } = await import("node:fs/promises");
|
|
495
495
|
const certPath = env["MPESA_CERTIFICATE_PATH"] ?? "./SandboxCertificate.cer";
|
|
496
496
|
const pem = await readFile(resolve(process.cwd(), certPath), "utf-8");
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFile as e}from"node:fs/promises";import{constants as t,publicEncrypt as n}from"node:crypto";function r(e){"@babel/helpers - typeof";return r=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},r(e)}function i(e,t){if(r(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var i=n.call(e,t||`default`);if(r(i)!=`object`)return i;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function a(e){var t=i(e,`string`);return r(t)==`symbol`?t:t+``}function o(e,t,n){return(t=a(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var s=class e extends Error{constructor(t){super(t.message),o(this,`code`,void 0),o(this,`statusCode`,void 0),o(this,`response`,void 0),o(this,`requestId`,void 0),o(this,`cause`,void 0),o(this,`retryable`,void 0),Object.defineProperty(this,`name`,{value:`PesafyError`}),this.code=t.code,this.statusCode=t.statusCode,this.response=t.response,this.requestId=t.requestId,this.cause=t.cause,this.retryable=t.retryable??(t.code===`NETWORK_ERROR`||t.code===`TIMEOUT`||t.code===`RATE_LIMITED`||t.code===`REQUEST_FAILED`),Error.captureStackTrace&&Error.captureStackTrace(this,e)}get isValidation(){return this.code===`VALIDATION_ERROR`}get isAuth(){return this.code===`AUTH_FAILED`||this.code===`INVALID_CREDENTIALS`}toJSON(){return{name:this.name,code:this.code,message:this.message,statusCode:this.statusCode,requestId:this.requestId,retryable:this.retryable}}};function c(e){return new s(e)}function l(e){return e instanceof s}const ee=new Set([429,500,502,503,504]);function te(e){return new Promise(t=>setTimeout(t,e))}function u(e){let t=e*.25;return e+(Math.random()*t*2-t)}async function d(e,t){let n=t.retries??4,r=t.retryDelay??2e3,i=t.timeout??3e4,a={"Content-Type":`application/json`,Accept:`application/json`,...t.headers};t.idempotencyKey&&(a[`Idempotency-Key`]=t.idempotencyKey);let o={method:t.method,headers:a,...t.body===void 0?{}:{body:JSON.stringify(t.body)}},c=null;for(let a=0;a<=n;a++){if(a>0){let i=u(r*2**(a-1));console.warn(`[pesafy] Retry ${a}/${n} → ${t.method} ${e} in ${Math.round(i)} ms`),await te(i)}let l=new AbortController,d=setTimeout(()=>l.abort(),i),f;try{f=await fetch(e,{...o,signal:l.signal})}catch(t){if(clearTimeout(d),c=t instanceof Error&&t.name===`AbortError`?new s({code:`TIMEOUT`,message:`Request to ${e} timed out after ${i} ms`,cause:t,retryable:!0}):new s({code:`NETWORK_ERROR`,message:`Network error: ${t instanceof Error?t.message:String(t)}`,cause:t,retryable:!0}),a<n)continue;throw c}finally{clearTimeout(d)}let p=``,m=null,h=f.headers.get(`content-type`)??``;try{p=await f.text(),p&&(m=h.includes(`application/json`)?JSON.parse(p):p)}catch{m=p||null}let g={};if(f.headers.forEach((e,t)=>{g[t]=e}),f.ok)return{data:m,status:f.status,headers:g};let _=ee.has(f.status),v=typeof m==`object`&&m?m:{},y=v.errorMessage??v.ResponseDescription??v.resultDesc??p??`HTTP ${f.status}`;if(c=new s({code:_?`REQUEST_FAILED`:`API_ERROR`,message:y,statusCode:f.status,response:m,requestId:v.requestId,retryable:_}),!(_&&a<n))throw c}throw c}var f=class{constructor(e,t,n){o(this,`consumerKey`,void 0),o(this,`consumerSecret`,void 0),o(this,`baseUrl`,void 0),o(this,`cachedToken`,null),o(this,`tokenExpiresAt`,0),this.consumerKey=e,this.consumerSecret=t,this.baseUrl=n}getBasicAuthHeader(){let e=`${this.consumerKey}:${this.consumerSecret}`;return`Basic ${Buffer.from(e,`utf-8`).toString(`base64`)}`}async getAccessToken(){let e=Date.now()/1e3;if(this.cachedToken&&this.tokenExpiresAt>e+60)return this.cachedToken;let t=await d(`${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`,{method:`GET`,headers:{Authorization:this.getBasicAuthHeader()}}),{access_token:n,expires_in:r}=t.data;if(!n)throw new s({code:`AUTH_FAILED`,message:`Daraja did not return an access token. Check your consumer key and secret.`,response:t.data});return this.cachedToken=n,this.tokenExpiresAt=e+(r??3600),this.cachedToken}clearCache(){this.cachedToken=null,this.tokenExpiresAt=0}};function p(e,r){try{let i=Buffer.from(e,`utf-8`);return n({key:r,padding:t.RSA_PKCS1_PADDING},i).toString(`base64`)}catch(e){throw new s({code:`ENCRYPTION_FAILED`,message:`Failed to encrypt security credential. Ensure the certificate PEM is valid and matches the environment (sandbox/production).`,cause:e})}}function m(e){let t=Math.round(e);if(!Number.isFinite(t)||t<1)throw TypeError(`KesAmount must be a whole number ≥ 1, got ${e}`);return t}function h(e){let t=e.replace(/\D/g,``),n;if(t.startsWith(`254`)&&t.length===12)n=t;else if(t.startsWith(`0`)&&t.length===10)n=`254${t.slice(1)}`;else if(t.length===9)n=`254${t}`;else throw TypeError(`Cannot normalise "${e}" to 254XXXXXXXXX. Use 07XX…, 2547XX…, or +2547XX….`);if(n.length!==12)throw TypeError(`Phone "${e}" normalised to "${n}" — expected 12 digits.`);return n}function g(e){return String(e)}function _(e){return String(e)}function v(e){return String(e)}function y(e){return{ok:!0,data:e}}function b(e){return{ok:!1,error:e}}function ne(e){if(!e.trim())throw TypeError(`String must not be empty`);return e}async function re(e,t,n,r,i){if(!i.partyA?.trim())throw c({code:`VALIDATION_ERROR`,message:`partyA is required.`});if(![`1`,`2`,`4`].includes(i.identifierType))throw c({code:`VALIDATION_ERROR`,message:`identifierType must be "1" (MSISDN), "2" (Till), or "4" (ShortCode).`});if(!i.resultUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`resultUrl is required.`});if(!i.queueTimeOutUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required.`});let a={Initiator:r,SecurityCredential:n,CommandID:`AccountBalance`,PartyA:String(i.partyA),IdentifierType:i.identifierType,ResultURL:i.resultUrl,QueueTimeOutURL:i.queueTimeOutUrl,Remarks:i.remarks??`Account Balance Query`},{data:o}=await d(`${e}/mpesa/accountbalance/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}function ie(e){let t=e.split(`|`),n=[];for(let e=0;e+2<t.length;e+=3){let r=t[e]?.trim(),i=t[e+1]?.trim(),a=t[e+2]?.trim();r&&i&&a!==void 0&&n.push({name:r,currency:i,amount:a})}return n}function ae(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}function oe(e){return e.Result.ResultCode===0}function se(){try{if(typeof crypto<`u`&&crypto.randomUUID)return crypto.randomUUID()}catch{}return`${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`}async function x(e,t,n){if(!n.primaryShortCode?.trim())throw c({code:`VALIDATION_ERROR`,message:`primaryShortCode is required — the merchant's till number (debit party).`});if(!n.receiverShortCode?.trim())throw c({code:`VALIDATION_ERROR`,message:`receiverShortCode is required — the vendor's Paybill account (credit party).`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw c({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${n.amount} which rounds to ${r}).`});if(!n.paymentRef?.trim())throw c({code:`VALIDATION_ERROR`,message:`paymentRef is required — shown in the merchant's USSD prompt as the payment reference.`});if(!n.callbackUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`callbackUrl is required — Safaricom POSTs the transaction result here.`});if(!n.partnerName?.trim())throw c({code:`VALIDATION_ERROR`,message:`partnerName is required — your friendly name shown in the merchant's USSD prompt.`});let i={primaryShortCode:String(n.primaryShortCode),receiverShortCode:String(n.receiverShortCode),amount:String(r),paymentRef:n.paymentRef,callbackUrl:n.callbackUrl,partnerName:n.partnerName,RequestRefID:n.requestRefId??se()},{data:a}=await d(`${e}/v1/ussdpush/get-msisdn`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i});return a}function S(e){return e.resultCode===`0`}function ce(e){return e.resultCode===`4001`}function le(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 ue(e){return S(e)?e.transactionId??null:null}function de(e){return Number(e.amount)}function fe(e){return e.requestId}function pe(e){return S(e)?e.conversationID??null:null}async function C(e,t,n,r,i){if(!i.commandId)throw c({code:`VALIDATION_ERROR`,message:`commandId is required: "BusinessPayToBulk" | "BusinessPayment" | "SalaryPayment" | "PromotionPayment"`});let a=[`BusinessPayToBulk`,`BusinessPayment`,`SalaryPayment`,`PromotionPayment`];if(!a.includes(i.commandId))throw c({code:`VALIDATION_ERROR`,message:`commandId must be one of: ${a.join(`, `)}. Got: "${i.commandId}"`});let o=Math.round(i.amount);if(!Number.isFinite(o)||o<1)throw c({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${i.amount} which rounds to ${o}).`});if(!i.partyA?.trim())throw c({code:`VALIDATION_ERROR`,message:`partyA is required — your business shortcode from which money is deducted.`});if(!i.partyB?.trim())throw c({code:`VALIDATION_ERROR`,message:`partyB is required — the recipient shortcode (BusinessPayToBulk) or customer MSISDN (other commands).`});if(!i.accountReference?.trim())throw c({code:`VALIDATION_ERROR`,message:`accountReference is required — a reference for this transaction.`});if(!i.resultUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the B2C result here.`});if(!i.queueTimeOutUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let s={Initiator:r,SecurityCredential:n,CommandID:i.commandId,SenderIdentifierType:i.senderIdentifierType??`4`,RecieverIdentifierType:i.receiverIdentifierType??`4`,Amount:String(o),PartyA:String(i.partyA),PartyB:String(i.partyB),AccountReference:i.accountReference,Remarks:i.remarks??`B2C Payment`,QueueTimeOutURL:i.queueTimeOutUrl,ResultURL:i.resultUrl};i.requester?.trim()&&(s.Requester=String(i.requester));let{data:l}=await d(`${e}/mpesa/b2b/v1/paymentrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return l}function me(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 w(e){return e.Result.ResultCode===0}function T(e){return e.Result.ResultCode!==0}function E(e){return e.Result.TransactionID??null}function D(e){return e.Result.ConversationID}function O(e){return e.Result.OriginatorConversationID}function k(e){return e.Result.ResultDesc}function A(e){let t=L(e,`Amount`);return t===void 0?null:Number(t)}function j(e){let t=L(e,`TransCompletedTime`);return t===void 0?null:String(t)}function M(e){let t=L(e,`DebitPartyCharges`);return t===void 0||t===``?null:String(t)}function N(e){let t=L(e,`ReceiverPartyPublicName`);return t===void 0?null:String(t)}function P(e){let t=L(e,`Currency`);return t===void 0?`KES`:String(t)}function F(e){let t=L(e,`DebitAccountBalance`);return t===void 0?null:String(t)}function I(e){let t=L(e,`InitiatorAccountCurrentBalance`);return t===void 0?null:String(t)}function L(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}async function R(e,t,n){if(!n.shortcode?.trim())throw c({code:`VALIDATION_ERROR`,message:`shortcode is required.`});if(!n.email?.trim())throw c({code:`VALIDATION_ERROR`,message:`email is required.`});if(!n.callbackUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`callbackUrl is required.`});let r={shortcode:n.shortcode,email:n.email,officialContact:n.officialContact,sendReminders:n.sendReminders,logo:n.logo??``,callbackUrl:n.callbackUrl},{data:i}=await d(`${e}/v1/billmanager-invoice/optin`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:r});return i}async function z(e,t,n){if(!n.externalReference?.trim())throw c({code:`VALIDATION_ERROR`,message:`externalReference is required.`});if(!n.partyA?.trim())throw c({code:`VALIDATION_ERROR`,message:`partyA (customer MSISDN) is required.`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw c({code:`VALIDATION_ERROR`,message:`amount must be ≥ 1 (got ${n.amount}).`});let i={externalReference:n.externalReference,billingPeriod:n.billingPeriod,invoiceName:n.invoiceName,dueDate:n.dueDate,accountReference:n.accountReference,amount:String(r),partyA:n.partyA,invoiceItems:n.invoiceItems?.map(e=>({itemName:e.itemName,amount:String(Math.round(e.amount))}))??[]},{data:a}=await d(`${e}/v1/billmanager-invoice/single-invoicing`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i});return a}async function he(e,t,n){if(!n.invoices?.length)throw c({code:`VALIDATION_ERROR`,message:`invoices array must not be empty.`});if(n.invoices.length>1e3)throw c({code:`VALIDATION_ERROR`,message:`Maximum 1 000 invoices per bulk request.`});let{data:r}=await d(`${e}/v1/billmanager-invoice/bulk-invoicing`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:n.invoices});return r}async function ge(e,t,n){if(!n.externalReference?.trim())throw c({code:`VALIDATION_ERROR`,message:`externalReference is required.`});let{data:r}=await d(`${e}/v1/billmanager-invoice/cancel-single-invoice`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:[{externalReference:n.externalReference}]});return r}const _e=[`mpesa`,`safaricom`,`.exe`,`.exec`,`cmd`,`sql`,`query`];function B(e,t){if(!e||!e.trim())throw c({code:`VALIDATION_ERROR`,message:`${t} is required`});let n=e.toLowerCase();for(let e of _e)if(n.includes(e))throw c({code:`VALIDATION_ERROR`,message:`${t} must not contain the keyword "${e}". Daraja rejects URLs containing: M-PESA, Safaricom, exe, exec, cmd, sql, query.`})}async function V(e,t,n){if(!n.shortCode)throw c({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(!n.responseType)throw c({code:`VALIDATION_ERROR`,message:`responseType is required: "Completed" or "Cancelled" (sentence case, exactly as spelled)`});if(n.responseType!==`Completed`&&n.responseType!==`Cancelled`)throw c({code:`VALIDATION_ERROR`,message:`responseType must be exactly "Completed" or "Cancelled" (sentence case). Got: "${String(n.responseType)}"`});B(n.confirmationUrl,`confirmationUrl`),B(n.validationUrl,`validationUrl`);let r=n.apiVersion??`v2`,i={ShortCode:String(n.shortCode),ResponseType:n.responseType,ConfirmationURL:n.confirmationUrl,ValidationURL:n.validationUrl},{data:a}=await d(`${e}/mpesa/c2b/${r}/registerurl`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i});return a}async function H(e,t,n){if(!e.includes(`sandbox`))throw c({code:`VALIDATION_ERROR`,message:`C2B simulate is only available in the Sandbox environment. In production, customers initiate payments via M-PESA App, USSD, or SIM Toolkit.`});if(!n.shortCode)throw c({code:`VALIDATION_ERROR`,message:`shortCode is required.`});if(n.commandId!==`CustomerPayBillOnline`&&n.commandId!==`CustomerBuyGoodsOnline`)throw c({code:`VALIDATION_ERROR`,message:`commandId must be "CustomerPayBillOnline" or "CustomerBuyGoodsOnline". Got: "${String(n.commandId)}"`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw c({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${n.amount}).`});if(!n.msisdn)throw c({code:`VALIDATION_ERROR`,message:`msisdn is required. Sandbox test MSISDN: 254708374149.`});let i=n.commandId===`CustomerBuyGoodsOnline`,a=n.apiVersion??`v2`,o={ShortCode:Number(n.shortCode),CommandID:n.commandId,Amount:r,Msisdn:Number(n.msisdn)};i||(o.BillRefNumber=n.billRefNumber??``),i&&Object.prototype.hasOwnProperty.call(o,`BillRefNumber`)&&(delete o.BillRefNumber,console.warn(`[pesafy/simulateC2B] BillRefNumber leaked into Buy Goods payload — removed. This is a library bug; please report it.`));let{data:s}=await d(`${e}/mpesa/c2b/${a}/simulate`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}function ve(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.TransID==`string`&&typeof t.BusinessShortCode==`string`&&typeof t.TransAmount==`string`}function ye(e){return{ResultCode:`0`,ResultDesc:`Accepted`,...e?{ThirdPartyTransID:e}:{}}}function be(e=`C2B00016`,t=`Rejected`){return{ResultCode:e,ResultDesc:t}}function xe(){return{ResultCode:0,ResultDesc:`Success`}}function Se(e){return Number(e.TransAmount)}function U(e){return e.TransID}function Ce(e){return e.BillRefNumber}function we(e){return[e.FirstName,e.MiddleName,e.LastName].filter(Boolean).join(` `).trim()}function Te(e){return e.TransactionType===`Pay Bill`||e.TransactionType===`CustomerPayBillOnline`}function Ee(e){return e.TransactionType===`Buy Goods`||e.TransactionType===`CustomerBuyGoodsOnline`}async function De(e,t,n){if(!n.merchantName?.trim())throw c({code:`VALIDATION_ERROR`,message:`merchantName is required`});if(!n.refNo?.trim())throw c({code:`VALIDATION_ERROR`,message:`refNo (transaction reference) is required`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw c({code:`VALIDATION_ERROR`,message:`Amount must be at least 1 (got ${n.amount} which rounds to ${r}).`});if(!n.trxCode)throw c({code:`VALIDATION_ERROR`,message:`trxCode is required. Supported values: "BG" | "WA" | "PB" | "SM" | "SB"`});if(!n.cpi?.trim())throw c({code:`VALIDATION_ERROR`,message:`cpi (Credit Party Identifier) is required — e.g. till number, paybill, or MSISDN`});let i=n.size??300;if(i<1)throw c({code:`VALIDATION_ERROR`,message:`size must be a positive number of pixels`});let a={MerchantName:n.merchantName,RefNo:n.refNo,Amount:r,TrxCode:n.trxCode,CPI:n.cpi,Size:String(i)},{data:o}=await d(`${e}/mpesa/qrcode/v1/generate`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}async function Oe(e,t,n,r,i){if(!i.transactionId?.trim())throw c({code:`VALIDATION_ERROR`,message:`transactionId is required.`});if(!i.receiverParty?.trim())throw c({code:`VALIDATION_ERROR`,message:`receiverParty is required.`});if(![`1`,`2`,`4`].includes(i.receiverIdentifierType))throw c({code:`VALIDATION_ERROR`,message:`receiverIdentifierType must be "1", "2", or "4".`});let a=Math.round(i.amount);if(!Number.isFinite(a)||a<1)throw c({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${i.amount}).`});if(!i.resultUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`resultUrl is required.`});if(!i.queueTimeOutUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required.`});let o={Initiator:r,SecurityCredential:n,CommandID:`TransactionReversal`,TransactionID:i.transactionId,Amount:String(a),ReceiverParty:String(i.receiverParty),RecieverIdentifierType:i.receiverIdentifierType,ResultURL:i.resultUrl,QueueTimeOutURL:i.queueTimeOutUrl,Remarks:i.remarks??`Transaction Reversal`,Occasion:i.occasion??``},{data:s}=await d(`${e}/mpesa/reversal/v1/request`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}function ke(e){return e.Result.ResultCode===0}function Ae(e){return e.Result.TransactionID??null}function je(e){return e.Result.ConversationID}function W(e){let t=e.replace(/\D/g,``),n;if(t.startsWith(`254`)&&t.length===12)n=t;else if(t.startsWith(`0`)&&t.length===10)n=`254${t.slice(1)}`;else if(t.length===9)n=`254${t}`;else throw new s({code:`INVALID_PHONE`,message:`Cannot parse "${e}". Use 07XXXXXXXX, 2547XXXXXXXX, or +2547XXXXXXXX.`});if(n.length!==12)throw new s({code:`INVALID_PHONE`,message:`"${e}" normalised to "${n}" — expected 12 digits.`});return n}function G(e,t,n){return btoa(`${e}${t}${n}`)}function K(){let e=new Date,t=e=>e.toString().padStart(2,`0`);return[e.getFullYear(),t(e.getMonth()+1),t(e.getDate()),t(e.getHours()),t(e.getMinutes()),t(e.getSeconds())].join(``)}async function Me(e,t,n){let r=Math.round(n.amount);if(r<1)throw new s({code:`VALIDATION_ERROR`,message:`Amount must be at least KES 1 (got ${n.amount} which rounds to ${r}).`});let i=K(),a=n.partyB??n.shortCode,o={BusinessShortCode:n.shortCode,Password:G(n.shortCode,n.passKey,i),Timestamp:i,TransactionType:n.transactionType??`CustomerPayBillOnline`,Amount:r,PartyA:W(n.phoneNumber),PartyB:a,PhoneNumber:W(n.phoneNumber),CallBackURL:n.callbackUrl,AccountReference:n.accountReference.slice(0,12),TransactionDesc:n.transactionDesc.slice(0,13)},{data:c}=await d(`${e}/mpesa/stkpush/v1/processrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o,retries:5,retryDelay:3e3});return c}async function Ne(e,t,n){let r=K(),i={BusinessShortCode:n.shortCode,Password:G(n.shortCode,n.passKey,r),Timestamp:r,CheckoutRequestID:n.checkoutRequestId},{data:a}=await d(`${e}/mpesa/stkpushquery/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i});return a}function q(e){return e.ResultCode===0}function Pe(e,t){let n=e.Body.stkCallback;if(q(n))return n.CallbackMetadata.Item.find(e=>e.Name===t)?.Value}const Fe=`572572`,J=`PayTaxToKRA`;async function Y(e,t,n,r,i){let a=Math.round(i.amount);if(!Number.isFinite(a)||a<1)throw c({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${i.amount} which rounds to ${a}).`});if(!i.partyA)throw c({code:`VALIDATION_ERROR`,message:`partyA is required — your M-PESA business shortcode from which tax is deducted.`});if(!i.accountReference?.trim())throw c({code:`VALIDATION_ERROR`,message:`accountReference is required — the Payment Registration Number (PRN) issued by KRA.`});if(!i.resultUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the tax remittance result here.`});if(!i.queueTimeOutUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let o={Initiator:r,SecurityCredential:n,CommandID:J,SenderIdentifierType:`4`,RecieverIdentifierType:`4`,Amount:String(a),PartyA:String(i.partyA),PartyB:i.partyB??`572572`,AccountReference:i.accountReference,Remarks:i.remarks??`Tax Remittance`,QueueTimeOutURL:i.queueTimeOutUrl,ResultURL:i.resultUrl},{data:s}=await d(`${e}/mpesa/b2b/v1/remittax`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}async function Ie(e,t,n,r,i){if(!i.transactionId)throw c({code:`VALIDATION_ERROR`,message:`transactionId is required`});if(!i.partyA)throw c({code:`VALIDATION_ERROR`,message:`partyA is required (your business shortcode, till number, or MSISDN)`});if(!i.identifierType)throw c({code:`VALIDATION_ERROR`,message:`identifierType is required: "1" (MSISDN) | "2" (Till) | "4" (ShortCode)`});if(!i.resultUrl)throw c({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the transaction result here`});if(!i.queueTimeOutUrl)throw c({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on timeout`});let a={Initiator:r,SecurityCredential:n,CommandID:i.commandId??`TransactionStatusQuery`,TransactionID:i.transactionId,PartyA:i.partyA,IdentifierType:i.identifierType,ResultURL:i.resultUrl,QueueTimeOutURL:i.queueTimeOutUrl,Remarks:i.remarks??`Transaction Status Query`,Occasion:i.occasion??``},{data:o}=await d(`${e}/mpesa/transactionstatus/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}const X={sandbox:`https://sandbox.safaricom.co.ke`,production:`https://api.safaricom.co.ke`};var Le=class{constructor(e){if(o(this,`config`,void 0),o(this,`tokenManager`,void 0),o(this,`baseUrl`,void 0),!e.consumerKey||!e.consumerSecret)throw new s({code:`INVALID_CREDENTIALS`,message:`consumerKey and consumerSecret are required.`});this.config=e,this.baseUrl=X[e.environment],this.tokenManager=new f(e.consumerKey,e.consumerSecret,this.baseUrl)}getToken(){return this.tokenManager.getAccessToken()}async buildSecurityCredential(){if(this.config.securityCredential)return this.config.securityCredential;if(!this.config.initiatorPassword)throw new s({code:`INVALID_CREDENTIALS`,message:`Provide securityCredential (pre-encrypted) OR (initiatorPassword + certificatePath/certificatePem).`});let t;if(this.config.certificatePem)t=this.config.certificatePem;else if(this.config.certificatePath)t=await e(this.config.certificatePath,`utf-8`);else throw new s({code:`INVALID_CREDENTIALS`,message:`certificatePath or certificatePem is required to encrypt the initiator password.`});return p(this.config.initiatorPassword,t)}requireInitiator(e){let t=this.config.initiatorName??``;if(!t)throw new s({code:`VALIDATION_ERROR`,message:`initiatorName is required for ${e}.`});return t}async stkPushSafe(e){try{return y(await this.stkPush(e))}catch(e){return b(e)}}async stkPush(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new s({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push.`});let r=await this.getToken();return Me(this.baseUrl,r,{...e,shortCode:t,passKey:n})}async stkQuery(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new s({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Query.`});let r=await this.getToken();return Ne(this.baseUrl,r,{...e,shortCode:t,passKey:n})}async transactionStatus(e){let t=this.requireInitiator(`Transaction Status`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Ie(this.baseUrl,n,r,t,e)}async accountBalance(e){let t=this.requireInitiator(`Account Balance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return re(this.baseUrl,n,r,t,e)}async reverseTransaction(e){let t=this.requireInitiator(`Reversal`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Oe(this.baseUrl,n,r,t,e)}async generateDynamicQR(e){let t=await this.getToken();return De(this.baseUrl,t,e)}async registerC2BUrls(e){let t=await this.getToken();return V(this.baseUrl,t,e)}async simulateC2B(e){let t=await this.getToken();return H(this.baseUrl,t,e)}async remitTax(e){let t=this.requireInitiator(`Tax Remittance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Y(this.baseUrl,n,r,t,e)}async b2bExpressCheckout(e){let t=await this.getToken();return x(this.baseUrl,t,e)}async b2cPayment(e){let t=this.requireInitiator(`B2C Payment`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return C(this.baseUrl,n,r,t,e)}async billManagerOptIn(e){let t=await this.getToken();return R(this.baseUrl,t,e)}async sendInvoice(e){let t=await this.getToken();return z(this.baseUrl,t,e)}async sendBulkInvoices(e){let t=await this.getToken();return he(this.baseUrl,t,e)}async cancelInvoice(e){let t=await this.getToken();return ge(this.baseUrl,t,e)}clearTokenCache(){this.tokenManager.clearCache()}get environment(){return this.config.environment}};const Re={maxRetries:1/0,initialDelay:1e3,maxDelay:36e5,backoffMultiplier:2,maxRetryDuration:720*60*60*1e3};async function ze(e,t={}){let n={...Re,...t},r=n.initialDelay,i=0,a=Date.now();for(;i<n.maxRetries;){if(i++,Date.now()-a>n.maxRetryDuration)return{success:!1,attempts:i,error:Error(`Max retry duration exceeded`)};try{return{success:!0,data:await e(),attempts:i}}catch(e){let t=e instanceof Error?e:Error(String(e));if(t.message.includes(`4`))return{success:!1,attempts:i,error:t};i<n.maxRetries&&(await new Promise(e=>setTimeout(e,r)),r=Math.min(r*n.backoffMultiplier,n.maxDelay))}}return{success:!1,attempts:i,error:Error(`Max retries exceeded`)}}const Z=[`196.201.214.200`,`196.201.214.206`,`196.201.213.114`,`196.201.214.207`,`196.201.214.208`,`196.201.213.44`,`196.201.212.127`,`196.201.212.138`,`196.201.212.129`,`196.201.212.136`,`196.201.212.74`,`196.201.212.69`];function Q(e,t=Z){return t.includes(e)}function $(e){try{let t=e;return t?.Body?.stkCallback?t:null}catch{return null}}function Be(e,t={}){if(!t.skipIPCheck&&t.requestIP&&!Q(t.requestIP,t.allowedIPs))return{success:!1,eventType:null,data:null,error:`IP address ${t.requestIP} is not in the Safaricom whitelist`};let n=$(e);return n?{success:!0,eventType:`stk_push`,data:n}:{success:!1,eventType:null,data:null,error:`Unknown or malformed webhook payload`}}function Ve(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`MpesaReceiptNumber`);return t?String(t.Value):null}function He(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`Amount`);return t?Number(t.Value):null}function Ue(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`PhoneNumber`);return t?String(t.Value):null}function We(e){return e.Body?.stkCallback?.ResultCode===0}export{X as DARAJA_BASE_URLS,Fe as KRA_SHORTCODE,Le as Mpesa,s as PesafyError,Z as SAFARICOM_IPS,J as TAX_COMMAND_ID,ye as acceptC2BValidation,xe as acknowledgeC2BConfirmation,c as createError,p as encryptSecurityCredential,b as err,He as extractAmount,Ue as extractPhoneNumber,Ve as extractTransactionId,W as formatPhoneNumber,W as formatSafaricomPhone,ae as getAccountBalanceParam,de as getB2BAmount,pe as getB2BConversationId,fe as getB2BRequestId,ue as getB2BTransactionId,A as getB2CAmount,D as getB2CConversationId,P as getB2CCurrency,F as getB2CDebitAccountBalance,M as getB2CDebitPartyCharges,I as getB2CInitiatorAccountBalance,O as getB2COriginatorConversationId,N as getB2CReceiverPublicName,k as getB2CResultDesc,L as getB2CResultParam,j as getB2CTransactionCompletedTime,E as getB2CTransactionId,Ce as getC2BAccountRef,Se as getC2BAmount,we as getC2BCustomerName,U as getC2BTransactionId,Pe as getCallbackValue,je as getReversalConversationId,Ae as getReversalTransactionId,K as getTimestamp,Be as handleWebhook,d as httpRequest,x as initiateB2BExpressCheckout,C as initiateB2CPayment,oe as isAccountBalanceSuccess,le as isB2BCheckoutCallback,ce as isB2BCheckoutCancelled,S as isB2BCheckoutSuccess,T as isB2CFailure,me as isB2CResult,w as isB2CSuccess,Ee as isBuyGoodsPayment,ve as isC2BPayload,Te as isPaybillPayment,l as isPesafyError,ke as isReversalSuccess,q as isStkCallbackSuccess,We as isSuccessfulCallback,y as ok,ie as parseAccountBalance,$ as parseStkPushWebhook,V as registerC2BUrls,be as rejectC2BValidation,Y as remitTax,ze as retryWithBackoff,H as simulateC2B,m as toKesAmount,h as toMsisdn,ne as toNonEmpty,g as toPaybill,v as toShortCode,_ as toTill,Q as verifyWebhookIP};
|
|
1
|
+
import{readFile as e}from"node:fs/promises";import{constants as t,publicEncrypt as n}from"node:crypto";function r(e){"@babel/helpers - typeof";return r=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},r(e)}function i(e,t){if(r(e)!=`object`||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var i=n.call(e,t||`default`);if(r(i)!=`object`)return i;throw TypeError(`@@toPrimitive must return a primitive value.`)}return(t===`string`?String:Number)(e)}function a(e){var t=i(e,`string`);return r(t)==`symbol`?t:t+``}function o(e,t,n){return(t=a(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var s=class e extends Error{constructor(t){super(t.message),o(this,`code`,void 0),o(this,`statusCode`,void 0),o(this,`response`,void 0),o(this,`requestId`,void 0),o(this,`cause`,void 0),o(this,`retryable`,void 0),Object.defineProperty(this,`name`,{value:`PesafyError`}),this.code=t.code,this.statusCode=t.statusCode,this.response=t.response,this.requestId=t.requestId,this.cause=t.cause,this.retryable=t.retryable??(t.code===`NETWORK_ERROR`||t.code===`TIMEOUT`||t.code===`RATE_LIMITED`||t.code===`REQUEST_FAILED`),Error.captureStackTrace&&Error.captureStackTrace(this,e)}get isValidation(){return this.code===`VALIDATION_ERROR`}get isAuth(){return this.code===`AUTH_FAILED`||this.code===`INVALID_CREDENTIALS`}toJSON(){return{name:this.name,code:this.code,message:this.message,statusCode:this.statusCode,requestId:this.requestId,retryable:this.retryable}}};function c(e){return new s(e)}function l(e){return e instanceof s}const ee=new Set([429,500,502,503,504]);function te(e){return new Promise(t=>setTimeout(t,e))}function u(e){let t=e*.25;return e+(Math.random()*t*2-t)}async function d(e,t){let n=t.retries??4,r=t.retryDelay??2e3,i=t.timeout??3e4,a={"Content-Type":`application/json`,Accept:`application/json`,...t.headers};t.idempotencyKey&&(a[`Idempotency-Key`]=t.idempotencyKey);let o={method:t.method,headers:a,...t.body===void 0?{}:{body:JSON.stringify(t.body)}},c=null;for(let a=0;a<=n;a++){if(a>0){let i=u(r*2**(a-1));console.warn(`[pesafy] Retry ${a}/${n} → ${t.method} ${e} in ${Math.round(i)} ms`),await te(i)}let l=new AbortController,d=setTimeout(()=>l.abort(),i),f;try{f=await fetch(e,{...o,signal:l.signal})}catch(t){if(clearTimeout(d),c=t instanceof Error&&t.name===`AbortError`?new s({code:`TIMEOUT`,message:`Request to ${e} timed out after ${i} ms`,cause:t,retryable:!0}):new s({code:`NETWORK_ERROR`,message:`Network error: ${t instanceof Error?t.message:String(t)}`,cause:t,retryable:!0}),a<n)continue;throw c}finally{clearTimeout(d)}let p=``,m=null,h=f.headers.get(`content-type`)??``;try{p=await f.text(),p&&(m=h.includes(`application/json`)?JSON.parse(p):p)}catch{m=p||null}let g={};if(f.headers.forEach((e,t)=>{g[t]=e}),f.ok)return{data:m,status:f.status,headers:g};let _=ee.has(f.status),v=typeof m==`object`&&m?m:{},y=v.errorMessage??v.ResponseDescription??v.resultDesc??p??`HTTP ${f.status}`;if(c=new s({code:_?`REQUEST_FAILED`:`API_ERROR`,message:y,statusCode:f.status,response:m,retryable:_,...typeof v.requestId==`string`?{requestId:v.requestId}:{}}),!(_&&a<n))throw c}throw c}var f=class{constructor(e,t,n){o(this,`consumerKey`,void 0),o(this,`consumerSecret`,void 0),o(this,`baseUrl`,void 0),o(this,`cachedToken`,null),o(this,`tokenExpiresAt`,0),this.consumerKey=e,this.consumerSecret=t,this.baseUrl=n}getBasicAuthHeader(){let e=`${this.consumerKey}:${this.consumerSecret}`;return`Basic ${Buffer.from(e,`utf-8`).toString(`base64`)}`}async getAccessToken(){let e=Date.now()/1e3;if(this.cachedToken&&this.tokenExpiresAt>e+60)return this.cachedToken;let t=await d(`${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`,{method:`GET`,headers:{Authorization:this.getBasicAuthHeader()}}),{access_token:n,expires_in:r}=t.data;if(!n)throw new s({code:`AUTH_FAILED`,message:`Daraja did not return an access token. Check your consumer key and secret.`,response:t.data});return this.cachedToken=n,this.tokenExpiresAt=e+(r??3600),this.cachedToken}clearCache(){this.cachedToken=null,this.tokenExpiresAt=0}};function p(e,r){try{let i=Buffer.from(e,`utf-8`);return n({key:r,padding:t.RSA_PKCS1_PADDING},i).toString(`base64`)}catch(e){throw new s({code:`ENCRYPTION_FAILED`,message:`Failed to encrypt security credential. Ensure the certificate PEM is valid and matches the environment (sandbox/production).`,cause:e})}}function m(e){let t=Math.round(e);if(!Number.isFinite(t)||t<1)throw TypeError(`KesAmount must be a whole number ≥ 1, got ${e}`);return t}function h(e){let t=e.replace(/\D/g,``),n;if(t.startsWith(`254`)&&t.length===12)n=t;else if(t.startsWith(`0`)&&t.length===10)n=`254${t.slice(1)}`;else if(t.length===9)n=`254${t}`;else throw TypeError(`Cannot normalise "${e}" to 254XXXXXXXXX. Use 07XX…, 2547XX…, or +2547XX….`);if(n.length!==12)throw TypeError(`Phone "${e}" normalised to "${n}" — expected 12 digits.`);return n}function g(e){return String(e)}function _(e){return String(e)}function v(e){return String(e)}function y(e){return{ok:!0,data:e}}function b(e){return{ok:!1,error:e}}function ne(e){if(!e.trim())throw TypeError(`String must not be empty`);return e}async function re(e,t,n,r,i){if(!i.partyA?.trim())throw c({code:`VALIDATION_ERROR`,message:`partyA is required.`});if(![`1`,`2`,`4`].includes(i.identifierType))throw c({code:`VALIDATION_ERROR`,message:`identifierType must be "1" (MSISDN), "2" (Till), or "4" (ShortCode).`});if(!i.resultUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`resultUrl is required.`});if(!i.queueTimeOutUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required.`});let a={Initiator:r,SecurityCredential:n,CommandID:`AccountBalance`,PartyA:String(i.partyA),IdentifierType:i.identifierType,ResultURL:i.resultUrl,QueueTimeOutURL:i.queueTimeOutUrl,Remarks:i.remarks??`Account Balance Query`},{data:o}=await d(`${e}/mpesa/accountbalance/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}function ie(e){let t=e.split(`|`),n=[];for(let e=0;e+2<t.length;e+=3){let r=t[e]?.trim(),i=t[e+1]?.trim(),a=t[e+2]?.trim();r&&i&&a!==void 0&&n.push({name:r,currency:i,amount:a})}return n}function ae(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}function oe(e){return e.Result.ResultCode===0}function se(){try{if(typeof crypto<`u`&&crypto.randomUUID)return crypto.randomUUID()}catch{}return`${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`}async function x(e,t,n){if(!n.primaryShortCode?.trim())throw c({code:`VALIDATION_ERROR`,message:`primaryShortCode is required — the merchant's till number (debit party).`});if(!n.receiverShortCode?.trim())throw c({code:`VALIDATION_ERROR`,message:`receiverShortCode is required — the vendor's Paybill account (credit party).`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw c({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${n.amount} which rounds to ${r}).`});if(!n.paymentRef?.trim())throw c({code:`VALIDATION_ERROR`,message:`paymentRef is required — shown in the merchant's USSD prompt as the payment reference.`});if(!n.callbackUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`callbackUrl is required — Safaricom POSTs the transaction result here.`});if(!n.partnerName?.trim())throw c({code:`VALIDATION_ERROR`,message:`partnerName is required — your friendly name shown in the merchant's USSD prompt.`});let i={primaryShortCode:String(n.primaryShortCode),receiverShortCode:String(n.receiverShortCode),amount:String(r),paymentRef:n.paymentRef,callbackUrl:n.callbackUrl,partnerName:n.partnerName,RequestRefID:n.requestRefId??se()},{data:a}=await d(`${e}/v1/ussdpush/get-msisdn`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i});return a}function S(e){return e.resultCode===`0`}function ce(e){return e.resultCode===`4001`}function le(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 ue(e){return S(e)?e.transactionId??null:null}function de(e){return Number(e.amount)}function fe(e){return e.requestId}function pe(e){return S(e)?e.conversationID??null:null}async function C(e,t,n,r,i){if(!i.commandId)throw c({code:`VALIDATION_ERROR`,message:`commandId is required: "BusinessPayToBulk" | "BusinessPayment" | "SalaryPayment" | "PromotionPayment"`});let a=[`BusinessPayToBulk`,`BusinessPayment`,`SalaryPayment`,`PromotionPayment`];if(!a.includes(i.commandId))throw c({code:`VALIDATION_ERROR`,message:`commandId must be one of: ${a.join(`, `)}. Got: "${i.commandId}"`});let o=Math.round(i.amount);if(!Number.isFinite(o)||o<1)throw c({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${i.amount} which rounds to ${o}).`});if(!i.partyA?.trim())throw c({code:`VALIDATION_ERROR`,message:`partyA is required — your business shortcode from which money is deducted.`});if(!i.partyB?.trim())throw c({code:`VALIDATION_ERROR`,message:`partyB is required — the recipient shortcode (BusinessPayToBulk) or customer MSISDN (other commands).`});if(!i.accountReference?.trim())throw c({code:`VALIDATION_ERROR`,message:`accountReference is required — a reference for this transaction.`});if(!i.resultUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the B2C result here.`});if(!i.queueTimeOutUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let s={Initiator:r,SecurityCredential:n,CommandID:i.commandId,SenderIdentifierType:i.senderIdentifierType??`4`,RecieverIdentifierType:i.receiverIdentifierType??`4`,Amount:String(o),PartyA:String(i.partyA),PartyB:String(i.partyB),AccountReference:i.accountReference,Remarks:i.remarks??`B2C Payment`,QueueTimeOutURL:i.queueTimeOutUrl,ResultURL:i.resultUrl};i.requester?.trim()&&(s.Requester=String(i.requester));let{data:l}=await d(`${e}/mpesa/b2b/v1/paymentrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:s});return l}function me(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 w(e){return e.Result.ResultCode===0}function T(e){return e.Result.ResultCode!==0}function E(e){return e.Result.TransactionID??null}function D(e){return e.Result.ConversationID}function O(e){return e.Result.OriginatorConversationID}function k(e){return e.Result.ResultDesc}function A(e){let t=L(e,`Amount`);return t===void 0?null:Number(t)}function j(e){let t=L(e,`TransCompletedTime`);return t===void 0?null:String(t)}function M(e){let t=L(e,`DebitPartyCharges`);return t===void 0||t===``?null:String(t)}function N(e){let t=L(e,`ReceiverPartyPublicName`);return t===void 0?null:String(t)}function P(e){let t=L(e,`Currency`);return t===void 0?`KES`:String(t)}function F(e){let t=L(e,`DebitAccountBalance`);return t===void 0?null:String(t)}function I(e){let t=L(e,`InitiatorAccountCurrentBalance`);return t===void 0?null:String(t)}function L(e,t){let n=e.Result.ResultParameters?.ResultParameter;if(n)return(Array.isArray(n)?n:[n]).find(e=>e.Key===t)?.Value}async function R(e,t,n){if(!n.shortcode?.trim())throw c({code:`VALIDATION_ERROR`,message:`shortcode is required.`});if(!n.email?.trim())throw c({code:`VALIDATION_ERROR`,message:`email is required.`});if(!n.callbackUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`callbackUrl is required.`});let r={shortcode:n.shortcode,email:n.email,officialContact:n.officialContact,sendReminders:n.sendReminders,logo:n.logo??``,callbackUrl:n.callbackUrl},{data:i}=await d(`${e}/v1/billmanager-invoice/optin`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:r});return i}async function z(e,t,n){if(!n.externalReference?.trim())throw c({code:`VALIDATION_ERROR`,message:`externalReference is required.`});if(!n.partyA?.trim())throw c({code:`VALIDATION_ERROR`,message:`partyA (customer MSISDN) is required.`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw c({code:`VALIDATION_ERROR`,message:`amount must be ≥ 1 (got ${n.amount}).`});let i={externalReference:n.externalReference,billingPeriod:n.billingPeriod,invoiceName:n.invoiceName,dueDate:n.dueDate,accountReference:n.accountReference,amount:String(r),partyA:n.partyA,invoiceItems:n.invoiceItems?.map(e=>({itemName:e.itemName,amount:String(Math.round(e.amount))}))??[]},{data:a}=await d(`${e}/v1/billmanager-invoice/single-invoicing`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i});return a}async function he(e,t,n){if(!n.invoices?.length)throw c({code:`VALIDATION_ERROR`,message:`invoices array must not be empty.`});if(n.invoices.length>1e3)throw c({code:`VALIDATION_ERROR`,message:`Maximum 1 000 invoices per bulk request.`});let{data:r}=await d(`${e}/v1/billmanager-invoice/bulk-invoicing`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:n.invoices});return r}async function ge(e,t,n){if(!n.externalReference?.trim())throw c({code:`VALIDATION_ERROR`,message:`externalReference is required.`});let{data:r}=await d(`${e}/v1/billmanager-invoice/cancel-single-invoice`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:[{externalReference:n.externalReference}]});return r}const _e=[`mpesa`,`safaricom`,`.exe`,`.exec`,`cmd`,`sql`,`query`];function B(e,t){if(!e||!e.trim())throw c({code:`VALIDATION_ERROR`,message:`${t} is required`});let n=e.toLowerCase();for(let e of _e)if(n.includes(e))throw c({code:`VALIDATION_ERROR`,message:`${t} must not contain the keyword "${e}". Daraja rejects URLs containing: M-PESA, Safaricom, exe, exec, cmd, sql, query.`})}async function V(e,t,n){if(!n.shortCode)throw c({code:`VALIDATION_ERROR`,message:`shortCode is required`});if(!n.responseType)throw c({code:`VALIDATION_ERROR`,message:`responseType is required: "Completed" or "Cancelled" (sentence case, exactly as spelled)`});if(n.responseType!==`Completed`&&n.responseType!==`Cancelled`)throw c({code:`VALIDATION_ERROR`,message:`responseType must be exactly "Completed" or "Cancelled" (sentence case). Got: "${String(n.responseType)}"`});B(n.confirmationUrl,`confirmationUrl`),B(n.validationUrl,`validationUrl`);let r=n.apiVersion??`v2`,i={ShortCode:String(n.shortCode),ResponseType:n.responseType,ConfirmationURL:n.confirmationUrl,ValidationURL:n.validationUrl},{data:a}=await d(`${e}/mpesa/c2b/${r}/registerurl`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i});return a}async function H(e,t,n){if(!e.includes(`sandbox`))throw c({code:`VALIDATION_ERROR`,message:`C2B simulate is only available in the Sandbox environment. In production, customers initiate payments via M-PESA App, USSD, or SIM Toolkit.`});if(!n.shortCode)throw c({code:`VALIDATION_ERROR`,message:`shortCode is required.`});if(n.commandId!==`CustomerPayBillOnline`&&n.commandId!==`CustomerBuyGoodsOnline`)throw c({code:`VALIDATION_ERROR`,message:`commandId must be "CustomerPayBillOnline" or "CustomerBuyGoodsOnline". Got: "${String(n.commandId)}"`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw c({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${n.amount}).`});if(!n.msisdn)throw c({code:`VALIDATION_ERROR`,message:`msisdn is required. Sandbox test MSISDN: 254708374149.`});let i=n.commandId===`CustomerBuyGoodsOnline`,a=n.apiVersion??`v2`,o={ShortCode:Number(n.shortCode),CommandID:n.commandId,Amount:r,Msisdn:Number(n.msisdn)};i||(o.BillRefNumber=n.billRefNumber??``),i&&Object.prototype.hasOwnProperty.call(o,`BillRefNumber`)&&(delete o.BillRefNumber,console.warn(`[pesafy/simulateC2B] BillRefNumber leaked into Buy Goods payload — removed. This is a library bug; please report it.`));let{data:s}=await d(`${e}/mpesa/c2b/${a}/simulate`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}function ve(e){if(!e||typeof e!=`object`)return!1;let t=e;return typeof t.TransID==`string`&&typeof t.BusinessShortCode==`string`&&typeof t.TransAmount==`string`}function ye(e){return{ResultCode:`0`,ResultDesc:`Accepted`,...e?{ThirdPartyTransID:e}:{}}}function be(e=`C2B00016`,t=`Rejected`){return{ResultCode:e,ResultDesc:t}}function xe(){return{ResultCode:0,ResultDesc:`Success`}}function Se(e){return Number(e.TransAmount)}function U(e){return e.TransID}function Ce(e){return e.BillRefNumber}function we(e){return[e.FirstName,e.MiddleName,e.LastName].filter(Boolean).join(` `).trim()}function Te(e){return e.TransactionType===`Pay Bill`||e.TransactionType===`CustomerPayBillOnline`}function Ee(e){return e.TransactionType===`Buy Goods`||e.TransactionType===`CustomerBuyGoodsOnline`}async function De(e,t,n){if(!n.merchantName?.trim())throw c({code:`VALIDATION_ERROR`,message:`merchantName is required`});if(!n.refNo?.trim())throw c({code:`VALIDATION_ERROR`,message:`refNo (transaction reference) is required`});let r=Math.round(n.amount);if(!Number.isFinite(r)||r<1)throw c({code:`VALIDATION_ERROR`,message:`Amount must be at least 1 (got ${n.amount} which rounds to ${r}).`});if(!n.trxCode)throw c({code:`VALIDATION_ERROR`,message:`trxCode is required. Supported values: "BG" | "WA" | "PB" | "SM" | "SB"`});if(!n.cpi?.trim())throw c({code:`VALIDATION_ERROR`,message:`cpi (Credit Party Identifier) is required — e.g. till number, paybill, or MSISDN`});let i=n.size??300;if(i<1)throw c({code:`VALIDATION_ERROR`,message:`size must be a positive number of pixels`});let a={MerchantName:n.merchantName,RefNo:n.refNo,Amount:r,TrxCode:n.trxCode,CPI:n.cpi,Size:String(i)},{data:o}=await d(`${e}/mpesa/qrcode/v1/generate`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}async function Oe(e,t,n,r,i){if(!i.transactionId?.trim())throw c({code:`VALIDATION_ERROR`,message:`transactionId is required.`});if(!i.receiverParty?.trim())throw c({code:`VALIDATION_ERROR`,message:`receiverParty is required.`});if(![`1`,`2`,`4`].includes(i.receiverIdentifierType))throw c({code:`VALIDATION_ERROR`,message:`receiverIdentifierType must be "1", "2", or "4".`});let a=Math.round(i.amount);if(!Number.isFinite(a)||a<1)throw c({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${i.amount}).`});if(!i.resultUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`resultUrl is required.`});if(!i.queueTimeOutUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required.`});let o={Initiator:r,SecurityCredential:n,CommandID:`TransactionReversal`,TransactionID:i.transactionId,Amount:String(a),ReceiverParty:String(i.receiverParty),RecieverIdentifierType:i.receiverIdentifierType,ResultURL:i.resultUrl,QueueTimeOutURL:i.queueTimeOutUrl,Remarks:i.remarks??`Transaction Reversal`,Occasion:i.occasion??``},{data:s}=await d(`${e}/mpesa/reversal/v1/request`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}function ke(e){return e.Result.ResultCode===0}function Ae(e){return e.Result.TransactionID??null}function je(e){return e.Result.ConversationID}function W(e){let t=e.replace(/\D/g,``),n;if(t.startsWith(`254`)&&t.length===12)n=t;else if(t.startsWith(`0`)&&t.length===10)n=`254${t.slice(1)}`;else if(t.length===9)n=`254${t}`;else throw new s({code:`INVALID_PHONE`,message:`Cannot parse "${e}". Use 07XXXXXXXX, 2547XXXXXXXX, or +2547XXXXXXXX.`});if(n.length!==12)throw new s({code:`INVALID_PHONE`,message:`"${e}" normalised to "${n}" — expected 12 digits.`});return n}function G(e,t,n){return btoa(`${e}${t}${n}`)}function K(){let e=new Date,t=e=>e.toString().padStart(2,`0`);return[e.getFullYear(),t(e.getMonth()+1),t(e.getDate()),t(e.getHours()),t(e.getMinutes()),t(e.getSeconds())].join(``)}async function Me(e,t,n){let r=Math.round(n.amount);if(r<1)throw new s({code:`VALIDATION_ERROR`,message:`Amount must be at least KES 1 (got ${n.amount} which rounds to ${r}).`});let i=K(),a=n.partyB??n.shortCode,o={BusinessShortCode:n.shortCode,Password:G(n.shortCode,n.passKey,i),Timestamp:i,TransactionType:n.transactionType??`CustomerPayBillOnline`,Amount:r,PartyA:W(n.phoneNumber),PartyB:a,PhoneNumber:W(n.phoneNumber),CallBackURL:n.callbackUrl,AccountReference:n.accountReference.slice(0,12),TransactionDesc:n.transactionDesc.slice(0,13)},{data:c}=await d(`${e}/mpesa/stkpush/v1/processrequest`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o,retries:5,retryDelay:3e3});return c}async function Ne(e,t,n){let r=K(),i={BusinessShortCode:n.shortCode,Password:G(n.shortCode,n.passKey,r),Timestamp:r,CheckoutRequestID:n.checkoutRequestId},{data:a}=await d(`${e}/mpesa/stkpushquery/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:i});return a}function q(e){return e.ResultCode===0}function Pe(e,t){let n=e.Body.stkCallback;if(q(n))return n.CallbackMetadata.Item.find(e=>e.Name===t)?.Value}const Fe=`572572`,J=`PayTaxToKRA`;async function Y(e,t,n,r,i){let a=Math.round(i.amount);if(!Number.isFinite(a)||a<1)throw c({code:`VALIDATION_ERROR`,message:`amount must be a whole number ≥ 1 (got ${i.amount} which rounds to ${a}).`});if(!i.partyA)throw c({code:`VALIDATION_ERROR`,message:`partyA is required — your M-PESA business shortcode from which tax is deducted.`});if(!i.accountReference?.trim())throw c({code:`VALIDATION_ERROR`,message:`accountReference is required — the Payment Registration Number (PRN) issued by KRA.`});if(!i.resultUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the tax remittance result here.`});if(!i.queueTimeOutUrl?.trim())throw c({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on request timeout.`});let o={Initiator:r,SecurityCredential:n,CommandID:J,SenderIdentifierType:`4`,RecieverIdentifierType:`4`,Amount:String(a),PartyA:String(i.partyA),PartyB:i.partyB??`572572`,AccountReference:i.accountReference,Remarks:i.remarks??`Tax Remittance`,QueueTimeOutURL:i.queueTimeOutUrl,ResultURL:i.resultUrl},{data:s}=await d(`${e}/mpesa/b2b/v1/remittax`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:o});return s}async function Ie(e,t,n,r,i){if(!i.transactionId)throw c({code:`VALIDATION_ERROR`,message:`transactionId is required`});if(!i.partyA)throw c({code:`VALIDATION_ERROR`,message:`partyA is required (your business shortcode, till number, or MSISDN)`});if(!i.identifierType)throw c({code:`VALIDATION_ERROR`,message:`identifierType is required: "1" (MSISDN) | "2" (Till) | "4" (ShortCode)`});if(!i.resultUrl)throw c({code:`VALIDATION_ERROR`,message:`resultUrl is required — Safaricom POSTs the transaction result here`});if(!i.queueTimeOutUrl)throw c({code:`VALIDATION_ERROR`,message:`queueTimeOutUrl is required — Safaricom calls this on timeout`});let a={Initiator:r,SecurityCredential:n,CommandID:i.commandId??`TransactionStatusQuery`,TransactionID:i.transactionId,PartyA:i.partyA,IdentifierType:i.identifierType,ResultURL:i.resultUrl,QueueTimeOutURL:i.queueTimeOutUrl,Remarks:i.remarks??`Transaction Status Query`,Occasion:i.occasion??``},{data:o}=await d(`${e}/mpesa/transactionstatus/v1/query`,{method:`POST`,headers:{Authorization:`Bearer ${t}`},body:a});return o}const X={sandbox:`https://sandbox.safaricom.co.ke`,production:`https://api.safaricom.co.ke`};var Le=class{constructor(e){if(o(this,`config`,void 0),o(this,`tokenManager`,void 0),o(this,`baseUrl`,void 0),!e.consumerKey||!e.consumerSecret)throw new s({code:`INVALID_CREDENTIALS`,message:`consumerKey and consumerSecret are required.`});this.config=e,this.baseUrl=X[e.environment],this.tokenManager=new f(e.consumerKey,e.consumerSecret,this.baseUrl)}getToken(){return this.tokenManager.getAccessToken()}async buildSecurityCredential(){if(this.config.securityCredential)return this.config.securityCredential;if(!this.config.initiatorPassword)throw new s({code:`INVALID_CREDENTIALS`,message:`Provide securityCredential (pre-encrypted) OR (initiatorPassword + certificatePath/certificatePem).`});let t;if(this.config.certificatePem)t=this.config.certificatePem;else if(this.config.certificatePath)t=await e(this.config.certificatePath,`utf-8`);else throw new s({code:`INVALID_CREDENTIALS`,message:`certificatePath or certificatePem is required to encrypt the initiator password.`});return p(this.config.initiatorPassword,t)}requireInitiator(e){let t=this.config.initiatorName??``;if(!t)throw new s({code:`VALIDATION_ERROR`,message:`initiatorName is required for ${e}.`});return t}async stkPushSafe(e){try{return y(await this.stkPush(e))}catch(e){return b(e)}}async stkPush(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new s({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push.`});let r=await this.getToken();return Me(this.baseUrl,r,{...e,shortCode:t,passKey:n})}async stkQuery(e){let t=this.config.lipaNaMpesaShortCode??``,n=this.config.lipaNaMpesaPassKey??``;if(!t||!n)throw new s({code:`VALIDATION_ERROR`,message:`lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Query.`});let r=await this.getToken();return Ne(this.baseUrl,r,{...e,shortCode:t,passKey:n})}async transactionStatus(e){let t=this.requireInitiator(`Transaction Status`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Ie(this.baseUrl,n,r,t,e)}async accountBalance(e){let t=this.requireInitiator(`Account Balance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return re(this.baseUrl,n,r,t,e)}async reverseTransaction(e){let t=this.requireInitiator(`Reversal`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Oe(this.baseUrl,n,r,t,e)}async generateDynamicQR(e){let t=await this.getToken();return De(this.baseUrl,t,e)}async registerC2BUrls(e){let t=await this.getToken();return V(this.baseUrl,t,e)}async simulateC2B(e){let t=await this.getToken();return H(this.baseUrl,t,e)}async remitTax(e){let t=this.requireInitiator(`Tax Remittance`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return Y(this.baseUrl,n,r,t,e)}async b2bExpressCheckout(e){let t=await this.getToken();return x(this.baseUrl,t,e)}async b2cPayment(e){let t=this.requireInitiator(`B2C Payment`),[n,r]=await Promise.all([this.getToken(),this.buildSecurityCredential()]);return C(this.baseUrl,n,r,t,e)}async billManagerOptIn(e){let t=await this.getToken();return R(this.baseUrl,t,e)}async sendInvoice(e){let t=await this.getToken();return z(this.baseUrl,t,e)}async sendBulkInvoices(e){let t=await this.getToken();return he(this.baseUrl,t,e)}async cancelInvoice(e){let t=await this.getToken();return ge(this.baseUrl,t,e)}clearTokenCache(){this.tokenManager.clearCache()}get environment(){return this.config.environment}};const Re={maxRetries:1/0,initialDelay:1e3,maxDelay:36e5,backoffMultiplier:2,maxRetryDuration:720*60*60*1e3};async function ze(e,t={}){let n={...Re,...t},r=n.initialDelay,i=0,a=Date.now();for(;i<n.maxRetries;){if(i++,Date.now()-a>n.maxRetryDuration)return{success:!1,attempts:i,error:Error(`Max retry duration exceeded`)};try{return{success:!0,data:await e(),attempts:i}}catch(e){let t=e instanceof Error?e:Error(String(e));if(t.message.includes(`4`))return{success:!1,attempts:i,error:t};i<n.maxRetries&&(await new Promise(e=>setTimeout(e,r)),r=Math.min(r*n.backoffMultiplier,n.maxDelay))}}return{success:!1,attempts:i,error:Error(`Max retries exceeded`)}}const Z=[`196.201.214.200`,`196.201.214.206`,`196.201.213.114`,`196.201.214.207`,`196.201.214.208`,`196.201.213.44`,`196.201.212.127`,`196.201.212.138`,`196.201.212.129`,`196.201.212.136`,`196.201.212.74`,`196.201.212.69`];function Q(e,t=Z){return t.includes(e)}function $(e){try{let t=e;return t?.Body?.stkCallback?t:null}catch{return null}}function Be(e,t={}){if(!t.skipIPCheck&&t.requestIP&&!Q(t.requestIP,t.allowedIPs))return{success:!1,eventType:null,data:null,error:`IP address ${t.requestIP} is not in the Safaricom whitelist`};let n=$(e);return n?{success:!0,eventType:`stk_push`,data:n}:{success:!1,eventType:null,data:null,error:`Unknown or malformed webhook payload`}}function Ve(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`MpesaReceiptNumber`);return t?String(t.Value):null}function He(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`Amount`);return t?Number(t.Value):null}function Ue(e){let t=(e.Body?.stkCallback?.CallbackMetadata?.Item)?.find(e=>e.Name===`PhoneNumber`);return t?String(t.Value):null}function We(e){return e.Body?.stkCallback?.ResultCode===0}export{X as DARAJA_BASE_URLS,Fe as KRA_SHORTCODE,Le as Mpesa,s as PesafyError,Z as SAFARICOM_IPS,J as TAX_COMMAND_ID,ye as acceptC2BValidation,xe as acknowledgeC2BConfirmation,c as createError,p as encryptSecurityCredential,b as err,He as extractAmount,Ue as extractPhoneNumber,Ve as extractTransactionId,W as formatPhoneNumber,W as formatSafaricomPhone,ae as getAccountBalanceParam,de as getB2BAmount,pe as getB2BConversationId,fe as getB2BRequestId,ue as getB2BTransactionId,A as getB2CAmount,D as getB2CConversationId,P as getB2CCurrency,F as getB2CDebitAccountBalance,M as getB2CDebitPartyCharges,I as getB2CInitiatorAccountBalance,O as getB2COriginatorConversationId,N as getB2CReceiverPublicName,k as getB2CResultDesc,L as getB2CResultParam,j as getB2CTransactionCompletedTime,E as getB2CTransactionId,Ce as getC2BAccountRef,Se as getC2BAmount,we as getC2BCustomerName,U as getC2BTransactionId,Pe as getCallbackValue,je as getReversalConversationId,Ae as getReversalTransactionId,K as getTimestamp,Be as handleWebhook,d as httpRequest,x as initiateB2BExpressCheckout,C as initiateB2CPayment,oe as isAccountBalanceSuccess,le as isB2BCheckoutCallback,ce as isB2BCheckoutCancelled,S as isB2BCheckoutSuccess,T as isB2CFailure,me as isB2CResult,w as isB2CSuccess,Ee as isBuyGoodsPayment,ve as isC2BPayload,Te as isPaybillPayment,l as isPesafyError,ke as isReversalSuccess,q as isStkCallbackSuccess,We as isSuccessfulCallback,y as ok,ie as parseAccountBalance,$ as parseStkPushWebhook,V as registerC2BUrls,be as rejectC2BValidation,Y as remitTax,ze as retryWithBackoff,H as simulateC2B,m as toKesAmount,h as toMsisdn,ne as toNonEmpty,g as toPaybill,v as toShortCode,_ as toTill,Q as verifyWebhookIP};
|