pesafy 0.3.8 → 0.3.9
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 +64 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +64 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -64,7 +64,17 @@ function encryptSecurityCredential(initiatorPassword, certificatePem) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// src/utils/http/index.ts
|
|
67
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
68
|
+
function sleep(ms) {
|
|
69
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
70
|
+
}
|
|
71
|
+
function jitter(baseMs) {
|
|
72
|
+
const spread = baseMs * 0.25;
|
|
73
|
+
return baseMs + (Math.random() * spread * 2 - spread);
|
|
74
|
+
}
|
|
67
75
|
async function httpRequest(url, options) {
|
|
76
|
+
const maxRetries = options.retries ?? 4;
|
|
77
|
+
const baseDelay = options.retryDelay ?? 2e3;
|
|
68
78
|
const headers = {
|
|
69
79
|
"Content-Type": "application/json",
|
|
70
80
|
Accept: "application/json",
|
|
@@ -72,47 +82,61 @@ async function httpRequest(url, options) {
|
|
|
72
82
|
};
|
|
73
83
|
const init = {
|
|
74
84
|
method: options.method,
|
|
75
|
-
headers
|
|
85
|
+
headers,
|
|
86
|
+
...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
|
|
76
87
|
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
let lastError = null;
|
|
89
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
90
|
+
if (attempt > 0) {
|
|
91
|
+
const delay = jitter(baseDelay * Math.pow(2, attempt - 1));
|
|
92
|
+
await sleep(delay);
|
|
93
|
+
}
|
|
94
|
+
let response;
|
|
95
|
+
try {
|
|
96
|
+
response = await fetch(url, init);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
lastError = new PesafyError({
|
|
99
|
+
code: "NETWORK_ERROR",
|
|
100
|
+
message: `Network error calling ${url}: ${String(err)}`,
|
|
101
|
+
cause: err
|
|
102
|
+
});
|
|
103
|
+
if (attempt < maxRetries) continue;
|
|
104
|
+
throw lastError;
|
|
105
|
+
}
|
|
106
|
+
let data;
|
|
107
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
108
|
+
try {
|
|
109
|
+
data = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
110
|
+
} catch {
|
|
111
|
+
data = null;
|
|
112
|
+
}
|
|
113
|
+
const responseHeaders = {};
|
|
114
|
+
response.headers.forEach((value, key) => {
|
|
115
|
+
responseHeaders[key] = value;
|
|
88
116
|
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
code: "HTTP_ERROR",
|
|
117
|
+
if (response.ok) {
|
|
118
|
+
return {
|
|
119
|
+
data,
|
|
120
|
+
status: response.status,
|
|
121
|
+
headers: responseHeaders
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const isTransient = RETRYABLE_STATUSES.has(response.status);
|
|
125
|
+
const daraja = data ?? {};
|
|
126
|
+
const message = daraja.errorMessage ?? daraja.ResponseDescription ?? `HTTP ${response.status}`;
|
|
127
|
+
lastError = new PesafyError({
|
|
128
|
+
code: isTransient ? "REQUEST_FAILED" : "HTTP_ERROR",
|
|
102
129
|
message,
|
|
103
130
|
statusCode: response.status,
|
|
104
|
-
response: data
|
|
131
|
+
response: data,
|
|
132
|
+
requestId: daraja.requestId
|
|
105
133
|
});
|
|
134
|
+
if (isTransient && attempt < maxRetries) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
throw lastError;
|
|
106
138
|
}
|
|
107
|
-
|
|
108
|
-
response.headers.forEach((value, key) => {
|
|
109
|
-
responseHeaders[key] = value;
|
|
110
|
-
});
|
|
111
|
-
return {
|
|
112
|
-
data,
|
|
113
|
-
status: response.status,
|
|
114
|
-
headers: responseHeaders
|
|
115
|
-
};
|
|
139
|
+
throw lastError;
|
|
116
140
|
}
|
|
117
141
|
|
|
118
142
|
// src/core/auth/token-manager.ts
|
|
@@ -237,6 +261,7 @@ async function processStkPush(baseUrl, accessToken, request) {
|
|
|
237
261
|
PartyB: partyB,
|
|
238
262
|
PhoneNumber: formatSafaricomPhone(request.phoneNumber),
|
|
239
263
|
CallBackURL: request.callbackUrl,
|
|
264
|
+
// Daraja docs: AccountReference max 12 chars, TransactionDesc max 13 chars
|
|
240
265
|
AccountReference: request.accountReference.slice(0, 12),
|
|
241
266
|
TransactionDesc: request.transactionDesc.slice(0, 13)
|
|
242
267
|
};
|
|
@@ -245,7 +270,10 @@ async function processStkPush(baseUrl, accessToken, request) {
|
|
|
245
270
|
{
|
|
246
271
|
method: "POST",
|
|
247
272
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
248
|
-
body
|
|
273
|
+
body,
|
|
274
|
+
// Daraja sandbox needs more retries and longer gaps due to instability
|
|
275
|
+
retries: 5,
|
|
276
|
+
retryDelay: 3e3
|
|
249
277
|
}
|
|
250
278
|
);
|
|
251
279
|
return data;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/errors/index.ts","../src/core/encryption/security-credentials.ts","../src/utils/http/index.ts","../src/core/auth/token-manager.ts","../src/utils/phone/index.ts","../src/mpesa/stk-push/utils.ts","../src/mpesa/stk-push/stk-push.ts","../src/mpesa/stk-push/stk-query.ts","../src/mpesa/stk-push/types.ts","../src/mpesa/transaction-status/query.ts","../src/mpesa/types.ts","../src/mpesa/index.ts","../src/mpesa/webhooks/retry.ts","../src/mpesa/webhooks/signature-verifier.ts","../src/mpesa/webhooks/webhook-handler.ts"],"names":["publicEncrypt","constants"],"mappings":";;;;;;;;;;;AAyBO,IAAM,WAAA,GAAN,MAAM,YAAA,SAAoB,KAAA,CAAM;AAAA,EAOrC,YAAY,OAAA,EAA6B;AACvC,IAAA,KAAA,CAAM,QAAQ,OAAO,CAAA;AAPvB,IAAA,aAAA,CAAA,IAAA,EAAS,MAAA,CAAA;AACT,IAAA,aAAA,CAAA,IAAA,EAAS,YAAA,CAAA;AACT,IAAA,aAAA,CAAA,IAAA,EAAS,UAAA,CAAA;AACT,IAAA,aAAA,CAAA,IAAA,EAAS,WAAA,CAAA;AACT,IAAA,aAAA,CAAA,IAAA,EAAkB,OAAA,CAAA;AAIhB,IAAA,MAAA,CAAO,eAAe,IAAA,EAAM,MAAA,EAAQ,EAAE,KAAA,EAAO,eAAe,CAAA;AAC5D,IAAA,IAAA,CAAK,OAAO,OAAA,CAAQ,IAAA;AACpB,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,UAAA;AAC1B,IAAA,IAAA,CAAK,WAAW,OAAA,CAAQ,QAAA;AACxB,IAAA,IAAA,CAAK,YAAY,OAAA,CAAQ,SAAA;AACzB,IAAA,IAAA,CAAK,QAAQ,OAAA,CAAQ,KAAA;AACrB,IAAA,IAAI,MAAM,iBAAA,EAAmB;AAC3B,MAAA,KAAA,CAAM,iBAAA,CAAkB,MAAM,YAAW,CAAA;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,MAAA,GAAS;AACP,IAAA,OAAO;AAAA,MACL,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,YAAY,IAAA,CAAK,UAAA;AAAA,MACjB,WAAW,IAAA,CAAK;AAAA,KAClB;AAAA,EACF;AACF;AAGO,SAAS,YAAY,OAAA,EAA0C;AACpE,EAAA,OAAO,IAAI,YAAY,OAAO,CAAA;AAChC;;;AC9BO,SAAS,yBAAA,CACd,mBACA,cAAA,EACQ;AACR,EAAA,IAAI;AACF,IAAA,MAAM,cAAA,GAAiB,MAAA,CAAO,IAAA,CAAK,iBAAA,EAAmB,OAAO,CAAA;AAE7D,IAAA,MAAM,SAAA,GAAYA,oBAAA;AAAA,MAChB;AAAA,QACE,GAAA,EAAK,cAAA;AAAA;AAAA,QAEL,SAASC,gBAAA,CAAU;AAAA,OACrB;AAAA,MACA;AAAA,KACF;AAEA,IAAA,OAAO,SAAA,CAAU,SAAS,QAAQ,CAAA;AAAA,EACpC,SAAS,KAAA,EAAO;AACd,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB,IAAA,EAAM,mBAAA;AAAA,MACN,OAAA,EACE,8HAAA;AAAA,MAEF,KAAA,EAAO;AAAA,KACR,CAAA;AAAA,EACH;AACF;;;ACzBA,eAAsB,WAAA,CACpB,KACA,OAAA,EAC0B;AAC1B,EAAA,MAAM,OAAA,GAAkC;AAAA,IACtC,cAAA,EAAgB,kBAAA;AAAA,IAChB,MAAA,EAAQ,kBAAA;AAAA,IACR,GAAG,OAAA,CAAQ;AAAA,GACb;AAEA,EAAA,MAAM,IAAA,GAAoB;AAAA,IACxB,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB;AAAA,GACF;AAEA,EAAA,IAAI,OAAA,CAAQ,SAAS,MAAA,EAAW;AAC9B,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,IAAI,CAAA;AAAA,EACzC;AAEA,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI;AACF,IAAA,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK,IAAI,CAAA;AAAA,EAClC,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB,IAAA,EAAM,gBAAA;AAAA,MACN,SAAS,CAAA,sBAAA,EAAyB,GAAG,CAAA,EAAA,EAAK,MAAA,CAAO,GAAG,CAAC,CAAA,CAAA;AAAA,MACrD,KAAA,EAAO;AAAA,KACR,CAAA;AAAA,EACH;AAGA,EAAA,IAAI,IAAA;AACJ,EAAA,MAAM,WAAA,GAAc,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,cAAc,CAAA,IAAK,EAAA;AAC5D,EAAA,IAAI,WAAA,CAAY,QAAA,CAAS,kBAAkB,CAAA,EAAG;AAC5C,IAAA,IAAA,GAAO,MAAM,SAAS,IAAA,EAAK;AAAA,EAC7B,CAAA,MAAO;AACL,IAAA,IAAA,GAAO,MAAM,SAAS,IAAA,EAAK;AAAA,EAC7B;AAEA,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAEhB,IAAA,MAAM,MAAA,GAAS,IAAA;AACf,IAAA,MAAM,UACH,MAAA,EAAQ,YAAA,IACR,QAAQ,mBAAA,IACT,CAAA,KAAA,EAAQ,SAAS,MAAM,CAAA,CAAA;AAEzB,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB,IAAA,EAAM,YAAA;AAAA,MACN,OAAA;AAAA,MACA,YAAY,QAAA,CAAS,MAAA;AAAA,MACrB,QAAA,EAAU;AAAA,KACX,CAAA;AAAA,EACH;AAGA,EAAA,MAAM,kBAA0C,EAAC;AACjD,EAAA,QAAA,CAAS,OAAA,CAAQ,OAAA,CAAQ,CAAC,KAAA,EAAO,GAAA,KAAQ;AACvC,IAAA,eAAA,CAAgB,GAAG,CAAA,GAAI,KAAA;AAAA,EACzB,CAAC,CAAA;AAED,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,QAAQ,QAAA,CAAS,MAAA;AAAA,IACjB,OAAA,EAAS;AAAA,GACX;AACF;;;AC9EA,IAAM,oBAAA,GAAuB,EAAA;AAEtB,IAAM,eAAN,MAAmB;AAAA;AAAA,EAQxB,WAAA,CAAY,WAAA,EAAqB,cAAA,EAAwB,OAAA,EAAiB;AAP1E,IAAA,aAAA,CAAA,IAAA,EAAiB,aAAA,CAAA;AACjB,IAAA,aAAA,CAAA,IAAA,EAAiB,gBAAA,CAAA;AACjB,IAAA,aAAA,CAAA,IAAA,EAAiB,SAAA,CAAA;AAEjB,IAAA,aAAA,CAAA,IAAA,EAAQ,aAAA,EAA6B,IAAA,CAAA;AACrC,IAAA,aAAA,CAAA,IAAA,EAAQ,gBAAA,EAAiB,CAAA,CAAA;AAGvB,IAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AACnB,IAAA,IAAA,CAAK,cAAA,GAAiB,cAAA;AACtB,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AAAA,EACjB;AAAA,EAEQ,kBAAA,GAA6B;AAEnC,IAAA,MAAM,cAAc,CAAA,EAAG,IAAA,CAAK,WAAW,CAAA,CAAA,EAAI,KAAK,cAAc,CAAA,CAAA;AAC9D,IAAA,MAAM,UAAU,MAAA,CAAO,IAAA,CAAK,aAAa,OAAO,CAAA,CAAE,SAAS,QAAQ,CAAA;AACnE,IAAA,OAAO,SAAS,OAAO,CAAA,CAAA;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAA,GAAkC;AACtC,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,EAAI,GAAI,GAAA;AAEzB,IAAA,IAAI,IAAA,CAAK,WAAA,IAAe,IAAA,CAAK,cAAA,GAAiB,MAAM,oBAAA,EAAsB;AACxE,MAAA,OAAO,IAAA,CAAK,WAAA;AAAA,IACd;AAGA,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,IAAA,CAAK,OAAO,CAAA,gDAAA,CAAA;AAE3B,IAAA,MAAM,QAAA,GAAW,MAAM,WAAA,CAA2B,GAAA,EAAK;AAAA,MACrD,MAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,aAAA,EAAe,KAAK,kBAAA;AAAmB;AACzC,KACD,CAAA;AAED,IAAA,MAAM,EAAE,YAAA,EAAc,UAAA,EAAW,GAAI,QAAA,CAAS,IAAA;AAE9C,IAAA,IAAI,CAAC,YAAA,EAAc;AACjB,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,aAAA;AAAA,QACN,OAAA,EACE,4EAAA;AAAA,QACF,UAAU,QAAA,CAAS;AAAA,OACpB,CAAA;AAAA,IACH;AAEA,IAAA,IAAA,CAAK,WAAA,GAAc,YAAA;AAEnB,IAAA,IAAA,CAAK,cAAA,GAAiB,OAAO,UAAA,IAAc,IAAA,CAAA;AAE3C,IAAA,OAAO,IAAA,CAAK,WAAA;AAAA,EACd;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,IAAA,CAAK,WAAA,GAAc,IAAA;AACnB,IAAA,IAAA,CAAK,cAAA,GAAiB,CAAA;AAAA,EACxB;AACF,CAAA;;;ACrEO,SAAS,qBAAqB,KAAA,EAAuB;AAC1D,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAEtC,EAAA,IAAI,UAAA;AAEJ,EAAA,IAAI,OAAO,UAAA,CAAW,KAAK,CAAA,IAAK,MAAA,CAAO,WAAW,EAAA,EAAI;AACpD,IAAA,UAAA,GAAa,MAAA;AAAA,EACf,WAAW,MAAA,CAAO,UAAA,CAAW,GAAG,CAAA,IAAK,MAAA,CAAO,WAAW,EAAA,EAAI;AACzD,IAAA,UAAA,GAAa,CAAA,GAAA,EAAM,MAAA,CAAO,KAAA,CAAM,CAAC,CAAC,CAAA,CAAA;AAAA,EACpC,CAAA,MAAA,IAAW,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG;AAE9B,IAAA,UAAA,GAAa,MAAM,MAAM,CAAA,CAAA;AAAA,EAC3B,WAAW,MAAA,CAAO,UAAA,CAAW,KAAK,CAAA,IAAK,MAAA,CAAO,WAAW,EAAA,EAAI;AAC3D,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB,IAAA,EAAM,eAAA;AAAA,MACN,OAAA,EAAS,yBAAyB,KAAK,CAAA,qCAAA;AAAA,KACxC,CAAA;AAAA,EACH,CAAA,MAAO;AACL,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB,IAAA,EAAM,eAAA;AAAA,MACN,OAAA,EAAS,8BAA8B,KAAK,CAAA,kDAAA;AAAA,KAC7C,CAAA;AAAA,EACH;AAGA,EAAA,IAAI,UAAA,CAAW,WAAW,EAAA,EAAI;AAC5B,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB,IAAA,EAAM,eAAA;AAAA,MACN,OAAA,EAAS,CAAA,cAAA,EAAiB,KAAK,CAAA,iBAAA,EAAoB,UAAU,CAAA,yBAAA;AAAA,KAC9D,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,UAAA;AACT;;;AC3BO,SAAS,kBAAA,CACd,SAAA,EACA,OAAA,EACA,SAAA,EACQ;AACR,EAAA,OAAO,KAAK,CAAA,EAAG,SAAS,GAAG,OAAO,CAAA,EAAG,SAAS,CAAA,CAAE,CAAA;AAClD;AAOO,SAAS,YAAA,GAAuB;AACrC,EAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,EAAA,MAAM,GAAA,GAAM,CAAC,CAAA,KAAsB,CAAA,CAAE,UAAS,CAAE,QAAA,CAAS,GAAG,GAAG,CAAA;AAC/D,EAAA,OAAO;AAAA,IACL,IAAI,WAAA,EAAY;AAAA,IAChB,GAAA,CAAI,GAAA,CAAI,QAAA,EAAS,GAAI,CAAC,CAAA;AAAA,IACtB,GAAA,CAAI,GAAA,CAAI,OAAA,EAAS,CAAA;AAAA,IACjB,GAAA,CAAI,GAAA,CAAI,QAAA,EAAU,CAAA;AAAA,IAClB,GAAA,CAAI,GAAA,CAAI,UAAA,EAAY,CAAA;AAAA,IACpB,GAAA,CAAI,GAAA,CAAI,UAAA,EAAY;AAAA,GACtB,CAAE,KAAK,EAAE,CAAA;AACX;;;ACZA,eAAsB,cAAA,CACpB,OAAA,EACA,WAAA,EACA,OAAA,EAC0B;AAG1B,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA;AACxC,EAAA,IAAI,SAAS,CAAA,EAAG;AACd,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EAAS,CAAA,mCAAA,EAAsC,OAAA,CAAQ,MAAM,oBAAoB,MAAM,CAAA,EAAA;AAAA,KACxF,CAAA;AAAA,EACH;AAIA,EAAA,MAAM,YAAY,YAAA,EAAa;AAK/B,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,MAAA,IAAU,OAAA,CAAQ,SAAA;AAEzC,EAAA,MAAM,IAAA,GAAO;AAAA,IACX,mBAAmB,OAAA,CAAQ,SAAA;AAAA,IAC3B,UAAU,kBAAA,CAAmB,OAAA,CAAQ,SAAA,EAAW,OAAA,CAAQ,SAAS,SAAS,CAAA;AAAA,IAC1E,SAAA,EAAW,SAAA;AAAA,IACX,eAAA,EAAiB,QAAQ,eAAA,IAAmB,uBAAA;AAAA,IAC5C,MAAA,EAAQ,MAAA;AAAA,IACR,MAAA,EAAQ,oBAAA,CAAkB,OAAA,CAAQ,WAAW,CAAA;AAAA,IAC7C,MAAA,EAAQ,MAAA;AAAA,IACR,WAAA,EAAa,oBAAA,CAAkB,OAAA,CAAQ,WAAW,CAAA;AAAA,IAClD,aAAa,OAAA,CAAQ,WAAA;AAAA,IACrB,gBAAA,EAAkB,OAAA,CAAQ,gBAAA,CAAiB,KAAA,CAAM,GAAG,EAAE,CAAA;AAAA,IACtD,eAAA,EAAiB,OAAA,CAAQ,eAAA,CAAgB,KAAA,CAAM,GAAG,EAAE;AAAA,GACtD;AAEA,EAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,WAAA;AAAA,IACrB,GAAG,OAAO,CAAA,gCAAA,CAAA;AAAA,IACV;AAAA,MACE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,aAAA,EAAe,CAAA,OAAA,EAAU,WAAW,CAAA,CAAA,EAAG;AAAA,MAClD;AAAA;AACF,GACF;AAEA,EAAA,OAAO,IAAA;AACT;;;ACzDA,eAAsB,YAAA,CACpB,OAAA,EACA,WAAA,EACA,OAAA,EAC2B;AAE3B,EAAA,MAAM,YAAY,YAAA,EAAa;AAE/B,EAAA,MAAM,IAAA,GAAO;AAAA,IACX,mBAAmB,OAAA,CAAQ,SAAA;AAAA,IAC3B,UAAU,kBAAA,CAAmB,OAAA,CAAQ,SAAA,EAAW,OAAA,CAAQ,SAAS,SAAS,CAAA;AAAA,IAC1E,SAAA,EAAW,SAAA;AAAA,IACX,mBAAmB,OAAA,CAAQ;AAAA,GAC7B;AAEA,EAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,WAAA;AAAA,IACrB,GAAG,OAAO,CAAA,4BAAA,CAAA;AAAA,IACV;AAAA,MACE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,aAAA,EAAe,CAAA,OAAA,EAAU,WAAW,CAAA,CAAA,EAAG;AAAA,MAClD;AAAA;AACF,GACF;AAEA,EAAA,OAAO,IAAA;AACT;;;AC6HO,SAAS,qBACd,EAAA,EAC0B;AAC1B,EAAA,OAAO,GAAG,UAAA,KAAe,CAAA;AAC3B;AAMO,SAAS,gBAAA,CACd,UACA,IAAA,EAC6B;AAC7B,EAAA,MAAM,KAAA,GAAQ,SAAS,IAAA,CAAK,WAAA;AAC5B,EAAA,IAAI,CAAC,oBAAA,CAAqB,KAAK,CAAA,EAAG,OAAO,MAAA;AACzC,EAAA,OAAO,KAAA,CAAM,iBAAiB,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,KAAS,IAAI,CAAA,EAAG,KAAA;AACnE;;;AC9KA,eAAsB,sBAAA,CACpB,OAAA,EACA,KAAA,EACA,kBAAA,EACA,WACA,OAAA,EACoC;AAGpC,EAAA,IAAI,CAAC,QAAQ,aAAA,EAAe;AAC1B,IAAA,MAAM,WAAA,CAAY;AAAA,MAChB,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,CAAC,QAAQ,MAAA,EAAQ;AACnB,IAAA,MAAM,WAAA,CAAY;AAAA,MAChB,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EACE;AAAA,KACH,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,CAAC,QAAQ,cAAA,EAAgB;AAC3B,IAAA,MAAM,WAAA,CAAY;AAAA,MAChB,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EACE;AAAA,KACH,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,CAAC,QAAQ,SAAA,EAAW;AACtB,IAAA,MAAM,WAAA,CAAY;AAAA,MAChB,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EACE;AAAA,KACH,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,CAAC,QAAQ,eAAA,EAAiB;AAC5B,IAAA,MAAM,WAAA,CAAY;AAAA,MAChB,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,EACH;AAIA,EAAA,MAAM,OAAA,GAAU;AAAA,IACd,SAAA,EAAW,SAAA;AAAA,IACX,kBAAA,EAAoB,kBAAA;AAAA,IACpB,SAAA,EAAW,QAAQ,SAAA,IAAa,wBAAA;AAAA,IAChC,eAAe,OAAA,CAAQ,aAAA;AAAA,IACvB,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,gBAAgB,OAAA,CAAQ,cAAA;AAAA,IACxB,WAAW,OAAA,CAAQ,SAAA;AAAA,IACnB,iBAAiB,OAAA,CAAQ,eAAA;AAAA,IACzB,OAAA,EAAS,QAAQ,OAAA,IAAW,0BAAA;AAAA,IAC5B,QAAA,EAAU,QAAQ,QAAA,IAAY;AAAA,GAChC;AAEA,EAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,WAAA;AAAA,IACrB,GAAG,OAAO,CAAA,iCAAA,CAAA;AAAA,IACV;AAAA,MACE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,aAAA,EAAe,CAAA,OAAA,EAAU,KAAK,CAAA,CAAA,EAAG;AAAA,MAC5C,IAAA,EAAM;AAAA;AACR,GACF;AAEA,EAAA,OAAO,IAAA;AACT;;;ACnFO,IAAM,gBAAA,GAAgD;AAAA,EAC3D,OAAA,EAAS,iCAAA;AAAA,EACT,UAAA,EAAY;AACd;;;AC0BO,IAAM,QAAN,MAAY;AAAA,EAKjB,YAAY,MAAA,EAAqB;AAJjC,IAAA,aAAA,CAAA,IAAA,EAAiB,QAAA,CAAA;AACjB,IAAA,aAAA,CAAA,IAAA,EAAiB,cAAA,CAAA;AACjB,IAAA,aAAA,CAAA,IAAA,EAAiB,SAAA,CAAA;AAGf,IAAA,IAAI,CAAC,MAAA,CAAO,WAAA,IAAe,CAAC,OAAO,cAAA,EAAgB;AACjD,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,qBAAA;AAAA,QACN,OAAA,EAAS;AAAA,OACV,CAAA;AAAA,IACH;AAEA,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,OAAA,GAAU,gBAAA,CAAiB,MAAA,CAAO,WAAW,CAAA;AAClD,IAAA,IAAA,CAAK,eAAe,IAAI,YAAA;AAAA,MACtB,MAAA,CAAO,WAAA;AAAA,MACP,MAAA,CAAO,cAAA;AAAA,MACP,IAAA,CAAK;AAAA,KACP;AAAA,EACF;AAAA;AAAA,EAIQ,QAAA,GAA4B;AAClC,IAAA,OAAO,IAAA,CAAK,aAAa,cAAA,EAAe;AAAA,EAC1C;AAAA,EAEA,MAAc,uBAAA,GAA2C;AAEvD,IAAA,IAAI,IAAA,CAAK,MAAA,CAAO,kBAAA,EAAoB,OAAO,KAAK,MAAA,CAAO,kBAAA;AAGvD,IAAA,IAAI,CAAC,IAAA,CAAK,MAAA,CAAO,iBAAA,EAAmB;AAClC,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,qBAAA;AAAA,QACN,OAAA,EACE;AAAA,OAEH,CAAA;AAAA,IACH;AAEA,IAAA,IAAI,IAAA;AACJ,IAAA,IAAI,IAAA,CAAK,OAAO,cAAA,EAAgB;AAC9B,MAAA,IAAA,GAAO,KAAK,MAAA,CAAO,cAAA;AAAA,IACrB,CAAA,MAAA,IAAW,IAAA,CAAK,MAAA,CAAO,eAAA,EAAiB;AAEtC,MAAA,IAAI,OAAO,QAAQ,WAAA,EAAa;AAC9B,QAAA,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,CAAK,KAAK,MAAA,CAAO,eAAe,EAAE,IAAA,EAAK;AAAA,MAC1D,CAAA,MAAO;AAEL,QAAA,MAAM,EAAE,QAAA,EAAS,GAAI,MAAM,OAAO,aAAkB,CAAA;AACpD,QAAA,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,CAAK,MAAA,CAAO,iBAAiB,OAAO,CAAA;AAAA,MAC5D;AAAA,IACF,CAAA,MAAO;AACL,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,qBAAA;AAAA,QACN,OAAA,EACE;AAAA,OACH,CAAA;AAAA,IACH;AAEA,IAAA,OAAO,yBAAA,CAA0B,IAAA,CAAK,MAAA,CAAO,iBAAA,EAAmB,IAAI,CAAA;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,MAAM,QAAQ,OAAA,EAAwD;AACpE,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,MAAA,CAAO,oBAAA,IAAwB,EAAA;AACtD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,kBAAA,IAAsB,EAAA;AAElD,IAAA,IAAI,CAAC,SAAA,IAAa,CAAC,OAAA,EAAS;AAC1B,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,kBAAA;AAAA,QACN,OAAA,EACE;AAAA,OACH,CAAA;AAAA,IACH;AAEA,IAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,QAAA,EAAS;AAClC,IAAA,OAAO,cAAA,CAAe,IAAA,CAAK,OAAA,EAAS,KAAA,EAAO;AAAA,MACzC,GAAG,OAAA;AAAA,MACH,SAAA;AAAA,MACA;AAAA,KACD,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,SAAS,OAAA,EAAyD;AACtE,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,MAAA,CAAO,oBAAA,IAAwB,EAAA;AACtD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,kBAAA,IAAsB,EAAA;AAElD,IAAA,IAAI,CAAC,SAAA,IAAa,CAAC,OAAA,EAAS;AAC1B,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,kBAAA;AAAA,QACN,OAAA,EACE;AAAA,OACH,CAAA;AAAA,IACH;AAEA,IAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,QAAA,EAAS;AAClC,IAAA,OAAO,YAAA,CAAa,IAAA,CAAK,OAAA,EAAS,KAAA,EAAO;AAAA,MACvC,GAAG,OAAA;AAAA,MACH,SAAA;AAAA,MACA;AAAA,KACD,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,MAAM,kBAAkB,OAAA,EAAmC;AACzD,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,MAAA,CAAO,aAAA,IAAiB,EAAA;AAC/C,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,kBAAA;AAAA,QACN,OAAA,EAAS;AAAA,OACV,CAAA;AAAA,IACH;AAGA,IAAA,MAAM,CAAC,KAAA,EAAO,YAAY,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,MAC9C,KAAK,QAAA,EAAS;AAAA,MACd,KAAK,uBAAA;AAAwB,KAC9B,CAAA;AAED,IAAA,OAAO,sBAAA;AAAA,MACL,IAAA,CAAK,OAAA;AAAA,MACL,KAAA;AAAA,MACA,YAAA;AAAA,MACA,SAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AAAA;AAAA,EAGA,eAAA,GAAwB;AACtB,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAAA,EAC/B;AACF;;;AChMA,IAAM,eAAA,GAA0C;AAAA,EAC9C,UAAA,EAAY,QAAA;AAAA,EACZ,YAAA,EAAc,GAAA;AAAA,EACd,QAAA,EAAU,IAAA;AAAA,EACV,iBAAA,EAAmB,CAAA;AAAA,EACnB,gBAAA,EAAkB,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK;AAAA;AACxC,CAAA;AAkBA,eAAsB,gBAAA,CACpB,EAAA,EACA,OAAA,GAAwB,EAAC,EACA;AACzB,EAAA,MAAM,IAAA,GAAO,EAAE,GAAG,eAAA,EAAiB,GAAG,OAAA,EAAQ;AAC9C,EAAA,IAAI,QAAQ,IAAA,CAAK,YAAA;AACjB,EAAA,IAAI,QAAA,GAAW,CAAA;AACf,EAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAE3B,EAAA,OAAO,QAAA,GAAW,KAAK,UAAA,EAAY;AACjC,IAAA,QAAA,EAAA;AAEA,IAAA,IAAI,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA,GAAY,KAAK,gBAAA,EAAkB;AAClD,MAAA,OAAO;AAAA,QACL,OAAA,EAAS,KAAA;AAAA,QACT,QAAA;AAAA,QACA,KAAA,EAAO,IAAI,KAAA,CAAM,6BAA6B;AAAA,OAChD;AAAA,IACF;AAEA,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAO,MAAM,EAAA,EAAG;AACtB,MAAA,OAAO,EAAE,OAAA,EAAS,IAAA,EAAM,IAAA,EAAM,QAAA,EAAS;AAAA,IACzC,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,GAAA,GAAM,iBAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC,CAAA;AAGpE,MAAA,IAAI,GAAA,CAAI,OAAA,CAAQ,QAAA,CAAS,GAAG,CAAA,EAAG;AAC7B,QAAA,OAAO,EAAE,OAAA,EAAS,KAAA,EAAO,QAAA,EAAU,OAAO,GAAA,EAAI;AAAA,MAChD;AAEA,MAAA,IAAI,QAAA,GAAW,KAAK,UAAA,EAAY;AAC9B,QAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,YAAY,UAAA,CAAW,OAAA,EAAS,KAAK,CAAC,CAAA;AACzD,QAAA,KAAA,GAAQ,KAAK,GAAA,CAAI,KAAA,GAAQ,IAAA,CAAK,iBAAA,EAAmB,KAAK,QAAQ,CAAA;AAAA,MAChE;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,KAAA;AAAA,IACT,QAAA;AAAA,IACA,KAAA,EAAO,IAAI,KAAA,CAAM,sBAAsB;AAAA,GACzC;AACF;;;AChEO,IAAM,aAAA,GAAmC;AAAA,EAC9C,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,gBAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,gBAAA;AAAA,EACA;AACF;AAMO,SAAS,eAAA,CACd,SAAA,EACA,UAAA,GAAgC,aAAA,EACvB;AACT,EAAA,OAAO,UAAA,CAAW,SAAS,SAAS,CAAA;AACtC;AAMO,SAAS,oBAAoB,IAAA,EAAsC;AACxE,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAA;AACf,IAAA,IAAI,MAAA,EAAQ,IAAA,EAAM,WAAA,EAAa,OAAO,MAAA;AACtC,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;;;AC3BO,SAAS,aAAA,CACd,IAAA,EACA,OAAA,GAAiC,EAAC,EACZ;AAEtB,EAAA,IAAI,CAAC,OAAA,CAAQ,WAAA,IAAe,OAAA,CAAQ,SAAA,EAAW;AAC7C,IAAA,IAAI,CAAC,eAAA,CAAgB,OAAA,CAAQ,SAAA,EAAW,OAAA,CAAQ,UAAU,CAAA,EAAG;AAC3D,MAAA,OAAO;AAAA,QACL,OAAA,EAAS,KAAA;AAAA,QACT,SAAA,EAAW,IAAA;AAAA,QACX,IAAA,EAAM,IAAA;AAAA,QACN,KAAA,EAAO,CAAA,WAAA,EAAc,OAAA,CAAQ,SAAS,CAAA,kCAAA;AAAA,OACxC;AAAA,IACF;AAAA,EACF;AAGA,EAAA,MAAM,OAAA,GAAU,oBAAoB,IAAI,CAAA;AACxC,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,IAAA;AAAA,MACT,SAAA,EAAW,UAAA;AAAA,MACX,IAAA,EAAM;AAAA,KACR;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,KAAA;AAAA,IACT,SAAA,EAAW,IAAA;AAAA,IACX,IAAA,EAAM,IAAA;AAAA,IACN,KAAA,EAAO;AAAA,GACT;AACF;AAKO,SAAS,qBAAqB,OAAA,EAAwC;AAC3E,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,IAAA,EAAM,WAAA,EAAa,gBAAA,EAAkB,IAAA;AAC3D,EAAA,MAAM,OAAO,KAAA,EAAO,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,oBAAoB,CAAA;AAC/D,EAAA,OAAO,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,GAAI,IAAA;AACrC;AAGO,SAAS,cAAc,OAAA,EAAwC;AACpE,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,IAAA,EAAM,WAAA,EAAa,gBAAA,EAAkB,IAAA;AAC3D,EAAA,MAAM,OAAO,KAAA,EAAO,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,QAAQ,CAAA;AACnD,EAAA,OAAO,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,GAAI,IAAA;AACrC;AAGO,SAAS,mBAAmB,OAAA,EAAwC;AACzE,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,IAAA,EAAM,WAAA,EAAa,gBAAA,EAAkB,IAAA;AAC3D,EAAA,MAAM,OAAO,KAAA,EAAO,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,aAAa,CAAA;AACxD,EAAA,OAAO,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,GAAI,IAAA;AACrC;AAGO,SAAS,qBAAqB,OAAA,EAAkC;AACrE,EAAA,OAAO,OAAA,CAAQ,IAAA,EAAM,WAAA,EAAa,UAAA,KAAe,CAAA;AACnD","file":"index.cjs","sourcesContent":["/**\n * Pesafy error utilities\n */\nexport type ErrorCode =\n | \"INVALID_CREDENTIALS\"\n | \"AUTH_FAILED\"\n | \"VALIDATION_ERROR\"\n | \"ENCRYPTION_FAILED\"\n | \"REQUEST_FAILED\"\n | \"INVALID_PHONE\"\n | \"HTTP_ERROR\"\n | \"API_ERROR\"\n | \"NETWORK_ERROR\"\n | \"TIMEOUT\"\n | \"INVALID_RESPONSE\";\n\nexport interface PesafyErrorOptions {\n code: ErrorCode;\n message: string;\n statusCode?: number;\n response?: unknown;\n cause?: unknown;\n requestId?: string;\n}\n\nexport class PesafyError extends Error {\n readonly code: ErrorCode;\n readonly statusCode: number | undefined;\n readonly response: unknown;\n readonly requestId: string | undefined;\n override readonly cause: unknown;\n\n constructor(options: PesafyErrorOptions) {\n super(options.message);\n Object.defineProperty(this, \"name\", { value: \"PesafyError\" });\n this.code = options.code;\n this.statusCode = options.statusCode;\n this.response = options.response;\n this.requestId = options.requestId;\n this.cause = options.cause;\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, PesafyError);\n }\n }\n\n toJSON() {\n return {\n name: this.name,\n code: this.code,\n message: this.message,\n statusCode: this.statusCode,\n requestId: this.requestId,\n };\n }\n}\n\n/** Convenience factory — identical API to `new PesafyError(...)` */\nexport function createError(options: PesafyErrorOptions): PesafyError {\n return new PesafyError(options);\n}\n","/**\n * Security credential encryption for Daraja APIs that require it:\n * B2C, B2B, Transaction Status Query, Reversals, Tax Remittance.\n *\n * Algorithm (from Safaricom \"Getting Started\" docs):\n * 1. Write the unencrypted initiator password into a byte array.\n * 2. Encrypt using the M-Pesa public key certificate:\n * - RSA algorithm\n * - PKCS #1 v1.5 padding (NOT OAEP)\n * 3. Base64-encode the encrypted byte array.\n *\n * Certificate source:\n * Sandbox: https://developer.safaricom.co.ke (SandboxCertificate.cer)\n * Production: https://developer.safaricom.co.ke (ProductionCertificate.cer)\n *\n * NOTE: Use the correct certificate for each environment or credentials\n * will be rejected.\n */\n\nimport { constants, publicEncrypt } from \"node:crypto\";\nimport { PesafyError } from \"../../utils/errors\";\n\n/**\n * Encrypts `initiatorPassword` with the given PEM certificate and returns\n * the base64-encoded security credential ready to send to Daraja.\n *\n * @param initiatorPassword - Plain-text password set on the M-PESA org portal\n * @param certificatePem - Full PEM string (the .cer file contents)\n */\nexport function encryptSecurityCredential(\n initiatorPassword: string,\n certificatePem: string\n): string {\n try {\n const passwordBuffer = Buffer.from(initiatorPassword, \"utf-8\");\n\n const encrypted = publicEncrypt(\n {\n key: certificatePem,\n // RSA_PKCS1_PADDING = 1 (NOT RSA_PKCS1_OAEP_PADDING = 4)\n padding: constants.RSA_PKCS1_PADDING,\n },\n passwordBuffer\n );\n\n return encrypted.toString(\"base64\");\n } catch (error) {\n throw new PesafyError({\n code: \"ENCRYPTION_FAILED\",\n message:\n \"Failed to encrypt security credential. \" +\n \"Ensure the certificate PEM is valid and matches the environment (sandbox/production).\",\n cause: error,\n });\n }\n}\n","/**\n * Minimal HTTP client for Daraja API calls.\n *\n * Why not use axios/got/ky?\n * - Zero extra dependencies\n * - Works in Node.js, Bun, and edge runtimes unchanged\n * - Daraja only needs POST + GET with JSON bodies\n *\n * Exported: httpRequest (the ONLY export — never export \"httpClient\")\n */\n\nimport { PesafyError } from \"../errors\";\n\nexport interface HttpRequestOptions {\n method: \"GET\" | \"POST\";\n headers?: Record<string, string>;\n /** Will be JSON-serialised and sent as application/json */\n body?: unknown;\n}\n\nexport interface HttpResponse<T> {\n data: T;\n status: number;\n headers: Record<string, string>;\n}\n\n/**\n * Sends an HTTP request and returns parsed JSON.\n * Throws PesafyError on non-2xx responses.\n */\nexport async function httpRequest<T = unknown>(\n url: string,\n options: HttpRequestOptions\n): Promise<HttpResponse<T>> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...options.headers,\n };\n\n const init: RequestInit = {\n method: options.method,\n headers,\n };\n\n if (options.body !== undefined) {\n init.body = JSON.stringify(options.body);\n }\n\n let response: Response;\n try {\n response = await fetch(url, init);\n } catch (err) {\n throw new PesafyError({\n code: \"REQUEST_FAILED\",\n message: `Network error calling ${url}: ${String(err)}`,\n cause: err,\n });\n }\n\n // Parse body regardless of status so we can attach it to the error\n let data: unknown;\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n data = await response.json();\n } else {\n data = await response.text();\n }\n\n if (!response.ok) {\n // Daraja error shape: { requestId, errorCode, errorMessage }\n const daraja = data as Record<string, unknown>;\n const message =\n (daraja?.errorMessage as string) ??\n (daraja?.ResponseDescription as string) ??\n `HTTP ${response.status}`;\n\n throw new PesafyError({\n code: \"HTTP_ERROR\",\n message,\n statusCode: response.status,\n response: data,\n });\n }\n\n // Collect response headers into a plain object\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n return {\n data: data as T,\n status: response.status,\n headers: responseHeaders,\n };\n}\n","/**\n * OAuth token manager for Daraja API.\n *\n * Daraja Authorization endpoint (GET, Basic Auth):\n * https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials\n * https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials\n *\n * Token validity: 3600 seconds (1 hour).\n * We refresh 60 s early to avoid edge-case expiry mid-request.\n *\n * Ref: Authorization By Safaricom docs\n */\n\nimport { PesafyError } from \"../../utils/errors\";\nimport { httpRequest } from \"../../utils/http\";\nimport type { TokenResponse } from \"./types\";\n\n/** Refresh the token this many seconds before it actually expires */\nconst TOKEN_BUFFER_SECONDS = 60;\n\nexport class TokenManager {\n private readonly consumerKey: string;\n private readonly consumerSecret: string;\n private readonly baseUrl: string;\n\n private cachedToken: string | null = null;\n private tokenExpiresAt = 0; // Unix seconds\n\n constructor(consumerKey: string, consumerSecret: string, baseUrl: string) {\n this.consumerKey = consumerKey;\n this.consumerSecret = consumerSecret;\n this.baseUrl = baseUrl;\n }\n\n private getBasicAuthHeader(): string {\n // Daraja spec: Base64(consumerKey:consumerSecret)\n const credentials = `${this.consumerKey}:${this.consumerSecret}`;\n const encoded = Buffer.from(credentials, \"utf-8\").toString(\"base64\");\n return `Basic ${encoded}`;\n }\n\n /**\n * Returns a valid access token, fetching a new one when the cached token\n * is absent or within TOKEN_BUFFER_SECONDS of expiry.\n */\n async getAccessToken(): Promise<string> {\n const now = Date.now() / 1000;\n\n if (this.cachedToken && this.tokenExpiresAt > now + TOKEN_BUFFER_SECONDS) {\n return this.cachedToken;\n }\n\n // Daraja Authorization API: GET with Basic Auth + grant_type query param\n const url = `${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`;\n\n const response = await httpRequest<TokenResponse>(url, {\n method: \"GET\",\n headers: {\n Authorization: this.getBasicAuthHeader(),\n },\n });\n\n const { access_token, expires_in } = response.data;\n\n if (!access_token) {\n throw new PesafyError({\n code: \"AUTH_FAILED\",\n message:\n \"Daraja did not return an access token. Check your consumer key and secret.\",\n response: response.data,\n });\n }\n\n this.cachedToken = access_token;\n // expires_in is 3599 per Daraja docs; default to 3600 if missing\n this.tokenExpiresAt = now + (expires_in ?? 3600);\n\n return this.cachedToken;\n }\n\n /** Force token refresh on the next call (e.g. after a 401 response) */\n clearCache(): void {\n this.cachedToken = null;\n this.tokenExpiresAt = 0;\n }\n}\n","/**\n * Phone number utilities for Daraja API.\n *\n * Daraja spec: PartyA and PhoneNumber must be in the format 2547XXXXXXXX\n * (12-digit, starts with 254, no +, no spaces, no dashes).\n *\n * Accepted input formats:\n * 0712345678 → 254712345678\n * +254712345678 → 254712345678\n * 254712345678 → 254712345678 (already correct)\n * 712345678 → 254712345678\n */\n\nimport { PesafyError } from \"../errors\";\n\n/** Normalises any common Kenyan phone format to 254XXXXXXXXX (12 digits) */\nexport function formatSafaricomPhone(phone: string): string {\n const digits = phone.replace(/\\D/g, \"\");\n\n let normalised: string;\n\n if (digits.startsWith(\"254\") && digits.length === 12) {\n normalised = digits;\n } else if (digits.startsWith(\"0\") && digits.length === 10) {\n normalised = `254${digits.slice(1)}`;\n } else if (digits.length === 9) {\n // e.g. 712345678 → 254712345678\n normalised = `254${digits}`;\n } else if (digits.startsWith(\"254\") && digits.length !== 12) {\n throw new PesafyError({\n code: \"INVALID_PHONE\",\n message: `Invalid phone number \"${phone}\". Expected 254XXXXXXXXX (12 digits).`,\n });\n } else {\n throw new PesafyError({\n code: \"INVALID_PHONE\",\n message: `Cannot parse phone number \"${phone}\". Use 07XXXXXXXX, 2547XXXXXXXX, or +2547XXXXXXXX.`,\n });\n }\n\n // Final sanity check: must be exactly 12 digits\n if (normalised.length !== 12) {\n throw new PesafyError({\n code: \"INVALID_PHONE\",\n message: `Phone number \"${phone}\" normalised to \"${normalised}\" which is not 12 digits.`,\n });\n }\n\n return normalised;\n}\n","/**\n * STK Push utility functions\n *\n * Password spec (from Daraja docs):\n * Password = Base64( BusinessShortCode + Passkey + Timestamp )\n * Timestamp = YYYYMMDDHHmmss\n *\n * IMPORTANT: Generate the timestamp ONCE per request and pass the same\n * value to BOTH getStkPushPassword() and the request body's Timestamp field.\n * Safaricom validates that Base64(Shortcode+Passkey+Timestamp) matches the\n * Timestamp sent in the body — two separate calls to getTimestamp() will\n * produce different values and cause auth failures.\n */\n\nexport { formatSafaricomPhone as formatPhoneNumber } from \"../../utils/phone\";\n\n/**\n * Generates the STK Push password.\n * Formula: Base64( Shortcode + Passkey + Timestamp )\n *\n * Uses btoa() — works in Node.js ≥18, Bun, browsers, and edge runtimes.\n */\nexport function getStkPushPassword(\n shortCode: string,\n passKey: string,\n timestamp: string\n): string {\n return btoa(`${shortCode}${passKey}${timestamp}`);\n}\n\n/**\n * Returns a Daraja-compatible timestamp: YYYYMMDDHHmmss\n *\n * Call this ONCE per request and reuse the result.\n */\nexport function getTimestamp(): string {\n const now = new Date();\n const pad = (n: number): string => n.toString().padStart(2, \"0\");\n return [\n now.getFullYear(),\n pad(now.getMonth() + 1),\n pad(now.getDate()),\n pad(now.getHours()),\n pad(now.getMinutes()),\n pad(now.getSeconds()),\n ].join(\"\");\n}\n","/**\n * M-Pesa Express (STK Push) — initiates a payment prompt on the customer's phone.\n *\n * API: POST /mpesa/stkpush/v1/processrequest\n *\n * Daraja request body (from docs):\n * {\n * \"BusinessShortCode\": 174379,\n * \"Password\": \"base64(Shortcode+Passkey+Timestamp)\",\n * \"Timestamp\": \"20210628092408\",\n * \"TransactionType\": \"CustomerPayBillOnline\",\n * \"Amount\": \"1\",\n * \"PartyA\": \"254722000000\",\n * \"PartyB\": \"174379\",\n * \"PhoneNumber\": \"254722111111\",\n * \"CallBackURL\": \"https://mydomain.com/path\",\n * \"AccountReference\": \"accountref\", ← max 12 chars\n * \"TransactionDesc\": \"txndesc\" ← max 13 chars\n * }\n *\n * Notes from docs:\n * - All fields except TransactionDesc are mandatory.\n * - Amount must be a whole number ≥ 1 (KES).\n * - PartyA = phone sending money (2547XXXXXXXX).\n * - PartyB = shortCode for Paybill, Till Number for Buy Goods.\n * - AccountReference max 12 chars (longer values cause USSD prompt too long).\n * - TransactionDesc max 13 chars.\n */\n\nimport { PesafyError } from \"../../utils/errors\";\nimport { httpRequest } from \"../../utils/http\";\nimport type { StkPushRequest, StkPushResponse } from \"./types\";\nimport { formatPhoneNumber, getStkPushPassword, getTimestamp } from \"./utils\";\n\nexport async function processStkPush(\n baseUrl: string,\n accessToken: string,\n request: StkPushRequest\n): Promise<StkPushResponse> {\n // ── Amount validation ───────────────────────────────────────────────────────\n // Daraja minimum is KES 1. Math.round(0.4) = 0 → reject with clear message.\n const amount = Math.round(request.amount);\n if (amount < 1) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message: `Amount must be at least KES 1 (got ${request.amount} which rounds to ${amount}).`,\n });\n }\n\n // ── Generate timestamp ONCE ─────────────────────────────────────────────────\n // Must be identical in Password (encoded) and Timestamp (body) fields.\n const timestamp = getTimestamp();\n\n // ── PartyB ──────────────────────────────────────────────────────────────────\n // Paybill → PartyB = shortCode\n // Buy Goods (Till) → PartyB = till number\n const partyB = request.partyB ?? request.shortCode;\n\n const body = {\n BusinessShortCode: request.shortCode,\n Password: getStkPushPassword(request.shortCode, request.passKey, timestamp),\n Timestamp: timestamp,\n TransactionType: request.transactionType ?? \"CustomerPayBillOnline\",\n Amount: amount,\n PartyA: formatPhoneNumber(request.phoneNumber),\n PartyB: partyB,\n PhoneNumber: formatPhoneNumber(request.phoneNumber),\n CallBackURL: request.callbackUrl,\n AccountReference: request.accountReference.slice(0, 12),\n TransactionDesc: request.transactionDesc.slice(0, 13),\n };\n\n const { data } = await httpRequest<StkPushResponse>(\n `${baseUrl}/mpesa/stkpush/v1/processrequest`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${accessToken}` },\n body,\n }\n );\n\n return data;\n}\n","/**\n * STK Push Query — checks the status of a Lipa Na M-Pesa Online Payment.\n *\n * API: POST /mpesa/stkpushquery/v1/query\n *\n * Daraja request body (from Discover APIs M-Pesa Express Query docs):\n * {\n * \"BusinessShortCode\": \"174379\",\n * \"Password\": \"base64(Shortcode+Passkey+Timestamp)\",\n * \"Timestamp\": \"20160216165627\",\n * \"CheckoutRequestID\": \"ws_CO_260520211133524545\"\n * }\n *\n * Response ResultCode values (from docs):\n * 0 = The service request is processed successfully.\n * 1032 = Request cancelled by user\n * 1037 = DS timeout user cannot be reached\n * 2001 = Wrong PIN\n * (and more — see STK Push docs result code table)\n */\n\nimport { httpRequest } from \"../../utils/http\";\nimport type { StkQueryRequest, StkQueryResponse } from \"./types\";\nimport { getStkPushPassword, getTimestamp } from \"./utils\";\n\nexport async function queryStkPush(\n baseUrl: string,\n accessToken: string,\n request: StkQueryRequest\n): Promise<StkQueryResponse> {\n // Generate timestamp ONCE — Password and Timestamp field MUST match.\n const timestamp = getTimestamp();\n\n const body = {\n BusinessShortCode: request.shortCode,\n Password: getStkPushPassword(request.shortCode, request.passKey, timestamp),\n Timestamp: timestamp,\n CheckoutRequestID: request.checkoutRequestId,\n };\n\n const { data } = await httpRequest<StkQueryResponse>(\n `${baseUrl}/mpesa/stkpushquery/v1/query`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${accessToken}` },\n body,\n }\n );\n\n return data;\n}\n","/**\n * STK Push (M-Pesa Express) types\n *\n * API: POST /mpesa/stkpush/v1/processrequest\n * Query: POST /mpesa/stkpushquery/v1/query\n *\n * Ref: M-Pesa Express Simulate docs + Discover APIs M-Pesa Express Query docs\n */\n\n// ── Transaction type ─────────────────────────────────────────────────────────\n\n/**\n * CustomerPayBillOnline → Paybill numbers (PartyB = shortcode)\n * CustomerBuyGoodsOnline → Till numbers (PartyB = till number)\n */\nexport type TransactionType =\n | \"CustomerPayBillOnline\"\n | \"CustomerBuyGoodsOnline\";\n\n// ── STK Push request ─────────────────────────────────────────────────────────\n\nexport interface StkPushRequest {\n /** Transaction amount (minimum KES 1, must round to a whole number ≥ 1) */\n amount: number;\n\n /**\n * Phone number sending the money. Format: 2547XXXXXXXX.\n * Must be a valid Safaricom M-PESA number.\n * Daraja docs field name: PartyA / PhoneNumber\n */\n phoneNumber: string;\n\n /**\n * URL where Safaricom will POST the callback result.\n * Must be publicly accessible (use ngrok/localtunnel for local dev).\n * Daraja docs field name: CallBackURL\n */\n callbackUrl: string;\n\n /**\n * Alpha-numeric reference shown to customer in the USSD prompt.\n * Max 12 characters.\n * Daraja docs field name: AccountReference\n */\n accountReference: string;\n\n /**\n * Additional description for the transaction.\n * Max 13 characters.\n * Daraja docs field name: TransactionDesc\n */\n transactionDesc: string;\n\n /**\n * Business shortcode — Paybill number or HO/Store number for Till.\n * Daraja docs field name: BusinessShortCode\n */\n shortCode: string;\n\n /**\n * Passkey used to generate the Password.\n * Sandbox value: from Daraja simulator test data.\n * Production value: emailed after Go Live.\n */\n passKey: string;\n\n /**\n * \"CustomerPayBillOnline\" (default) for Paybill.\n * \"CustomerBuyGoodsOnline\" for Till Numbers.\n */\n transactionType?: TransactionType;\n\n /**\n * Credit party receiving funds.\n * - CustomerPayBillOnline: defaults to shortCode\n * - CustomerBuyGoodsOnline: set to the Till Number\n * Daraja docs field name: PartyB\n */\n partyB?: string;\n}\n\n// ── STK Push response ────────────────────────────────────────────────────────\n\nexport interface StkPushResponse {\n /** Global unique identifier for the submitted payment request */\n MerchantRequestID: string;\n /** Global unique identifier for the checkout transaction */\n CheckoutRequestID: string;\n /** \"0\" = successful submission */\n ResponseCode: string;\n ResponseDescription: string;\n CustomerMessage: string;\n}\n\n// ── STK Query request/response ───────────────────────────────────────────────\n\nexport interface StkQueryRequest {\n /** CheckoutRequestID from the STK Push response */\n checkoutRequestId: string;\n shortCode: string;\n passKey: string;\n}\n\nexport interface StkQueryResponse {\n ResponseCode: string;\n ResponseDescription: string;\n MerchantRequestID: string;\n CheckoutRequestID: string;\n /**\n * Daraja returns ResultCode as a NUMBER.\n * 0 = success\n * 1 = insufficient balance\n * 1032 = cancelled by user\n * 1037 = timeout / unreachable\n * 2001 = wrong PIN\n */\n ResultCode: number;\n ResultDesc: string;\n}\n\n// ── Callback payload types ────────────────────────────────────────────────────\n// Safaricom POSTs these to your CallBackURL after the customer responds.\n\n/** Single metadata item in a successful STK callback */\nexport interface StkCallbackMetadataItem {\n Name:\n | \"Amount\"\n | \"MpesaReceiptNumber\"\n | \"TransactionDate\"\n | \"PhoneNumber\"\n | \"Balance\";\n /** Present on successful transactions; absent on failure */\n Value?: number | string;\n}\n\n/** Inner callback for a SUCCESSFUL STK Push (ResultCode === 0) */\nexport interface StkCallbackSuccess {\n MerchantRequestID: string;\n CheckoutRequestID: string;\n ResultCode: 0;\n ResultDesc: string;\n CallbackMetadata: {\n Item: StkCallbackMetadataItem[];\n };\n}\n\n/** Inner callback for a FAILED / CANCELLED STK Push (ResultCode !== 0) */\nexport interface StkCallbackFailure {\n MerchantRequestID: string;\n CheckoutRequestID: string;\n /** e.g. 1032 = cancelled by user, 1037 = timeout */\n ResultCode: number;\n ResultDesc: string;\n CallbackMetadata?: never;\n}\n\nexport type StkCallbackInner = StkCallbackSuccess | StkCallbackFailure;\n\n/** Full wrapper Safaricom POSTs to your CallBackURL */\nexport interface StkPushCallback {\n Body: {\n stkCallback: StkCallbackInner;\n };\n}\n\n// ── Type guards & helpers ─────────────────────────────────────────────────────\n\n/**\n * Narrows StkCallbackInner to the success shape.\n *\n * @example\n * if (isStkCallbackSuccess(callback.Body.stkCallback)) {\n * const receipt = getCallbackValue(callback, \"MpesaReceiptNumber\");\n * }\n */\nexport function isStkCallbackSuccess(\n cb: StkCallbackInner\n): cb is StkCallbackSuccess {\n return cb.ResultCode === 0;\n}\n\n/**\n * Extracts a named value from a successful callback's metadata.\n * Returns undefined if the key is absent or the transaction failed.\n */\nexport function getCallbackValue(\n callback: StkPushCallback,\n name: StkCallbackMetadataItem[\"Name\"]\n): string | number | undefined {\n const inner = callback.Body.stkCallback;\n if (!isStkCallbackSuccess(inner)) return undefined;\n return inner.CallbackMetadata.Item.find((i) => i.Name === name)?.Value;\n}\n","/**\n * Transaction Status Query implementation\n *\n * API: POST /mpesa/transactionstatus/v1/query\n *\n * This is ASYNCHRONOUS. The synchronous response only acknowledges receipt.\n * Final results arrive via POST to your ResultURL.\n *\n * Required M-PESA org portal role: \"Transaction Status query ORG API\"\n */\n\nimport { createError } from \"../../utils/errors\";\nimport { httpRequest } from \"../../utils/http\"; // ← httpRequest, NOT httpClient\nimport type {\n TransactionStatusRequest,\n TransactionStatusResponse,\n} from \"./types\";\n\nexport async function queryTransactionStatus(\n baseUrl: string,\n token: string,\n securityCredential: string,\n initiator: string,\n request: TransactionStatusRequest\n): Promise<TransactionStatusResponse> {\n // ── Validation ──────────────────────────────────────────────────────────────\n\n if (!request.transactionId) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message: \"transactionId is required\",\n });\n }\n\n if (!request.partyA) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message:\n \"partyA is required (your business shortcode, till number, or MSISDN)\",\n });\n }\n\n if (!request.identifierType) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message:\n 'identifierType is required: \"1\" (MSISDN) | \"2\" (Till) | \"4\" (ShortCode)',\n });\n }\n\n if (!request.resultUrl) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message:\n \"resultUrl is required — Safaricom POSTs the transaction result here\",\n });\n }\n\n if (!request.queueTimeOutUrl) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message: \"queueTimeOutUrl is required — Safaricom calls this on timeout\",\n });\n }\n\n // ── Build payload matching Daraja spec exactly ──────────────────────────────\n\n const payload = {\n Initiator: initiator,\n SecurityCredential: securityCredential,\n CommandID: request.commandId ?? \"TransactionStatusQuery\",\n TransactionID: request.transactionId,\n PartyA: request.partyA,\n IdentifierType: request.identifierType,\n ResultURL: request.resultUrl,\n QueueTimeOutURL: request.queueTimeOutUrl,\n Remarks: request.remarks ?? \"Transaction Status Query\",\n Occasion: request.occasion ?? \"\",\n };\n\n const { data } = await httpRequest<TransactionStatusResponse>(\n `${baseUrl}/mpesa/transactionstatus/v1/query`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${token}` },\n body: payload,\n }\n );\n\n return data;\n}\n","/**\n * Core M-Pesa / Daraja API types\n */\n\nexport type Environment = \"sandbox\" | \"production\";\n\n/** Base URLs per Daraja environment */\nexport const DARAJA_BASE_URLS: Record<Environment, string> = {\n sandbox: \"https://sandbox.safaricom.co.ke\",\n production: \"https://api.safaricom.co.ke\",\n} as const;\n\nexport interface MpesaConfig {\n // ── Required for all APIs ─────────────────────────────────────────────────\n consumerKey: string;\n consumerSecret: string;\n environment: Environment;\n\n // ── Required for STK Push (M-Pesa Express) ────────────────────────────────\n /** Paybill / HO shortcode (5–7 digits). Required for STK Push & STK Query. */\n lipaNaMpesaShortCode?: string;\n /**\n * Passkey from Daraja portal.\n * Sandbox: visible in the simulator test data section.\n * Production: emailed after Go Live.\n */\n lipaNaMpesaPassKey?: string;\n\n // ── Required for Transaction Status / B2C / Reversals ────────────────────\n /** M-PESA org portal API operator username */\n initiatorName?: string;\n /** Plain-text password for the API operator (will be RSA-encrypted) */\n initiatorPassword?: string;\n\n // ── Certificate options (choose one) ─────────────────────────────────────\n /**\n * Path to the .cer file on disk.\n * Bun: read via `Bun.file(path).text()`\n * Node: read via `fs.promises.readFile(path, \"utf-8\")`\n */\n certificatePath?: string;\n /** PEM string contents of the certificate (alternative to certificatePath) */\n certificatePem?: string;\n /**\n * Pre-computed base64 security credential.\n * Use this if you encrypt outside the library (e.g. at startup).\n * Skips the RSA encryption step entirely.\n */\n securityCredential?: string;\n}\n","/**\n * M-Pesa Daraja API client\n *\n * Supports:\n * - STK Push (M-Pesa Express) — stkPush()\n * - STK Query — stkQuery()\n * - Transaction Status Query — transactionStatus()\n *\n * @example\n * const mpesa = new Mpesa({\n * consumerKey: process.env.MPESA_CONSUMER_KEY!,\n * consumerSecret: process.env.MPESA_CONSUMER_SECRET!,\n * environment: \"sandbox\",\n * lipaNaMpesaShortCode: \"174379\",\n * lipaNaMpesaPassKey: \"bfb279...\",\n * initiatorName: \"testapi\",\n * initiatorPassword: \"Safaricom123!\",\n * certificatePath: \"./SandboxCertificate.cer\",\n * });\n */\n\nimport { TokenManager } from \"../core/auth\";\nimport { encryptSecurityCredential } from \"../core/encryption\";\nimport { PesafyError } from \"../utils/errors\";\nimport {\n processStkPush,\n queryStkPush,\n type StkPushRequest,\n type StkQueryRequest,\n} from \"./stk-push\";\nimport {\n queryTransactionStatus,\n type TransactionStatusRequest,\n} from \"./transaction-status\";\nimport { DARAJA_BASE_URLS, type MpesaConfig } from \"./types\";\n\nexport class Mpesa {\n private readonly config: MpesaConfig;\n private readonly tokenManager: TokenManager;\n private readonly baseUrl: string;\n\n constructor(config: MpesaConfig) {\n if (!config.consumerKey || !config.consumerSecret) {\n throw new PesafyError({\n code: \"INVALID_CREDENTIALS\",\n message: \"consumerKey and consumerSecret are required\",\n });\n }\n\n this.config = config;\n this.baseUrl = DARAJA_BASE_URLS[config.environment];\n this.tokenManager = new TokenManager(\n config.consumerKey,\n config.consumerSecret,\n this.baseUrl\n );\n }\n\n // ── Internal helpers ────────────────────────────────────────────────────────\n\n private getToken(): Promise<string> {\n return this.tokenManager.getAccessToken();\n }\n\n private async buildSecurityCredential(): Promise<string> {\n // Option 1: caller pre-computed it\n if (this.config.securityCredential) return this.config.securityCredential;\n\n // Option 2: we encrypt it\n if (!this.config.initiatorPassword) {\n throw new PesafyError({\n code: \"INVALID_CREDENTIALS\",\n message:\n \"Provide securityCredential (pre-encrypted) \" +\n \"OR (initiatorPassword + certificatePath/certificatePem)\",\n });\n }\n\n let cert: string;\n if (this.config.certificatePem) {\n cert = this.config.certificatePem;\n } else if (this.config.certificatePath) {\n // Bun runtime\n if (typeof Bun !== \"undefined\") {\n cert = await Bun.file(this.config.certificatePath).text();\n } else {\n // Node.js fallback\n const { readFile } = await import(\"node:fs/promises\");\n cert = await readFile(this.config.certificatePath, \"utf-8\");\n }\n } else {\n throw new PesafyError({\n code: \"INVALID_CREDENTIALS\",\n message:\n \"certificatePath or certificatePem required to encrypt the initiator password\",\n });\n }\n\n return encryptSecurityCredential(this.config.initiatorPassword, cert);\n }\n\n // ── STK Push ──────────────────────────────────────────────────────────────\n\n /**\n * M-Pesa Express — sends a payment prompt to the customer's phone.\n *\n * Requires: lipaNaMpesaShortCode + lipaNaMpesaPassKey in config.\n *\n * @example\n * const res = await mpesa.stkPush({\n * amount: 100,\n * phoneNumber: \"0712345678\",\n * callbackUrl: \"https://yourdomain.com/mpesa/callback\",\n * accountReference: \"INV-001\",\n * transactionDesc: \"Payment\",\n * });\n * console.log(res.CheckoutRequestID); // use to poll status\n */\n async stkPush(request: Omit<StkPushRequest, \"shortCode\" | \"passKey\">) {\n const shortCode = this.config.lipaNaMpesaShortCode ?? \"\";\n const passKey = this.config.lipaNaMpesaPassKey ?? \"\";\n\n if (!shortCode || !passKey) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message:\n \"lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push\",\n });\n }\n\n const token = await this.getToken();\n return processStkPush(this.baseUrl, token, {\n ...request,\n shortCode,\n passKey,\n });\n }\n\n /**\n * STK Query — checks the status of a previous STK Push.\n *\n * @example\n * const status = await mpesa.stkQuery({\n * checkoutRequestId: \"ws_CO_1007202409152617172396192\",\n * });\n * if (status.ResultCode === 0) // payment confirmed\n */\n async stkQuery(request: Omit<StkQueryRequest, \"shortCode\" | \"passKey\">) {\n const shortCode = this.config.lipaNaMpesaShortCode ?? \"\";\n const passKey = this.config.lipaNaMpesaPassKey ?? \"\";\n\n if (!shortCode || !passKey) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message:\n \"lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Query\",\n });\n }\n\n const token = await this.getToken();\n return queryStkPush(this.baseUrl, token, {\n ...request,\n shortCode,\n passKey,\n });\n }\n\n /**\n * Transaction Status — queries the result of a completed M-Pesa transaction.\n *\n * Requires: initiatorName + (initiatorPassword + certificate) OR securityCredential.\n *\n * This is ASYNCHRONOUS. The synchronous response only confirms receipt.\n * Final details are POSTed to your resultUrl.\n *\n * @example\n * await mpesa.transactionStatus({\n * transactionId: \"OEI2AK4XXXX\",\n * partyA: \"174379\",\n * identifierType: \"4\",\n * resultUrl: \"https://yourdomain.com/mpesa/result\",\n * queueTimeOutUrl: \"https://yourdomain.com/mpesa/timeout\",\n * remarks: \"Check payment status\",\n * });\n */\n async transactionStatus(request: TransactionStatusRequest) {\n const initiator = this.config.initiatorName ?? \"\";\n if (!initiator) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message: \"initiatorName is required for Transaction Status\",\n });\n }\n\n // Fetch token and encrypt credential concurrently\n const [token, securityCred] = await Promise.all([\n this.getToken(),\n this.buildSecurityCredential(),\n ]);\n\n return queryTransactionStatus(\n this.baseUrl,\n token,\n securityCred,\n initiator,\n request\n );\n }\n\n /** Force the cached OAuth token to be refreshed on the next API call */\n clearTokenCache(): void {\n this.tokenManager.clearCache();\n }\n}\n","/**\n * Exponential backoff retry for webhook at-least-once delivery.\n *\n * Daraja is asynchronous — if your callback endpoint is down, the API\n * Gateway logs a 503 and discards the result. Use this utility to\n * retry your own internal processing after receiving a webhook.\n */\n\nexport interface RetryOptions {\n /** Maximum number of attempts (default: Infinity) */\n maxRetries?: number;\n /** Initial delay in ms (default: 1000 = 1 second) */\n initialDelay?: number;\n /** Maximum delay cap in ms (default: 3_600_000 = 1 hour) */\n maxDelay?: number;\n /** Multiplier per retry (default: 2 — doubles each time) */\n backoffMultiplier?: number;\n /** Maximum total duration in ms (default: 30 days) */\n maxRetryDuration?: number;\n}\n\nconst DEFAULT_OPTIONS: Required<RetryOptions> = {\n maxRetries: Infinity,\n initialDelay: 1_000,\n maxDelay: 3_600_000,\n backoffMultiplier: 2,\n maxRetryDuration: 30 * 24 * 60 * 60 * 1_000, // 30 days\n};\n\nexport interface RetryResult<T> {\n success: boolean;\n data?: T;\n attempts: number;\n error?: Error;\n}\n\n/**\n * Retries `fn` with exponential backoff until it resolves, or limits are hit.\n *\n * @example\n * const result = await retryWithBackoff(\n * () => sendToDatabase(webhookData),\n * { maxRetries: 5, initialDelay: 500 }\n * );\n */\nexport async function retryWithBackoff<T>(\n fn: () => Promise<T>,\n options: RetryOptions = {}\n): Promise<RetryResult<T>> {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n let delay = opts.initialDelay;\n let attempts = 0;\n const startTime = Date.now();\n\n while (attempts < opts.maxRetries) {\n attempts++;\n\n if (Date.now() - startTime > opts.maxRetryDuration) {\n return {\n success: false,\n attempts,\n error: new Error(\"Max retry duration exceeded\"),\n };\n }\n\n try {\n const data = await fn();\n return { success: true, data, attempts };\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n\n // Don't retry client errors (4xx) — they won't self-heal\n if (err.message.includes(\"4\")) {\n return { success: false, attempts, error: err };\n }\n\n if (attempts < opts.maxRetries) {\n await new Promise((resolve) => setTimeout(resolve, delay));\n delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelay);\n }\n }\n }\n\n return {\n success: false,\n attempts,\n error: new Error(\"Max retries exceeded\"),\n };\n}\n","/**\n * Webhook verification utilities\n *\n * Daraja does NOT use HMAC webhook signatures like Stripe.\n * Instead, verify that callbacks come from whitelisted Safaricom IPs.\n *\n * Official Safaricom IP whitelist (from Getting Started docs):\n * 196.201.214.200\n * 196.201.214.206\n * 196.201.213.114\n * 196.201.214.207\n * 196.201.214.208\n * 196.201.213.44\n * 196.201.212.127\n * 196.201.212.138\n * 196.201.212.129\n * 196.201.212.136\n * 196.201.212.74\n * 196.201.212.69\n */\n\nimport type { StkPushWebhook } from \"./types\";\n\n/** Official Safaricom API Gateway IP addresses */\nexport const SAFARICOM_IPS: readonly string[] = [\n \"196.201.214.200\",\n \"196.201.214.206\",\n \"196.201.213.114\",\n \"196.201.214.207\",\n \"196.201.214.208\",\n \"196.201.213.44\",\n \"196.201.212.127\",\n \"196.201.212.138\",\n \"196.201.212.129\",\n \"196.201.212.136\",\n \"196.201.212.74\",\n \"196.201.212.69\",\n] as const;\n\n/**\n * Returns true if requestIP is in the allowed list.\n * Defaults to the official Safaricom IP whitelist.\n */\nexport function verifyWebhookIP(\n requestIP: string,\n allowedIPs: readonly string[] = SAFARICOM_IPS\n): boolean {\n return allowedIPs.includes(requestIP);\n}\n\n/**\n * Parses and validates an STK Push webhook body.\n * Returns the typed payload or null if it doesn't match the expected shape.\n */\nexport function parseStkPushWebhook(body: unknown): StkPushWebhook | null {\n try {\n const parsed = body as StkPushWebhook;\n if (parsed?.Body?.stkCallback) return parsed;\n return null;\n } catch {\n return null;\n }\n}\n","/**\n * High-level webhook event handler\n */\n\nimport { parseStkPushWebhook, verifyWebhookIP } from \"./signature-verifier\";\nimport type { StkPushWebhook, WebhookEventType } from \"./types\";\n\nexport interface WebhookHandlerOptions {\n /** IP address of the incoming request (from req.ip or x-forwarded-for) */\n requestIP?: string;\n /** Override the default Safaricom IP whitelist */\n allowedIPs?: string[];\n /** Skip IP verification — ONLY for local development/testing */\n skipIPCheck?: boolean;\n}\n\nexport interface WebhookHandlerResult<T = unknown> {\n success: boolean;\n eventType: WebhookEventType | null;\n data: T | null;\n error?: string;\n}\n\n/**\n * Parses and validates an inbound Daraja webhook payload.\n *\n * @example\n * // Express route\n * app.post(\"/mpesa/callback\", (req, res) => {\n * const result = handleWebhook(req.body, { requestIP: req.ip });\n * if (!result.success) return res.status(400).json({ error: result.error });\n * // process result.data (StkPushWebhook)\n * res.json({ ResultCode: 0, ResultDesc: \"Accepted\" });\n * });\n */\nexport function handleWebhook(\n body: unknown,\n options: WebhookHandlerOptions = {}\n): WebhookHandlerResult {\n // ── IP verification ─────────────────────────────────────────────────────────\n if (!options.skipIPCheck && options.requestIP) {\n if (!verifyWebhookIP(options.requestIP, options.allowedIPs)) {\n return {\n success: false,\n eventType: null,\n data: null,\n error: `IP address ${options.requestIP} is not in the Safaricom whitelist`,\n };\n }\n }\n\n // ── Parse STK Push callback ─────────────────────────────────────────────────\n const stkPush = parseStkPushWebhook(body);\n if (stkPush) {\n return {\n success: true,\n eventType: \"stk_push\",\n data: stkPush,\n };\n }\n\n return {\n success: false,\n eventType: null,\n data: null,\n error: \"Unknown or malformed webhook payload\",\n };\n}\n\n// ── Convenience extractors ────────────────────────────────────────────────────\n\n/** Extracts the M-Pesa receipt number from a successful STK Push callback */\nexport function extractTransactionId(webhook: StkPushWebhook): string | null {\n const items = webhook.Body?.stkCallback?.CallbackMetadata?.Item;\n const item = items?.find((i) => i.Name === \"MpesaReceiptNumber\");\n return item ? String(item.Value) : null;\n}\n\n/** Extracts the transaction amount from a successful STK Push callback */\nexport function extractAmount(webhook: StkPushWebhook): number | null {\n const items = webhook.Body?.stkCallback?.CallbackMetadata?.Item;\n const item = items?.find((i) => i.Name === \"Amount\");\n return item ? Number(item.Value) : null;\n}\n\n/** Extracts the phone number from a successful STK Push callback */\nexport function extractPhoneNumber(webhook: StkPushWebhook): string | null {\n const items = webhook.Body?.stkCallback?.CallbackMetadata?.Item;\n const item = items?.find((i) => i.Name === \"PhoneNumber\");\n return item ? String(item.Value) : null;\n}\n\n/** Returns true if the STK Push callback represents a successful transaction */\nexport function isSuccessfulCallback(webhook: StkPushWebhook): boolean {\n return webhook.Body?.stkCallback?.ResultCode === 0;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/errors/index.ts","../src/core/encryption/security-credentials.ts","../src/utils/http/index.ts","../src/core/auth/token-manager.ts","../src/utils/phone/index.ts","../src/mpesa/stk-push/utils.ts","../src/mpesa/stk-push/stk-push.ts","../src/mpesa/stk-push/stk-query.ts","../src/mpesa/stk-push/types.ts","../src/mpesa/transaction-status/query.ts","../src/mpesa/types.ts","../src/mpesa/index.ts","../src/mpesa/webhooks/retry.ts","../src/mpesa/webhooks/signature-verifier.ts","../src/mpesa/webhooks/webhook-handler.ts"],"names":["publicEncrypt","constants"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Pesafy error utilities\n */\nexport type ErrorCode =\n | \"INVALID_CREDENTIALS\"\n | \"AUTH_FAILED\"\n | \"VALIDATION_ERROR\"\n | \"ENCRYPTION_FAILED\"\n | \"REQUEST_FAILED\"\n | \"INVALID_PHONE\"\n | \"HTTP_ERROR\"\n | \"API_ERROR\"\n | \"NETWORK_ERROR\"\n | \"TIMEOUT\"\n | \"INVALID_RESPONSE\";\n\nexport interface PesafyErrorOptions {\n code: ErrorCode;\n message: string;\n statusCode?: number;\n response?: unknown;\n cause?: unknown;\n requestId?: string;\n}\n\nexport class PesafyError extends Error {\n readonly code: ErrorCode;\n readonly statusCode: number | undefined;\n readonly response: unknown;\n readonly requestId: string | undefined;\n override readonly cause: unknown;\n\n constructor(options: PesafyErrorOptions) {\n super(options.message);\n Object.defineProperty(this, \"name\", { value: \"PesafyError\" });\n this.code = options.code;\n this.statusCode = options.statusCode;\n this.response = options.response;\n this.requestId = options.requestId;\n this.cause = options.cause;\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, PesafyError);\n }\n }\n\n toJSON() {\n return {\n name: this.name,\n code: this.code,\n message: this.message,\n statusCode: this.statusCode,\n requestId: this.requestId,\n };\n }\n}\n\n/** Convenience factory — identical API to `new PesafyError(...)` */\nexport function createError(options: PesafyErrorOptions): PesafyError {\n return new PesafyError(options);\n}\n","/**\n * Security credential encryption for Daraja APIs that require it:\n * B2C, B2B, Transaction Status Query, Reversals, Tax Remittance.\n *\n * Algorithm (from Safaricom \"Getting Started\" docs):\n * 1. Write the unencrypted initiator password into a byte array.\n * 2. Encrypt using the M-Pesa public key certificate:\n * - RSA algorithm\n * - PKCS #1 v1.5 padding (NOT OAEP)\n * 3. Base64-encode the encrypted byte array.\n *\n * Certificate source:\n * Sandbox: https://developer.safaricom.co.ke (SandboxCertificate.cer)\n * Production: https://developer.safaricom.co.ke (ProductionCertificate.cer)\n *\n * NOTE: Use the correct certificate for each environment or credentials\n * will be rejected.\n */\n\nimport { constants, publicEncrypt } from \"node:crypto\";\nimport { PesafyError } from \"../../utils/errors\";\n\n/**\n * Encrypts `initiatorPassword` with the given PEM certificate and returns\n * the base64-encoded security credential ready to send to Daraja.\n *\n * @param initiatorPassword - Plain-text password set on the M-PESA org portal\n * @param certificatePem - Full PEM string (the .cer file contents)\n */\nexport function encryptSecurityCredential(\n initiatorPassword: string,\n certificatePem: string\n): string {\n try {\n const passwordBuffer = Buffer.from(initiatorPassword, \"utf-8\");\n\n const encrypted = publicEncrypt(\n {\n key: certificatePem,\n // RSA_PKCS1_PADDING = 1 (NOT RSA_PKCS1_OAEP_PADDING = 4)\n padding: constants.RSA_PKCS1_PADDING,\n },\n passwordBuffer\n );\n\n return encrypted.toString(\"base64\");\n } catch (error) {\n throw new PesafyError({\n code: \"ENCRYPTION_FAILED\",\n message:\n \"Failed to encrypt security credential. \" +\n \"Ensure the certificate PEM is valid and matches the environment (sandbox/production).\",\n cause: error,\n });\n }\n}\n","/**\n * Minimal HTTP client for Daraja API calls.\n *\n * Why not use axios/got/ky?\n * - Zero extra dependencies\n * - Works in Node.js, Bun, and edge runtimes unchanged\n * - Daraja only needs POST + GET with JSON bodies\n *\n * Exported: httpRequest (the ONLY export — never export \"httpClient\")\n */\n\n// src/utils/http/index.ts\n\nimport { PesafyError } from \"../errors\";\n\nexport interface HttpRequestOptions {\n method: \"GET\" | \"POST\";\n headers?: Record<string, string>;\n /** Will be JSON-serialised and sent as application/json */\n body?: unknown;\n /**\n * Number of times to retry on transient errors (503, 429, network failures).\n * Default: 4. Set to 0 to disable retries.\n */\n retries?: number;\n /**\n * Base delay in ms before first retry. Doubles each attempt + jitter.\n * Default: 2000 (2 s). Daraja sandbox needs longer gaps than typical APIs.\n */\n retryDelay?: number;\n}\n\nexport interface HttpResponse<T> {\n data: T;\n status: number;\n headers: Record<string, string>;\n}\n\n/** Status codes that are transient and safe to retry */\nconst RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);\n\n/** Sleep for `ms` milliseconds */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Returns a delay with ±25 % jitter to avoid retry storms.\n * Daraja sandbox 503s are caused by sandbox overload — spreading\n * retries prevents making it worse.\n */\nfunction jitter(baseMs: number): number {\n const spread = baseMs * 0.25;\n return baseMs + (Math.random() * spread * 2 - spread);\n}\n\n/**\n * Sends an HTTP request and returns parsed JSON.\n *\n * Automatically retries on transient errors (503, 429, network failures)\n * with exponential backoff + jitter. Never retries on 4xx client errors.\n *\n * NOTE: `httpClient` is NOT exported — the only export is `httpRequest`.\n */\nexport async function httpRequest<T = unknown>(\n url: string,\n options: HttpRequestOptions\n): Promise<HttpResponse<T>> {\n const maxRetries = options.retries ?? 4;\n const baseDelay = options.retryDelay ?? 2000;\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...options.headers,\n };\n\n const init: RequestInit = {\n method: options.method,\n headers,\n ...(options.body !== undefined\n ? { body: JSON.stringify(options.body) }\n : {}),\n };\n\n let lastError: PesafyError | null = null;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n // Wait before retry (not before the first attempt)\n if (attempt > 0) {\n const delay = jitter(baseDelay * Math.pow(2, attempt - 1));\n await sleep(delay);\n }\n\n let response: Response;\n\n try {\n response = await fetch(url, init);\n } catch (err) {\n // Network-level failure (DNS, connection refused, etc.)\n lastError = new PesafyError({\n code: \"NETWORK_ERROR\",\n message: `Network error calling ${url}: ${String(err)}`,\n cause: err,\n });\n\n if (attempt < maxRetries) continue; // retry\n throw lastError;\n }\n\n // Parse body regardless of status so we can include it in errors\n let data: unknown;\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n try {\n data = contentType.includes(\"application/json\")\n ? await response.json()\n : await response.text();\n } catch {\n data = null;\n }\n\n // Collect response headers\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n if (response.ok) {\n return {\n data: data as T,\n status: response.status,\n headers: responseHeaders,\n };\n }\n\n // ── Error response ────────────────────────────────────────────────────────\n const isTransient = RETRYABLE_STATUSES.has(response.status);\n\n // Daraja error shape: { requestId, errorCode, errorMessage }\n const daraja = (data ?? {}) as Record<string, unknown>;\n const message =\n (daraja.errorMessage as string) ??\n (daraja.ResponseDescription as string) ??\n `HTTP ${response.status}`;\n\n lastError = new PesafyError({\n code: isTransient ? \"REQUEST_FAILED\" : \"HTTP_ERROR\",\n message,\n statusCode: response.status,\n response: data,\n requestId: daraja.requestId as string | undefined,\n });\n\n // Only retry transient errors — never retry 4xx client errors\n if (isTransient && attempt < maxRetries) {\n continue;\n }\n\n throw lastError;\n }\n\n // Should never reach here, but TypeScript requires it\n throw lastError!;\n}\n","/**\n * OAuth token manager for Daraja API.\n *\n * Daraja Authorization endpoint (GET, Basic Auth):\n * https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials\n * https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials\n *\n * Token validity: 3600 seconds (1 hour).\n * We refresh 60 s early to avoid edge-case expiry mid-request.\n *\n * Ref: Authorization By Safaricom docs\n */\n\nimport { PesafyError } from \"../../utils/errors\";\nimport { httpRequest } from \"../../utils/http\";\nimport type { TokenResponse } from \"./types\";\n\n/** Refresh the token this many seconds before it actually expires */\nconst TOKEN_BUFFER_SECONDS = 60;\n\nexport class TokenManager {\n private readonly consumerKey: string;\n private readonly consumerSecret: string;\n private readonly baseUrl: string;\n\n private cachedToken: string | null = null;\n private tokenExpiresAt = 0; // Unix seconds\n\n constructor(consumerKey: string, consumerSecret: string, baseUrl: string) {\n this.consumerKey = consumerKey;\n this.consumerSecret = consumerSecret;\n this.baseUrl = baseUrl;\n }\n\n private getBasicAuthHeader(): string {\n // Daraja spec: Base64(consumerKey:consumerSecret)\n const credentials = `${this.consumerKey}:${this.consumerSecret}`;\n const encoded = Buffer.from(credentials, \"utf-8\").toString(\"base64\");\n return `Basic ${encoded}`;\n }\n\n /**\n * Returns a valid access token, fetching a new one when the cached token\n * is absent or within TOKEN_BUFFER_SECONDS of expiry.\n */\n async getAccessToken(): Promise<string> {\n const now = Date.now() / 1000;\n\n if (this.cachedToken && this.tokenExpiresAt > now + TOKEN_BUFFER_SECONDS) {\n return this.cachedToken;\n }\n\n // Daraja Authorization API: GET with Basic Auth + grant_type query param\n const url = `${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`;\n\n const response = await httpRequest<TokenResponse>(url, {\n method: \"GET\",\n headers: {\n Authorization: this.getBasicAuthHeader(),\n },\n });\n\n const { access_token, expires_in } = response.data;\n\n if (!access_token) {\n throw new PesafyError({\n code: \"AUTH_FAILED\",\n message:\n \"Daraja did not return an access token. Check your consumer key and secret.\",\n response: response.data,\n });\n }\n\n this.cachedToken = access_token;\n // expires_in is 3599 per Daraja docs; default to 3600 if missing\n this.tokenExpiresAt = now + (expires_in ?? 3600);\n\n return this.cachedToken;\n }\n\n /** Force token refresh on the next call (e.g. after a 401 response) */\n clearCache(): void {\n this.cachedToken = null;\n this.tokenExpiresAt = 0;\n }\n}\n","/**\n * Phone number utilities for Daraja API.\n *\n * Daraja spec: PartyA and PhoneNumber must be in the format 2547XXXXXXXX\n * (12-digit, starts with 254, no +, no spaces, no dashes).\n *\n * Accepted input formats:\n * 0712345678 → 254712345678\n * +254712345678 → 254712345678\n * 254712345678 → 254712345678 (already correct)\n * 712345678 → 254712345678\n */\n\nimport { PesafyError } from \"../errors\";\n\n/** Normalises any common Kenyan phone format to 254XXXXXXXXX (12 digits) */\nexport function formatSafaricomPhone(phone: string): string {\n const digits = phone.replace(/\\D/g, \"\");\n\n let normalised: string;\n\n if (digits.startsWith(\"254\") && digits.length === 12) {\n normalised = digits;\n } else if (digits.startsWith(\"0\") && digits.length === 10) {\n normalised = `254${digits.slice(1)}`;\n } else if (digits.length === 9) {\n // e.g. 712345678 → 254712345678\n normalised = `254${digits}`;\n } else if (digits.startsWith(\"254\") && digits.length !== 12) {\n throw new PesafyError({\n code: \"INVALID_PHONE\",\n message: `Invalid phone number \"${phone}\". Expected 254XXXXXXXXX (12 digits).`,\n });\n } else {\n throw new PesafyError({\n code: \"INVALID_PHONE\",\n message: `Cannot parse phone number \"${phone}\". Use 07XXXXXXXX, 2547XXXXXXXX, or +2547XXXXXXXX.`,\n });\n }\n\n // Final sanity check: must be exactly 12 digits\n if (normalised.length !== 12) {\n throw new PesafyError({\n code: \"INVALID_PHONE\",\n message: `Phone number \"${phone}\" normalised to \"${normalised}\" which is not 12 digits.`,\n });\n }\n\n return normalised;\n}\n","/**\n * STK Push utility functions\n *\n * Password spec (from Daraja docs):\n * Password = Base64( BusinessShortCode + Passkey + Timestamp )\n * Timestamp = YYYYMMDDHHmmss\n *\n * IMPORTANT: Generate the timestamp ONCE per request and pass the same\n * value to BOTH getStkPushPassword() and the request body's Timestamp field.\n * Safaricom validates that Base64(Shortcode+Passkey+Timestamp) matches the\n * Timestamp sent in the body — two separate calls to getTimestamp() will\n * produce different values and cause auth failures.\n */\n\nexport { formatSafaricomPhone as formatPhoneNumber } from \"../../utils/phone\";\n\n/**\n * Generates the STK Push password.\n * Formula: Base64( Shortcode + Passkey + Timestamp )\n *\n * Uses btoa() — works in Node.js ≥18, Bun, browsers, and edge runtimes.\n */\nexport function getStkPushPassword(\n shortCode: string,\n passKey: string,\n timestamp: string\n): string {\n return btoa(`${shortCode}${passKey}${timestamp}`);\n}\n\n/**\n * Returns a Daraja-compatible timestamp: YYYYMMDDHHmmss\n *\n * Call this ONCE per request and reuse the result.\n */\nexport function getTimestamp(): string {\n const now = new Date();\n const pad = (n: number): string => n.toString().padStart(2, \"0\");\n return [\n now.getFullYear(),\n pad(now.getMonth() + 1),\n pad(now.getDate()),\n pad(now.getHours()),\n pad(now.getMinutes()),\n pad(now.getSeconds()),\n ].join(\"\");\n}\n","// src/mpesa/stk-push/stk-push.ts\n\n/**\n * M-Pesa Express (STK Push) — initiates a payment prompt on the customer's phone.\n *\n * API: POST /mpesa/stkpush/v1/processrequest\n *\n * Daraja request body (from docs):\n * {\n * \"BusinessShortCode\": 174379,\n * \"Password\": \"base64(Shortcode+Passkey+Timestamp)\",\n * \"Timestamp\": \"20210628092408\",\n * \"TransactionType\": \"CustomerPayBillOnline\",\n * \"Amount\": \"1\",\n * \"PartyA\": \"254722000000\",\n * \"PartyB\": \"174379\",\n * \"PhoneNumber\": \"254722111111\",\n * \"CallBackURL\": \"https://mydomain.com/path\",\n * \"AccountReference\": \"accountref\", ← max 12 chars\n * \"TransactionDesc\": \"txndesc\" ← max 13 chars\n * }\n *\n * Notes from docs:\n * - All fields except TransactionDesc are mandatory.\n * - Amount must be a whole number ≥ 1 (KES).\n * - PartyA/PhoneNumber must be 254XXXXXXXXX format.\n * - AccountReference max 12 chars.\n * - TransactionDesc max 13 chars.\n */\n\nimport { PesafyError } from \"../../utils/errors\";\nimport { httpRequest } from \"../../utils/http\";\nimport type { StkPushRequest, StkPushResponse } from \"./types\";\nimport { formatPhoneNumber, getStkPushPassword, getTimestamp } from \"./utils\";\n\nexport async function processStkPush(\n baseUrl: string,\n accessToken: string,\n request: StkPushRequest\n): Promise<StkPushResponse> {\n // ── Amount validation ───────────────────────────────────────────────────────\n const amount = Math.round(request.amount);\n if (amount < 1) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message: `Amount must be at least KES 1 (got ${request.amount} which rounds to ${amount}).`,\n });\n }\n\n // ── Generate timestamp ONCE ─────────────────────────────────────────────────\n // Must be identical in Password (encoded) and Timestamp (body) fields.\n const timestamp = getTimestamp();\n\n // ── PartyB logic ────────────────────────────────────────────────────────────\n // Paybill → PartyB = shortCode\n // Buy Goods (Till) → PartyB = till number (passed as request.partyB)\n const partyB = request.partyB ?? request.shortCode;\n\n const body = {\n BusinessShortCode: request.shortCode,\n Password: getStkPushPassword(request.shortCode, request.passKey, timestamp),\n Timestamp: timestamp,\n TransactionType: request.transactionType ?? \"CustomerPayBillOnline\",\n Amount: amount,\n PartyA: formatPhoneNumber(request.phoneNumber),\n PartyB: partyB,\n PhoneNumber: formatPhoneNumber(request.phoneNumber),\n CallBackURL: request.callbackUrl,\n // Daraja docs: AccountReference max 12 chars, TransactionDesc max 13 chars\n AccountReference: request.accountReference.slice(0, 12),\n TransactionDesc: request.transactionDesc.slice(0, 13),\n };\n\n // httpRequest already retries 503/429/5xx with exponential backoff + jitter.\n // If all retries are exhausted it throws PesafyError with code \"REQUEST_FAILED\"\n // and statusCode 503 — callers should treat this as TRANSIENT, not a final\n // failure. Never mark a transaction \"failed\" on a 503.\n const { data } = await httpRequest<StkPushResponse>(\n `${baseUrl}/mpesa/stkpush/v1/processrequest`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${accessToken}` },\n body,\n // Daraja sandbox needs more retries and longer gaps due to instability\n retries: 5,\n retryDelay: 3000,\n }\n );\n\n return data;\n}\n","/**\n * STK Push Query — checks the status of a Lipa Na M-Pesa Online Payment.\n *\n * API: POST /mpesa/stkpushquery/v1/query\n *\n * Daraja request body (from Discover APIs M-Pesa Express Query docs):\n * {\n * \"BusinessShortCode\": \"174379\",\n * \"Password\": \"base64(Shortcode+Passkey+Timestamp)\",\n * \"Timestamp\": \"20160216165627\",\n * \"CheckoutRequestID\": \"ws_CO_260520211133524545\"\n * }\n *\n * Response ResultCode values (from docs):\n * 0 = The service request is processed successfully.\n * 1032 = Request cancelled by user\n * 1037 = DS timeout user cannot be reached\n * 2001 = Wrong PIN\n * (and more — see STK Push docs result code table)\n */\n\nimport { httpRequest } from \"../../utils/http\";\nimport type { StkQueryRequest, StkQueryResponse } from \"./types\";\nimport { getStkPushPassword, getTimestamp } from \"./utils\";\n\nexport async function queryStkPush(\n baseUrl: string,\n accessToken: string,\n request: StkQueryRequest\n): Promise<StkQueryResponse> {\n // Generate timestamp ONCE — Password and Timestamp field MUST match.\n const timestamp = getTimestamp();\n\n const body = {\n BusinessShortCode: request.shortCode,\n Password: getStkPushPassword(request.shortCode, request.passKey, timestamp),\n Timestamp: timestamp,\n CheckoutRequestID: request.checkoutRequestId,\n };\n\n const { data } = await httpRequest<StkQueryResponse>(\n `${baseUrl}/mpesa/stkpushquery/v1/query`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${accessToken}` },\n body,\n }\n );\n\n return data;\n}\n","/**\n * STK Push (M-Pesa Express) types\n *\n * API: POST /mpesa/stkpush/v1/processrequest\n * Query: POST /mpesa/stkpushquery/v1/query\n *\n * Ref: M-Pesa Express Simulate docs + Discover APIs M-Pesa Express Query docs\n */\n\n// ── Transaction type ─────────────────────────────────────────────────────────\n\n/**\n * CustomerPayBillOnline → Paybill numbers (PartyB = shortcode)\n * CustomerBuyGoodsOnline → Till numbers (PartyB = till number)\n */\nexport type TransactionType =\n | \"CustomerPayBillOnline\"\n | \"CustomerBuyGoodsOnline\";\n\n// ── STK Push request ─────────────────────────────────────────────────────────\n\nexport interface StkPushRequest {\n /** Transaction amount (minimum KES 1, must round to a whole number ≥ 1) */\n amount: number;\n\n /**\n * Phone number sending the money. Format: 2547XXXXXXXX.\n * Must be a valid Safaricom M-PESA number.\n * Daraja docs field name: PartyA / PhoneNumber\n */\n phoneNumber: string;\n\n /**\n * URL where Safaricom will POST the callback result.\n * Must be publicly accessible (use ngrok/localtunnel for local dev).\n * Daraja docs field name: CallBackURL\n */\n callbackUrl: string;\n\n /**\n * Alpha-numeric reference shown to customer in the USSD prompt.\n * Max 12 characters.\n * Daraja docs field name: AccountReference\n */\n accountReference: string;\n\n /**\n * Additional description for the transaction.\n * Max 13 characters.\n * Daraja docs field name: TransactionDesc\n */\n transactionDesc: string;\n\n /**\n * Business shortcode — Paybill number or HO/Store number for Till.\n * Daraja docs field name: BusinessShortCode\n */\n shortCode: string;\n\n /**\n * Passkey used to generate the Password.\n * Sandbox value: from Daraja simulator test data.\n * Production value: emailed after Go Live.\n */\n passKey: string;\n\n /**\n * \"CustomerPayBillOnline\" (default) for Paybill.\n * \"CustomerBuyGoodsOnline\" for Till Numbers.\n */\n transactionType?: TransactionType;\n\n /**\n * Credit party receiving funds.\n * - CustomerPayBillOnline: defaults to shortCode\n * - CustomerBuyGoodsOnline: set to the Till Number\n * Daraja docs field name: PartyB\n */\n partyB?: string;\n}\n\n// ── STK Push response ────────────────────────────────────────────────────────\n\nexport interface StkPushResponse {\n /** Global unique identifier for the submitted payment request */\n MerchantRequestID: string;\n /** Global unique identifier for the checkout transaction */\n CheckoutRequestID: string;\n /** \"0\" = successful submission */\n ResponseCode: string;\n ResponseDescription: string;\n CustomerMessage: string;\n}\n\n// ── STK Query request/response ───────────────────────────────────────────────\n\nexport interface StkQueryRequest {\n /** CheckoutRequestID from the STK Push response */\n checkoutRequestId: string;\n shortCode: string;\n passKey: string;\n}\n\nexport interface StkQueryResponse {\n ResponseCode: string;\n ResponseDescription: string;\n MerchantRequestID: string;\n CheckoutRequestID: string;\n /**\n * Daraja returns ResultCode as a NUMBER.\n * 0 = success\n * 1 = insufficient balance\n * 1032 = cancelled by user\n * 1037 = timeout / unreachable\n * 2001 = wrong PIN\n */\n ResultCode: number;\n ResultDesc: string;\n}\n\n// ── Callback payload types ────────────────────────────────────────────────────\n// Safaricom POSTs these to your CallBackURL after the customer responds.\n\n/** Single metadata item in a successful STK callback */\nexport interface StkCallbackMetadataItem {\n Name:\n | \"Amount\"\n | \"MpesaReceiptNumber\"\n | \"TransactionDate\"\n | \"PhoneNumber\"\n | \"Balance\";\n /** Present on successful transactions; absent on failure */\n Value?: number | string;\n}\n\n/** Inner callback for a SUCCESSFUL STK Push (ResultCode === 0) */\nexport interface StkCallbackSuccess {\n MerchantRequestID: string;\n CheckoutRequestID: string;\n ResultCode: 0;\n ResultDesc: string;\n CallbackMetadata: {\n Item: StkCallbackMetadataItem[];\n };\n}\n\n/** Inner callback for a FAILED / CANCELLED STK Push (ResultCode !== 0) */\nexport interface StkCallbackFailure {\n MerchantRequestID: string;\n CheckoutRequestID: string;\n /** e.g. 1032 = cancelled by user, 1037 = timeout */\n ResultCode: number;\n ResultDesc: string;\n CallbackMetadata?: never;\n}\n\nexport type StkCallbackInner = StkCallbackSuccess | StkCallbackFailure;\n\n/** Full wrapper Safaricom POSTs to your CallBackURL */\nexport interface StkPushCallback {\n Body: {\n stkCallback: StkCallbackInner;\n };\n}\n\n// ── Type guards & helpers ─────────────────────────────────────────────────────\n\n/**\n * Narrows StkCallbackInner to the success shape.\n *\n * @example\n * if (isStkCallbackSuccess(callback.Body.stkCallback)) {\n * const receipt = getCallbackValue(callback, \"MpesaReceiptNumber\");\n * }\n */\nexport function isStkCallbackSuccess(\n cb: StkCallbackInner\n): cb is StkCallbackSuccess {\n return cb.ResultCode === 0;\n}\n\n/**\n * Extracts a named value from a successful callback's metadata.\n * Returns undefined if the key is absent or the transaction failed.\n */\nexport function getCallbackValue(\n callback: StkPushCallback,\n name: StkCallbackMetadataItem[\"Name\"]\n): string | number | undefined {\n const inner = callback.Body.stkCallback;\n if (!isStkCallbackSuccess(inner)) return undefined;\n return inner.CallbackMetadata.Item.find((i) => i.Name === name)?.Value;\n}\n","/**\n * Transaction Status Query implementation\n *\n * API: POST /mpesa/transactionstatus/v1/query\n *\n * This is ASYNCHRONOUS. The synchronous response only acknowledges receipt.\n * Final results arrive via POST to your ResultURL.\n *\n * Required M-PESA org portal role: \"Transaction Status query ORG API\"\n */\n\nimport { createError } from \"../../utils/errors\";\nimport { httpRequest } from \"../../utils/http\"; // ← httpRequest, NOT httpClient\nimport type {\n TransactionStatusRequest,\n TransactionStatusResponse,\n} from \"./types\";\n\nexport async function queryTransactionStatus(\n baseUrl: string,\n token: string,\n securityCredential: string,\n initiator: string,\n request: TransactionStatusRequest\n): Promise<TransactionStatusResponse> {\n // ── Validation ──────────────────────────────────────────────────────────────\n\n if (!request.transactionId) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message: \"transactionId is required\",\n });\n }\n\n if (!request.partyA) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message:\n \"partyA is required (your business shortcode, till number, or MSISDN)\",\n });\n }\n\n if (!request.identifierType) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message:\n 'identifierType is required: \"1\" (MSISDN) | \"2\" (Till) | \"4\" (ShortCode)',\n });\n }\n\n if (!request.resultUrl) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message:\n \"resultUrl is required — Safaricom POSTs the transaction result here\",\n });\n }\n\n if (!request.queueTimeOutUrl) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message: \"queueTimeOutUrl is required — Safaricom calls this on timeout\",\n });\n }\n\n // ── Build payload matching Daraja spec exactly ──────────────────────────────\n\n const payload = {\n Initiator: initiator,\n SecurityCredential: securityCredential,\n CommandID: request.commandId ?? \"TransactionStatusQuery\",\n TransactionID: request.transactionId,\n PartyA: request.partyA,\n IdentifierType: request.identifierType,\n ResultURL: request.resultUrl,\n QueueTimeOutURL: request.queueTimeOutUrl,\n Remarks: request.remarks ?? \"Transaction Status Query\",\n Occasion: request.occasion ?? \"\",\n };\n\n const { data } = await httpRequest<TransactionStatusResponse>(\n `${baseUrl}/mpesa/transactionstatus/v1/query`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${token}` },\n body: payload,\n }\n );\n\n return data;\n}\n","/**\n * Core M-Pesa / Daraja API types\n */\n\nexport type Environment = \"sandbox\" | \"production\";\n\n/** Base URLs per Daraja environment */\nexport const DARAJA_BASE_URLS: Record<Environment, string> = {\n sandbox: \"https://sandbox.safaricom.co.ke\",\n production: \"https://api.safaricom.co.ke\",\n} as const;\n\nexport interface MpesaConfig {\n // ── Required for all APIs ─────────────────────────────────────────────────\n consumerKey: string;\n consumerSecret: string;\n environment: Environment;\n\n // ── Required for STK Push (M-Pesa Express) ────────────────────────────────\n /** Paybill / HO shortcode (5–7 digits). Required for STK Push & STK Query. */\n lipaNaMpesaShortCode?: string;\n /**\n * Passkey from Daraja portal.\n * Sandbox: visible in the simulator test data section.\n * Production: emailed after Go Live.\n */\n lipaNaMpesaPassKey?: string;\n\n // ── Required for Transaction Status / B2C / Reversals ────────────────────\n /** M-PESA org portal API operator username */\n initiatorName?: string;\n /** Plain-text password for the API operator (will be RSA-encrypted) */\n initiatorPassword?: string;\n\n // ── Certificate options (choose one) ─────────────────────────────────────\n /**\n * Path to the .cer file on disk.\n * Bun: read via `Bun.file(path).text()`\n * Node: read via `fs.promises.readFile(path, \"utf-8\")`\n */\n certificatePath?: string;\n /** PEM string contents of the certificate (alternative to certificatePath) */\n certificatePem?: string;\n /**\n * Pre-computed base64 security credential.\n * Use this if you encrypt outside the library (e.g. at startup).\n * Skips the RSA encryption step entirely.\n */\n securityCredential?: string;\n}\n","/**\n * M-Pesa Daraja API client\n *\n * Supports:\n * - STK Push (M-Pesa Express) — stkPush()\n * - STK Query — stkQuery()\n * - Transaction Status Query — transactionStatus()\n *\n * @example\n * const mpesa = new Mpesa({\n * consumerKey: process.env.MPESA_CONSUMER_KEY!,\n * consumerSecret: process.env.MPESA_CONSUMER_SECRET!,\n * environment: \"sandbox\",\n * lipaNaMpesaShortCode: \"174379\",\n * lipaNaMpesaPassKey: \"bfb279...\",\n * initiatorName: \"testapi\",\n * initiatorPassword: \"Safaricom123!\",\n * certificatePath: \"./SandboxCertificate.cer\",\n * });\n */\n\nimport { TokenManager } from \"../core/auth\";\nimport { encryptSecurityCredential } from \"../core/encryption\";\nimport { PesafyError } from \"../utils/errors\";\nimport {\n processStkPush,\n queryStkPush,\n type StkPushRequest,\n type StkQueryRequest,\n} from \"./stk-push\";\nimport {\n queryTransactionStatus,\n type TransactionStatusRequest,\n} from \"./transaction-status\";\nimport { DARAJA_BASE_URLS, type MpesaConfig } from \"./types\";\n\nexport class Mpesa {\n private readonly config: MpesaConfig;\n private readonly tokenManager: TokenManager;\n private readonly baseUrl: string;\n\n constructor(config: MpesaConfig) {\n if (!config.consumerKey || !config.consumerSecret) {\n throw new PesafyError({\n code: \"INVALID_CREDENTIALS\",\n message: \"consumerKey and consumerSecret are required\",\n });\n }\n\n this.config = config;\n this.baseUrl = DARAJA_BASE_URLS[config.environment];\n this.tokenManager = new TokenManager(\n config.consumerKey,\n config.consumerSecret,\n this.baseUrl\n );\n }\n\n // ── Internal helpers ────────────────────────────────────────────────────────\n\n private getToken(): Promise<string> {\n return this.tokenManager.getAccessToken();\n }\n\n private async buildSecurityCredential(): Promise<string> {\n // Option 1: caller pre-computed it\n if (this.config.securityCredential) return this.config.securityCredential;\n\n // Option 2: we encrypt it\n if (!this.config.initiatorPassword) {\n throw new PesafyError({\n code: \"INVALID_CREDENTIALS\",\n message:\n \"Provide securityCredential (pre-encrypted) \" +\n \"OR (initiatorPassword + certificatePath/certificatePem)\",\n });\n }\n\n let cert: string;\n if (this.config.certificatePem) {\n cert = this.config.certificatePem;\n } else if (this.config.certificatePath) {\n // Bun runtime\n if (typeof Bun !== \"undefined\") {\n cert = await Bun.file(this.config.certificatePath).text();\n } else {\n // Node.js fallback\n const { readFile } = await import(\"node:fs/promises\");\n cert = await readFile(this.config.certificatePath, \"utf-8\");\n }\n } else {\n throw new PesafyError({\n code: \"INVALID_CREDENTIALS\",\n message:\n \"certificatePath or certificatePem required to encrypt the initiator password\",\n });\n }\n\n return encryptSecurityCredential(this.config.initiatorPassword, cert);\n }\n\n // ── STK Push ──────────────────────────────────────────────────────────────\n\n /**\n * M-Pesa Express — sends a payment prompt to the customer's phone.\n *\n * Requires: lipaNaMpesaShortCode + lipaNaMpesaPassKey in config.\n *\n * @example\n * const res = await mpesa.stkPush({\n * amount: 100,\n * phoneNumber: \"0712345678\",\n * callbackUrl: \"https://yourdomain.com/mpesa/callback\",\n * accountReference: \"INV-001\",\n * transactionDesc: \"Payment\",\n * });\n * console.log(res.CheckoutRequestID); // use to poll status\n */\n async stkPush(request: Omit<StkPushRequest, \"shortCode\" | \"passKey\">) {\n const shortCode = this.config.lipaNaMpesaShortCode ?? \"\";\n const passKey = this.config.lipaNaMpesaPassKey ?? \"\";\n\n if (!shortCode || !passKey) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message:\n \"lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push\",\n });\n }\n\n const token = await this.getToken();\n return processStkPush(this.baseUrl, token, {\n ...request,\n shortCode,\n passKey,\n });\n }\n\n /**\n * STK Query — checks the status of a previous STK Push.\n *\n * @example\n * const status = await mpesa.stkQuery({\n * checkoutRequestId: \"ws_CO_1007202409152617172396192\",\n * });\n * if (status.ResultCode === 0) // payment confirmed\n */\n async stkQuery(request: Omit<StkQueryRequest, \"shortCode\" | \"passKey\">) {\n const shortCode = this.config.lipaNaMpesaShortCode ?? \"\";\n const passKey = this.config.lipaNaMpesaPassKey ?? \"\";\n\n if (!shortCode || !passKey) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message:\n \"lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Query\",\n });\n }\n\n const token = await this.getToken();\n return queryStkPush(this.baseUrl, token, {\n ...request,\n shortCode,\n passKey,\n });\n }\n\n /**\n * Transaction Status — queries the result of a completed M-Pesa transaction.\n *\n * Requires: initiatorName + (initiatorPassword + certificate) OR securityCredential.\n *\n * This is ASYNCHRONOUS. The synchronous response only confirms receipt.\n * Final details are POSTed to your resultUrl.\n *\n * @example\n * await mpesa.transactionStatus({\n * transactionId: \"OEI2AK4XXXX\",\n * partyA: \"174379\",\n * identifierType: \"4\",\n * resultUrl: \"https://yourdomain.com/mpesa/result\",\n * queueTimeOutUrl: \"https://yourdomain.com/mpesa/timeout\",\n * remarks: \"Check payment status\",\n * });\n */\n async transactionStatus(request: TransactionStatusRequest) {\n const initiator = this.config.initiatorName ?? \"\";\n if (!initiator) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message: \"initiatorName is required for Transaction Status\",\n });\n }\n\n // Fetch token and encrypt credential concurrently\n const [token, securityCred] = await Promise.all([\n this.getToken(),\n this.buildSecurityCredential(),\n ]);\n\n return queryTransactionStatus(\n this.baseUrl,\n token,\n securityCred,\n initiator,\n request\n );\n }\n\n /** Force the cached OAuth token to be refreshed on the next API call */\n clearTokenCache(): void {\n this.tokenManager.clearCache();\n }\n}\n","/**\n * Exponential backoff retry for webhook at-least-once delivery.\n *\n * Daraja is asynchronous — if your callback endpoint is down, the API\n * Gateway logs a 503 and discards the result. Use this utility to\n * retry your own internal processing after receiving a webhook.\n */\n\nexport interface RetryOptions {\n /** Maximum number of attempts (default: Infinity) */\n maxRetries?: number;\n /** Initial delay in ms (default: 1000 = 1 second) */\n initialDelay?: number;\n /** Maximum delay cap in ms (default: 3_600_000 = 1 hour) */\n maxDelay?: number;\n /** Multiplier per retry (default: 2 — doubles each time) */\n backoffMultiplier?: number;\n /** Maximum total duration in ms (default: 30 days) */\n maxRetryDuration?: number;\n}\n\nconst DEFAULT_OPTIONS: Required<RetryOptions> = {\n maxRetries: Infinity,\n initialDelay: 1_000,\n maxDelay: 3_600_000,\n backoffMultiplier: 2,\n maxRetryDuration: 30 * 24 * 60 * 60 * 1_000, // 30 days\n};\n\nexport interface RetryResult<T> {\n success: boolean;\n data?: T;\n attempts: number;\n error?: Error;\n}\n\n/**\n * Retries `fn` with exponential backoff until it resolves, or limits are hit.\n *\n * @example\n * const result = await retryWithBackoff(\n * () => sendToDatabase(webhookData),\n * { maxRetries: 5, initialDelay: 500 }\n * );\n */\nexport async function retryWithBackoff<T>(\n fn: () => Promise<T>,\n options: RetryOptions = {}\n): Promise<RetryResult<T>> {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n let delay = opts.initialDelay;\n let attempts = 0;\n const startTime = Date.now();\n\n while (attempts < opts.maxRetries) {\n attempts++;\n\n if (Date.now() - startTime > opts.maxRetryDuration) {\n return {\n success: false,\n attempts,\n error: new Error(\"Max retry duration exceeded\"),\n };\n }\n\n try {\n const data = await fn();\n return { success: true, data, attempts };\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n\n // Don't retry client errors (4xx) — they won't self-heal\n if (err.message.includes(\"4\")) {\n return { success: false, attempts, error: err };\n }\n\n if (attempts < opts.maxRetries) {\n await new Promise((resolve) => setTimeout(resolve, delay));\n delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelay);\n }\n }\n }\n\n return {\n success: false,\n attempts,\n error: new Error(\"Max retries exceeded\"),\n };\n}\n","/**\n * Webhook verification utilities\n *\n * Daraja does NOT use HMAC webhook signatures like Stripe.\n * Instead, verify that callbacks come from whitelisted Safaricom IPs.\n *\n * Official Safaricom IP whitelist (from Getting Started docs):\n * 196.201.214.200\n * 196.201.214.206\n * 196.201.213.114\n * 196.201.214.207\n * 196.201.214.208\n * 196.201.213.44\n * 196.201.212.127\n * 196.201.212.138\n * 196.201.212.129\n * 196.201.212.136\n * 196.201.212.74\n * 196.201.212.69\n */\n\nimport type { StkPushWebhook } from \"./types\";\n\n/** Official Safaricom API Gateway IP addresses */\nexport const SAFARICOM_IPS: readonly string[] = [\n \"196.201.214.200\",\n \"196.201.214.206\",\n \"196.201.213.114\",\n \"196.201.214.207\",\n \"196.201.214.208\",\n \"196.201.213.44\",\n \"196.201.212.127\",\n \"196.201.212.138\",\n \"196.201.212.129\",\n \"196.201.212.136\",\n \"196.201.212.74\",\n \"196.201.212.69\",\n] as const;\n\n/**\n * Returns true if requestIP is in the allowed list.\n * Defaults to the official Safaricom IP whitelist.\n */\nexport function verifyWebhookIP(\n requestIP: string,\n allowedIPs: readonly string[] = SAFARICOM_IPS\n): boolean {\n return allowedIPs.includes(requestIP);\n}\n\n/**\n * Parses and validates an STK Push webhook body.\n * Returns the typed payload or null if it doesn't match the expected shape.\n */\nexport function parseStkPushWebhook(body: unknown): StkPushWebhook | null {\n try {\n const parsed = body as StkPushWebhook;\n if (parsed?.Body?.stkCallback) return parsed;\n return null;\n } catch {\n return null;\n }\n}\n","/**\n * High-level webhook event handler\n */\n\nimport { parseStkPushWebhook, verifyWebhookIP } from \"./signature-verifier\";\nimport type { StkPushWebhook, WebhookEventType } from \"./types\";\n\nexport interface WebhookHandlerOptions {\n /** IP address of the incoming request (from req.ip or x-forwarded-for) */\n requestIP?: string;\n /** Override the default Safaricom IP whitelist */\n allowedIPs?: string[];\n /** Skip IP verification — ONLY for local development/testing */\n skipIPCheck?: boolean;\n}\n\nexport interface WebhookHandlerResult<T = unknown> {\n success: boolean;\n eventType: WebhookEventType | null;\n data: T | null;\n error?: string;\n}\n\n/**\n * Parses and validates an inbound Daraja webhook payload.\n *\n * @example\n * // Express route\n * app.post(\"/mpesa/callback\", (req, res) => {\n * const result = handleWebhook(req.body, { requestIP: req.ip });\n * if (!result.success) return res.status(400).json({ error: result.error });\n * // process result.data (StkPushWebhook)\n * res.json({ ResultCode: 0, ResultDesc: \"Accepted\" });\n * });\n */\nexport function handleWebhook(\n body: unknown,\n options: WebhookHandlerOptions = {}\n): WebhookHandlerResult {\n // ── IP verification ─────────────────────────────────────────────────────────\n if (!options.skipIPCheck && options.requestIP) {\n if (!verifyWebhookIP(options.requestIP, options.allowedIPs)) {\n return {\n success: false,\n eventType: null,\n data: null,\n error: `IP address ${options.requestIP} is not in the Safaricom whitelist`,\n };\n }\n }\n\n // ── Parse STK Push callback ─────────────────────────────────────────────────\n const stkPush = parseStkPushWebhook(body);\n if (stkPush) {\n return {\n success: true,\n eventType: \"stk_push\",\n data: stkPush,\n };\n }\n\n return {\n success: false,\n eventType: null,\n data: null,\n error: \"Unknown or malformed webhook payload\",\n };\n}\n\n// ── Convenience extractors ────────────────────────────────────────────────────\n\n/** Extracts the M-Pesa receipt number from a successful STK Push callback */\nexport function extractTransactionId(webhook: StkPushWebhook): string | null {\n const items = webhook.Body?.stkCallback?.CallbackMetadata?.Item;\n const item = items?.find((i) => i.Name === \"MpesaReceiptNumber\");\n return item ? String(item.Value) : null;\n}\n\n/** Extracts the transaction amount from a successful STK Push callback */\nexport function extractAmount(webhook: StkPushWebhook): number | null {\n const items = webhook.Body?.stkCallback?.CallbackMetadata?.Item;\n const item = items?.find((i) => i.Name === \"Amount\");\n return item ? Number(item.Value) : null;\n}\n\n/** Extracts the phone number from a successful STK Push callback */\nexport function extractPhoneNumber(webhook: StkPushWebhook): string | null {\n const items = webhook.Body?.stkCallback?.CallbackMetadata?.Item;\n const item = items?.find((i) => i.Name === \"PhoneNumber\");\n return item ? String(item.Value) : null;\n}\n\n/** Returns true if the STK Push callback represents a successful transaction */\nexport function isSuccessfulCallback(webhook: StkPushWebhook): boolean {\n return webhook.Body?.stkCallback?.ResultCode === 0;\n}\n"]}
|
package/dist/index.js
CHANGED
|
@@ -62,7 +62,17 @@ function encryptSecurityCredential(initiatorPassword, certificatePem) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// src/utils/http/index.ts
|
|
65
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
66
|
+
function sleep(ms) {
|
|
67
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
68
|
+
}
|
|
69
|
+
function jitter(baseMs) {
|
|
70
|
+
const spread = baseMs * 0.25;
|
|
71
|
+
return baseMs + (Math.random() * spread * 2 - spread);
|
|
72
|
+
}
|
|
65
73
|
async function httpRequest(url, options) {
|
|
74
|
+
const maxRetries = options.retries ?? 4;
|
|
75
|
+
const baseDelay = options.retryDelay ?? 2e3;
|
|
66
76
|
const headers = {
|
|
67
77
|
"Content-Type": "application/json",
|
|
68
78
|
Accept: "application/json",
|
|
@@ -70,47 +80,61 @@ async function httpRequest(url, options) {
|
|
|
70
80
|
};
|
|
71
81
|
const init = {
|
|
72
82
|
method: options.method,
|
|
73
|
-
headers
|
|
83
|
+
headers,
|
|
84
|
+
...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
|
|
74
85
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
let lastError = null;
|
|
87
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
88
|
+
if (attempt > 0) {
|
|
89
|
+
const delay = jitter(baseDelay * Math.pow(2, attempt - 1));
|
|
90
|
+
await sleep(delay);
|
|
91
|
+
}
|
|
92
|
+
let response;
|
|
93
|
+
try {
|
|
94
|
+
response = await fetch(url, init);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
lastError = new PesafyError({
|
|
97
|
+
code: "NETWORK_ERROR",
|
|
98
|
+
message: `Network error calling ${url}: ${String(err)}`,
|
|
99
|
+
cause: err
|
|
100
|
+
});
|
|
101
|
+
if (attempt < maxRetries) continue;
|
|
102
|
+
throw lastError;
|
|
103
|
+
}
|
|
104
|
+
let data;
|
|
105
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
106
|
+
try {
|
|
107
|
+
data = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
108
|
+
} catch {
|
|
109
|
+
data = null;
|
|
110
|
+
}
|
|
111
|
+
const responseHeaders = {};
|
|
112
|
+
response.headers.forEach((value, key) => {
|
|
113
|
+
responseHeaders[key] = value;
|
|
86
114
|
});
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
code: "HTTP_ERROR",
|
|
115
|
+
if (response.ok) {
|
|
116
|
+
return {
|
|
117
|
+
data,
|
|
118
|
+
status: response.status,
|
|
119
|
+
headers: responseHeaders
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const isTransient = RETRYABLE_STATUSES.has(response.status);
|
|
123
|
+
const daraja = data ?? {};
|
|
124
|
+
const message = daraja.errorMessage ?? daraja.ResponseDescription ?? `HTTP ${response.status}`;
|
|
125
|
+
lastError = new PesafyError({
|
|
126
|
+
code: isTransient ? "REQUEST_FAILED" : "HTTP_ERROR",
|
|
100
127
|
message,
|
|
101
128
|
statusCode: response.status,
|
|
102
|
-
response: data
|
|
129
|
+
response: data,
|
|
130
|
+
requestId: daraja.requestId
|
|
103
131
|
});
|
|
132
|
+
if (isTransient && attempt < maxRetries) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
throw lastError;
|
|
104
136
|
}
|
|
105
|
-
|
|
106
|
-
response.headers.forEach((value, key) => {
|
|
107
|
-
responseHeaders[key] = value;
|
|
108
|
-
});
|
|
109
|
-
return {
|
|
110
|
-
data,
|
|
111
|
-
status: response.status,
|
|
112
|
-
headers: responseHeaders
|
|
113
|
-
};
|
|
137
|
+
throw lastError;
|
|
114
138
|
}
|
|
115
139
|
|
|
116
140
|
// src/core/auth/token-manager.ts
|
|
@@ -235,6 +259,7 @@ async function processStkPush(baseUrl, accessToken, request) {
|
|
|
235
259
|
PartyB: partyB,
|
|
236
260
|
PhoneNumber: formatSafaricomPhone(request.phoneNumber),
|
|
237
261
|
CallBackURL: request.callbackUrl,
|
|
262
|
+
// Daraja docs: AccountReference max 12 chars, TransactionDesc max 13 chars
|
|
238
263
|
AccountReference: request.accountReference.slice(0, 12),
|
|
239
264
|
TransactionDesc: request.transactionDesc.slice(0, 13)
|
|
240
265
|
};
|
|
@@ -243,7 +268,10 @@ async function processStkPush(baseUrl, accessToken, request) {
|
|
|
243
268
|
{
|
|
244
269
|
method: "POST",
|
|
245
270
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
246
|
-
body
|
|
271
|
+
body,
|
|
272
|
+
// Daraja sandbox needs more retries and longer gaps due to instability
|
|
273
|
+
retries: 5,
|
|
274
|
+
retryDelay: 3e3
|
|
247
275
|
}
|
|
248
276
|
);
|
|
249
277
|
return data;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/errors/index.ts","../src/core/encryption/security-credentials.ts","../src/utils/http/index.ts","../src/core/auth/token-manager.ts","../src/utils/phone/index.ts","../src/mpesa/stk-push/utils.ts","../src/mpesa/stk-push/stk-push.ts","../src/mpesa/stk-push/stk-query.ts","../src/mpesa/stk-push/types.ts","../src/mpesa/transaction-status/query.ts","../src/mpesa/types.ts","../src/mpesa/index.ts","../src/mpesa/webhooks/retry.ts","../src/mpesa/webhooks/signature-verifier.ts","../src/mpesa/webhooks/webhook-handler.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Pesafy error utilities\n */\nexport type ErrorCode =\n | \"INVALID_CREDENTIALS\"\n | \"AUTH_FAILED\"\n | \"VALIDATION_ERROR\"\n | \"ENCRYPTION_FAILED\"\n | \"REQUEST_FAILED\"\n | \"INVALID_PHONE\"\n | \"HTTP_ERROR\"\n | \"API_ERROR\"\n | \"NETWORK_ERROR\"\n | \"TIMEOUT\"\n | \"INVALID_RESPONSE\";\n\nexport interface PesafyErrorOptions {\n code: ErrorCode;\n message: string;\n statusCode?: number;\n response?: unknown;\n cause?: unknown;\n requestId?: string;\n}\n\nexport class PesafyError extends Error {\n readonly code: ErrorCode;\n readonly statusCode: number | undefined;\n readonly response: unknown;\n readonly requestId: string | undefined;\n override readonly cause: unknown;\n\n constructor(options: PesafyErrorOptions) {\n super(options.message);\n Object.defineProperty(this, \"name\", { value: \"PesafyError\" });\n this.code = options.code;\n this.statusCode = options.statusCode;\n this.response = options.response;\n this.requestId = options.requestId;\n this.cause = options.cause;\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, PesafyError);\n }\n }\n\n toJSON() {\n return {\n name: this.name,\n code: this.code,\n message: this.message,\n statusCode: this.statusCode,\n requestId: this.requestId,\n };\n }\n}\n\n/** Convenience factory — identical API to `new PesafyError(...)` */\nexport function createError(options: PesafyErrorOptions): PesafyError {\n return new PesafyError(options);\n}\n","/**\n * Security credential encryption for Daraja APIs that require it:\n * B2C, B2B, Transaction Status Query, Reversals, Tax Remittance.\n *\n * Algorithm (from Safaricom \"Getting Started\" docs):\n * 1. Write the unencrypted initiator password into a byte array.\n * 2. Encrypt using the M-Pesa public key certificate:\n * - RSA algorithm\n * - PKCS #1 v1.5 padding (NOT OAEP)\n * 3. Base64-encode the encrypted byte array.\n *\n * Certificate source:\n * Sandbox: https://developer.safaricom.co.ke (SandboxCertificate.cer)\n * Production: https://developer.safaricom.co.ke (ProductionCertificate.cer)\n *\n * NOTE: Use the correct certificate for each environment or credentials\n * will be rejected.\n */\n\nimport { constants, publicEncrypt } from \"node:crypto\";\nimport { PesafyError } from \"../../utils/errors\";\n\n/**\n * Encrypts `initiatorPassword` with the given PEM certificate and returns\n * the base64-encoded security credential ready to send to Daraja.\n *\n * @param initiatorPassword - Plain-text password set on the M-PESA org portal\n * @param certificatePem - Full PEM string (the .cer file contents)\n */\nexport function encryptSecurityCredential(\n initiatorPassword: string,\n certificatePem: string\n): string {\n try {\n const passwordBuffer = Buffer.from(initiatorPassword, \"utf-8\");\n\n const encrypted = publicEncrypt(\n {\n key: certificatePem,\n // RSA_PKCS1_PADDING = 1 (NOT RSA_PKCS1_OAEP_PADDING = 4)\n padding: constants.RSA_PKCS1_PADDING,\n },\n passwordBuffer\n );\n\n return encrypted.toString(\"base64\");\n } catch (error) {\n throw new PesafyError({\n code: \"ENCRYPTION_FAILED\",\n message:\n \"Failed to encrypt security credential. \" +\n \"Ensure the certificate PEM is valid and matches the environment (sandbox/production).\",\n cause: error,\n });\n }\n}\n","/**\n * Minimal HTTP client for Daraja API calls.\n *\n * Why not use axios/got/ky?\n * - Zero extra dependencies\n * - Works in Node.js, Bun, and edge runtimes unchanged\n * - Daraja only needs POST + GET with JSON bodies\n *\n * Exported: httpRequest (the ONLY export — never export \"httpClient\")\n */\n\nimport { PesafyError } from \"../errors\";\n\nexport interface HttpRequestOptions {\n method: \"GET\" | \"POST\";\n headers?: Record<string, string>;\n /** Will be JSON-serialised and sent as application/json */\n body?: unknown;\n}\n\nexport interface HttpResponse<T> {\n data: T;\n status: number;\n headers: Record<string, string>;\n}\n\n/**\n * Sends an HTTP request and returns parsed JSON.\n * Throws PesafyError on non-2xx responses.\n */\nexport async function httpRequest<T = unknown>(\n url: string,\n options: HttpRequestOptions\n): Promise<HttpResponse<T>> {\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...options.headers,\n };\n\n const init: RequestInit = {\n method: options.method,\n headers,\n };\n\n if (options.body !== undefined) {\n init.body = JSON.stringify(options.body);\n }\n\n let response: Response;\n try {\n response = await fetch(url, init);\n } catch (err) {\n throw new PesafyError({\n code: \"REQUEST_FAILED\",\n message: `Network error calling ${url}: ${String(err)}`,\n cause: err,\n });\n }\n\n // Parse body regardless of status so we can attach it to the error\n let data: unknown;\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n data = await response.json();\n } else {\n data = await response.text();\n }\n\n if (!response.ok) {\n // Daraja error shape: { requestId, errorCode, errorMessage }\n const daraja = data as Record<string, unknown>;\n const message =\n (daraja?.errorMessage as string) ??\n (daraja?.ResponseDescription as string) ??\n `HTTP ${response.status}`;\n\n throw new PesafyError({\n code: \"HTTP_ERROR\",\n message,\n statusCode: response.status,\n response: data,\n });\n }\n\n // Collect response headers into a plain object\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n return {\n data: data as T,\n status: response.status,\n headers: responseHeaders,\n };\n}\n","/**\n * OAuth token manager for Daraja API.\n *\n * Daraja Authorization endpoint (GET, Basic Auth):\n * https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials\n * https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials\n *\n * Token validity: 3600 seconds (1 hour).\n * We refresh 60 s early to avoid edge-case expiry mid-request.\n *\n * Ref: Authorization By Safaricom docs\n */\n\nimport { PesafyError } from \"../../utils/errors\";\nimport { httpRequest } from \"../../utils/http\";\nimport type { TokenResponse } from \"./types\";\n\n/** Refresh the token this many seconds before it actually expires */\nconst TOKEN_BUFFER_SECONDS = 60;\n\nexport class TokenManager {\n private readonly consumerKey: string;\n private readonly consumerSecret: string;\n private readonly baseUrl: string;\n\n private cachedToken: string | null = null;\n private tokenExpiresAt = 0; // Unix seconds\n\n constructor(consumerKey: string, consumerSecret: string, baseUrl: string) {\n this.consumerKey = consumerKey;\n this.consumerSecret = consumerSecret;\n this.baseUrl = baseUrl;\n }\n\n private getBasicAuthHeader(): string {\n // Daraja spec: Base64(consumerKey:consumerSecret)\n const credentials = `${this.consumerKey}:${this.consumerSecret}`;\n const encoded = Buffer.from(credentials, \"utf-8\").toString(\"base64\");\n return `Basic ${encoded}`;\n }\n\n /**\n * Returns a valid access token, fetching a new one when the cached token\n * is absent or within TOKEN_BUFFER_SECONDS of expiry.\n */\n async getAccessToken(): Promise<string> {\n const now = Date.now() / 1000;\n\n if (this.cachedToken && this.tokenExpiresAt > now + TOKEN_BUFFER_SECONDS) {\n return this.cachedToken;\n }\n\n // Daraja Authorization API: GET with Basic Auth + grant_type query param\n const url = `${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`;\n\n const response = await httpRequest<TokenResponse>(url, {\n method: \"GET\",\n headers: {\n Authorization: this.getBasicAuthHeader(),\n },\n });\n\n const { access_token, expires_in } = response.data;\n\n if (!access_token) {\n throw new PesafyError({\n code: \"AUTH_FAILED\",\n message:\n \"Daraja did not return an access token. Check your consumer key and secret.\",\n response: response.data,\n });\n }\n\n this.cachedToken = access_token;\n // expires_in is 3599 per Daraja docs; default to 3600 if missing\n this.tokenExpiresAt = now + (expires_in ?? 3600);\n\n return this.cachedToken;\n }\n\n /** Force token refresh on the next call (e.g. after a 401 response) */\n clearCache(): void {\n this.cachedToken = null;\n this.tokenExpiresAt = 0;\n }\n}\n","/**\n * Phone number utilities for Daraja API.\n *\n * Daraja spec: PartyA and PhoneNumber must be in the format 2547XXXXXXXX\n * (12-digit, starts with 254, no +, no spaces, no dashes).\n *\n * Accepted input formats:\n * 0712345678 → 254712345678\n * +254712345678 → 254712345678\n * 254712345678 → 254712345678 (already correct)\n * 712345678 → 254712345678\n */\n\nimport { PesafyError } from \"../errors\";\n\n/** Normalises any common Kenyan phone format to 254XXXXXXXXX (12 digits) */\nexport function formatSafaricomPhone(phone: string): string {\n const digits = phone.replace(/\\D/g, \"\");\n\n let normalised: string;\n\n if (digits.startsWith(\"254\") && digits.length === 12) {\n normalised = digits;\n } else if (digits.startsWith(\"0\") && digits.length === 10) {\n normalised = `254${digits.slice(1)}`;\n } else if (digits.length === 9) {\n // e.g. 712345678 → 254712345678\n normalised = `254${digits}`;\n } else if (digits.startsWith(\"254\") && digits.length !== 12) {\n throw new PesafyError({\n code: \"INVALID_PHONE\",\n message: `Invalid phone number \"${phone}\". Expected 254XXXXXXXXX (12 digits).`,\n });\n } else {\n throw new PesafyError({\n code: \"INVALID_PHONE\",\n message: `Cannot parse phone number \"${phone}\". Use 07XXXXXXXX, 2547XXXXXXXX, or +2547XXXXXXXX.`,\n });\n }\n\n // Final sanity check: must be exactly 12 digits\n if (normalised.length !== 12) {\n throw new PesafyError({\n code: \"INVALID_PHONE\",\n message: `Phone number \"${phone}\" normalised to \"${normalised}\" which is not 12 digits.`,\n });\n }\n\n return normalised;\n}\n","/**\n * STK Push utility functions\n *\n * Password spec (from Daraja docs):\n * Password = Base64( BusinessShortCode + Passkey + Timestamp )\n * Timestamp = YYYYMMDDHHmmss\n *\n * IMPORTANT: Generate the timestamp ONCE per request and pass the same\n * value to BOTH getStkPushPassword() and the request body's Timestamp field.\n * Safaricom validates that Base64(Shortcode+Passkey+Timestamp) matches the\n * Timestamp sent in the body — two separate calls to getTimestamp() will\n * produce different values and cause auth failures.\n */\n\nexport { formatSafaricomPhone as formatPhoneNumber } from \"../../utils/phone\";\n\n/**\n * Generates the STK Push password.\n * Formula: Base64( Shortcode + Passkey + Timestamp )\n *\n * Uses btoa() — works in Node.js ≥18, Bun, browsers, and edge runtimes.\n */\nexport function getStkPushPassword(\n shortCode: string,\n passKey: string,\n timestamp: string\n): string {\n return btoa(`${shortCode}${passKey}${timestamp}`);\n}\n\n/**\n * Returns a Daraja-compatible timestamp: YYYYMMDDHHmmss\n *\n * Call this ONCE per request and reuse the result.\n */\nexport function getTimestamp(): string {\n const now = new Date();\n const pad = (n: number): string => n.toString().padStart(2, \"0\");\n return [\n now.getFullYear(),\n pad(now.getMonth() + 1),\n pad(now.getDate()),\n pad(now.getHours()),\n pad(now.getMinutes()),\n pad(now.getSeconds()),\n ].join(\"\");\n}\n","/**\n * M-Pesa Express (STK Push) — initiates a payment prompt on the customer's phone.\n *\n * API: POST /mpesa/stkpush/v1/processrequest\n *\n * Daraja request body (from docs):\n * {\n * \"BusinessShortCode\": 174379,\n * \"Password\": \"base64(Shortcode+Passkey+Timestamp)\",\n * \"Timestamp\": \"20210628092408\",\n * \"TransactionType\": \"CustomerPayBillOnline\",\n * \"Amount\": \"1\",\n * \"PartyA\": \"254722000000\",\n * \"PartyB\": \"174379\",\n * \"PhoneNumber\": \"254722111111\",\n * \"CallBackURL\": \"https://mydomain.com/path\",\n * \"AccountReference\": \"accountref\", ← max 12 chars\n * \"TransactionDesc\": \"txndesc\" ← max 13 chars\n * }\n *\n * Notes from docs:\n * - All fields except TransactionDesc are mandatory.\n * - Amount must be a whole number ≥ 1 (KES).\n * - PartyA = phone sending money (2547XXXXXXXX).\n * - PartyB = shortCode for Paybill, Till Number for Buy Goods.\n * - AccountReference max 12 chars (longer values cause USSD prompt too long).\n * - TransactionDesc max 13 chars.\n */\n\nimport { PesafyError } from \"../../utils/errors\";\nimport { httpRequest } from \"../../utils/http\";\nimport type { StkPushRequest, StkPushResponse } from \"./types\";\nimport { formatPhoneNumber, getStkPushPassword, getTimestamp } from \"./utils\";\n\nexport async function processStkPush(\n baseUrl: string,\n accessToken: string,\n request: StkPushRequest\n): Promise<StkPushResponse> {\n // ── Amount validation ───────────────────────────────────────────────────────\n // Daraja minimum is KES 1. Math.round(0.4) = 0 → reject with clear message.\n const amount = Math.round(request.amount);\n if (amount < 1) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message: `Amount must be at least KES 1 (got ${request.amount} which rounds to ${amount}).`,\n });\n }\n\n // ── Generate timestamp ONCE ─────────────────────────────────────────────────\n // Must be identical in Password (encoded) and Timestamp (body) fields.\n const timestamp = getTimestamp();\n\n // ── PartyB ──────────────────────────────────────────────────────────────────\n // Paybill → PartyB = shortCode\n // Buy Goods (Till) → PartyB = till number\n const partyB = request.partyB ?? request.shortCode;\n\n const body = {\n BusinessShortCode: request.shortCode,\n Password: getStkPushPassword(request.shortCode, request.passKey, timestamp),\n Timestamp: timestamp,\n TransactionType: request.transactionType ?? \"CustomerPayBillOnline\",\n Amount: amount,\n PartyA: formatPhoneNumber(request.phoneNumber),\n PartyB: partyB,\n PhoneNumber: formatPhoneNumber(request.phoneNumber),\n CallBackURL: request.callbackUrl,\n AccountReference: request.accountReference.slice(0, 12),\n TransactionDesc: request.transactionDesc.slice(0, 13),\n };\n\n const { data } = await httpRequest<StkPushResponse>(\n `${baseUrl}/mpesa/stkpush/v1/processrequest`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${accessToken}` },\n body,\n }\n );\n\n return data;\n}\n","/**\n * STK Push Query — checks the status of a Lipa Na M-Pesa Online Payment.\n *\n * API: POST /mpesa/stkpushquery/v1/query\n *\n * Daraja request body (from Discover APIs M-Pesa Express Query docs):\n * {\n * \"BusinessShortCode\": \"174379\",\n * \"Password\": \"base64(Shortcode+Passkey+Timestamp)\",\n * \"Timestamp\": \"20160216165627\",\n * \"CheckoutRequestID\": \"ws_CO_260520211133524545\"\n * }\n *\n * Response ResultCode values (from docs):\n * 0 = The service request is processed successfully.\n * 1032 = Request cancelled by user\n * 1037 = DS timeout user cannot be reached\n * 2001 = Wrong PIN\n * (and more — see STK Push docs result code table)\n */\n\nimport { httpRequest } from \"../../utils/http\";\nimport type { StkQueryRequest, StkQueryResponse } from \"./types\";\nimport { getStkPushPassword, getTimestamp } from \"./utils\";\n\nexport async function queryStkPush(\n baseUrl: string,\n accessToken: string,\n request: StkQueryRequest\n): Promise<StkQueryResponse> {\n // Generate timestamp ONCE — Password and Timestamp field MUST match.\n const timestamp = getTimestamp();\n\n const body = {\n BusinessShortCode: request.shortCode,\n Password: getStkPushPassword(request.shortCode, request.passKey, timestamp),\n Timestamp: timestamp,\n CheckoutRequestID: request.checkoutRequestId,\n };\n\n const { data } = await httpRequest<StkQueryResponse>(\n `${baseUrl}/mpesa/stkpushquery/v1/query`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${accessToken}` },\n body,\n }\n );\n\n return data;\n}\n","/**\n * STK Push (M-Pesa Express) types\n *\n * API: POST /mpesa/stkpush/v1/processrequest\n * Query: POST /mpesa/stkpushquery/v1/query\n *\n * Ref: M-Pesa Express Simulate docs + Discover APIs M-Pesa Express Query docs\n */\n\n// ── Transaction type ─────────────────────────────────────────────────────────\n\n/**\n * CustomerPayBillOnline → Paybill numbers (PartyB = shortcode)\n * CustomerBuyGoodsOnline → Till numbers (PartyB = till number)\n */\nexport type TransactionType =\n | \"CustomerPayBillOnline\"\n | \"CustomerBuyGoodsOnline\";\n\n// ── STK Push request ─────────────────────────────────────────────────────────\n\nexport interface StkPushRequest {\n /** Transaction amount (minimum KES 1, must round to a whole number ≥ 1) */\n amount: number;\n\n /**\n * Phone number sending the money. Format: 2547XXXXXXXX.\n * Must be a valid Safaricom M-PESA number.\n * Daraja docs field name: PartyA / PhoneNumber\n */\n phoneNumber: string;\n\n /**\n * URL where Safaricom will POST the callback result.\n * Must be publicly accessible (use ngrok/localtunnel for local dev).\n * Daraja docs field name: CallBackURL\n */\n callbackUrl: string;\n\n /**\n * Alpha-numeric reference shown to customer in the USSD prompt.\n * Max 12 characters.\n * Daraja docs field name: AccountReference\n */\n accountReference: string;\n\n /**\n * Additional description for the transaction.\n * Max 13 characters.\n * Daraja docs field name: TransactionDesc\n */\n transactionDesc: string;\n\n /**\n * Business shortcode — Paybill number or HO/Store number for Till.\n * Daraja docs field name: BusinessShortCode\n */\n shortCode: string;\n\n /**\n * Passkey used to generate the Password.\n * Sandbox value: from Daraja simulator test data.\n * Production value: emailed after Go Live.\n */\n passKey: string;\n\n /**\n * \"CustomerPayBillOnline\" (default) for Paybill.\n * \"CustomerBuyGoodsOnline\" for Till Numbers.\n */\n transactionType?: TransactionType;\n\n /**\n * Credit party receiving funds.\n * - CustomerPayBillOnline: defaults to shortCode\n * - CustomerBuyGoodsOnline: set to the Till Number\n * Daraja docs field name: PartyB\n */\n partyB?: string;\n}\n\n// ── STK Push response ────────────────────────────────────────────────────────\n\nexport interface StkPushResponse {\n /** Global unique identifier for the submitted payment request */\n MerchantRequestID: string;\n /** Global unique identifier for the checkout transaction */\n CheckoutRequestID: string;\n /** \"0\" = successful submission */\n ResponseCode: string;\n ResponseDescription: string;\n CustomerMessage: string;\n}\n\n// ── STK Query request/response ───────────────────────────────────────────────\n\nexport interface StkQueryRequest {\n /** CheckoutRequestID from the STK Push response */\n checkoutRequestId: string;\n shortCode: string;\n passKey: string;\n}\n\nexport interface StkQueryResponse {\n ResponseCode: string;\n ResponseDescription: string;\n MerchantRequestID: string;\n CheckoutRequestID: string;\n /**\n * Daraja returns ResultCode as a NUMBER.\n * 0 = success\n * 1 = insufficient balance\n * 1032 = cancelled by user\n * 1037 = timeout / unreachable\n * 2001 = wrong PIN\n */\n ResultCode: number;\n ResultDesc: string;\n}\n\n// ── Callback payload types ────────────────────────────────────────────────────\n// Safaricom POSTs these to your CallBackURL after the customer responds.\n\n/** Single metadata item in a successful STK callback */\nexport interface StkCallbackMetadataItem {\n Name:\n | \"Amount\"\n | \"MpesaReceiptNumber\"\n | \"TransactionDate\"\n | \"PhoneNumber\"\n | \"Balance\";\n /** Present on successful transactions; absent on failure */\n Value?: number | string;\n}\n\n/** Inner callback for a SUCCESSFUL STK Push (ResultCode === 0) */\nexport interface StkCallbackSuccess {\n MerchantRequestID: string;\n CheckoutRequestID: string;\n ResultCode: 0;\n ResultDesc: string;\n CallbackMetadata: {\n Item: StkCallbackMetadataItem[];\n };\n}\n\n/** Inner callback for a FAILED / CANCELLED STK Push (ResultCode !== 0) */\nexport interface StkCallbackFailure {\n MerchantRequestID: string;\n CheckoutRequestID: string;\n /** e.g. 1032 = cancelled by user, 1037 = timeout */\n ResultCode: number;\n ResultDesc: string;\n CallbackMetadata?: never;\n}\n\nexport type StkCallbackInner = StkCallbackSuccess | StkCallbackFailure;\n\n/** Full wrapper Safaricom POSTs to your CallBackURL */\nexport interface StkPushCallback {\n Body: {\n stkCallback: StkCallbackInner;\n };\n}\n\n// ── Type guards & helpers ─────────────────────────────────────────────────────\n\n/**\n * Narrows StkCallbackInner to the success shape.\n *\n * @example\n * if (isStkCallbackSuccess(callback.Body.stkCallback)) {\n * const receipt = getCallbackValue(callback, \"MpesaReceiptNumber\");\n * }\n */\nexport function isStkCallbackSuccess(\n cb: StkCallbackInner\n): cb is StkCallbackSuccess {\n return cb.ResultCode === 0;\n}\n\n/**\n * Extracts a named value from a successful callback's metadata.\n * Returns undefined if the key is absent or the transaction failed.\n */\nexport function getCallbackValue(\n callback: StkPushCallback,\n name: StkCallbackMetadataItem[\"Name\"]\n): string | number | undefined {\n const inner = callback.Body.stkCallback;\n if (!isStkCallbackSuccess(inner)) return undefined;\n return inner.CallbackMetadata.Item.find((i) => i.Name === name)?.Value;\n}\n","/**\n * Transaction Status Query implementation\n *\n * API: POST /mpesa/transactionstatus/v1/query\n *\n * This is ASYNCHRONOUS. The synchronous response only acknowledges receipt.\n * Final results arrive via POST to your ResultURL.\n *\n * Required M-PESA org portal role: \"Transaction Status query ORG API\"\n */\n\nimport { createError } from \"../../utils/errors\";\nimport { httpRequest } from \"../../utils/http\"; // ← httpRequest, NOT httpClient\nimport type {\n TransactionStatusRequest,\n TransactionStatusResponse,\n} from \"./types\";\n\nexport async function queryTransactionStatus(\n baseUrl: string,\n token: string,\n securityCredential: string,\n initiator: string,\n request: TransactionStatusRequest\n): Promise<TransactionStatusResponse> {\n // ── Validation ──────────────────────────────────────────────────────────────\n\n if (!request.transactionId) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message: \"transactionId is required\",\n });\n }\n\n if (!request.partyA) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message:\n \"partyA is required (your business shortcode, till number, or MSISDN)\",\n });\n }\n\n if (!request.identifierType) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message:\n 'identifierType is required: \"1\" (MSISDN) | \"2\" (Till) | \"4\" (ShortCode)',\n });\n }\n\n if (!request.resultUrl) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message:\n \"resultUrl is required — Safaricom POSTs the transaction result here\",\n });\n }\n\n if (!request.queueTimeOutUrl) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message: \"queueTimeOutUrl is required — Safaricom calls this on timeout\",\n });\n }\n\n // ── Build payload matching Daraja spec exactly ──────────────────────────────\n\n const payload = {\n Initiator: initiator,\n SecurityCredential: securityCredential,\n CommandID: request.commandId ?? \"TransactionStatusQuery\",\n TransactionID: request.transactionId,\n PartyA: request.partyA,\n IdentifierType: request.identifierType,\n ResultURL: request.resultUrl,\n QueueTimeOutURL: request.queueTimeOutUrl,\n Remarks: request.remarks ?? \"Transaction Status Query\",\n Occasion: request.occasion ?? \"\",\n };\n\n const { data } = await httpRequest<TransactionStatusResponse>(\n `${baseUrl}/mpesa/transactionstatus/v1/query`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${token}` },\n body: payload,\n }\n );\n\n return data;\n}\n","/**\n * Core M-Pesa / Daraja API types\n */\n\nexport type Environment = \"sandbox\" | \"production\";\n\n/** Base URLs per Daraja environment */\nexport const DARAJA_BASE_URLS: Record<Environment, string> = {\n sandbox: \"https://sandbox.safaricom.co.ke\",\n production: \"https://api.safaricom.co.ke\",\n} as const;\n\nexport interface MpesaConfig {\n // ── Required for all APIs ─────────────────────────────────────────────────\n consumerKey: string;\n consumerSecret: string;\n environment: Environment;\n\n // ── Required for STK Push (M-Pesa Express) ────────────────────────────────\n /** Paybill / HO shortcode (5–7 digits). Required for STK Push & STK Query. */\n lipaNaMpesaShortCode?: string;\n /**\n * Passkey from Daraja portal.\n * Sandbox: visible in the simulator test data section.\n * Production: emailed after Go Live.\n */\n lipaNaMpesaPassKey?: string;\n\n // ── Required for Transaction Status / B2C / Reversals ────────────────────\n /** M-PESA org portal API operator username */\n initiatorName?: string;\n /** Plain-text password for the API operator (will be RSA-encrypted) */\n initiatorPassword?: string;\n\n // ── Certificate options (choose one) ─────────────────────────────────────\n /**\n * Path to the .cer file on disk.\n * Bun: read via `Bun.file(path).text()`\n * Node: read via `fs.promises.readFile(path, \"utf-8\")`\n */\n certificatePath?: string;\n /** PEM string contents of the certificate (alternative to certificatePath) */\n certificatePem?: string;\n /**\n * Pre-computed base64 security credential.\n * Use this if you encrypt outside the library (e.g. at startup).\n * Skips the RSA encryption step entirely.\n */\n securityCredential?: string;\n}\n","/**\n * M-Pesa Daraja API client\n *\n * Supports:\n * - STK Push (M-Pesa Express) — stkPush()\n * - STK Query — stkQuery()\n * - Transaction Status Query — transactionStatus()\n *\n * @example\n * const mpesa = new Mpesa({\n * consumerKey: process.env.MPESA_CONSUMER_KEY!,\n * consumerSecret: process.env.MPESA_CONSUMER_SECRET!,\n * environment: \"sandbox\",\n * lipaNaMpesaShortCode: \"174379\",\n * lipaNaMpesaPassKey: \"bfb279...\",\n * initiatorName: \"testapi\",\n * initiatorPassword: \"Safaricom123!\",\n * certificatePath: \"./SandboxCertificate.cer\",\n * });\n */\n\nimport { TokenManager } from \"../core/auth\";\nimport { encryptSecurityCredential } from \"../core/encryption\";\nimport { PesafyError } from \"../utils/errors\";\nimport {\n processStkPush,\n queryStkPush,\n type StkPushRequest,\n type StkQueryRequest,\n} from \"./stk-push\";\nimport {\n queryTransactionStatus,\n type TransactionStatusRequest,\n} from \"./transaction-status\";\nimport { DARAJA_BASE_URLS, type MpesaConfig } from \"./types\";\n\nexport class Mpesa {\n private readonly config: MpesaConfig;\n private readonly tokenManager: TokenManager;\n private readonly baseUrl: string;\n\n constructor(config: MpesaConfig) {\n if (!config.consumerKey || !config.consumerSecret) {\n throw new PesafyError({\n code: \"INVALID_CREDENTIALS\",\n message: \"consumerKey and consumerSecret are required\",\n });\n }\n\n this.config = config;\n this.baseUrl = DARAJA_BASE_URLS[config.environment];\n this.tokenManager = new TokenManager(\n config.consumerKey,\n config.consumerSecret,\n this.baseUrl\n );\n }\n\n // ── Internal helpers ────────────────────────────────────────────────────────\n\n private getToken(): Promise<string> {\n return this.tokenManager.getAccessToken();\n }\n\n private async buildSecurityCredential(): Promise<string> {\n // Option 1: caller pre-computed it\n if (this.config.securityCredential) return this.config.securityCredential;\n\n // Option 2: we encrypt it\n if (!this.config.initiatorPassword) {\n throw new PesafyError({\n code: \"INVALID_CREDENTIALS\",\n message:\n \"Provide securityCredential (pre-encrypted) \" +\n \"OR (initiatorPassword + certificatePath/certificatePem)\",\n });\n }\n\n let cert: string;\n if (this.config.certificatePem) {\n cert = this.config.certificatePem;\n } else if (this.config.certificatePath) {\n // Bun runtime\n if (typeof Bun !== \"undefined\") {\n cert = await Bun.file(this.config.certificatePath).text();\n } else {\n // Node.js fallback\n const { readFile } = await import(\"node:fs/promises\");\n cert = await readFile(this.config.certificatePath, \"utf-8\");\n }\n } else {\n throw new PesafyError({\n code: \"INVALID_CREDENTIALS\",\n message:\n \"certificatePath or certificatePem required to encrypt the initiator password\",\n });\n }\n\n return encryptSecurityCredential(this.config.initiatorPassword, cert);\n }\n\n // ── STK Push ──────────────────────────────────────────────────────────────\n\n /**\n * M-Pesa Express — sends a payment prompt to the customer's phone.\n *\n * Requires: lipaNaMpesaShortCode + lipaNaMpesaPassKey in config.\n *\n * @example\n * const res = await mpesa.stkPush({\n * amount: 100,\n * phoneNumber: \"0712345678\",\n * callbackUrl: \"https://yourdomain.com/mpesa/callback\",\n * accountReference: \"INV-001\",\n * transactionDesc: \"Payment\",\n * });\n * console.log(res.CheckoutRequestID); // use to poll status\n */\n async stkPush(request: Omit<StkPushRequest, \"shortCode\" | \"passKey\">) {\n const shortCode = this.config.lipaNaMpesaShortCode ?? \"\";\n const passKey = this.config.lipaNaMpesaPassKey ?? \"\";\n\n if (!shortCode || !passKey) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message:\n \"lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push\",\n });\n }\n\n const token = await this.getToken();\n return processStkPush(this.baseUrl, token, {\n ...request,\n shortCode,\n passKey,\n });\n }\n\n /**\n * STK Query — checks the status of a previous STK Push.\n *\n * @example\n * const status = await mpesa.stkQuery({\n * checkoutRequestId: \"ws_CO_1007202409152617172396192\",\n * });\n * if (status.ResultCode === 0) // payment confirmed\n */\n async stkQuery(request: Omit<StkQueryRequest, \"shortCode\" | \"passKey\">) {\n const shortCode = this.config.lipaNaMpesaShortCode ?? \"\";\n const passKey = this.config.lipaNaMpesaPassKey ?? \"\";\n\n if (!shortCode || !passKey) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message:\n \"lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Query\",\n });\n }\n\n const token = await this.getToken();\n return queryStkPush(this.baseUrl, token, {\n ...request,\n shortCode,\n passKey,\n });\n }\n\n /**\n * Transaction Status — queries the result of a completed M-Pesa transaction.\n *\n * Requires: initiatorName + (initiatorPassword + certificate) OR securityCredential.\n *\n * This is ASYNCHRONOUS. The synchronous response only confirms receipt.\n * Final details are POSTed to your resultUrl.\n *\n * @example\n * await mpesa.transactionStatus({\n * transactionId: \"OEI2AK4XXXX\",\n * partyA: \"174379\",\n * identifierType: \"4\",\n * resultUrl: \"https://yourdomain.com/mpesa/result\",\n * queueTimeOutUrl: \"https://yourdomain.com/mpesa/timeout\",\n * remarks: \"Check payment status\",\n * });\n */\n async transactionStatus(request: TransactionStatusRequest) {\n const initiator = this.config.initiatorName ?? \"\";\n if (!initiator) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message: \"initiatorName is required for Transaction Status\",\n });\n }\n\n // Fetch token and encrypt credential concurrently\n const [token, securityCred] = await Promise.all([\n this.getToken(),\n this.buildSecurityCredential(),\n ]);\n\n return queryTransactionStatus(\n this.baseUrl,\n token,\n securityCred,\n initiator,\n request\n );\n }\n\n /** Force the cached OAuth token to be refreshed on the next API call */\n clearTokenCache(): void {\n this.tokenManager.clearCache();\n }\n}\n","/**\n * Exponential backoff retry for webhook at-least-once delivery.\n *\n * Daraja is asynchronous — if your callback endpoint is down, the API\n * Gateway logs a 503 and discards the result. Use this utility to\n * retry your own internal processing after receiving a webhook.\n */\n\nexport interface RetryOptions {\n /** Maximum number of attempts (default: Infinity) */\n maxRetries?: number;\n /** Initial delay in ms (default: 1000 = 1 second) */\n initialDelay?: number;\n /** Maximum delay cap in ms (default: 3_600_000 = 1 hour) */\n maxDelay?: number;\n /** Multiplier per retry (default: 2 — doubles each time) */\n backoffMultiplier?: number;\n /** Maximum total duration in ms (default: 30 days) */\n maxRetryDuration?: number;\n}\n\nconst DEFAULT_OPTIONS: Required<RetryOptions> = {\n maxRetries: Infinity,\n initialDelay: 1_000,\n maxDelay: 3_600_000,\n backoffMultiplier: 2,\n maxRetryDuration: 30 * 24 * 60 * 60 * 1_000, // 30 days\n};\n\nexport interface RetryResult<T> {\n success: boolean;\n data?: T;\n attempts: number;\n error?: Error;\n}\n\n/**\n * Retries `fn` with exponential backoff until it resolves, or limits are hit.\n *\n * @example\n * const result = await retryWithBackoff(\n * () => sendToDatabase(webhookData),\n * { maxRetries: 5, initialDelay: 500 }\n * );\n */\nexport async function retryWithBackoff<T>(\n fn: () => Promise<T>,\n options: RetryOptions = {}\n): Promise<RetryResult<T>> {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n let delay = opts.initialDelay;\n let attempts = 0;\n const startTime = Date.now();\n\n while (attempts < opts.maxRetries) {\n attempts++;\n\n if (Date.now() - startTime > opts.maxRetryDuration) {\n return {\n success: false,\n attempts,\n error: new Error(\"Max retry duration exceeded\"),\n };\n }\n\n try {\n const data = await fn();\n return { success: true, data, attempts };\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n\n // Don't retry client errors (4xx) — they won't self-heal\n if (err.message.includes(\"4\")) {\n return { success: false, attempts, error: err };\n }\n\n if (attempts < opts.maxRetries) {\n await new Promise((resolve) => setTimeout(resolve, delay));\n delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelay);\n }\n }\n }\n\n return {\n success: false,\n attempts,\n error: new Error(\"Max retries exceeded\"),\n };\n}\n","/**\n * Webhook verification utilities\n *\n * Daraja does NOT use HMAC webhook signatures like Stripe.\n * Instead, verify that callbacks come from whitelisted Safaricom IPs.\n *\n * Official Safaricom IP whitelist (from Getting Started docs):\n * 196.201.214.200\n * 196.201.214.206\n * 196.201.213.114\n * 196.201.214.207\n * 196.201.214.208\n * 196.201.213.44\n * 196.201.212.127\n * 196.201.212.138\n * 196.201.212.129\n * 196.201.212.136\n * 196.201.212.74\n * 196.201.212.69\n */\n\nimport type { StkPushWebhook } from \"./types\";\n\n/** Official Safaricom API Gateway IP addresses */\nexport const SAFARICOM_IPS: readonly string[] = [\n \"196.201.214.200\",\n \"196.201.214.206\",\n \"196.201.213.114\",\n \"196.201.214.207\",\n \"196.201.214.208\",\n \"196.201.213.44\",\n \"196.201.212.127\",\n \"196.201.212.138\",\n \"196.201.212.129\",\n \"196.201.212.136\",\n \"196.201.212.74\",\n \"196.201.212.69\",\n] as const;\n\n/**\n * Returns true if requestIP is in the allowed list.\n * Defaults to the official Safaricom IP whitelist.\n */\nexport function verifyWebhookIP(\n requestIP: string,\n allowedIPs: readonly string[] = SAFARICOM_IPS\n): boolean {\n return allowedIPs.includes(requestIP);\n}\n\n/**\n * Parses and validates an STK Push webhook body.\n * Returns the typed payload or null if it doesn't match the expected shape.\n */\nexport function parseStkPushWebhook(body: unknown): StkPushWebhook | null {\n try {\n const parsed = body as StkPushWebhook;\n if (parsed?.Body?.stkCallback) return parsed;\n return null;\n } catch {\n return null;\n }\n}\n","/**\n * High-level webhook event handler\n */\n\nimport { parseStkPushWebhook, verifyWebhookIP } from \"./signature-verifier\";\nimport type { StkPushWebhook, WebhookEventType } from \"./types\";\n\nexport interface WebhookHandlerOptions {\n /** IP address of the incoming request (from req.ip or x-forwarded-for) */\n requestIP?: string;\n /** Override the default Safaricom IP whitelist */\n allowedIPs?: string[];\n /** Skip IP verification — ONLY for local development/testing */\n skipIPCheck?: boolean;\n}\n\nexport interface WebhookHandlerResult<T = unknown> {\n success: boolean;\n eventType: WebhookEventType | null;\n data: T | null;\n error?: string;\n}\n\n/**\n * Parses and validates an inbound Daraja webhook payload.\n *\n * @example\n * // Express route\n * app.post(\"/mpesa/callback\", (req, res) => {\n * const result = handleWebhook(req.body, { requestIP: req.ip });\n * if (!result.success) return res.status(400).json({ error: result.error });\n * // process result.data (StkPushWebhook)\n * res.json({ ResultCode: 0, ResultDesc: \"Accepted\" });\n * });\n */\nexport function handleWebhook(\n body: unknown,\n options: WebhookHandlerOptions = {}\n): WebhookHandlerResult {\n // ── IP verification ─────────────────────────────────────────────────────────\n if (!options.skipIPCheck && options.requestIP) {\n if (!verifyWebhookIP(options.requestIP, options.allowedIPs)) {\n return {\n success: false,\n eventType: null,\n data: null,\n error: `IP address ${options.requestIP} is not in the Safaricom whitelist`,\n };\n }\n }\n\n // ── Parse STK Push callback ─────────────────────────────────────────────────\n const stkPush = parseStkPushWebhook(body);\n if (stkPush) {\n return {\n success: true,\n eventType: \"stk_push\",\n data: stkPush,\n };\n }\n\n return {\n success: false,\n eventType: null,\n data: null,\n error: \"Unknown or malformed webhook payload\",\n };\n}\n\n// ── Convenience extractors ────────────────────────────────────────────────────\n\n/** Extracts the M-Pesa receipt number from a successful STK Push callback */\nexport function extractTransactionId(webhook: StkPushWebhook): string | null {\n const items = webhook.Body?.stkCallback?.CallbackMetadata?.Item;\n const item = items?.find((i) => i.Name === \"MpesaReceiptNumber\");\n return item ? String(item.Value) : null;\n}\n\n/** Extracts the transaction amount from a successful STK Push callback */\nexport function extractAmount(webhook: StkPushWebhook): number | null {\n const items = webhook.Body?.stkCallback?.CallbackMetadata?.Item;\n const item = items?.find((i) => i.Name === \"Amount\");\n return item ? Number(item.Value) : null;\n}\n\n/** Extracts the phone number from a successful STK Push callback */\nexport function extractPhoneNumber(webhook: StkPushWebhook): string | null {\n const items = webhook.Body?.stkCallback?.CallbackMetadata?.Item;\n const item = items?.find((i) => i.Name === \"PhoneNumber\");\n return item ? String(item.Value) : null;\n}\n\n/** Returns true if the STK Push callback represents a successful transaction */\nexport function isSuccessfulCallback(webhook: StkPushWebhook): boolean {\n return webhook.Body?.stkCallback?.ResultCode === 0;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils/errors/index.ts","../src/core/encryption/security-credentials.ts","../src/utils/http/index.ts","../src/core/auth/token-manager.ts","../src/utils/phone/index.ts","../src/mpesa/stk-push/utils.ts","../src/mpesa/stk-push/stk-push.ts","../src/mpesa/stk-push/stk-query.ts","../src/mpesa/stk-push/types.ts","../src/mpesa/transaction-status/query.ts","../src/mpesa/types.ts","../src/mpesa/index.ts","../src/mpesa/webhooks/retry.ts","../src/mpesa/webhooks/signature-verifier.ts","../src/mpesa/webhooks/webhook-handler.ts"],"names":[],"mappings":";;;;;;;;;AAyBO,IAAM,WAAA,GAAN,MAAM,YAAA,SAAoB,KAAA,CAAM;AAAA,EAOrC,YAAY,OAAA,EAA6B;AACvC,IAAA,KAAA,CAAM,QAAQ,OAAO,CAAA;AAPvB,IAAA,aAAA,CAAA,IAAA,EAAS,MAAA,CAAA;AACT,IAAA,aAAA,CAAA,IAAA,EAAS,YAAA,CAAA;AACT,IAAA,aAAA,CAAA,IAAA,EAAS,UAAA,CAAA;AACT,IAAA,aAAA,CAAA,IAAA,EAAS,WAAA,CAAA;AACT,IAAA,aAAA,CAAA,IAAA,EAAkB,OAAA,CAAA;AAIhB,IAAA,MAAA,CAAO,eAAe,IAAA,EAAM,MAAA,EAAQ,EAAE,KAAA,EAAO,eAAe,CAAA;AAC5D,IAAA,IAAA,CAAK,OAAO,OAAA,CAAQ,IAAA;AACpB,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,UAAA;AAC1B,IAAA,IAAA,CAAK,WAAW,OAAA,CAAQ,QAAA;AACxB,IAAA,IAAA,CAAK,YAAY,OAAA,CAAQ,SAAA;AACzB,IAAA,IAAA,CAAK,QAAQ,OAAA,CAAQ,KAAA;AACrB,IAAA,IAAI,MAAM,iBAAA,EAAmB;AAC3B,MAAA,KAAA,CAAM,iBAAA,CAAkB,MAAM,YAAW,CAAA;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,MAAA,GAAS;AACP,IAAA,OAAO;AAAA,MACL,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,SAAS,IAAA,CAAK,OAAA;AAAA,MACd,YAAY,IAAA,CAAK,UAAA;AAAA,MACjB,WAAW,IAAA,CAAK;AAAA,KAClB;AAAA,EACF;AACF;AAGO,SAAS,YAAY,OAAA,EAA0C;AACpE,EAAA,OAAO,IAAI,YAAY,OAAO,CAAA;AAChC;;;AC9BO,SAAS,yBAAA,CACd,mBACA,cAAA,EACQ;AACR,EAAA,IAAI;AACF,IAAA,MAAM,cAAA,GAAiB,MAAA,CAAO,IAAA,CAAK,iBAAA,EAAmB,OAAO,CAAA;AAE7D,IAAA,MAAM,SAAA,GAAY,aAAA;AAAA,MAChB;AAAA,QACE,GAAA,EAAK,cAAA;AAAA;AAAA,QAEL,SAAS,SAAA,CAAU;AAAA,OACrB;AAAA,MACA;AAAA,KACF;AAEA,IAAA,OAAO,SAAA,CAAU,SAAS,QAAQ,CAAA;AAAA,EACpC,SAAS,KAAA,EAAO;AACd,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB,IAAA,EAAM,mBAAA;AAAA,MACN,OAAA,EACE,8HAAA;AAAA,MAEF,KAAA,EAAO;AAAA,KACR,CAAA;AAAA,EACH;AACF;;;AChBA,IAAM,kBAAA,uBAAyB,GAAA,CAAI,CAAC,KAAK,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAC,CAAA;AAG5D,SAAS,MAAM,EAAA,EAA2B;AACxC,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,YAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AACzD;AAOA,SAAS,OAAO,MAAA,EAAwB;AACtC,EAAA,MAAM,SAAS,MAAA,GAAS,IAAA;AACxB,EAAA,OAAO,MAAA,IAAU,IAAA,CAAK,MAAA,EAAO,GAAI,SAAS,CAAA,GAAI,MAAA,CAAA;AAChD;AAUA,eAAsB,WAAA,CACpB,KACA,OAAA,EAC0B;AAC1B,EAAA,MAAM,UAAA,GAAa,QAAQ,OAAA,IAAW,CAAA;AACtC,EAAA,MAAM,SAAA,GAAY,QAAQ,UAAA,IAAc,GAAA;AAExC,EAAA,MAAM,OAAA,GAAkC;AAAA,IACtC,cAAA,EAAgB,kBAAA;AAAA,IAChB,MAAA,EAAQ,kBAAA;AAAA,IACR,GAAG,OAAA,CAAQ;AAAA,GACb;AAEA,EAAA,MAAM,IAAA,GAAoB;AAAA,IACxB,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,OAAA;AAAA,IACA,GAAI,OAAA,CAAQ,IAAA,KAAS,MAAA,GACjB,EAAE,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,IAAI,CAAA,EAAE,GACrC;AAAC,GACP;AAEA,EAAA,IAAI,SAAA,GAAgC,IAAA;AAEpC,EAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,UAAA,EAAY,OAAA,EAAA,EAAW;AAEtD,IAAA,IAAI,UAAU,CAAA,EAAG;AACf,MAAA,MAAM,KAAA,GAAQ,OAAO,SAAA,GAAY,IAAA,CAAK,IAAI,CAAA,EAAG,OAAA,GAAU,CAAC,CAAC,CAAA;AACzD,MAAA,MAAM,MAAM,KAAK,CAAA;AAAA,IACnB;AAEA,IAAA,IAAI,QAAA;AAEJ,IAAA,IAAI;AACF,MAAA,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK,IAAI,CAAA;AAAA,IAClC,SAAS,GAAA,EAAK;AAEZ,MAAA,SAAA,GAAY,IAAI,WAAA,CAAY;AAAA,QAC1B,IAAA,EAAM,eAAA;AAAA,QACN,SAAS,CAAA,sBAAA,EAAyB,GAAG,CAAA,EAAA,EAAK,MAAA,CAAO,GAAG,CAAC,CAAA,CAAA;AAAA,QACrD,KAAA,EAAO;AAAA,OACR,CAAA;AAED,MAAA,IAAI,UAAU,UAAA,EAAY;AAC1B,MAAA,MAAM,SAAA;AAAA,IACR;AAGA,IAAA,IAAI,IAAA;AACJ,IAAA,MAAM,WAAA,GAAc,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,cAAc,CAAA,IAAK,EAAA;AAC5D,IAAA,IAAI;AACF,MAAA,IAAA,GAAO,WAAA,CAAY,QAAA,CAAS,kBAAkB,CAAA,GAC1C,MAAM,SAAS,IAAA,EAAK,GACpB,MAAM,QAAA,CAAS,IAAA,EAAK;AAAA,IAC1B,CAAA,CAAA,MAAQ;AACN,MAAA,IAAA,GAAO,IAAA;AAAA,IACT;AAGA,IAAA,MAAM,kBAA0C,EAAC;AACjD,IAAA,QAAA,CAAS,OAAA,CAAQ,OAAA,CAAQ,CAAC,KAAA,EAAO,GAAA,KAAQ;AACvC,MAAA,eAAA,CAAgB,GAAG,CAAA,GAAI,KAAA;AAAA,IACzB,CAAC,CAAA;AAED,IAAA,IAAI,SAAS,EAAA,EAAI;AACf,MAAA,OAAO;AAAA,QACL,IAAA;AAAA,QACA,QAAQ,QAAA,CAAS,MAAA;AAAA,QACjB,OAAA,EAAS;AAAA,OACX;AAAA,IACF;AAGA,IAAA,MAAM,WAAA,GAAc,kBAAA,CAAmB,GAAA,CAAI,QAAA,CAAS,MAAM,CAAA;AAG1D,IAAA,MAAM,MAAA,GAAU,QAAQ,EAAC;AACzB,IAAA,MAAM,UACH,MAAA,CAAO,YAAA,IACP,OAAO,mBAAA,IACR,CAAA,KAAA,EAAQ,SAAS,MAAM,CAAA,CAAA;AAEzB,IAAA,SAAA,GAAY,IAAI,WAAA,CAAY;AAAA,MAC1B,IAAA,EAAM,cAAc,gBAAA,GAAmB,YAAA;AAAA,MACvC,OAAA;AAAA,MACA,YAAY,QAAA,CAAS,MAAA;AAAA,MACrB,QAAA,EAAU,IAAA;AAAA,MACV,WAAW,MAAA,CAAO;AAAA,KACnB,CAAA;AAGD,IAAA,IAAI,WAAA,IAAe,UAAU,UAAA,EAAY;AACvC,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,SAAA;AAAA,EACR;AAGA,EAAA,MAAM,SAAA;AACR;;;ACjJA,IAAM,oBAAA,GAAuB,EAAA;AAEtB,IAAM,eAAN,MAAmB;AAAA;AAAA,EAQxB,WAAA,CAAY,WAAA,EAAqB,cAAA,EAAwB,OAAA,EAAiB;AAP1E,IAAA,aAAA,CAAA,IAAA,EAAiB,aAAA,CAAA;AACjB,IAAA,aAAA,CAAA,IAAA,EAAiB,gBAAA,CAAA;AACjB,IAAA,aAAA,CAAA,IAAA,EAAiB,SAAA,CAAA;AAEjB,IAAA,aAAA,CAAA,IAAA,EAAQ,aAAA,EAA6B,IAAA,CAAA;AACrC,IAAA,aAAA,CAAA,IAAA,EAAQ,gBAAA,EAAiB,CAAA,CAAA;AAGvB,IAAA,IAAA,CAAK,WAAA,GAAc,WAAA;AACnB,IAAA,IAAA,CAAK,cAAA,GAAiB,cAAA;AACtB,IAAA,IAAA,CAAK,OAAA,GAAU,OAAA;AAAA,EACjB;AAAA,EAEQ,kBAAA,GAA6B;AAEnC,IAAA,MAAM,cAAc,CAAA,EAAG,IAAA,CAAK,WAAW,CAAA,CAAA,EAAI,KAAK,cAAc,CAAA,CAAA;AAC9D,IAAA,MAAM,UAAU,MAAA,CAAO,IAAA,CAAK,aAAa,OAAO,CAAA,CAAE,SAAS,QAAQ,CAAA;AACnE,IAAA,OAAO,SAAS,OAAO,CAAA,CAAA;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAA,GAAkC;AACtC,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,EAAI,GAAI,GAAA;AAEzB,IAAA,IAAI,IAAA,CAAK,WAAA,IAAe,IAAA,CAAK,cAAA,GAAiB,MAAM,oBAAA,EAAsB;AACxE,MAAA,OAAO,IAAA,CAAK,WAAA;AAAA,IACd;AAGA,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,IAAA,CAAK,OAAO,CAAA,gDAAA,CAAA;AAE3B,IAAA,MAAM,QAAA,GAAW,MAAM,WAAA,CAA2B,GAAA,EAAK;AAAA,MACrD,MAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,aAAA,EAAe,KAAK,kBAAA;AAAmB;AACzC,KACD,CAAA;AAED,IAAA,MAAM,EAAE,YAAA,EAAc,UAAA,EAAW,GAAI,QAAA,CAAS,IAAA;AAE9C,IAAA,IAAI,CAAC,YAAA,EAAc;AACjB,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,aAAA;AAAA,QACN,OAAA,EACE,4EAAA;AAAA,QACF,UAAU,QAAA,CAAS;AAAA,OACpB,CAAA;AAAA,IACH;AAEA,IAAA,IAAA,CAAK,WAAA,GAAc,YAAA;AAEnB,IAAA,IAAA,CAAK,cAAA,GAAiB,OAAO,UAAA,IAAc,IAAA,CAAA;AAE3C,IAAA,OAAO,IAAA,CAAK,WAAA;AAAA,EACd;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,IAAA,CAAK,WAAA,GAAc,IAAA;AACnB,IAAA,IAAA,CAAK,cAAA,GAAiB,CAAA;AAAA,EACxB;AACF,CAAA;;;ACrEO,SAAS,qBAAqB,KAAA,EAAuB;AAC1D,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAEtC,EAAA,IAAI,UAAA;AAEJ,EAAA,IAAI,OAAO,UAAA,CAAW,KAAK,CAAA,IAAK,MAAA,CAAO,WAAW,EAAA,EAAI;AACpD,IAAA,UAAA,GAAa,MAAA;AAAA,EACf,WAAW,MAAA,CAAO,UAAA,CAAW,GAAG,CAAA,IAAK,MAAA,CAAO,WAAW,EAAA,EAAI;AACzD,IAAA,UAAA,GAAa,CAAA,GAAA,EAAM,MAAA,CAAO,KAAA,CAAM,CAAC,CAAC,CAAA,CAAA;AAAA,EACpC,CAAA,MAAA,IAAW,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG;AAE9B,IAAA,UAAA,GAAa,MAAM,MAAM,CAAA,CAAA;AAAA,EAC3B,WAAW,MAAA,CAAO,UAAA,CAAW,KAAK,CAAA,IAAK,MAAA,CAAO,WAAW,EAAA,EAAI;AAC3D,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB,IAAA,EAAM,eAAA;AAAA,MACN,OAAA,EAAS,yBAAyB,KAAK,CAAA,qCAAA;AAAA,KACxC,CAAA;AAAA,EACH,CAAA,MAAO;AACL,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB,IAAA,EAAM,eAAA;AAAA,MACN,OAAA,EAAS,8BAA8B,KAAK,CAAA,kDAAA;AAAA,KAC7C,CAAA;AAAA,EACH;AAGA,EAAA,IAAI,UAAA,CAAW,WAAW,EAAA,EAAI;AAC5B,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB,IAAA,EAAM,eAAA;AAAA,MACN,OAAA,EAAS,CAAA,cAAA,EAAiB,KAAK,CAAA,iBAAA,EAAoB,UAAU,CAAA,yBAAA;AAAA,KAC9D,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,UAAA;AACT;;;AC3BO,SAAS,kBAAA,CACd,SAAA,EACA,OAAA,EACA,SAAA,EACQ;AACR,EAAA,OAAO,KAAK,CAAA,EAAG,SAAS,GAAG,OAAO,CAAA,EAAG,SAAS,CAAA,CAAE,CAAA;AAClD;AAOO,SAAS,YAAA,GAAuB;AACrC,EAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,EAAA,MAAM,GAAA,GAAM,CAAC,CAAA,KAAsB,CAAA,CAAE,UAAS,CAAE,QAAA,CAAS,GAAG,GAAG,CAAA;AAC/D,EAAA,OAAO;AAAA,IACL,IAAI,WAAA,EAAY;AAAA,IAChB,GAAA,CAAI,GAAA,CAAI,QAAA,EAAS,GAAI,CAAC,CAAA;AAAA,IACtB,GAAA,CAAI,GAAA,CAAI,OAAA,EAAS,CAAA;AAAA,IACjB,GAAA,CAAI,GAAA,CAAI,QAAA,EAAU,CAAA;AAAA,IAClB,GAAA,CAAI,GAAA,CAAI,UAAA,EAAY,CAAA;AAAA,IACpB,GAAA,CAAI,GAAA,CAAI,UAAA,EAAY;AAAA,GACtB,CAAE,KAAK,EAAE,CAAA;AACX;;;ACXA,eAAsB,cAAA,CACpB,OAAA,EACA,WAAA,EACA,OAAA,EAC0B;AAE1B,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA;AACxC,EAAA,IAAI,SAAS,CAAA,EAAG;AACd,IAAA,MAAM,IAAI,WAAA,CAAY;AAAA,MACpB,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EAAS,CAAA,mCAAA,EAAsC,OAAA,CAAQ,MAAM,oBAAoB,MAAM,CAAA,EAAA;AAAA,KACxF,CAAA;AAAA,EACH;AAIA,EAAA,MAAM,YAAY,YAAA,EAAa;AAK/B,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,MAAA,IAAU,OAAA,CAAQ,SAAA;AAEzC,EAAA,MAAM,IAAA,GAAO;AAAA,IACX,mBAAmB,OAAA,CAAQ,SAAA;AAAA,IAC3B,UAAU,kBAAA,CAAmB,OAAA,CAAQ,SAAA,EAAW,OAAA,CAAQ,SAAS,SAAS,CAAA;AAAA,IAC1E,SAAA,EAAW,SAAA;AAAA,IACX,eAAA,EAAiB,QAAQ,eAAA,IAAmB,uBAAA;AAAA,IAC5C,MAAA,EAAQ,MAAA;AAAA,IACR,MAAA,EAAQ,oBAAA,CAAkB,OAAA,CAAQ,WAAW,CAAA;AAAA,IAC7C,MAAA,EAAQ,MAAA;AAAA,IACR,WAAA,EAAa,oBAAA,CAAkB,OAAA,CAAQ,WAAW,CAAA;AAAA,IAClD,aAAa,OAAA,CAAQ,WAAA;AAAA;AAAA,IAErB,gBAAA,EAAkB,OAAA,CAAQ,gBAAA,CAAiB,KAAA,CAAM,GAAG,EAAE,CAAA;AAAA,IACtD,eAAA,EAAiB,OAAA,CAAQ,eAAA,CAAgB,KAAA,CAAM,GAAG,EAAE;AAAA,GACtD;AAMA,EAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,WAAA;AAAA,IACrB,GAAG,OAAO,CAAA,gCAAA,CAAA;AAAA,IACV;AAAA,MACE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,aAAA,EAAe,CAAA,OAAA,EAAU,WAAW,CAAA,CAAA,EAAG;AAAA,MAClD,IAAA;AAAA;AAAA,MAEA,OAAA,EAAS,CAAA;AAAA,MACT,UAAA,EAAY;AAAA;AACd,GACF;AAEA,EAAA,OAAO,IAAA;AACT;;;ACjEA,eAAsB,YAAA,CACpB,OAAA,EACA,WAAA,EACA,OAAA,EAC2B;AAE3B,EAAA,MAAM,YAAY,YAAA,EAAa;AAE/B,EAAA,MAAM,IAAA,GAAO;AAAA,IACX,mBAAmB,OAAA,CAAQ,SAAA;AAAA,IAC3B,UAAU,kBAAA,CAAmB,OAAA,CAAQ,SAAA,EAAW,OAAA,CAAQ,SAAS,SAAS,CAAA;AAAA,IAC1E,SAAA,EAAW,SAAA;AAAA,IACX,mBAAmB,OAAA,CAAQ;AAAA,GAC7B;AAEA,EAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,WAAA;AAAA,IACrB,GAAG,OAAO,CAAA,4BAAA,CAAA;AAAA,IACV;AAAA,MACE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,aAAA,EAAe,CAAA,OAAA,EAAU,WAAW,CAAA,CAAA,EAAG;AAAA,MAClD;AAAA;AACF,GACF;AAEA,EAAA,OAAO,IAAA;AACT;;;AC6HO,SAAS,qBACd,EAAA,EAC0B;AAC1B,EAAA,OAAO,GAAG,UAAA,KAAe,CAAA;AAC3B;AAMO,SAAS,gBAAA,CACd,UACA,IAAA,EAC6B;AAC7B,EAAA,MAAM,KAAA,GAAQ,SAAS,IAAA,CAAK,WAAA;AAC5B,EAAA,IAAI,CAAC,oBAAA,CAAqB,KAAK,CAAA,EAAG,OAAO,MAAA;AACzC,EAAA,OAAO,KAAA,CAAM,iBAAiB,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,KAAS,IAAI,CAAA,EAAG,KAAA;AACnE;;;AC9KA,eAAsB,sBAAA,CACpB,OAAA,EACA,KAAA,EACA,kBAAA,EACA,WACA,OAAA,EACoC;AAGpC,EAAA,IAAI,CAAC,QAAQ,aAAA,EAAe;AAC1B,IAAA,MAAM,WAAA,CAAY;AAAA,MAChB,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,CAAC,QAAQ,MAAA,EAAQ;AACnB,IAAA,MAAM,WAAA,CAAY;AAAA,MAChB,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EACE;AAAA,KACH,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,CAAC,QAAQ,cAAA,EAAgB;AAC3B,IAAA,MAAM,WAAA,CAAY;AAAA,MAChB,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EACE;AAAA,KACH,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,CAAC,QAAQ,SAAA,EAAW;AACtB,IAAA,MAAM,WAAA,CAAY;AAAA,MAChB,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EACE;AAAA,KACH,CAAA;AAAA,EACH;AAEA,EAAA,IAAI,CAAC,QAAQ,eAAA,EAAiB;AAC5B,IAAA,MAAM,WAAA,CAAY;AAAA,MAChB,IAAA,EAAM,kBAAA;AAAA,MACN,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,EACH;AAIA,EAAA,MAAM,OAAA,GAAU;AAAA,IACd,SAAA,EAAW,SAAA;AAAA,IACX,kBAAA,EAAoB,kBAAA;AAAA,IACpB,SAAA,EAAW,QAAQ,SAAA,IAAa,wBAAA;AAAA,IAChC,eAAe,OAAA,CAAQ,aAAA;AAAA,IACvB,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,gBAAgB,OAAA,CAAQ,cAAA;AAAA,IACxB,WAAW,OAAA,CAAQ,SAAA;AAAA,IACnB,iBAAiB,OAAA,CAAQ,eAAA;AAAA,IACzB,OAAA,EAAS,QAAQ,OAAA,IAAW,0BAAA;AAAA,IAC5B,QAAA,EAAU,QAAQ,QAAA,IAAY;AAAA,GAChC;AAEA,EAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,WAAA;AAAA,IACrB,GAAG,OAAO,CAAA,iCAAA,CAAA;AAAA,IACV;AAAA,MACE,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS,EAAE,aAAA,EAAe,CAAA,OAAA,EAAU,KAAK,CAAA,CAAA,EAAG;AAAA,MAC5C,IAAA,EAAM;AAAA;AACR,GACF;AAEA,EAAA,OAAO,IAAA;AACT;;;ACnFO,IAAM,gBAAA,GAAgD;AAAA,EAC3D,OAAA,EAAS,iCAAA;AAAA,EACT,UAAA,EAAY;AACd;;;AC0BO,IAAM,QAAN,MAAY;AAAA,EAKjB,YAAY,MAAA,EAAqB;AAJjC,IAAA,aAAA,CAAA,IAAA,EAAiB,QAAA,CAAA;AACjB,IAAA,aAAA,CAAA,IAAA,EAAiB,cAAA,CAAA;AACjB,IAAA,aAAA,CAAA,IAAA,EAAiB,SAAA,CAAA;AAGf,IAAA,IAAI,CAAC,MAAA,CAAO,WAAA,IAAe,CAAC,OAAO,cAAA,EAAgB;AACjD,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,qBAAA;AAAA,QACN,OAAA,EAAS;AAAA,OACV,CAAA;AAAA,IACH;AAEA,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,OAAA,GAAU,gBAAA,CAAiB,MAAA,CAAO,WAAW,CAAA;AAClD,IAAA,IAAA,CAAK,eAAe,IAAI,YAAA;AAAA,MACtB,MAAA,CAAO,WAAA;AAAA,MACP,MAAA,CAAO,cAAA;AAAA,MACP,IAAA,CAAK;AAAA,KACP;AAAA,EACF;AAAA;AAAA,EAIQ,QAAA,GAA4B;AAClC,IAAA,OAAO,IAAA,CAAK,aAAa,cAAA,EAAe;AAAA,EAC1C;AAAA,EAEA,MAAc,uBAAA,GAA2C;AAEvD,IAAA,IAAI,IAAA,CAAK,MAAA,CAAO,kBAAA,EAAoB,OAAO,KAAK,MAAA,CAAO,kBAAA;AAGvD,IAAA,IAAI,CAAC,IAAA,CAAK,MAAA,CAAO,iBAAA,EAAmB;AAClC,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,qBAAA;AAAA,QACN,OAAA,EACE;AAAA,OAEH,CAAA;AAAA,IACH;AAEA,IAAA,IAAI,IAAA;AACJ,IAAA,IAAI,IAAA,CAAK,OAAO,cAAA,EAAgB;AAC9B,MAAA,IAAA,GAAO,KAAK,MAAA,CAAO,cAAA;AAAA,IACrB,CAAA,MAAA,IAAW,IAAA,CAAK,MAAA,CAAO,eAAA,EAAiB;AAEtC,MAAA,IAAI,OAAO,QAAQ,WAAA,EAAa;AAC9B,QAAA,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,CAAK,KAAK,MAAA,CAAO,eAAe,EAAE,IAAA,EAAK;AAAA,MAC1D,CAAA,MAAO;AAEL,QAAA,MAAM,EAAE,QAAA,EAAS,GAAI,MAAM,OAAO,aAAkB,CAAA;AACpD,QAAA,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,CAAK,MAAA,CAAO,iBAAiB,OAAO,CAAA;AAAA,MAC5D;AAAA,IACF,CAAA,MAAO;AACL,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,qBAAA;AAAA,QACN,OAAA,EACE;AAAA,OACH,CAAA;AAAA,IACH;AAEA,IAAA,OAAO,yBAAA,CAA0B,IAAA,CAAK,MAAA,CAAO,iBAAA,EAAmB,IAAI,CAAA;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,MAAM,QAAQ,OAAA,EAAwD;AACpE,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,MAAA,CAAO,oBAAA,IAAwB,EAAA;AACtD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,kBAAA,IAAsB,EAAA;AAElD,IAAA,IAAI,CAAC,SAAA,IAAa,CAAC,OAAA,EAAS;AAC1B,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,kBAAA;AAAA,QACN,OAAA,EACE;AAAA,OACH,CAAA;AAAA,IACH;AAEA,IAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,QAAA,EAAS;AAClC,IAAA,OAAO,cAAA,CAAe,IAAA,CAAK,OAAA,EAAS,KAAA,EAAO;AAAA,MACzC,GAAG,OAAA;AAAA,MACH,SAAA;AAAA,MACA;AAAA,KACD,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,SAAS,OAAA,EAAyD;AACtE,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,MAAA,CAAO,oBAAA,IAAwB,EAAA;AACtD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,kBAAA,IAAsB,EAAA;AAElD,IAAA,IAAI,CAAC,SAAA,IAAa,CAAC,OAAA,EAAS;AAC1B,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,kBAAA;AAAA,QACN,OAAA,EACE;AAAA,OACH,CAAA;AAAA,IACH;AAEA,IAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,QAAA,EAAS;AAClC,IAAA,OAAO,YAAA,CAAa,IAAA,CAAK,OAAA,EAAS,KAAA,EAAO;AAAA,MACvC,GAAG,OAAA;AAAA,MACH,SAAA;AAAA,MACA;AAAA,KACD,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,MAAM,kBAAkB,OAAA,EAAmC;AACzD,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,MAAA,CAAO,aAAA,IAAiB,EAAA;AAC/C,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,MAAM,IAAI,WAAA,CAAY;AAAA,QACpB,IAAA,EAAM,kBAAA;AAAA,QACN,OAAA,EAAS;AAAA,OACV,CAAA;AAAA,IACH;AAGA,IAAA,MAAM,CAAC,KAAA,EAAO,YAAY,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,MAC9C,KAAK,QAAA,EAAS;AAAA,MACd,KAAK,uBAAA;AAAwB,KAC9B,CAAA;AAED,IAAA,OAAO,sBAAA;AAAA,MACL,IAAA,CAAK,OAAA;AAAA,MACL,KAAA;AAAA,MACA,YAAA;AAAA,MACA,SAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AAAA;AAAA,EAGA,eAAA,GAAwB;AACtB,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAAA,EAC/B;AACF;;;AChMA,IAAM,eAAA,GAA0C;AAAA,EAC9C,UAAA,EAAY,QAAA;AAAA,EACZ,YAAA,EAAc,GAAA;AAAA,EACd,QAAA,EAAU,IAAA;AAAA,EACV,iBAAA,EAAmB,CAAA;AAAA,EACnB,gBAAA,EAAkB,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK;AAAA;AACxC,CAAA;AAkBA,eAAsB,gBAAA,CACpB,EAAA,EACA,OAAA,GAAwB,EAAC,EACA;AACzB,EAAA,MAAM,IAAA,GAAO,EAAE,GAAG,eAAA,EAAiB,GAAG,OAAA,EAAQ;AAC9C,EAAA,IAAI,QAAQ,IAAA,CAAK,YAAA;AACjB,EAAA,IAAI,QAAA,GAAW,CAAA;AACf,EAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAE3B,EAAA,OAAO,QAAA,GAAW,KAAK,UAAA,EAAY;AACjC,IAAA,QAAA,EAAA;AAEA,IAAA,IAAI,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA,GAAY,KAAK,gBAAA,EAAkB;AAClD,MAAA,OAAO;AAAA,QACL,OAAA,EAAS,KAAA;AAAA,QACT,QAAA;AAAA,QACA,KAAA,EAAO,IAAI,KAAA,CAAM,6BAA6B;AAAA,OAChD;AAAA,IACF;AAEA,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAO,MAAM,EAAA,EAAG;AACtB,MAAA,OAAO,EAAE,OAAA,EAAS,IAAA,EAAM,IAAA,EAAM,QAAA,EAAS;AAAA,IACzC,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,GAAA,GAAM,iBAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC,CAAA;AAGpE,MAAA,IAAI,GAAA,CAAI,OAAA,CAAQ,QAAA,CAAS,GAAG,CAAA,EAAG;AAC7B,QAAA,OAAO,EAAE,OAAA,EAAS,KAAA,EAAO,QAAA,EAAU,OAAO,GAAA,EAAI;AAAA,MAChD;AAEA,MAAA,IAAI,QAAA,GAAW,KAAK,UAAA,EAAY;AAC9B,QAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,YAAY,UAAA,CAAW,OAAA,EAAS,KAAK,CAAC,CAAA;AACzD,QAAA,KAAA,GAAQ,KAAK,GAAA,CAAI,KAAA,GAAQ,IAAA,CAAK,iBAAA,EAAmB,KAAK,QAAQ,CAAA;AAAA,MAChE;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,KAAA;AAAA,IACT,QAAA;AAAA,IACA,KAAA,EAAO,IAAI,KAAA,CAAM,sBAAsB;AAAA,GACzC;AACF;;;AChEO,IAAM,aAAA,GAAmC;AAAA,EAC9C,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,gBAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,gBAAA;AAAA,EACA;AACF;AAMO,SAAS,eAAA,CACd,SAAA,EACA,UAAA,GAAgC,aAAA,EACvB;AACT,EAAA,OAAO,UAAA,CAAW,SAAS,SAAS,CAAA;AACtC;AAMO,SAAS,oBAAoB,IAAA,EAAsC;AACxE,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAA;AACf,IAAA,IAAI,MAAA,EAAQ,IAAA,EAAM,WAAA,EAAa,OAAO,MAAA;AACtC,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;;;AC3BO,SAAS,aAAA,CACd,IAAA,EACA,OAAA,GAAiC,EAAC,EACZ;AAEtB,EAAA,IAAI,CAAC,OAAA,CAAQ,WAAA,IAAe,OAAA,CAAQ,SAAA,EAAW;AAC7C,IAAA,IAAI,CAAC,eAAA,CAAgB,OAAA,CAAQ,SAAA,EAAW,OAAA,CAAQ,UAAU,CAAA,EAAG;AAC3D,MAAA,OAAO;AAAA,QACL,OAAA,EAAS,KAAA;AAAA,QACT,SAAA,EAAW,IAAA;AAAA,QACX,IAAA,EAAM,IAAA;AAAA,QACN,KAAA,EAAO,CAAA,WAAA,EAAc,OAAA,CAAQ,SAAS,CAAA,kCAAA;AAAA,OACxC;AAAA,IACF;AAAA,EACF;AAGA,EAAA,MAAM,OAAA,GAAU,oBAAoB,IAAI,CAAA;AACxC,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,OAAO;AAAA,MACL,OAAA,EAAS,IAAA;AAAA,MACT,SAAA,EAAW,UAAA;AAAA,MACX,IAAA,EAAM;AAAA,KACR;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,KAAA;AAAA,IACT,SAAA,EAAW,IAAA;AAAA,IACX,IAAA,EAAM,IAAA;AAAA,IACN,KAAA,EAAO;AAAA,GACT;AACF;AAKO,SAAS,qBAAqB,OAAA,EAAwC;AAC3E,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,IAAA,EAAM,WAAA,EAAa,gBAAA,EAAkB,IAAA;AAC3D,EAAA,MAAM,OAAO,KAAA,EAAO,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,oBAAoB,CAAA;AAC/D,EAAA,OAAO,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,GAAI,IAAA;AACrC;AAGO,SAAS,cAAc,OAAA,EAAwC;AACpE,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,IAAA,EAAM,WAAA,EAAa,gBAAA,EAAkB,IAAA;AAC3D,EAAA,MAAM,OAAO,KAAA,EAAO,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,QAAQ,CAAA;AACnD,EAAA,OAAO,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,GAAI,IAAA;AACrC;AAGO,SAAS,mBAAmB,OAAA,EAAwC;AACzE,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,IAAA,EAAM,WAAA,EAAa,gBAAA,EAAkB,IAAA;AAC3D,EAAA,MAAM,OAAO,KAAA,EAAO,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,aAAa,CAAA;AACxD,EAAA,OAAO,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,GAAI,IAAA;AACrC;AAGO,SAAS,qBAAqB,OAAA,EAAkC;AACrE,EAAA,OAAO,OAAA,CAAQ,IAAA,EAAM,WAAA,EAAa,UAAA,KAAe,CAAA;AACnD","file":"index.js","sourcesContent":["/**\n * Pesafy error utilities\n */\nexport type ErrorCode =\n | \"INVALID_CREDENTIALS\"\n | \"AUTH_FAILED\"\n | \"VALIDATION_ERROR\"\n | \"ENCRYPTION_FAILED\"\n | \"REQUEST_FAILED\"\n | \"INVALID_PHONE\"\n | \"HTTP_ERROR\"\n | \"API_ERROR\"\n | \"NETWORK_ERROR\"\n | \"TIMEOUT\"\n | \"INVALID_RESPONSE\";\n\nexport interface PesafyErrorOptions {\n code: ErrorCode;\n message: string;\n statusCode?: number;\n response?: unknown;\n cause?: unknown;\n requestId?: string;\n}\n\nexport class PesafyError extends Error {\n readonly code: ErrorCode;\n readonly statusCode: number | undefined;\n readonly response: unknown;\n readonly requestId: string | undefined;\n override readonly cause: unknown;\n\n constructor(options: PesafyErrorOptions) {\n super(options.message);\n Object.defineProperty(this, \"name\", { value: \"PesafyError\" });\n this.code = options.code;\n this.statusCode = options.statusCode;\n this.response = options.response;\n this.requestId = options.requestId;\n this.cause = options.cause;\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, PesafyError);\n }\n }\n\n toJSON() {\n return {\n name: this.name,\n code: this.code,\n message: this.message,\n statusCode: this.statusCode,\n requestId: this.requestId,\n };\n }\n}\n\n/** Convenience factory — identical API to `new PesafyError(...)` */\nexport function createError(options: PesafyErrorOptions): PesafyError {\n return new PesafyError(options);\n}\n","/**\n * Security credential encryption for Daraja APIs that require it:\n * B2C, B2B, Transaction Status Query, Reversals, Tax Remittance.\n *\n * Algorithm (from Safaricom \"Getting Started\" docs):\n * 1. Write the unencrypted initiator password into a byte array.\n * 2. Encrypt using the M-Pesa public key certificate:\n * - RSA algorithm\n * - PKCS #1 v1.5 padding (NOT OAEP)\n * 3. Base64-encode the encrypted byte array.\n *\n * Certificate source:\n * Sandbox: https://developer.safaricom.co.ke (SandboxCertificate.cer)\n * Production: https://developer.safaricom.co.ke (ProductionCertificate.cer)\n *\n * NOTE: Use the correct certificate for each environment or credentials\n * will be rejected.\n */\n\nimport { constants, publicEncrypt } from \"node:crypto\";\nimport { PesafyError } from \"../../utils/errors\";\n\n/**\n * Encrypts `initiatorPassword` with the given PEM certificate and returns\n * the base64-encoded security credential ready to send to Daraja.\n *\n * @param initiatorPassword - Plain-text password set on the M-PESA org portal\n * @param certificatePem - Full PEM string (the .cer file contents)\n */\nexport function encryptSecurityCredential(\n initiatorPassword: string,\n certificatePem: string\n): string {\n try {\n const passwordBuffer = Buffer.from(initiatorPassword, \"utf-8\");\n\n const encrypted = publicEncrypt(\n {\n key: certificatePem,\n // RSA_PKCS1_PADDING = 1 (NOT RSA_PKCS1_OAEP_PADDING = 4)\n padding: constants.RSA_PKCS1_PADDING,\n },\n passwordBuffer\n );\n\n return encrypted.toString(\"base64\");\n } catch (error) {\n throw new PesafyError({\n code: \"ENCRYPTION_FAILED\",\n message:\n \"Failed to encrypt security credential. \" +\n \"Ensure the certificate PEM is valid and matches the environment (sandbox/production).\",\n cause: error,\n });\n }\n}\n","/**\n * Minimal HTTP client for Daraja API calls.\n *\n * Why not use axios/got/ky?\n * - Zero extra dependencies\n * - Works in Node.js, Bun, and edge runtimes unchanged\n * - Daraja only needs POST + GET with JSON bodies\n *\n * Exported: httpRequest (the ONLY export — never export \"httpClient\")\n */\n\n// src/utils/http/index.ts\n\nimport { PesafyError } from \"../errors\";\n\nexport interface HttpRequestOptions {\n method: \"GET\" | \"POST\";\n headers?: Record<string, string>;\n /** Will be JSON-serialised and sent as application/json */\n body?: unknown;\n /**\n * Number of times to retry on transient errors (503, 429, network failures).\n * Default: 4. Set to 0 to disable retries.\n */\n retries?: number;\n /**\n * Base delay in ms before first retry. Doubles each attempt + jitter.\n * Default: 2000 (2 s). Daraja sandbox needs longer gaps than typical APIs.\n */\n retryDelay?: number;\n}\n\nexport interface HttpResponse<T> {\n data: T;\n status: number;\n headers: Record<string, string>;\n}\n\n/** Status codes that are transient and safe to retry */\nconst RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);\n\n/** Sleep for `ms` milliseconds */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Returns a delay with ±25 % jitter to avoid retry storms.\n * Daraja sandbox 503s are caused by sandbox overload — spreading\n * retries prevents making it worse.\n */\nfunction jitter(baseMs: number): number {\n const spread = baseMs * 0.25;\n return baseMs + (Math.random() * spread * 2 - spread);\n}\n\n/**\n * Sends an HTTP request and returns parsed JSON.\n *\n * Automatically retries on transient errors (503, 429, network failures)\n * with exponential backoff + jitter. Never retries on 4xx client errors.\n *\n * NOTE: `httpClient` is NOT exported — the only export is `httpRequest`.\n */\nexport async function httpRequest<T = unknown>(\n url: string,\n options: HttpRequestOptions\n): Promise<HttpResponse<T>> {\n const maxRetries = options.retries ?? 4;\n const baseDelay = options.retryDelay ?? 2000;\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n ...options.headers,\n };\n\n const init: RequestInit = {\n method: options.method,\n headers,\n ...(options.body !== undefined\n ? { body: JSON.stringify(options.body) }\n : {}),\n };\n\n let lastError: PesafyError | null = null;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n // Wait before retry (not before the first attempt)\n if (attempt > 0) {\n const delay = jitter(baseDelay * Math.pow(2, attempt - 1));\n await sleep(delay);\n }\n\n let response: Response;\n\n try {\n response = await fetch(url, init);\n } catch (err) {\n // Network-level failure (DNS, connection refused, etc.)\n lastError = new PesafyError({\n code: \"NETWORK_ERROR\",\n message: `Network error calling ${url}: ${String(err)}`,\n cause: err,\n });\n\n if (attempt < maxRetries) continue; // retry\n throw lastError;\n }\n\n // Parse body regardless of status so we can include it in errors\n let data: unknown;\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n try {\n data = contentType.includes(\"application/json\")\n ? await response.json()\n : await response.text();\n } catch {\n data = null;\n }\n\n // Collect response headers\n const responseHeaders: Record<string, string> = {};\n response.headers.forEach((value, key) => {\n responseHeaders[key] = value;\n });\n\n if (response.ok) {\n return {\n data: data as T,\n status: response.status,\n headers: responseHeaders,\n };\n }\n\n // ── Error response ────────────────────────────────────────────────────────\n const isTransient = RETRYABLE_STATUSES.has(response.status);\n\n // Daraja error shape: { requestId, errorCode, errorMessage }\n const daraja = (data ?? {}) as Record<string, unknown>;\n const message =\n (daraja.errorMessage as string) ??\n (daraja.ResponseDescription as string) ??\n `HTTP ${response.status}`;\n\n lastError = new PesafyError({\n code: isTransient ? \"REQUEST_FAILED\" : \"HTTP_ERROR\",\n message,\n statusCode: response.status,\n response: data,\n requestId: daraja.requestId as string | undefined,\n });\n\n // Only retry transient errors — never retry 4xx client errors\n if (isTransient && attempt < maxRetries) {\n continue;\n }\n\n throw lastError;\n }\n\n // Should never reach here, but TypeScript requires it\n throw lastError!;\n}\n","/**\n * OAuth token manager for Daraja API.\n *\n * Daraja Authorization endpoint (GET, Basic Auth):\n * https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials\n * https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials\n *\n * Token validity: 3600 seconds (1 hour).\n * We refresh 60 s early to avoid edge-case expiry mid-request.\n *\n * Ref: Authorization By Safaricom docs\n */\n\nimport { PesafyError } from \"../../utils/errors\";\nimport { httpRequest } from \"../../utils/http\";\nimport type { TokenResponse } from \"./types\";\n\n/** Refresh the token this many seconds before it actually expires */\nconst TOKEN_BUFFER_SECONDS = 60;\n\nexport class TokenManager {\n private readonly consumerKey: string;\n private readonly consumerSecret: string;\n private readonly baseUrl: string;\n\n private cachedToken: string | null = null;\n private tokenExpiresAt = 0; // Unix seconds\n\n constructor(consumerKey: string, consumerSecret: string, baseUrl: string) {\n this.consumerKey = consumerKey;\n this.consumerSecret = consumerSecret;\n this.baseUrl = baseUrl;\n }\n\n private getBasicAuthHeader(): string {\n // Daraja spec: Base64(consumerKey:consumerSecret)\n const credentials = `${this.consumerKey}:${this.consumerSecret}`;\n const encoded = Buffer.from(credentials, \"utf-8\").toString(\"base64\");\n return `Basic ${encoded}`;\n }\n\n /**\n * Returns a valid access token, fetching a new one when the cached token\n * is absent or within TOKEN_BUFFER_SECONDS of expiry.\n */\n async getAccessToken(): Promise<string> {\n const now = Date.now() / 1000;\n\n if (this.cachedToken && this.tokenExpiresAt > now + TOKEN_BUFFER_SECONDS) {\n return this.cachedToken;\n }\n\n // Daraja Authorization API: GET with Basic Auth + grant_type query param\n const url = `${this.baseUrl}/oauth/v1/generate?grant_type=client_credentials`;\n\n const response = await httpRequest<TokenResponse>(url, {\n method: \"GET\",\n headers: {\n Authorization: this.getBasicAuthHeader(),\n },\n });\n\n const { access_token, expires_in } = response.data;\n\n if (!access_token) {\n throw new PesafyError({\n code: \"AUTH_FAILED\",\n message:\n \"Daraja did not return an access token. Check your consumer key and secret.\",\n response: response.data,\n });\n }\n\n this.cachedToken = access_token;\n // expires_in is 3599 per Daraja docs; default to 3600 if missing\n this.tokenExpiresAt = now + (expires_in ?? 3600);\n\n return this.cachedToken;\n }\n\n /** Force token refresh on the next call (e.g. after a 401 response) */\n clearCache(): void {\n this.cachedToken = null;\n this.tokenExpiresAt = 0;\n }\n}\n","/**\n * Phone number utilities for Daraja API.\n *\n * Daraja spec: PartyA and PhoneNumber must be in the format 2547XXXXXXXX\n * (12-digit, starts with 254, no +, no spaces, no dashes).\n *\n * Accepted input formats:\n * 0712345678 → 254712345678\n * +254712345678 → 254712345678\n * 254712345678 → 254712345678 (already correct)\n * 712345678 → 254712345678\n */\n\nimport { PesafyError } from \"../errors\";\n\n/** Normalises any common Kenyan phone format to 254XXXXXXXXX (12 digits) */\nexport function formatSafaricomPhone(phone: string): string {\n const digits = phone.replace(/\\D/g, \"\");\n\n let normalised: string;\n\n if (digits.startsWith(\"254\") && digits.length === 12) {\n normalised = digits;\n } else if (digits.startsWith(\"0\") && digits.length === 10) {\n normalised = `254${digits.slice(1)}`;\n } else if (digits.length === 9) {\n // e.g. 712345678 → 254712345678\n normalised = `254${digits}`;\n } else if (digits.startsWith(\"254\") && digits.length !== 12) {\n throw new PesafyError({\n code: \"INVALID_PHONE\",\n message: `Invalid phone number \"${phone}\". Expected 254XXXXXXXXX (12 digits).`,\n });\n } else {\n throw new PesafyError({\n code: \"INVALID_PHONE\",\n message: `Cannot parse phone number \"${phone}\". Use 07XXXXXXXX, 2547XXXXXXXX, or +2547XXXXXXXX.`,\n });\n }\n\n // Final sanity check: must be exactly 12 digits\n if (normalised.length !== 12) {\n throw new PesafyError({\n code: \"INVALID_PHONE\",\n message: `Phone number \"${phone}\" normalised to \"${normalised}\" which is not 12 digits.`,\n });\n }\n\n return normalised;\n}\n","/**\n * STK Push utility functions\n *\n * Password spec (from Daraja docs):\n * Password = Base64( BusinessShortCode + Passkey + Timestamp )\n * Timestamp = YYYYMMDDHHmmss\n *\n * IMPORTANT: Generate the timestamp ONCE per request and pass the same\n * value to BOTH getStkPushPassword() and the request body's Timestamp field.\n * Safaricom validates that Base64(Shortcode+Passkey+Timestamp) matches the\n * Timestamp sent in the body — two separate calls to getTimestamp() will\n * produce different values and cause auth failures.\n */\n\nexport { formatSafaricomPhone as formatPhoneNumber } from \"../../utils/phone\";\n\n/**\n * Generates the STK Push password.\n * Formula: Base64( Shortcode + Passkey + Timestamp )\n *\n * Uses btoa() — works in Node.js ≥18, Bun, browsers, and edge runtimes.\n */\nexport function getStkPushPassword(\n shortCode: string,\n passKey: string,\n timestamp: string\n): string {\n return btoa(`${shortCode}${passKey}${timestamp}`);\n}\n\n/**\n * Returns a Daraja-compatible timestamp: YYYYMMDDHHmmss\n *\n * Call this ONCE per request and reuse the result.\n */\nexport function getTimestamp(): string {\n const now = new Date();\n const pad = (n: number): string => n.toString().padStart(2, \"0\");\n return [\n now.getFullYear(),\n pad(now.getMonth() + 1),\n pad(now.getDate()),\n pad(now.getHours()),\n pad(now.getMinutes()),\n pad(now.getSeconds()),\n ].join(\"\");\n}\n","// src/mpesa/stk-push/stk-push.ts\n\n/**\n * M-Pesa Express (STK Push) — initiates a payment prompt on the customer's phone.\n *\n * API: POST /mpesa/stkpush/v1/processrequest\n *\n * Daraja request body (from docs):\n * {\n * \"BusinessShortCode\": 174379,\n * \"Password\": \"base64(Shortcode+Passkey+Timestamp)\",\n * \"Timestamp\": \"20210628092408\",\n * \"TransactionType\": \"CustomerPayBillOnline\",\n * \"Amount\": \"1\",\n * \"PartyA\": \"254722000000\",\n * \"PartyB\": \"174379\",\n * \"PhoneNumber\": \"254722111111\",\n * \"CallBackURL\": \"https://mydomain.com/path\",\n * \"AccountReference\": \"accountref\", ← max 12 chars\n * \"TransactionDesc\": \"txndesc\" ← max 13 chars\n * }\n *\n * Notes from docs:\n * - All fields except TransactionDesc are mandatory.\n * - Amount must be a whole number ≥ 1 (KES).\n * - PartyA/PhoneNumber must be 254XXXXXXXXX format.\n * - AccountReference max 12 chars.\n * - TransactionDesc max 13 chars.\n */\n\nimport { PesafyError } from \"../../utils/errors\";\nimport { httpRequest } from \"../../utils/http\";\nimport type { StkPushRequest, StkPushResponse } from \"./types\";\nimport { formatPhoneNumber, getStkPushPassword, getTimestamp } from \"./utils\";\n\nexport async function processStkPush(\n baseUrl: string,\n accessToken: string,\n request: StkPushRequest\n): Promise<StkPushResponse> {\n // ── Amount validation ───────────────────────────────────────────────────────\n const amount = Math.round(request.amount);\n if (amount < 1) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message: `Amount must be at least KES 1 (got ${request.amount} which rounds to ${amount}).`,\n });\n }\n\n // ── Generate timestamp ONCE ─────────────────────────────────────────────────\n // Must be identical in Password (encoded) and Timestamp (body) fields.\n const timestamp = getTimestamp();\n\n // ── PartyB logic ────────────────────────────────────────────────────────────\n // Paybill → PartyB = shortCode\n // Buy Goods (Till) → PartyB = till number (passed as request.partyB)\n const partyB = request.partyB ?? request.shortCode;\n\n const body = {\n BusinessShortCode: request.shortCode,\n Password: getStkPushPassword(request.shortCode, request.passKey, timestamp),\n Timestamp: timestamp,\n TransactionType: request.transactionType ?? \"CustomerPayBillOnline\",\n Amount: amount,\n PartyA: formatPhoneNumber(request.phoneNumber),\n PartyB: partyB,\n PhoneNumber: formatPhoneNumber(request.phoneNumber),\n CallBackURL: request.callbackUrl,\n // Daraja docs: AccountReference max 12 chars, TransactionDesc max 13 chars\n AccountReference: request.accountReference.slice(0, 12),\n TransactionDesc: request.transactionDesc.slice(0, 13),\n };\n\n // httpRequest already retries 503/429/5xx with exponential backoff + jitter.\n // If all retries are exhausted it throws PesafyError with code \"REQUEST_FAILED\"\n // and statusCode 503 — callers should treat this as TRANSIENT, not a final\n // failure. Never mark a transaction \"failed\" on a 503.\n const { data } = await httpRequest<StkPushResponse>(\n `${baseUrl}/mpesa/stkpush/v1/processrequest`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${accessToken}` },\n body,\n // Daraja sandbox needs more retries and longer gaps due to instability\n retries: 5,\n retryDelay: 3000,\n }\n );\n\n return data;\n}\n","/**\n * STK Push Query — checks the status of a Lipa Na M-Pesa Online Payment.\n *\n * API: POST /mpesa/stkpushquery/v1/query\n *\n * Daraja request body (from Discover APIs M-Pesa Express Query docs):\n * {\n * \"BusinessShortCode\": \"174379\",\n * \"Password\": \"base64(Shortcode+Passkey+Timestamp)\",\n * \"Timestamp\": \"20160216165627\",\n * \"CheckoutRequestID\": \"ws_CO_260520211133524545\"\n * }\n *\n * Response ResultCode values (from docs):\n * 0 = The service request is processed successfully.\n * 1032 = Request cancelled by user\n * 1037 = DS timeout user cannot be reached\n * 2001 = Wrong PIN\n * (and more — see STK Push docs result code table)\n */\n\nimport { httpRequest } from \"../../utils/http\";\nimport type { StkQueryRequest, StkQueryResponse } from \"./types\";\nimport { getStkPushPassword, getTimestamp } from \"./utils\";\n\nexport async function queryStkPush(\n baseUrl: string,\n accessToken: string,\n request: StkQueryRequest\n): Promise<StkQueryResponse> {\n // Generate timestamp ONCE — Password and Timestamp field MUST match.\n const timestamp = getTimestamp();\n\n const body = {\n BusinessShortCode: request.shortCode,\n Password: getStkPushPassword(request.shortCode, request.passKey, timestamp),\n Timestamp: timestamp,\n CheckoutRequestID: request.checkoutRequestId,\n };\n\n const { data } = await httpRequest<StkQueryResponse>(\n `${baseUrl}/mpesa/stkpushquery/v1/query`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${accessToken}` },\n body,\n }\n );\n\n return data;\n}\n","/**\n * STK Push (M-Pesa Express) types\n *\n * API: POST /mpesa/stkpush/v1/processrequest\n * Query: POST /mpesa/stkpushquery/v1/query\n *\n * Ref: M-Pesa Express Simulate docs + Discover APIs M-Pesa Express Query docs\n */\n\n// ── Transaction type ─────────────────────────────────────────────────────────\n\n/**\n * CustomerPayBillOnline → Paybill numbers (PartyB = shortcode)\n * CustomerBuyGoodsOnline → Till numbers (PartyB = till number)\n */\nexport type TransactionType =\n | \"CustomerPayBillOnline\"\n | \"CustomerBuyGoodsOnline\";\n\n// ── STK Push request ─────────────────────────────────────────────────────────\n\nexport interface StkPushRequest {\n /** Transaction amount (minimum KES 1, must round to a whole number ≥ 1) */\n amount: number;\n\n /**\n * Phone number sending the money. Format: 2547XXXXXXXX.\n * Must be a valid Safaricom M-PESA number.\n * Daraja docs field name: PartyA / PhoneNumber\n */\n phoneNumber: string;\n\n /**\n * URL where Safaricom will POST the callback result.\n * Must be publicly accessible (use ngrok/localtunnel for local dev).\n * Daraja docs field name: CallBackURL\n */\n callbackUrl: string;\n\n /**\n * Alpha-numeric reference shown to customer in the USSD prompt.\n * Max 12 characters.\n * Daraja docs field name: AccountReference\n */\n accountReference: string;\n\n /**\n * Additional description for the transaction.\n * Max 13 characters.\n * Daraja docs field name: TransactionDesc\n */\n transactionDesc: string;\n\n /**\n * Business shortcode — Paybill number or HO/Store number for Till.\n * Daraja docs field name: BusinessShortCode\n */\n shortCode: string;\n\n /**\n * Passkey used to generate the Password.\n * Sandbox value: from Daraja simulator test data.\n * Production value: emailed after Go Live.\n */\n passKey: string;\n\n /**\n * \"CustomerPayBillOnline\" (default) for Paybill.\n * \"CustomerBuyGoodsOnline\" for Till Numbers.\n */\n transactionType?: TransactionType;\n\n /**\n * Credit party receiving funds.\n * - CustomerPayBillOnline: defaults to shortCode\n * - CustomerBuyGoodsOnline: set to the Till Number\n * Daraja docs field name: PartyB\n */\n partyB?: string;\n}\n\n// ── STK Push response ────────────────────────────────────────────────────────\n\nexport interface StkPushResponse {\n /** Global unique identifier for the submitted payment request */\n MerchantRequestID: string;\n /** Global unique identifier for the checkout transaction */\n CheckoutRequestID: string;\n /** \"0\" = successful submission */\n ResponseCode: string;\n ResponseDescription: string;\n CustomerMessage: string;\n}\n\n// ── STK Query request/response ───────────────────────────────────────────────\n\nexport interface StkQueryRequest {\n /** CheckoutRequestID from the STK Push response */\n checkoutRequestId: string;\n shortCode: string;\n passKey: string;\n}\n\nexport interface StkQueryResponse {\n ResponseCode: string;\n ResponseDescription: string;\n MerchantRequestID: string;\n CheckoutRequestID: string;\n /**\n * Daraja returns ResultCode as a NUMBER.\n * 0 = success\n * 1 = insufficient balance\n * 1032 = cancelled by user\n * 1037 = timeout / unreachable\n * 2001 = wrong PIN\n */\n ResultCode: number;\n ResultDesc: string;\n}\n\n// ── Callback payload types ────────────────────────────────────────────────────\n// Safaricom POSTs these to your CallBackURL after the customer responds.\n\n/** Single metadata item in a successful STK callback */\nexport interface StkCallbackMetadataItem {\n Name:\n | \"Amount\"\n | \"MpesaReceiptNumber\"\n | \"TransactionDate\"\n | \"PhoneNumber\"\n | \"Balance\";\n /** Present on successful transactions; absent on failure */\n Value?: number | string;\n}\n\n/** Inner callback for a SUCCESSFUL STK Push (ResultCode === 0) */\nexport interface StkCallbackSuccess {\n MerchantRequestID: string;\n CheckoutRequestID: string;\n ResultCode: 0;\n ResultDesc: string;\n CallbackMetadata: {\n Item: StkCallbackMetadataItem[];\n };\n}\n\n/** Inner callback for a FAILED / CANCELLED STK Push (ResultCode !== 0) */\nexport interface StkCallbackFailure {\n MerchantRequestID: string;\n CheckoutRequestID: string;\n /** e.g. 1032 = cancelled by user, 1037 = timeout */\n ResultCode: number;\n ResultDesc: string;\n CallbackMetadata?: never;\n}\n\nexport type StkCallbackInner = StkCallbackSuccess | StkCallbackFailure;\n\n/** Full wrapper Safaricom POSTs to your CallBackURL */\nexport interface StkPushCallback {\n Body: {\n stkCallback: StkCallbackInner;\n };\n}\n\n// ── Type guards & helpers ─────────────────────────────────────────────────────\n\n/**\n * Narrows StkCallbackInner to the success shape.\n *\n * @example\n * if (isStkCallbackSuccess(callback.Body.stkCallback)) {\n * const receipt = getCallbackValue(callback, \"MpesaReceiptNumber\");\n * }\n */\nexport function isStkCallbackSuccess(\n cb: StkCallbackInner\n): cb is StkCallbackSuccess {\n return cb.ResultCode === 0;\n}\n\n/**\n * Extracts a named value from a successful callback's metadata.\n * Returns undefined if the key is absent or the transaction failed.\n */\nexport function getCallbackValue(\n callback: StkPushCallback,\n name: StkCallbackMetadataItem[\"Name\"]\n): string | number | undefined {\n const inner = callback.Body.stkCallback;\n if (!isStkCallbackSuccess(inner)) return undefined;\n return inner.CallbackMetadata.Item.find((i) => i.Name === name)?.Value;\n}\n","/**\n * Transaction Status Query implementation\n *\n * API: POST /mpesa/transactionstatus/v1/query\n *\n * This is ASYNCHRONOUS. The synchronous response only acknowledges receipt.\n * Final results arrive via POST to your ResultURL.\n *\n * Required M-PESA org portal role: \"Transaction Status query ORG API\"\n */\n\nimport { createError } from \"../../utils/errors\";\nimport { httpRequest } from \"../../utils/http\"; // ← httpRequest, NOT httpClient\nimport type {\n TransactionStatusRequest,\n TransactionStatusResponse,\n} from \"./types\";\n\nexport async function queryTransactionStatus(\n baseUrl: string,\n token: string,\n securityCredential: string,\n initiator: string,\n request: TransactionStatusRequest\n): Promise<TransactionStatusResponse> {\n // ── Validation ──────────────────────────────────────────────────────────────\n\n if (!request.transactionId) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message: \"transactionId is required\",\n });\n }\n\n if (!request.partyA) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message:\n \"partyA is required (your business shortcode, till number, or MSISDN)\",\n });\n }\n\n if (!request.identifierType) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message:\n 'identifierType is required: \"1\" (MSISDN) | \"2\" (Till) | \"4\" (ShortCode)',\n });\n }\n\n if (!request.resultUrl) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message:\n \"resultUrl is required — Safaricom POSTs the transaction result here\",\n });\n }\n\n if (!request.queueTimeOutUrl) {\n throw createError({\n code: \"VALIDATION_ERROR\",\n message: \"queueTimeOutUrl is required — Safaricom calls this on timeout\",\n });\n }\n\n // ── Build payload matching Daraja spec exactly ──────────────────────────────\n\n const payload = {\n Initiator: initiator,\n SecurityCredential: securityCredential,\n CommandID: request.commandId ?? \"TransactionStatusQuery\",\n TransactionID: request.transactionId,\n PartyA: request.partyA,\n IdentifierType: request.identifierType,\n ResultURL: request.resultUrl,\n QueueTimeOutURL: request.queueTimeOutUrl,\n Remarks: request.remarks ?? \"Transaction Status Query\",\n Occasion: request.occasion ?? \"\",\n };\n\n const { data } = await httpRequest<TransactionStatusResponse>(\n `${baseUrl}/mpesa/transactionstatus/v1/query`,\n {\n method: \"POST\",\n headers: { Authorization: `Bearer ${token}` },\n body: payload,\n }\n );\n\n return data;\n}\n","/**\n * Core M-Pesa / Daraja API types\n */\n\nexport type Environment = \"sandbox\" | \"production\";\n\n/** Base URLs per Daraja environment */\nexport const DARAJA_BASE_URLS: Record<Environment, string> = {\n sandbox: \"https://sandbox.safaricom.co.ke\",\n production: \"https://api.safaricom.co.ke\",\n} as const;\n\nexport interface MpesaConfig {\n // ── Required for all APIs ─────────────────────────────────────────────────\n consumerKey: string;\n consumerSecret: string;\n environment: Environment;\n\n // ── Required for STK Push (M-Pesa Express) ────────────────────────────────\n /** Paybill / HO shortcode (5–7 digits). Required for STK Push & STK Query. */\n lipaNaMpesaShortCode?: string;\n /**\n * Passkey from Daraja portal.\n * Sandbox: visible in the simulator test data section.\n * Production: emailed after Go Live.\n */\n lipaNaMpesaPassKey?: string;\n\n // ── Required for Transaction Status / B2C / Reversals ────────────────────\n /** M-PESA org portal API operator username */\n initiatorName?: string;\n /** Plain-text password for the API operator (will be RSA-encrypted) */\n initiatorPassword?: string;\n\n // ── Certificate options (choose one) ─────────────────────────────────────\n /**\n * Path to the .cer file on disk.\n * Bun: read via `Bun.file(path).text()`\n * Node: read via `fs.promises.readFile(path, \"utf-8\")`\n */\n certificatePath?: string;\n /** PEM string contents of the certificate (alternative to certificatePath) */\n certificatePem?: string;\n /**\n * Pre-computed base64 security credential.\n * Use this if you encrypt outside the library (e.g. at startup).\n * Skips the RSA encryption step entirely.\n */\n securityCredential?: string;\n}\n","/**\n * M-Pesa Daraja API client\n *\n * Supports:\n * - STK Push (M-Pesa Express) — stkPush()\n * - STK Query — stkQuery()\n * - Transaction Status Query — transactionStatus()\n *\n * @example\n * const mpesa = new Mpesa({\n * consumerKey: process.env.MPESA_CONSUMER_KEY!,\n * consumerSecret: process.env.MPESA_CONSUMER_SECRET!,\n * environment: \"sandbox\",\n * lipaNaMpesaShortCode: \"174379\",\n * lipaNaMpesaPassKey: \"bfb279...\",\n * initiatorName: \"testapi\",\n * initiatorPassword: \"Safaricom123!\",\n * certificatePath: \"./SandboxCertificate.cer\",\n * });\n */\n\nimport { TokenManager } from \"../core/auth\";\nimport { encryptSecurityCredential } from \"../core/encryption\";\nimport { PesafyError } from \"../utils/errors\";\nimport {\n processStkPush,\n queryStkPush,\n type StkPushRequest,\n type StkQueryRequest,\n} from \"./stk-push\";\nimport {\n queryTransactionStatus,\n type TransactionStatusRequest,\n} from \"./transaction-status\";\nimport { DARAJA_BASE_URLS, type MpesaConfig } from \"./types\";\n\nexport class Mpesa {\n private readonly config: MpesaConfig;\n private readonly tokenManager: TokenManager;\n private readonly baseUrl: string;\n\n constructor(config: MpesaConfig) {\n if (!config.consumerKey || !config.consumerSecret) {\n throw new PesafyError({\n code: \"INVALID_CREDENTIALS\",\n message: \"consumerKey and consumerSecret are required\",\n });\n }\n\n this.config = config;\n this.baseUrl = DARAJA_BASE_URLS[config.environment];\n this.tokenManager = new TokenManager(\n config.consumerKey,\n config.consumerSecret,\n this.baseUrl\n );\n }\n\n // ── Internal helpers ────────────────────────────────────────────────────────\n\n private getToken(): Promise<string> {\n return this.tokenManager.getAccessToken();\n }\n\n private async buildSecurityCredential(): Promise<string> {\n // Option 1: caller pre-computed it\n if (this.config.securityCredential) return this.config.securityCredential;\n\n // Option 2: we encrypt it\n if (!this.config.initiatorPassword) {\n throw new PesafyError({\n code: \"INVALID_CREDENTIALS\",\n message:\n \"Provide securityCredential (pre-encrypted) \" +\n \"OR (initiatorPassword + certificatePath/certificatePem)\",\n });\n }\n\n let cert: string;\n if (this.config.certificatePem) {\n cert = this.config.certificatePem;\n } else if (this.config.certificatePath) {\n // Bun runtime\n if (typeof Bun !== \"undefined\") {\n cert = await Bun.file(this.config.certificatePath).text();\n } else {\n // Node.js fallback\n const { readFile } = await import(\"node:fs/promises\");\n cert = await readFile(this.config.certificatePath, \"utf-8\");\n }\n } else {\n throw new PesafyError({\n code: \"INVALID_CREDENTIALS\",\n message:\n \"certificatePath or certificatePem required to encrypt the initiator password\",\n });\n }\n\n return encryptSecurityCredential(this.config.initiatorPassword, cert);\n }\n\n // ── STK Push ──────────────────────────────────────────────────────────────\n\n /**\n * M-Pesa Express — sends a payment prompt to the customer's phone.\n *\n * Requires: lipaNaMpesaShortCode + lipaNaMpesaPassKey in config.\n *\n * @example\n * const res = await mpesa.stkPush({\n * amount: 100,\n * phoneNumber: \"0712345678\",\n * callbackUrl: \"https://yourdomain.com/mpesa/callback\",\n * accountReference: \"INV-001\",\n * transactionDesc: \"Payment\",\n * });\n * console.log(res.CheckoutRequestID); // use to poll status\n */\n async stkPush(request: Omit<StkPushRequest, \"shortCode\" | \"passKey\">) {\n const shortCode = this.config.lipaNaMpesaShortCode ?? \"\";\n const passKey = this.config.lipaNaMpesaPassKey ?? \"\";\n\n if (!shortCode || !passKey) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message:\n \"lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Push\",\n });\n }\n\n const token = await this.getToken();\n return processStkPush(this.baseUrl, token, {\n ...request,\n shortCode,\n passKey,\n });\n }\n\n /**\n * STK Query — checks the status of a previous STK Push.\n *\n * @example\n * const status = await mpesa.stkQuery({\n * checkoutRequestId: \"ws_CO_1007202409152617172396192\",\n * });\n * if (status.ResultCode === 0) // payment confirmed\n */\n async stkQuery(request: Omit<StkQueryRequest, \"shortCode\" | \"passKey\">) {\n const shortCode = this.config.lipaNaMpesaShortCode ?? \"\";\n const passKey = this.config.lipaNaMpesaPassKey ?? \"\";\n\n if (!shortCode || !passKey) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message:\n \"lipaNaMpesaShortCode and lipaNaMpesaPassKey are required for STK Query\",\n });\n }\n\n const token = await this.getToken();\n return queryStkPush(this.baseUrl, token, {\n ...request,\n shortCode,\n passKey,\n });\n }\n\n /**\n * Transaction Status — queries the result of a completed M-Pesa transaction.\n *\n * Requires: initiatorName + (initiatorPassword + certificate) OR securityCredential.\n *\n * This is ASYNCHRONOUS. The synchronous response only confirms receipt.\n * Final details are POSTed to your resultUrl.\n *\n * @example\n * await mpesa.transactionStatus({\n * transactionId: \"OEI2AK4XXXX\",\n * partyA: \"174379\",\n * identifierType: \"4\",\n * resultUrl: \"https://yourdomain.com/mpesa/result\",\n * queueTimeOutUrl: \"https://yourdomain.com/mpesa/timeout\",\n * remarks: \"Check payment status\",\n * });\n */\n async transactionStatus(request: TransactionStatusRequest) {\n const initiator = this.config.initiatorName ?? \"\";\n if (!initiator) {\n throw new PesafyError({\n code: \"VALIDATION_ERROR\",\n message: \"initiatorName is required for Transaction Status\",\n });\n }\n\n // Fetch token and encrypt credential concurrently\n const [token, securityCred] = await Promise.all([\n this.getToken(),\n this.buildSecurityCredential(),\n ]);\n\n return queryTransactionStatus(\n this.baseUrl,\n token,\n securityCred,\n initiator,\n request\n );\n }\n\n /** Force the cached OAuth token to be refreshed on the next API call */\n clearTokenCache(): void {\n this.tokenManager.clearCache();\n }\n}\n","/**\n * Exponential backoff retry for webhook at-least-once delivery.\n *\n * Daraja is asynchronous — if your callback endpoint is down, the API\n * Gateway logs a 503 and discards the result. Use this utility to\n * retry your own internal processing after receiving a webhook.\n */\n\nexport interface RetryOptions {\n /** Maximum number of attempts (default: Infinity) */\n maxRetries?: number;\n /** Initial delay in ms (default: 1000 = 1 second) */\n initialDelay?: number;\n /** Maximum delay cap in ms (default: 3_600_000 = 1 hour) */\n maxDelay?: number;\n /** Multiplier per retry (default: 2 — doubles each time) */\n backoffMultiplier?: number;\n /** Maximum total duration in ms (default: 30 days) */\n maxRetryDuration?: number;\n}\n\nconst DEFAULT_OPTIONS: Required<RetryOptions> = {\n maxRetries: Infinity,\n initialDelay: 1_000,\n maxDelay: 3_600_000,\n backoffMultiplier: 2,\n maxRetryDuration: 30 * 24 * 60 * 60 * 1_000, // 30 days\n};\n\nexport interface RetryResult<T> {\n success: boolean;\n data?: T;\n attempts: number;\n error?: Error;\n}\n\n/**\n * Retries `fn` with exponential backoff until it resolves, or limits are hit.\n *\n * @example\n * const result = await retryWithBackoff(\n * () => sendToDatabase(webhookData),\n * { maxRetries: 5, initialDelay: 500 }\n * );\n */\nexport async function retryWithBackoff<T>(\n fn: () => Promise<T>,\n options: RetryOptions = {}\n): Promise<RetryResult<T>> {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n let delay = opts.initialDelay;\n let attempts = 0;\n const startTime = Date.now();\n\n while (attempts < opts.maxRetries) {\n attempts++;\n\n if (Date.now() - startTime > opts.maxRetryDuration) {\n return {\n success: false,\n attempts,\n error: new Error(\"Max retry duration exceeded\"),\n };\n }\n\n try {\n const data = await fn();\n return { success: true, data, attempts };\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n\n // Don't retry client errors (4xx) — they won't self-heal\n if (err.message.includes(\"4\")) {\n return { success: false, attempts, error: err };\n }\n\n if (attempts < opts.maxRetries) {\n await new Promise((resolve) => setTimeout(resolve, delay));\n delay = Math.min(delay * opts.backoffMultiplier, opts.maxDelay);\n }\n }\n }\n\n return {\n success: false,\n attempts,\n error: new Error(\"Max retries exceeded\"),\n };\n}\n","/**\n * Webhook verification utilities\n *\n * Daraja does NOT use HMAC webhook signatures like Stripe.\n * Instead, verify that callbacks come from whitelisted Safaricom IPs.\n *\n * Official Safaricom IP whitelist (from Getting Started docs):\n * 196.201.214.200\n * 196.201.214.206\n * 196.201.213.114\n * 196.201.214.207\n * 196.201.214.208\n * 196.201.213.44\n * 196.201.212.127\n * 196.201.212.138\n * 196.201.212.129\n * 196.201.212.136\n * 196.201.212.74\n * 196.201.212.69\n */\n\nimport type { StkPushWebhook } from \"./types\";\n\n/** Official Safaricom API Gateway IP addresses */\nexport const SAFARICOM_IPS: readonly string[] = [\n \"196.201.214.200\",\n \"196.201.214.206\",\n \"196.201.213.114\",\n \"196.201.214.207\",\n \"196.201.214.208\",\n \"196.201.213.44\",\n \"196.201.212.127\",\n \"196.201.212.138\",\n \"196.201.212.129\",\n \"196.201.212.136\",\n \"196.201.212.74\",\n \"196.201.212.69\",\n] as const;\n\n/**\n * Returns true if requestIP is in the allowed list.\n * Defaults to the official Safaricom IP whitelist.\n */\nexport function verifyWebhookIP(\n requestIP: string,\n allowedIPs: readonly string[] = SAFARICOM_IPS\n): boolean {\n return allowedIPs.includes(requestIP);\n}\n\n/**\n * Parses and validates an STK Push webhook body.\n * Returns the typed payload or null if it doesn't match the expected shape.\n */\nexport function parseStkPushWebhook(body: unknown): StkPushWebhook | null {\n try {\n const parsed = body as StkPushWebhook;\n if (parsed?.Body?.stkCallback) return parsed;\n return null;\n } catch {\n return null;\n }\n}\n","/**\n * High-level webhook event handler\n */\n\nimport { parseStkPushWebhook, verifyWebhookIP } from \"./signature-verifier\";\nimport type { StkPushWebhook, WebhookEventType } from \"./types\";\n\nexport interface WebhookHandlerOptions {\n /** IP address of the incoming request (from req.ip or x-forwarded-for) */\n requestIP?: string;\n /** Override the default Safaricom IP whitelist */\n allowedIPs?: string[];\n /** Skip IP verification — ONLY for local development/testing */\n skipIPCheck?: boolean;\n}\n\nexport interface WebhookHandlerResult<T = unknown> {\n success: boolean;\n eventType: WebhookEventType | null;\n data: T | null;\n error?: string;\n}\n\n/**\n * Parses and validates an inbound Daraja webhook payload.\n *\n * @example\n * // Express route\n * app.post(\"/mpesa/callback\", (req, res) => {\n * const result = handleWebhook(req.body, { requestIP: req.ip });\n * if (!result.success) return res.status(400).json({ error: result.error });\n * // process result.data (StkPushWebhook)\n * res.json({ ResultCode: 0, ResultDesc: \"Accepted\" });\n * });\n */\nexport function handleWebhook(\n body: unknown,\n options: WebhookHandlerOptions = {}\n): WebhookHandlerResult {\n // ── IP verification ─────────────────────────────────────────────────────────\n if (!options.skipIPCheck && options.requestIP) {\n if (!verifyWebhookIP(options.requestIP, options.allowedIPs)) {\n return {\n success: false,\n eventType: null,\n data: null,\n error: `IP address ${options.requestIP} is not in the Safaricom whitelist`,\n };\n }\n }\n\n // ── Parse STK Push callback ─────────────────────────────────────────────────\n const stkPush = parseStkPushWebhook(body);\n if (stkPush) {\n return {\n success: true,\n eventType: \"stk_push\",\n data: stkPush,\n };\n }\n\n return {\n success: false,\n eventType: null,\n data: null,\n error: \"Unknown or malformed webhook payload\",\n };\n}\n\n// ── Convenience extractors ────────────────────────────────────────────────────\n\n/** Extracts the M-Pesa receipt number from a successful STK Push callback */\nexport function extractTransactionId(webhook: StkPushWebhook): string | null {\n const items = webhook.Body?.stkCallback?.CallbackMetadata?.Item;\n const item = items?.find((i) => i.Name === \"MpesaReceiptNumber\");\n return item ? String(item.Value) : null;\n}\n\n/** Extracts the transaction amount from a successful STK Push callback */\nexport function extractAmount(webhook: StkPushWebhook): number | null {\n const items = webhook.Body?.stkCallback?.CallbackMetadata?.Item;\n const item = items?.find((i) => i.Name === \"Amount\");\n return item ? Number(item.Value) : null;\n}\n\n/** Extracts the phone number from a successful STK Push callback */\nexport function extractPhoneNumber(webhook: StkPushWebhook): string | null {\n const items = webhook.Body?.stkCallback?.CallbackMetadata?.Item;\n const item = items?.find((i) => i.Name === \"PhoneNumber\");\n return item ? String(item.Value) : null;\n}\n\n/** Returns true if the STK Push callback represents a successful transaction */\nexport function isSuccessfulCallback(webhook: StkPushWebhook): boolean {\n return webhook.Body?.stkCallback?.ResultCode === 0;\n}\n"]}
|