pesafy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pesafy might be problematic. Click here for more details.
- package/README.md +177 -0
- package/dist/components/react/index.cjs +230 -0
- package/dist/components/react/index.cjs.map +1 -0
- package/dist/components/react/index.d.cts +63 -0
- package/dist/components/react/index.d.ts +63 -0
- package/dist/components/react/index.js +200 -0
- package/dist/components/react/index.js.map +1 -0
- package/dist/components/react/styles.css +90 -0
- package/dist/components/react/styles.css.map +1 -0
- package/dist/components/react/styles.d.cts +2 -0
- package/dist/components/react/styles.d.ts +2 -0
- package/dist/index.cjs +729 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +416 -0
- package/dist/index.d.ts +416 -0
- package/dist/index.js +717 -0
- package/dist/index.js.map +1 -0
- package/package.json +105 -0
- package/src/components/vue/PaymentButton.vue +71 -0
- package/src/components/vue/PaymentForm.vue +164 -0
- package/src/components/vue/PaymentStatus.vue +68 -0
- package/src/components/vue/QRCode.vue +39 -0
- package/src/components/vue/index.ts +13 -0
- package/src/components/vue/shims-vue.d.ts +39 -0
- package/src/components/vue/types.ts +15 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// Pesafy - Payment Gateway Library
|
|
6
|
+
// https://github.com/levos-snr/pesafy
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
10
|
+
|
|
11
|
+
// src/utils/errors/error-factory.ts
|
|
12
|
+
var PesafyError = class _PesafyError extends Error {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
super(options.message);
|
|
15
|
+
__publicField(this, "code");
|
|
16
|
+
__publicField(this, "statusCode");
|
|
17
|
+
__publicField(this, "response");
|
|
18
|
+
__publicField(this, "requestId");
|
|
19
|
+
__publicField(this, "cause");
|
|
20
|
+
Object.defineProperty(this, "name", { value: "PesafyError" });
|
|
21
|
+
this.code = options.code;
|
|
22
|
+
this.statusCode = options.statusCode;
|
|
23
|
+
this.response = options.response;
|
|
24
|
+
this.requestId = options.requestId;
|
|
25
|
+
this.cause = options.cause;
|
|
26
|
+
if (Error.captureStackTrace) {
|
|
27
|
+
Error.captureStackTrace(this, _PesafyError);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
toJSON() {
|
|
31
|
+
return {
|
|
32
|
+
name: this.name,
|
|
33
|
+
code: this.code,
|
|
34
|
+
message: this.message,
|
|
35
|
+
statusCode: this.statusCode,
|
|
36
|
+
requestId: this.requestId
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function createError(options) {
|
|
41
|
+
return new PesafyError(options);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/utils/http/client.ts
|
|
45
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
46
|
+
async function httpRequest(url, options = {}) {
|
|
47
|
+
const {
|
|
48
|
+
method = "GET",
|
|
49
|
+
headers = {},
|
|
50
|
+
body,
|
|
51
|
+
timeout = DEFAULT_TIMEOUT
|
|
52
|
+
} = options;
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(url, {
|
|
57
|
+
method,
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
...headers
|
|
61
|
+
},
|
|
62
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
63
|
+
signal: controller.signal
|
|
64
|
+
});
|
|
65
|
+
clearTimeout(timeoutId);
|
|
66
|
+
let data;
|
|
67
|
+
const text = await response.text();
|
|
68
|
+
try {
|
|
69
|
+
data = text ? JSON.parse(text) : {};
|
|
70
|
+
} catch {
|
|
71
|
+
data = { raw: text };
|
|
72
|
+
}
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new PesafyError({
|
|
75
|
+
code: "API_ERROR",
|
|
76
|
+
message: `Request failed with status ${response.status}`,
|
|
77
|
+
statusCode: response.status,
|
|
78
|
+
response: data
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
data,
|
|
83
|
+
status: response.status,
|
|
84
|
+
headers: response.headers
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
clearTimeout(timeoutId);
|
|
88
|
+
if (error instanceof PesafyError) throw error;
|
|
89
|
+
if (error instanceof Error) {
|
|
90
|
+
if (error.name === "AbortError") {
|
|
91
|
+
throw new PesafyError({
|
|
92
|
+
code: "TIMEOUT",
|
|
93
|
+
message: `Request timed out after ${timeout}ms`,
|
|
94
|
+
cause: error
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
throw new PesafyError({
|
|
98
|
+
code: "NETWORK_ERROR",
|
|
99
|
+
message: error.message,
|
|
100
|
+
cause: error
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
throw new PesafyError({
|
|
104
|
+
code: "REQUEST_FAILED",
|
|
105
|
+
message: "An unknown error occurred",
|
|
106
|
+
cause: error
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/core/auth/token-manager.ts
|
|
112
|
+
var TOKEN_BUFFER_SECONDS = 60;
|
|
113
|
+
var TokenManager = class {
|
|
114
|
+
constructor(consumerKey, consumerSecret, baseUrl) {
|
|
115
|
+
__publicField(this, "consumerKey");
|
|
116
|
+
__publicField(this, "consumerSecret");
|
|
117
|
+
__publicField(this, "baseUrl");
|
|
118
|
+
__publicField(this, "cachedToken", null);
|
|
119
|
+
__publicField(this, "tokenExpiresAt", 0);
|
|
120
|
+
this.consumerKey = consumerKey;
|
|
121
|
+
this.consumerSecret = consumerSecret;
|
|
122
|
+
this.baseUrl = baseUrl;
|
|
123
|
+
}
|
|
124
|
+
getAuthHeader() {
|
|
125
|
+
const credentials = `${this.consumerKey}:${this.consumerSecret}`;
|
|
126
|
+
const encoded = Buffer.from(credentials, "utf-8").toString("base64");
|
|
127
|
+
return `Basic ${encoded}`;
|
|
128
|
+
}
|
|
129
|
+
async getAccessToken() {
|
|
130
|
+
const now = Date.now() / 1e3;
|
|
131
|
+
if (this.cachedToken && this.tokenExpiresAt > now + TOKEN_BUFFER_SECONDS) {
|
|
132
|
+
return this.cachedToken;
|
|
133
|
+
}
|
|
134
|
+
const url = `${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`;
|
|
135
|
+
const response = await httpRequest(url, {
|
|
136
|
+
method: "GET",
|
|
137
|
+
headers: {
|
|
138
|
+
Authorization: this.getAuthHeader()
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
const data = response.data;
|
|
142
|
+
if (!data.access_token) {
|
|
143
|
+
throw new PesafyError({
|
|
144
|
+
code: "AUTH_FAILED",
|
|
145
|
+
message: "Failed to obtain access token",
|
|
146
|
+
response: data
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
this.cachedToken = data.access_token;
|
|
150
|
+
this.tokenExpiresAt = now + (data.expires_in ?? 3600);
|
|
151
|
+
return this.cachedToken;
|
|
152
|
+
}
|
|
153
|
+
clearCache() {
|
|
154
|
+
this.cachedToken = null;
|
|
155
|
+
this.tokenExpiresAt = 0;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
function encryptSecurityCredential(initiatorPassword, certificatePem) {
|
|
159
|
+
try {
|
|
160
|
+
const passwordBuffer = Buffer.from(initiatorPassword, "utf-8");
|
|
161
|
+
const encrypted = crypto.publicEncrypt(
|
|
162
|
+
{
|
|
163
|
+
key: certificatePem,
|
|
164
|
+
padding: 1
|
|
165
|
+
// RSA_PKCS1_PADDING
|
|
166
|
+
},
|
|
167
|
+
passwordBuffer
|
|
168
|
+
);
|
|
169
|
+
return encrypted.toString("base64");
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw new PesafyError({
|
|
172
|
+
code: "ENCRYPTION_FAILED",
|
|
173
|
+
message: "Failed to encrypt security credential",
|
|
174
|
+
cause: error
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/mpesa/b2b/b2b.ts
|
|
180
|
+
async function processB2B(baseUrl, accessToken, securityCredential, initiatorName, request) {
|
|
181
|
+
const body = {
|
|
182
|
+
Initiator: initiatorName,
|
|
183
|
+
SecurityCredential: securityCredential,
|
|
184
|
+
CommandID: request.commandId ?? "BusinessPayBill",
|
|
185
|
+
SenderIdentifierType: request.senderIdentifierType ?? 4,
|
|
186
|
+
RecieverIdentifierType: request.receiverIdentifierType ?? 4,
|
|
187
|
+
Amount: Math.round(request.amount),
|
|
188
|
+
PartyA: request.shortCode,
|
|
189
|
+
PartyB: request.receiverShortCode,
|
|
190
|
+
AccountReference: request.accountReference ?? "",
|
|
191
|
+
Remarks: request.remarks ?? "B2B Payment",
|
|
192
|
+
QueueTimeOutURL: request.timeoutUrl,
|
|
193
|
+
ResultURL: request.resultUrl
|
|
194
|
+
};
|
|
195
|
+
const { data } = await httpRequest(
|
|
196
|
+
`${baseUrl}/mpesa/b2b/v1/paymentrequest`,
|
|
197
|
+
{
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
200
|
+
body
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
return data;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/mpesa/stk-push/utils.ts
|
|
207
|
+
function formatPhoneNumber(phone) {
|
|
208
|
+
const cleaned = phone.replace(/\D/g, "");
|
|
209
|
+
if (cleaned.startsWith("0")) return "254" + cleaned.slice(1);
|
|
210
|
+
if (cleaned.startsWith("254")) return cleaned;
|
|
211
|
+
return "254" + cleaned;
|
|
212
|
+
}
|
|
213
|
+
function getStkPushPassword(shortCode, passKey) {
|
|
214
|
+
const timestamp = getTimestamp();
|
|
215
|
+
const password = `${shortCode}${passKey}${timestamp}`;
|
|
216
|
+
return Buffer.from(password, "utf-8").toString("base64");
|
|
217
|
+
}
|
|
218
|
+
function getTimestamp() {
|
|
219
|
+
const now = /* @__PURE__ */ new Date();
|
|
220
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
221
|
+
return [
|
|
222
|
+
now.getFullYear(),
|
|
223
|
+
pad(now.getMonth() + 1),
|
|
224
|
+
pad(now.getDate()),
|
|
225
|
+
pad(now.getHours()),
|
|
226
|
+
pad(now.getMinutes()),
|
|
227
|
+
pad(now.getSeconds())
|
|
228
|
+
].join("");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/mpesa/b2c/b2c.ts
|
|
232
|
+
async function processB2C(baseUrl, accessToken, securityCredential, initiatorName, request) {
|
|
233
|
+
const body = {
|
|
234
|
+
OriginatorConversationID: `AG_${Date.now()}_${Math.random().toString(36).slice(2)}`,
|
|
235
|
+
InitiatorName: initiatorName,
|
|
236
|
+
SecurityCredential: securityCredential,
|
|
237
|
+
CommandID: request.commandId ?? "BusinessPayment",
|
|
238
|
+
Amount: Math.round(request.amount),
|
|
239
|
+
PartyA: request.shortCode,
|
|
240
|
+
PartyB: formatPhoneNumber(request.phoneNumber),
|
|
241
|
+
Remarks: request.remarks ?? "Payment",
|
|
242
|
+
QueueTimeOutURL: request.timeoutUrl,
|
|
243
|
+
ResultURL: request.resultUrl,
|
|
244
|
+
Occasion: request.occasion ?? ""
|
|
245
|
+
};
|
|
246
|
+
const { data } = await httpRequest(
|
|
247
|
+
`${baseUrl}/mpesa/b2c/v3/paymentrequest`,
|
|
248
|
+
{
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
251
|
+
body
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
return data;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/mpesa/c2b/register-url.ts
|
|
258
|
+
async function registerC2BUrls(baseUrl, accessToken, request) {
|
|
259
|
+
const body = {
|
|
260
|
+
ShortCode: request.shortCode,
|
|
261
|
+
ResponseType: request.responseType ?? "Completed",
|
|
262
|
+
ConfirmationURL: request.confirmationUrl,
|
|
263
|
+
ValidationURL: request.validationUrl
|
|
264
|
+
};
|
|
265
|
+
const { data } = await httpRequest(
|
|
266
|
+
`${baseUrl}/mpesa/c2b/v2/registerurl`,
|
|
267
|
+
{
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
270
|
+
body
|
|
271
|
+
}
|
|
272
|
+
);
|
|
273
|
+
return data;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/mpesa/c2b/simulate.ts
|
|
277
|
+
async function simulateC2B(baseUrl, accessToken, request) {
|
|
278
|
+
const body = {
|
|
279
|
+
ShortCode: request.shortCode,
|
|
280
|
+
CommandID: "CustomerPayBillOnline",
|
|
281
|
+
Amount: Math.round(request.amount),
|
|
282
|
+
Msisdn: formatPhoneNumber(request.phoneNumber),
|
|
283
|
+
BillRefNumber: request.billRefNumber ?? "default"
|
|
284
|
+
};
|
|
285
|
+
const { data } = await httpRequest(
|
|
286
|
+
`${baseUrl}/mpesa/c2b/v2/simulate`,
|
|
287
|
+
{
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
290
|
+
body
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
return data;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/mpesa/qr-code/dynamic-qr.ts
|
|
297
|
+
async function generateDynamicQR(baseUrl, accessToken, request) {
|
|
298
|
+
const body = {
|
|
299
|
+
MerchantName: request.merchantName,
|
|
300
|
+
RefNo: request.refNo,
|
|
301
|
+
Amount: Math.round(request.amount),
|
|
302
|
+
TrxCode: request.trxCode,
|
|
303
|
+
CPI: request.cpi,
|
|
304
|
+
Size: request.size ?? "300"
|
|
305
|
+
};
|
|
306
|
+
const { data } = await httpRequest(
|
|
307
|
+
`${baseUrl}/mpesa/qrcode/v1/generate`,
|
|
308
|
+
{
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
311
|
+
body
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
return data;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/mpesa/reversal/reversal.ts
|
|
318
|
+
async function processReversal(baseUrl, accessToken, securityCredential, initiatorName, request) {
|
|
319
|
+
const body = {
|
|
320
|
+
Initiator: initiatorName,
|
|
321
|
+
SecurityCredential: securityCredential,
|
|
322
|
+
CommandID: "TransactionReversal",
|
|
323
|
+
TransactionID: request.transactionId,
|
|
324
|
+
Amount: Math.round(request.amount),
|
|
325
|
+
ReceiverParty: request.shortCode,
|
|
326
|
+
RecieverIdentifierType: 4,
|
|
327
|
+
ResultURL: request.resultUrl,
|
|
328
|
+
QueueTimeOutURL: request.timeoutUrl,
|
|
329
|
+
Remarks: request.remarks ?? "Reversal",
|
|
330
|
+
Occasion: request.occasion ?? "Reversal"
|
|
331
|
+
};
|
|
332
|
+
const { data } = await httpRequest(
|
|
333
|
+
`${baseUrl}/mpesa/reversal/v1/request`,
|
|
334
|
+
{
|
|
335
|
+
method: "POST",
|
|
336
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
337
|
+
body
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
return data;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/mpesa/stk-push/stk-push.ts
|
|
344
|
+
async function processStkPush(baseUrl, accessToken, request) {
|
|
345
|
+
const body = {
|
|
346
|
+
BusinessShortCode: request.shortCode,
|
|
347
|
+
Password: getStkPushPassword(request.shortCode, request.passKey),
|
|
348
|
+
Timestamp: getTimestamp(),
|
|
349
|
+
TransactionType: request.transactionType ?? "CustomerPayBillOnline",
|
|
350
|
+
Amount: Math.round(request.amount),
|
|
351
|
+
PartyA: formatPhoneNumber(request.phoneNumber),
|
|
352
|
+
PartyB: request.shortCode,
|
|
353
|
+
PhoneNumber: formatPhoneNumber(request.phoneNumber),
|
|
354
|
+
CallBackURL: request.callbackUrl,
|
|
355
|
+
AccountReference: request.accountReference.slice(0, 12),
|
|
356
|
+
TransactionDesc: request.transactionDesc.slice(0, 13)
|
|
357
|
+
};
|
|
358
|
+
const { data } = await httpRequest(
|
|
359
|
+
`${baseUrl}/mpesa/stkpush/v1/processrequest`,
|
|
360
|
+
{
|
|
361
|
+
method: "POST",
|
|
362
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
363
|
+
body
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
return data;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/mpesa/stk-push/stk-query.ts
|
|
370
|
+
async function queryStkPush(baseUrl, accessToken, request) {
|
|
371
|
+
const body = {
|
|
372
|
+
BusinessShortCode: request.shortCode,
|
|
373
|
+
Password: getStkPushPassword(request.shortCode, request.passKey),
|
|
374
|
+
Timestamp: getTimestamp(),
|
|
375
|
+
CheckoutRequestID: request.checkoutRequestId
|
|
376
|
+
};
|
|
377
|
+
const { data } = await httpRequest(
|
|
378
|
+
`${baseUrl}/mpesa/stkpushquery/v1/query`,
|
|
379
|
+
{
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
382
|
+
body
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
return data;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/mpesa/transaction-status/query.ts
|
|
389
|
+
async function queryTransactionStatus(baseUrl, accessToken, securityCredential, initiatorName, request) {
|
|
390
|
+
const body = {
|
|
391
|
+
Initiator: initiatorName,
|
|
392
|
+
SecurityCredential: securityCredential,
|
|
393
|
+
CommandID: "TransactionStatusQuery",
|
|
394
|
+
TransactionID: request.transactionId,
|
|
395
|
+
PartyA: request.shortCode,
|
|
396
|
+
IdentifierType: request.identifierType ?? 4,
|
|
397
|
+
ResultURL: request.resultUrl,
|
|
398
|
+
QueueTimeOutURL: request.timeoutUrl,
|
|
399
|
+
Remarks: "Status",
|
|
400
|
+
Occasion: "Query"
|
|
401
|
+
};
|
|
402
|
+
const { data } = await httpRequest(
|
|
403
|
+
`${baseUrl}/mpesa/transactionstatus/v1/query`,
|
|
404
|
+
{
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
407
|
+
body
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
return data;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/mpesa/types.ts
|
|
414
|
+
var DARAJA_BASE_URLS = {
|
|
415
|
+
sandbox: "https://sandbox.safaricom.co.ke",
|
|
416
|
+
production: "https://api.safaricom.co.ke"
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// src/mpesa/index.ts
|
|
420
|
+
var Mpesa = class {
|
|
421
|
+
constructor(config) {
|
|
422
|
+
__publicField(this, "config");
|
|
423
|
+
__publicField(this, "tokenManager");
|
|
424
|
+
__publicField(this, "baseUrl");
|
|
425
|
+
this.config = config;
|
|
426
|
+
this.baseUrl = DARAJA_BASE_URLS[config.environment];
|
|
427
|
+
this.tokenManager = new TokenManager(
|
|
428
|
+
config.consumerKey,
|
|
429
|
+
config.consumerSecret,
|
|
430
|
+
this.baseUrl
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
async getToken() {
|
|
434
|
+
return this.tokenManager.getAccessToken();
|
|
435
|
+
}
|
|
436
|
+
async getSecurityCredential() {
|
|
437
|
+
if (this.config.securityCredential) return this.config.securityCredential;
|
|
438
|
+
if (!this.config.initiatorPassword) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
"Security credential required: provide securityCredential or (initiatorPassword + certificatePath/certificatePem)"
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
let cert;
|
|
444
|
+
if (this.config.certificatePem) {
|
|
445
|
+
cert = this.config.certificatePem;
|
|
446
|
+
} else if (this.config.certificatePath) {
|
|
447
|
+
cert = await Bun.file(this.config.certificatePath).text();
|
|
448
|
+
} else {
|
|
449
|
+
throw new Error(
|
|
450
|
+
"certificatePath or certificatePem required for B2C/B2B/Reversal"
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return encryptSecurityCredential(this.config.initiatorPassword, cert);
|
|
454
|
+
}
|
|
455
|
+
/** STK Push (M-Pesa Express) - Initiate payment on customer phone */
|
|
456
|
+
async stkPush(request) {
|
|
457
|
+
const shortCode = this.config.lipaNaMpesaShortCode ?? "";
|
|
458
|
+
const passKey = this.config.lipaNaMpesaPassKey ?? "";
|
|
459
|
+
if (!shortCode || !passKey) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
"lipaNaMpesaShortCode and lipaNaMpesaPassKey required for STK Push"
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
const token = await this.getToken();
|
|
465
|
+
return processStkPush(this.baseUrl, token, {
|
|
466
|
+
...request,
|
|
467
|
+
shortCode,
|
|
468
|
+
passKey
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
/** STK Query - Check STK Push transaction status */
|
|
472
|
+
async stkQuery(request) {
|
|
473
|
+
const shortCode = this.config.lipaNaMpesaShortCode ?? "";
|
|
474
|
+
const passKey = this.config.lipaNaMpesaPassKey ?? "";
|
|
475
|
+
if (!shortCode || !passKey) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
"lipaNaMpesaShortCode and lipaNaMpesaPassKey required for STK Query"
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const token = await this.getToken();
|
|
481
|
+
return queryStkPush(this.baseUrl, token, {
|
|
482
|
+
...request,
|
|
483
|
+
shortCode,
|
|
484
|
+
passKey
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
/** B2C - Send money to customer */
|
|
488
|
+
async b2c(request) {
|
|
489
|
+
const initiator = this.config.initiatorName ?? "";
|
|
490
|
+
const securityCred = await this.getSecurityCredential();
|
|
491
|
+
if (!initiator) throw new Error("initiatorName required for B2C");
|
|
492
|
+
const token = await this.getToken();
|
|
493
|
+
return processB2C(this.baseUrl, token, securityCred, initiator, request);
|
|
494
|
+
}
|
|
495
|
+
/** B2B - Send money to business */
|
|
496
|
+
async b2b(request) {
|
|
497
|
+
const initiator = this.config.initiatorName ?? "";
|
|
498
|
+
const securityCred = await this.getSecurityCredential();
|
|
499
|
+
if (!initiator) throw new Error("initiatorName required for B2B");
|
|
500
|
+
const token = await this.getToken();
|
|
501
|
+
return processB2B(this.baseUrl, token, securityCred, initiator, request);
|
|
502
|
+
}
|
|
503
|
+
/** C2B - Register validation/confirmation URLs */
|
|
504
|
+
async c2bRegisterUrls(request) {
|
|
505
|
+
const token = await this.getToken();
|
|
506
|
+
return registerC2BUrls(this.baseUrl, token, request);
|
|
507
|
+
}
|
|
508
|
+
/** C2B - Simulate payment (sandbox only) */
|
|
509
|
+
async c2bSimulate(request) {
|
|
510
|
+
const token = await this.getToken();
|
|
511
|
+
return simulateC2B(this.baseUrl, token, request);
|
|
512
|
+
}
|
|
513
|
+
/** Dynamic QR - Generate LIPA NA M-PESA QR code */
|
|
514
|
+
async qrCode(request) {
|
|
515
|
+
const token = await this.getToken();
|
|
516
|
+
return generateDynamicQR(this.baseUrl, token, request);
|
|
517
|
+
}
|
|
518
|
+
/** Transaction Status - Query transaction status */
|
|
519
|
+
async transactionStatus(request) {
|
|
520
|
+
const initiator = this.config.initiatorName ?? "";
|
|
521
|
+
const securityCred = await this.getSecurityCredential();
|
|
522
|
+
if (!initiator)
|
|
523
|
+
throw new Error("initiatorName required for Transaction Status");
|
|
524
|
+
const token = await this.getToken();
|
|
525
|
+
return queryTransactionStatus(
|
|
526
|
+
this.baseUrl,
|
|
527
|
+
token,
|
|
528
|
+
securityCred,
|
|
529
|
+
initiator,
|
|
530
|
+
request
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
/** Reversal - Reverse a transaction */
|
|
534
|
+
async reversal(request) {
|
|
535
|
+
const initiator = this.config.initiatorName ?? "";
|
|
536
|
+
const securityCred = await this.getSecurityCredential();
|
|
537
|
+
if (!initiator) throw new Error("initiatorName required for Reversal");
|
|
538
|
+
const token = await this.getToken();
|
|
539
|
+
return processReversal(
|
|
540
|
+
this.baseUrl,
|
|
541
|
+
token,
|
|
542
|
+
securityCred,
|
|
543
|
+
initiator,
|
|
544
|
+
request
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// src/mpesa/webhooks/retry.ts
|
|
550
|
+
var DEFAULT_OPTIONS = {
|
|
551
|
+
maxRetries: Infinity,
|
|
552
|
+
initialDelay: 1e3,
|
|
553
|
+
// 1 second
|
|
554
|
+
maxDelay: 36e5,
|
|
555
|
+
// 1 hour
|
|
556
|
+
backoffMultiplier: 2,
|
|
557
|
+
maxRetryDuration: 30 * 24 * 60 * 60 * 1e3
|
|
558
|
+
// 30 days
|
|
559
|
+
};
|
|
560
|
+
async function retryWithBackoff(fn, options = {}) {
|
|
561
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
562
|
+
let delay = opts.initialDelay;
|
|
563
|
+
let attempts = 0;
|
|
564
|
+
const startTime = Date.now();
|
|
565
|
+
while (attempts < opts.maxRetries) {
|
|
566
|
+
attempts++;
|
|
567
|
+
if (Date.now() - startTime > opts.maxRetryDuration) {
|
|
568
|
+
return {
|
|
569
|
+
success: false,
|
|
570
|
+
attempts,
|
|
571
|
+
error: new Error("Max retry duration exceeded")
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
const data = await fn();
|
|
576
|
+
return { success: true, data, attempts };
|
|
577
|
+
} catch (error) {
|
|
578
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
579
|
+
if (err.message.includes("4")) {
|
|
580
|
+
return { success: false, attempts, error: err };
|
|
581
|
+
}
|
|
582
|
+
if (attempts < opts.maxRetries) {
|
|
583
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
584
|
+
delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelay);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
success: false,
|
|
590
|
+
attempts,
|
|
591
|
+
error: new Error("Max retries exceeded")
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/mpesa/webhooks/signature-verifier.ts
|
|
596
|
+
var SAFARICOM_IPS = [
|
|
597
|
+
"196.201.214.200",
|
|
598
|
+
"196.201.214.206",
|
|
599
|
+
"196.201.213.114",
|
|
600
|
+
"196.201.214.207",
|
|
601
|
+
"196.201.214.208",
|
|
602
|
+
"196.201.213.44",
|
|
603
|
+
"196.201.212.127",
|
|
604
|
+
"196.201.212.138",
|
|
605
|
+
"196.201.212.129",
|
|
606
|
+
"196.201.212.136",
|
|
607
|
+
"196.201.212.74",
|
|
608
|
+
"196.201.212.69"
|
|
609
|
+
];
|
|
610
|
+
function verifyWebhookIP(requestIP, allowedIPs = SAFARICOM_IPS) {
|
|
611
|
+
return allowedIPs.includes(requestIP);
|
|
612
|
+
}
|
|
613
|
+
function parseStkPushWebhook(body) {
|
|
614
|
+
try {
|
|
615
|
+
const parsed = body;
|
|
616
|
+
if (parsed.Body?.stkCallback) return parsed;
|
|
617
|
+
return null;
|
|
618
|
+
} catch {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function parseB2CWebhook(body) {
|
|
623
|
+
try {
|
|
624
|
+
const parsed = body;
|
|
625
|
+
if (parsed.Result?.ResultCode !== void 0) return parsed;
|
|
626
|
+
return null;
|
|
627
|
+
} catch {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function parseC2BWebhook(body) {
|
|
632
|
+
try {
|
|
633
|
+
const parsed = body;
|
|
634
|
+
if (parsed.TransID && parsed.TransAmount) return parsed;
|
|
635
|
+
return null;
|
|
636
|
+
} catch {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/mpesa/webhooks/webhook-handler.ts
|
|
642
|
+
function handleWebhook(body, options = {}) {
|
|
643
|
+
if (!options.skipIPCheck && options.requestIP) {
|
|
644
|
+
if (!verifyWebhookIP(options.requestIP, options.allowedIPs)) {
|
|
645
|
+
return {
|
|
646
|
+
success: false,
|
|
647
|
+
eventType: null,
|
|
648
|
+
data: null,
|
|
649
|
+
error: "IP address not whitelisted"
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const stkPush = parseStkPushWebhook(body);
|
|
654
|
+
if (stkPush) {
|
|
655
|
+
return {
|
|
656
|
+
success: true,
|
|
657
|
+
eventType: "stk_push",
|
|
658
|
+
data: stkPush
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const b2c = parseB2CWebhook(body);
|
|
662
|
+
if (b2c) {
|
|
663
|
+
return {
|
|
664
|
+
success: true,
|
|
665
|
+
eventType: "b2c",
|
|
666
|
+
data: b2c
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
const c2b = parseC2BWebhook(body);
|
|
670
|
+
if (c2b) {
|
|
671
|
+
return {
|
|
672
|
+
success: true,
|
|
673
|
+
eventType: "c2b",
|
|
674
|
+
data: c2b
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
success: false,
|
|
679
|
+
eventType: null,
|
|
680
|
+
data: null,
|
|
681
|
+
error: "Unknown webhook format"
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function extractTransactionId(webhook) {
|
|
685
|
+
if ("Body" in webhook && webhook.Body?.stkCallback) {
|
|
686
|
+
const items = webhook.Body.stkCallback.CallbackMetadata?.Item;
|
|
687
|
+
const mpesaReceipt = items?.find(
|
|
688
|
+
(item) => item.Name === "MpesaReceiptNumber"
|
|
689
|
+
);
|
|
690
|
+
return mpesaReceipt ? String(mpesaReceipt.Value) : null;
|
|
691
|
+
}
|
|
692
|
+
if ("Result" in webhook && webhook.Result?.TransactionID) {
|
|
693
|
+
return webhook.Result.TransactionID;
|
|
694
|
+
}
|
|
695
|
+
if ("TransID" in webhook) {
|
|
696
|
+
return webhook.TransID;
|
|
697
|
+
}
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
function extractAmount(webhook) {
|
|
701
|
+
if ("Body" in webhook && webhook.Body?.stkCallback) {
|
|
702
|
+
const items = webhook.Body.stkCallback.CallbackMetadata?.Item;
|
|
703
|
+
const amount = items?.find((item) => item.Name === "Amount");
|
|
704
|
+
return amount ? Number(amount.Value) : null;
|
|
705
|
+
}
|
|
706
|
+
if ("Result" in webhook && webhook.Result?.ResultParameters) {
|
|
707
|
+
const params = webhook.Result.ResultParameters.ResultParameter;
|
|
708
|
+
const amount = params?.find((p) => p.Key === "Amount");
|
|
709
|
+
return amount ? Number(amount.Value) : null;
|
|
710
|
+
}
|
|
711
|
+
if ("TransAmount" in webhook) {
|
|
712
|
+
return Number.parseFloat(webhook.TransAmount);
|
|
713
|
+
}
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
exports.DARAJA_BASE_URLS = DARAJA_BASE_URLS;
|
|
718
|
+
exports.Mpesa = Mpesa;
|
|
719
|
+
exports.PesafyError = PesafyError;
|
|
720
|
+
exports.TokenManager = TokenManager;
|
|
721
|
+
exports.createError = createError;
|
|
722
|
+
exports.encryptSecurityCredential = encryptSecurityCredential;
|
|
723
|
+
exports.extractAmount = extractAmount;
|
|
724
|
+
exports.extractTransactionId = extractTransactionId;
|
|
725
|
+
exports.handleWebhook = handleWebhook;
|
|
726
|
+
exports.retryWithBackoff = retryWithBackoff;
|
|
727
|
+
exports.verifyWebhookIP = verifyWebhookIP;
|
|
728
|
+
//# sourceMappingURL=index.cjs.map
|
|
729
|
+
//# sourceMappingURL=index.cjs.map
|