pesafy 0.3.10 → 0.3.12
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/dist/index.cjs +239 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +393 -1
- package/dist/index.d.ts +393 -1
- package/dist/index.js +228 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -193,6 +193,174 @@ var TokenManager = class {
|
|
|
193
193
|
}
|
|
194
194
|
};
|
|
195
195
|
|
|
196
|
+
// src/mpesa/c2b/register-url.ts
|
|
197
|
+
var FORBIDDEN_URL_KEYWORDS = [
|
|
198
|
+
"mpesa",
|
|
199
|
+
"safaricom",
|
|
200
|
+
".exe",
|
|
201
|
+
".exec",
|
|
202
|
+
"cmd",
|
|
203
|
+
"sql",
|
|
204
|
+
"query"
|
|
205
|
+
];
|
|
206
|
+
function validateCallbackUrl(url, fieldName) {
|
|
207
|
+
if (!url || !url.trim()) {
|
|
208
|
+
throw createError({
|
|
209
|
+
code: "VALIDATION_ERROR",
|
|
210
|
+
message: `${fieldName} is required`
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const lower = url.toLowerCase();
|
|
214
|
+
for (const keyword of FORBIDDEN_URL_KEYWORDS) {
|
|
215
|
+
if (lower.includes(keyword)) {
|
|
216
|
+
throw createError({
|
|
217
|
+
code: "VALIDATION_ERROR",
|
|
218
|
+
message: `${fieldName} must not contain the keyword "${keyword}". Daraja rejects URLs containing: M-PESA, Safaricom, exe, exec, cmd, sql, query.`
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function registerC2BUrls(baseUrl, accessToken, request) {
|
|
224
|
+
if (!request.shortCode) {
|
|
225
|
+
throw createError({
|
|
226
|
+
code: "VALIDATION_ERROR",
|
|
227
|
+
message: "shortCode is required"
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (!request.responseType) {
|
|
231
|
+
throw createError({
|
|
232
|
+
code: "VALIDATION_ERROR",
|
|
233
|
+
message: 'responseType is required: "Completed" or "Cancelled" (sentence case, exactly as spelled)'
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (request.responseType !== "Completed" && request.responseType !== "Cancelled") {
|
|
237
|
+
throw createError({
|
|
238
|
+
code: "VALIDATION_ERROR",
|
|
239
|
+
message: `responseType must be exactly "Completed" or "Cancelled" (sentence case). Got: "${request.responseType}"`
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
validateCallbackUrl(request.confirmationUrl, "confirmationUrl");
|
|
243
|
+
validateCallbackUrl(request.validationUrl, "validationUrl");
|
|
244
|
+
const version = request.apiVersion ?? "v2";
|
|
245
|
+
const payload = {
|
|
246
|
+
ShortCode: String(request.shortCode),
|
|
247
|
+
ResponseType: request.responseType,
|
|
248
|
+
ConfirmationURL: request.confirmationUrl,
|
|
249
|
+
ValidationURL: request.validationUrl
|
|
250
|
+
};
|
|
251
|
+
const { data } = await httpRequest(
|
|
252
|
+
`${baseUrl}/mpesa/c2b/${version}/registerurl`,
|
|
253
|
+
{
|
|
254
|
+
method: "POST",
|
|
255
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
256
|
+
body: payload
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
return data;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/mpesa/c2b/simulate.ts
|
|
263
|
+
async function simulateC2B(baseUrl, accessToken, request) {
|
|
264
|
+
if (!baseUrl.includes("sandbox")) {
|
|
265
|
+
throw createError({
|
|
266
|
+
code: "VALIDATION_ERROR",
|
|
267
|
+
message: "C2B simulate is only available in the Sandbox environment. Production M-PESA payments must be initiated by the customer via M-PESA App, USSD, or SIM Toolkit."
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (!request.shortCode) {
|
|
271
|
+
throw createError({
|
|
272
|
+
code: "VALIDATION_ERROR",
|
|
273
|
+
message: "shortCode is required"
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (!request.commandId) {
|
|
277
|
+
throw createError({
|
|
278
|
+
code: "VALIDATION_ERROR",
|
|
279
|
+
message: 'commandId is required: "CustomerPayBillOnline" | "CustomerBuyGoodsOnline"'
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (request.commandId !== "CustomerPayBillOnline" && request.commandId !== "CustomerBuyGoodsOnline") {
|
|
283
|
+
throw createError({
|
|
284
|
+
code: "VALIDATION_ERROR",
|
|
285
|
+
message: `commandId must be "CustomerPayBillOnline" or "CustomerBuyGoodsOnline". Got: "${request.commandId}"`
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const amount = Math.round(request.amount);
|
|
289
|
+
if (!Number.isFinite(amount) || amount < 1) {
|
|
290
|
+
throw createError({
|
|
291
|
+
code: "VALIDATION_ERROR",
|
|
292
|
+
message: `amount must be a whole number \u2265 1 (got ${request.amount})`
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (!request.msisdn) {
|
|
296
|
+
throw createError({
|
|
297
|
+
code: "VALIDATION_ERROR",
|
|
298
|
+
message: "msisdn is required. Use the test phone number from the Daraja simulator."
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
const version = request.apiVersion ?? "v2";
|
|
302
|
+
const isBuyGoods = request.commandId === "CustomerBuyGoodsOnline";
|
|
303
|
+
const payload = {
|
|
304
|
+
ShortCode: Number(request.shortCode),
|
|
305
|
+
CommandID: request.commandId,
|
|
306
|
+
Amount: amount,
|
|
307
|
+
Msisdn: Number(request.msisdn)
|
|
308
|
+
};
|
|
309
|
+
if (!isBuyGoods) {
|
|
310
|
+
payload.BillRefNumber = request.billRefNumber ?? "";
|
|
311
|
+
}
|
|
312
|
+
const { data } = await httpRequest(
|
|
313
|
+
`${baseUrl}/mpesa/c2b/${version}/simulate`,
|
|
314
|
+
{
|
|
315
|
+
method: "POST",
|
|
316
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
317
|
+
body: payload
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
return data;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/mpesa/c2b/webhooks.ts
|
|
324
|
+
function isC2BPayload(body) {
|
|
325
|
+
if (!body || typeof body !== "object") return false;
|
|
326
|
+
const b = body;
|
|
327
|
+
return typeof b["TransID"] === "string" && typeof b["BusinessShortCode"] === "string" && typeof b["TransAmount"] === "string";
|
|
328
|
+
}
|
|
329
|
+
function acceptC2BValidation(thirdPartyTransID) {
|
|
330
|
+
return {
|
|
331
|
+
ResultCode: "0",
|
|
332
|
+
ResultDesc: "Accepted",
|
|
333
|
+
...thirdPartyTransID ? { ThirdPartyTransID: thirdPartyTransID } : {}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function rejectC2BValidation(resultCode = "C2B00016", resultDesc = "Rejected") {
|
|
337
|
+
return {
|
|
338
|
+
ResultCode: resultCode,
|
|
339
|
+
ResultDesc: resultDesc
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function acknowledgeC2BConfirmation() {
|
|
343
|
+
return { ResultCode: 0, ResultDesc: "Success" };
|
|
344
|
+
}
|
|
345
|
+
function getC2BAmount(payload) {
|
|
346
|
+
return Number(payload.TransAmount);
|
|
347
|
+
}
|
|
348
|
+
function getC2BTransactionId(payload) {
|
|
349
|
+
return payload.TransID;
|
|
350
|
+
}
|
|
351
|
+
function getC2BAccountRef(payload) {
|
|
352
|
+
return payload.BillRefNumber;
|
|
353
|
+
}
|
|
354
|
+
function getC2BCustomerName(payload) {
|
|
355
|
+
return [payload.FirstName, payload.MiddleName, payload.LastName].filter(Boolean).join(" ").trim();
|
|
356
|
+
}
|
|
357
|
+
function isPaybillPayment(payload) {
|
|
358
|
+
return payload.TransactionType === "Pay Bill" || payload.TransactionType === "CustomerPayBillOnline";
|
|
359
|
+
}
|
|
360
|
+
function isBuyGoodsPayment(payload) {
|
|
361
|
+
return payload.TransactionType === "Buy Goods" || payload.TransactionType === "CustomerBuyGoodsOnline";
|
|
362
|
+
}
|
|
363
|
+
|
|
196
364
|
// src/mpesa/dynamic-qr/generate.ts
|
|
197
365
|
async function generateDynamicQR(baseUrl, accessToken, request) {
|
|
198
366
|
if (!request.merchantName?.trim()) {
|
|
@@ -598,6 +766,65 @@ var Mpesa = class {
|
|
|
598
766
|
const token = await this.getToken();
|
|
599
767
|
return generateDynamicQR(this.baseUrl, token, request);
|
|
600
768
|
}
|
|
769
|
+
// ── C2B Register URL ──────────────────────────────────────────────────────
|
|
770
|
+
/**
|
|
771
|
+
* Registers your Confirmation and Validation URLs with M-PESA.
|
|
772
|
+
*
|
|
773
|
+
* Use v2 (default) for new integrations — callbacks include a masked MSISDN.
|
|
774
|
+
* Use v1 only if you need SHA256-hashed MSISDN in callbacks.
|
|
775
|
+
*
|
|
776
|
+
* Sandbox: URLs can be re-registered freely (overwriting existing ones).
|
|
777
|
+
* Production: One-time call. To change URLs, delete them via Daraja Self
|
|
778
|
+
* Services → URL Management, then call this again.
|
|
779
|
+
*
|
|
780
|
+
* URL rules (Daraja docs — enforced by this library):
|
|
781
|
+
* ✓ Must be publicly accessible
|
|
782
|
+
* ✓ Production: HTTPS required
|
|
783
|
+
* ✗ Must NOT contain: M-PESA, Safaricom, exe, exec, cmd, sql, query
|
|
784
|
+
* ✗ Do NOT use ngrok, mockbin, requestbin in production
|
|
785
|
+
* ✓ responseType must be exactly "Completed" or "Cancelled" (sentence case)
|
|
786
|
+
*
|
|
787
|
+
* External Validation (optional):
|
|
788
|
+
* By default it is disabled. To enable, email apisupport@safaricom.co.ke.
|
|
789
|
+
* When enabled, Safaricom calls your validationUrl before processing payment.
|
|
790
|
+
* You must respond within ~8 seconds.
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* await mpesa.registerC2BUrls({
|
|
794
|
+
* shortCode: "600984",
|
|
795
|
+
* responseType: "Completed",
|
|
796
|
+
* confirmationUrl: "https://yourdomain.com/mpesa/c2b/confirmation",
|
|
797
|
+
* validationUrl: "https://yourdomain.com/mpesa/c2b/validation",
|
|
798
|
+
* apiVersion: "v2", // default — recommended
|
|
799
|
+
* });
|
|
800
|
+
*/
|
|
801
|
+
async registerC2BUrls(request) {
|
|
802
|
+
const token = await this.getToken();
|
|
803
|
+
return registerC2BUrls(this.baseUrl, token, request);
|
|
804
|
+
}
|
|
805
|
+
// ── C2B Simulate (Sandbox ONLY) ───────────────────────────────────────────
|
|
806
|
+
/**
|
|
807
|
+
* Simulates a C2B customer payment. SANDBOX ONLY.
|
|
808
|
+
*
|
|
809
|
+
* In production, real customers initiate payments via M-PESA App, USSD,
|
|
810
|
+
* or SIM Toolkit — simulation is not available.
|
|
811
|
+
*
|
|
812
|
+
* The API version used here should match the version used when registering URLs.
|
|
813
|
+
*
|
|
814
|
+
* @example
|
|
815
|
+
* await mpesa.simulateC2B({
|
|
816
|
+
* shortCode: 600984,
|
|
817
|
+
* commandId: "CustomerPayBillOnline",
|
|
818
|
+
* amount: 10,
|
|
819
|
+
* msisdn: 254708374149, // Daraja test MSISDN
|
|
820
|
+
* billRefNumber: "INV-001", // account ref for Paybill; null for Till
|
|
821
|
+
* apiVersion: "v2", // must match registered URL version
|
|
822
|
+
* });
|
|
823
|
+
*/
|
|
824
|
+
async simulateC2B(request) {
|
|
825
|
+
const token = await this.getToken();
|
|
826
|
+
return simulateC2B(this.baseUrl, token, request);
|
|
827
|
+
}
|
|
601
828
|
/** Force the cached OAuth token to be refreshed on the next API call */
|
|
602
829
|
clearTokenCache() {
|
|
603
830
|
this.tokenManager.clearCache();
|
|
@@ -726,19 +953,31 @@ exports.DARAJA_BASE_URLS = DARAJA_BASE_URLS;
|
|
|
726
953
|
exports.Mpesa = Mpesa;
|
|
727
954
|
exports.PesafyError = PesafyError;
|
|
728
955
|
exports.SAFARICOM_IPS = SAFARICOM_IPS;
|
|
956
|
+
exports.acceptC2BValidation = acceptC2BValidation;
|
|
957
|
+
exports.acknowledgeC2BConfirmation = acknowledgeC2BConfirmation;
|
|
729
958
|
exports.createError = createError;
|
|
730
959
|
exports.encryptSecurityCredential = encryptSecurityCredential;
|
|
731
960
|
exports.extractAmount = extractAmount;
|
|
732
961
|
exports.extractPhoneNumber = extractPhoneNumber;
|
|
733
962
|
exports.extractTransactionId = extractTransactionId;
|
|
734
963
|
exports.formatPhoneNumber = formatSafaricomPhone;
|
|
964
|
+
exports.getC2BAccountRef = getC2BAccountRef;
|
|
965
|
+
exports.getC2BAmount = getC2BAmount;
|
|
966
|
+
exports.getC2BCustomerName = getC2BCustomerName;
|
|
967
|
+
exports.getC2BTransactionId = getC2BTransactionId;
|
|
735
968
|
exports.getCallbackValue = getCallbackValue;
|
|
736
969
|
exports.getTimestamp = getTimestamp;
|
|
737
970
|
exports.handleWebhook = handleWebhook;
|
|
971
|
+
exports.isBuyGoodsPayment = isBuyGoodsPayment;
|
|
972
|
+
exports.isC2BPayload = isC2BPayload;
|
|
973
|
+
exports.isPaybillPayment = isPaybillPayment;
|
|
738
974
|
exports.isStkCallbackSuccess = isStkCallbackSuccess;
|
|
739
975
|
exports.isSuccessfulCallback = isSuccessfulCallback;
|
|
740
976
|
exports.parseStkPushWebhook = parseStkPushWebhook;
|
|
977
|
+
exports.registerC2BUrls = registerC2BUrls;
|
|
978
|
+
exports.rejectC2BValidation = rejectC2BValidation;
|
|
741
979
|
exports.retryWithBackoff = retryWithBackoff;
|
|
980
|
+
exports.simulateC2B = simulateC2B;
|
|
742
981
|
exports.verifyWebhookIP = verifyWebhookIP;
|
|
743
982
|
//# sourceMappingURL=index.cjs.map
|
|
744
983
|
//# sourceMappingURL=index.cjs.map
|