remita-checkout-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +138 -0
- package/dist/chunk-MGQQHIIZ.mjs +275 -0
- package/dist/chunk-MGQQHIIZ.mjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +453 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +231 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +225 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +347 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +102 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
Binary file
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# remita-checkout-client
|
|
2
|
+
|
|
3
|
+
A typed client for the Remita Checkout REST API (`api-checkout*.systemspecsng.com`).
|
|
4
|
+
Works in the browser and in Node 18+ (both have global `fetch`).
|
|
5
|
+
|
|
6
|
+
## ⚠️ Before you use this in anything real
|
|
7
|
+
|
|
8
|
+
The request/response **field names** in `src/types.ts` were written from
|
|
9
|
+
endpoint paths + one-line descriptions only — not the actual DTOs. Every
|
|
10
|
+
interface has an index signature so unexpected fields won't break at the
|
|
11
|
+
type level, but treat the field names themselves as **unverified**. To
|
|
12
|
+
tighten them, pull the actual interfaces out of:
|
|
13
|
+
|
|
14
|
+
- `remita-api-client.service.ts`
|
|
15
|
+
- `payment-status.service.ts`
|
|
16
|
+
|
|
17
|
+
and update `src/types.ts` to match. The transport layer, URLs, HTTP
|
|
18
|
+
methods, and the pre → fee → initiate → authorize → validate → verify
|
|
19
|
+
sequencing are all solid; it's specifically the shape of each request/
|
|
20
|
+
response body that needs confirming.
|
|
21
|
+
|
|
22
|
+
The **environment auto-detection from key prefix** (`src/environment.ts`)
|
|
23
|
+
is also a guess (Stripe-style `pk_test_`/`pk_live_` conventions) — it has
|
|
24
|
+
not been verified against Remita's real key format. It intentionally never
|
|
25
|
+
silently resolves to `"production"` — an unrecognized prefix falls back to
|
|
26
|
+
`"qa"` and reports `detected: false` so you can log/warn on it. Until you
|
|
27
|
+
verify the real prefixes, pass `environment` explicitly and skip detection.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install remita-checkout-client
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Low-level client
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { RemitaCheckoutClient } from "remita-checkout-client";
|
|
41
|
+
|
|
42
|
+
const client = new RemitaCheckoutClient({
|
|
43
|
+
publicKey: "YOUR_PUBLIC_KEY",
|
|
44
|
+
// environment: "qa", // recommended explicitly until prefix detection is verified
|
|
45
|
+
onEnvironmentDetectionFallback: (guessed) =>
|
|
46
|
+
console.warn(`Couldn't detect environment from key prefix, defaulting to ${guessed}`),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const pre = await client.prePayment({ publicKey: client.publicKey, rrr: "RRR12345" });
|
|
50
|
+
const fee = await client.calculateFee({ publicKey: client.publicKey, amount: 5000, channel: "card" });
|
|
51
|
+
const initiated = await client.initiatePayment({ publicKey: client.publicKey, amount: 5000, channel: "card" });
|
|
52
|
+
|
|
53
|
+
if (initiated.actionUrl) {
|
|
54
|
+
await client.authorize(initiated.actionUrl, { /* OTP / PIN / 3DS fields */ });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const verified = await client.verifyPayment(initiated.paymentReference!);
|
|
58
|
+
console.log(verified.status); // "APPROVED" | "PENDING" | "FAILED" | ...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Higher-level session (tracks refs between steps for you)
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { RemitaCheckoutClient, PaymentSession } from "remita-checkout-client";
|
|
65
|
+
|
|
66
|
+
const client = new RemitaCheckoutClient({ publicKey: "YOUR_PUBLIC_KEY", environment: "qa" });
|
|
67
|
+
const session = new PaymentSession(client);
|
|
68
|
+
|
|
69
|
+
await session.pre({ publicKey: client.publicKey, rrr: "RRR12345" });
|
|
70
|
+
await session.fee({ publicKey: client.publicKey, amount: 5000, channel: "card" });
|
|
71
|
+
await session.initiate({ publicKey: client.publicKey, amount: 5000, channel: "card" });
|
|
72
|
+
|
|
73
|
+
// actionUrl from initiate() is remembered automatically
|
|
74
|
+
await session.authorize({ otp: "123456" });
|
|
75
|
+
await session.validate({ otp: "123456" });
|
|
76
|
+
|
|
77
|
+
const result = await session.verify(); // uses paymentReference captured from initiate()
|
|
78
|
+
console.log(session.stage, result.status);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Real-time status via SSE
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const handle = client.streamPaymentStatus(trxRef, {
|
|
85
|
+
onOpen: () => console.log("stream open"),
|
|
86
|
+
onMessage: (event) => console.log("status update:", event.data),
|
|
87
|
+
onError: (err) => console.error("stream error:", err),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// later
|
|
91
|
+
handle.close();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This is implemented as a manual `fetch` + `ReadableStream` reader (not the
|
|
95
|
+
browser-only `EventSource` global), so it behaves the same in the browser
|
|
96
|
+
and in Node.
|
|
97
|
+
|
|
98
|
+
### Channel config endpoints
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const banks = await client.getBankList();
|
|
102
|
+
const ussdCodes = await client.getUssdCodes();
|
|
103
|
+
const wallets = await client.getWalletTypes();
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Node < 18
|
|
107
|
+
|
|
108
|
+
Global `fetch` isn't available before Node 18. Pass a `fetchImpl` explicitly:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import fetch from "node-fetch";
|
|
112
|
+
|
|
113
|
+
const client = new RemitaCheckoutClient({
|
|
114
|
+
publicKey: "...",
|
|
115
|
+
environment: "qa",
|
|
116
|
+
fetchImpl: fetch as unknown as typeof globalThis.fetch,
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Not covered in this version
|
|
121
|
+
|
|
122
|
+
Per the agreed v1 scope, this does **not** include:
|
|
123
|
+
|
|
124
|
+
- Google Pay / Apple Pay config endpoints (`/misc/googlepay`, `/misc/applepay`)
|
|
125
|
+
- CyberSource / Cardinal 3DS flow (Cruise Collect iframe, Centinel API, the
|
|
126
|
+
return listener endpoint)
|
|
127
|
+
|
|
128
|
+
Both are separate, non-trivial integrations (iframe orchestration for DDC,
|
|
129
|
+
CyberSource's Accept.js script loading) — worth their own follow-up rather
|
|
130
|
+
than bolting on here.
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm install
|
|
136
|
+
npm run typecheck
|
|
137
|
+
npm run build # outputs dist/ as CJS + ESM + .d.ts
|
|
138
|
+
```
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __esm = (fn, res) => function __init() {
|
|
3
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
4
|
+
};
|
|
5
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
6
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// src/environment.ts
|
|
10
|
+
function resolveEnvironment(publicKey, options = {}) {
|
|
11
|
+
const baseUrls = { ...ENVIRONMENT_BASE_URLS, ...options.baseUrls };
|
|
12
|
+
if (options.environment) {
|
|
13
|
+
return {
|
|
14
|
+
environment: options.environment,
|
|
15
|
+
baseUrl: baseUrls[options.environment],
|
|
16
|
+
detected: true
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
for (const rule of PREFIX_RULES) {
|
|
20
|
+
if (rule.pattern.test(publicKey)) {
|
|
21
|
+
return {
|
|
22
|
+
environment: rule.environment,
|
|
23
|
+
baseUrl: baseUrls[rule.environment],
|
|
24
|
+
detected: true
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { environment: "qa", baseUrl: baseUrls.qa, detected: false };
|
|
29
|
+
}
|
|
30
|
+
var ENVIRONMENT_BASE_URLS, PREFIX_RULES;
|
|
31
|
+
var init_environment = __esm({
|
|
32
|
+
"src/environment.ts"() {
|
|
33
|
+
"use strict";
|
|
34
|
+
ENVIRONMENT_BASE_URLS = {
|
|
35
|
+
qa: "https://api-checkout-qa.systemspecsng.com/api/v1",
|
|
36
|
+
demo: "https://api-checkout-demo.systemspecsng.com/api/v1",
|
|
37
|
+
production: "https://api-checkout.systemspecsng.com/api/v1"
|
|
38
|
+
};
|
|
39
|
+
PREFIX_RULES = [
|
|
40
|
+
{ pattern: /^(qa[-_]|pk_qa_)/i, environment: "qa" },
|
|
41
|
+
{ pattern: /^(demo[-_]|pk_demo_|pk_test_)/i, environment: "demo" }
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// src/stream.ts
|
|
47
|
+
function openStatusStream(baseUrl, trxRef, publicKey, options) {
|
|
48
|
+
const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
49
|
+
if (!fetchImpl) {
|
|
50
|
+
throw new Error("No fetch implementation available for streamPaymentStatus.");
|
|
51
|
+
}
|
|
52
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/payment/status/stream/${encodeURIComponent(
|
|
53
|
+
trxRef
|
|
54
|
+
)}?publicKey=${encodeURIComponent(publicKey)}`;
|
|
55
|
+
const headers = { Accept: "text/event-stream" };
|
|
56
|
+
if (options.sendPublicKeyHeader) {
|
|
57
|
+
headers.publicKey = publicKey;
|
|
58
|
+
}
|
|
59
|
+
const controller = new AbortController();
|
|
60
|
+
(async () => {
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetchImpl(url, {
|
|
63
|
+
headers,
|
|
64
|
+
signal: controller.signal
|
|
65
|
+
});
|
|
66
|
+
if (!response.ok || !response.body) {
|
|
67
|
+
throw new Error(`Status stream request failed with status ${response.status}`);
|
|
68
|
+
}
|
|
69
|
+
options.onOpen?.();
|
|
70
|
+
const reader = response.body.getReader();
|
|
71
|
+
const decoder = new TextDecoder();
|
|
72
|
+
let buffer = "";
|
|
73
|
+
while (true) {
|
|
74
|
+
const { done, value } = await reader.read();
|
|
75
|
+
if (done) break;
|
|
76
|
+
buffer += decoder.decode(value, { stream: true });
|
|
77
|
+
const events = buffer.split("\n\n");
|
|
78
|
+
buffer = events.pop() ?? "";
|
|
79
|
+
for (const rawEvent of events) {
|
|
80
|
+
const dataLines = rawEvent.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
|
|
81
|
+
if (dataLines.length === 0) continue;
|
|
82
|
+
const raw = dataLines.join("\n");
|
|
83
|
+
let data = raw;
|
|
84
|
+
try {
|
|
85
|
+
data = JSON.parse(raw);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
options.onMessage({ data, raw });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (err instanceof Error && err.name === "AbortError") return;
|
|
93
|
+
options.onError?.(err);
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
return {
|
|
97
|
+
close: () => controller.abort()
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
var init_stream = __esm({
|
|
101
|
+
"src/stream.ts"() {
|
|
102
|
+
"use strict";
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// src/http-client.ts
|
|
107
|
+
var RemitaApiError, HttpClient;
|
|
108
|
+
var init_http_client = __esm({
|
|
109
|
+
"src/http-client.ts"() {
|
|
110
|
+
"use strict";
|
|
111
|
+
RemitaApiError = class extends Error {
|
|
112
|
+
constructor(message, status, payload) {
|
|
113
|
+
super(message);
|
|
114
|
+
this.status = status;
|
|
115
|
+
this.payload = payload;
|
|
116
|
+
this.name = "RemitaApiError";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
HttpClient = class {
|
|
120
|
+
constructor(options) {
|
|
121
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
122
|
+
this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
123
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
124
|
+
this.defaultHeaders = options.defaultHeaders ?? {};
|
|
125
|
+
if (!this.fetchImpl) {
|
|
126
|
+
throw new Error("No fetch implementation available for HttpClient.");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async request(method, path, opts = {}) {
|
|
130
|
+
const url = opts.absoluteUrl ? opts.absoluteUrl : `${this.baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
|
|
131
|
+
const headers = {
|
|
132
|
+
Accept: "application/json",
|
|
133
|
+
...this.defaultHeaders,
|
|
134
|
+
...opts.headers
|
|
135
|
+
};
|
|
136
|
+
let body;
|
|
137
|
+
if (opts.body !== void 0) {
|
|
138
|
+
if (opts.contentType === "application/x-www-form-urlencoded") {
|
|
139
|
+
headers["Content-Type"] = opts.contentType;
|
|
140
|
+
body = new URLSearchParams(
|
|
141
|
+
Object.entries(opts.body).map(([k, v]) => [
|
|
142
|
+
k,
|
|
143
|
+
String(v)
|
|
144
|
+
])
|
|
145
|
+
).toString();
|
|
146
|
+
} else {
|
|
147
|
+
headers["Content-Type"] = opts.contentType ?? "application/json";
|
|
148
|
+
body = JSON.stringify(opts.body);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const timer = this.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.timeoutMs) : void 0;
|
|
153
|
+
try {
|
|
154
|
+
const response = await this.fetchImpl(url, {
|
|
155
|
+
method,
|
|
156
|
+
headers,
|
|
157
|
+
body,
|
|
158
|
+
signal: controller.signal
|
|
159
|
+
});
|
|
160
|
+
const text = await response.text();
|
|
161
|
+
let parsed = text;
|
|
162
|
+
try {
|
|
163
|
+
parsed = text ? JSON.parse(text) : void 0;
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
const payload = parsed ?? {};
|
|
168
|
+
const message = payload.responseMessage ?? payload.message ?? `Request failed with status ${response.status}`;
|
|
169
|
+
throw new RemitaApiError(message, response.status, payload);
|
|
170
|
+
}
|
|
171
|
+
return parsed;
|
|
172
|
+
} finally {
|
|
173
|
+
if (timer) clearTimeout(timer);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// src/client.ts
|
|
181
|
+
var RemitaCheckoutClient;
|
|
182
|
+
var init_client = __esm({
|
|
183
|
+
"src/client.ts"() {
|
|
184
|
+
"use strict";
|
|
185
|
+
init_http_client();
|
|
186
|
+
init_environment();
|
|
187
|
+
init_stream();
|
|
188
|
+
RemitaCheckoutClient = class {
|
|
189
|
+
constructor(options) {
|
|
190
|
+
this.publicKey = options.publicKey;
|
|
191
|
+
const resolved = resolveEnvironment(options.publicKey, {
|
|
192
|
+
environment: options.environment,
|
|
193
|
+
baseUrls: options.baseUrls
|
|
194
|
+
});
|
|
195
|
+
if (!resolved.detected) {
|
|
196
|
+
options.onEnvironmentDetectionFallback?.("qa");
|
|
197
|
+
}
|
|
198
|
+
this.environment = resolved.environment;
|
|
199
|
+
this.baseUrl = resolved.baseUrl;
|
|
200
|
+
this.http = new HttpClient({
|
|
201
|
+
baseUrl: resolved.baseUrl,
|
|
202
|
+
fetchImpl: options.fetchImpl,
|
|
203
|
+
timeoutMs: options.timeoutMs,
|
|
204
|
+
defaultHeaders: {
|
|
205
|
+
// publicKey is required as a header on every checkout API call.
|
|
206
|
+
publicKey: options.publicKey,
|
|
207
|
+
...options.defaultHeaders
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// --- Core payment lifecycle ---------------------------------------------
|
|
212
|
+
prePayment(request) {
|
|
213
|
+
return this.http.request("POST", "/payment/pre", { body: request });
|
|
214
|
+
}
|
|
215
|
+
calculateFee(request) {
|
|
216
|
+
return this.http.request("POST", "/payment/pre/feeCharge", { body: request });
|
|
217
|
+
}
|
|
218
|
+
initiatePayment(request) {
|
|
219
|
+
return this.http.request("POST", "/payment/initiate", { body: request });
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Posts authorization params (OTP, PIN, 3DS form fields, etc) to the
|
|
223
|
+
* processor-provided actionUrl from initiatePayment().
|
|
224
|
+
*
|
|
225
|
+
* NOTE: CyberSource/Cardinal 3DS endpoints expect
|
|
226
|
+
* `application/x-www-form-urlencoded`. Pass `contentType` if the
|
|
227
|
+
* actionUrl is a 3DS processor endpoint.
|
|
228
|
+
*/
|
|
229
|
+
authorize(actionUrl, payload, contentType) {
|
|
230
|
+
void contentType;
|
|
231
|
+
return this.http.request("POST", "", {
|
|
232
|
+
body: payload,
|
|
233
|
+
absoluteUrl: actionUrl
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Validates second-factor authorization (e.g. OTP). Per the endpoint docs
|
|
238
|
+
* this may go to a fixed /payment/validate endpoint OR to a dynamic
|
|
239
|
+
* actionUrl — pass `actionUrl` if the preceding step returned one.
|
|
240
|
+
*/
|
|
241
|
+
validatePayment(request, actionUrl) {
|
|
242
|
+
return actionUrl ? this.http.request("POST", "", { body: request, absoluteUrl: actionUrl }) : this.http.request("POST", "/payment/validate", { body: request });
|
|
243
|
+
}
|
|
244
|
+
verifyPayment(paymentRef) {
|
|
245
|
+
return this.http.request("GET", `/payment/verify/${encodeURIComponent(paymentRef)}`);
|
|
246
|
+
}
|
|
247
|
+
/** Opens an SSE stream for real-time status updates. Call the returned `close()` when done. */
|
|
248
|
+
streamPaymentStatus(trxRef, options) {
|
|
249
|
+
return openStatusStream(this.baseUrl, trxRef, this.publicKey, options);
|
|
250
|
+
}
|
|
251
|
+
// --- Channel data / configuration ---------------------------------------
|
|
252
|
+
getBankList() {
|
|
253
|
+
return this.http.request("GET", "/payment/bank-list");
|
|
254
|
+
}
|
|
255
|
+
getUssdCodes() {
|
|
256
|
+
return this.http.request("GET", "/misc/ussd-codes");
|
|
257
|
+
}
|
|
258
|
+
getWalletTypes() {
|
|
259
|
+
return this.http.request("GET", "/misc/wallet-type");
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
export {
|
|
266
|
+
__commonJS,
|
|
267
|
+
ENVIRONMENT_BASE_URLS,
|
|
268
|
+
resolveEnvironment,
|
|
269
|
+
init_environment,
|
|
270
|
+
openStatusStream,
|
|
271
|
+
init_stream,
|
|
272
|
+
RemitaCheckoutClient,
|
|
273
|
+
init_client
|
|
274
|
+
};
|
|
275
|
+
//# sourceMappingURL=chunk-MGQQHIIZ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/environment.ts","../src/stream.ts","../src/http-client.ts","../src/client.ts"],"sourcesContent":["export type RemitaEnvironment = \"qa\" | \"demo\" | \"production\";\n\nexport const ENVIRONMENT_BASE_URLS: Record<RemitaEnvironment, string> = {\n qa: \"https://api-checkout-qa.systemspecsng.com/api/v1\",\n demo: \"https://api-checkout-demo.systemspecsng.com/api/v1\",\n production: \"https://api-checkout.systemspecsng.com/api/v1\",\n};\n\n/**\n * Best-effort prefix-based environment detection from a public key.\n *\n * IMPORTANT: `pk_live_` is deliberately NOT mapped here. Remita's real key\n * format has not been verified, and silently routing a key to production\n * is worse than failing loudly. Callers who want production MUST pass\n * `environment: \"production\"` (or `--env production` on the CLI) explicitly.\n */\nconst PREFIX_RULES: Array<{ pattern: RegExp; environment: RemitaEnvironment }> = [\n { pattern: /^(qa[-_]|pk_qa_)/i, environment: \"qa\" },\n { pattern: /^(demo[-_]|pk_demo_|pk_test_)/i, environment: \"demo\" },\n];\n\nexport interface ResolveEnvironmentOptions {\n /** Explicit override — always wins over prefix detection. */\n environment?: RemitaEnvironment;\n /** Custom base URLs, e.g. to point at a local/staging mirror. */\n baseUrls?: Partial<Record<RemitaEnvironment, string>>;\n}\n\nexport interface ResolvedEnvironment {\n environment: RemitaEnvironment;\n baseUrl: string;\n /** False when detection couldn't confidently match a prefix and we fell back to a default. */\n detected: boolean;\n}\n\nexport function resolveEnvironment(\n publicKey: string,\n options: ResolveEnvironmentOptions = {}\n): ResolvedEnvironment {\n const baseUrls = { ...ENVIRONMENT_BASE_URLS, ...options.baseUrls };\n\n if (options.environment) {\n return {\n environment: options.environment,\n baseUrl: baseUrls[options.environment],\n detected: true,\n };\n }\n\n for (const rule of PREFIX_RULES) {\n if (rule.pattern.test(publicKey)) {\n return {\n environment: rule.environment,\n baseUrl: baseUrls[rule.environment],\n detected: true,\n };\n }\n }\n\n return { environment: \"qa\", baseUrl: baseUrls.qa, detected: false };\n}","export interface StatusStreamOptions {\n onMessage: (event: { data: unknown; raw: string }) => void;\n onError?: (err: unknown) => void;\n onOpen?: () => void;\n fetchImpl?: typeof fetch;\n /** Optional override; publicKey is sent as a query param by default. */\n sendPublicKeyHeader?: boolean;\n}\n\nexport interface StatusStreamHandle {\n close: () => void;\n}\n\n/**\n * Opens the payment status SSE stream using a manual fetch + ReadableStream\n * reader rather than the browser-only `EventSource` global, so this works\n * identically in the browser and in Node 18+.\n */\nexport function openStatusStream(\n baseUrl: string,\n trxRef: string,\n publicKey: string,\n options: StatusStreamOptions\n): StatusStreamHandle {\n const fetchImpl =\n options.fetchImpl ?? (typeof fetch !== \"undefined\" ? fetch : undefined);\n if (!fetchImpl) {\n throw new Error(\"No fetch implementation available for streamPaymentStatus.\");\n }\n\n const url = `${baseUrl.replace(/\\/+$/, \"\")}/payment/status/stream/${encodeURIComponent(\n trxRef\n )}?publicKey=${encodeURIComponent(publicKey)}`;\n\n const headers: Record<string, string> = { Accept: \"text/event-stream\" };\n if (options.sendPublicKeyHeader) {\n headers.publicKey = publicKey;\n }\n\n const controller = new AbortController();\n\n (async () => {\n try {\n const response = await fetchImpl(url, {\n headers,\n signal: controller.signal,\n });\n\n if (!response.ok || !response.body) {\n throw new Error(`Status stream request failed with status ${response.status}`);\n }\n\n options.onOpen?.();\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n buffer += decoder.decode(value, { stream: true });\n const events = buffer.split(\"\\n\\n\");\n buffer = events.pop() ?? \"\";\n\n for (const rawEvent of events) {\n const dataLines = rawEvent\n .split(\"\\n\")\n .filter((line) => line.startsWith(\"data:\"))\n .map((line) => line.slice(5).trim());\n\n if (dataLines.length === 0) continue;\n\n const raw = dataLines.join(\"\\n\");\n let data: unknown = raw;\n try {\n data = JSON.parse(raw);\n } catch {\n // not JSON — leave as the raw string\n }\n\n options.onMessage({ data, raw });\n }\n }\n } catch (err) {\n if (err instanceof Error && err.name === \"AbortError\") return;\n options.onError?.(err);\n }\n })();\n\n return {\n close: () => controller.abort(),\n };\n}","export interface HttpRequestOptions {\n body?: unknown;\n headers?: Record<string, string>;\n absoluteUrl?: string;\n /** Overrides the default application/json for a single request. */\n contentType?: string;\n}\n\nexport interface HttpClientOptions {\n baseUrl: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n defaultHeaders?: Record<string, string>;\n}\n\nexport interface ApiErrorPayload {\n responseMessage?: string;\n message?: string;\n [key: string]: unknown;\n}\n\nexport class RemitaApiError extends Error {\n constructor(\n message: string,\n public readonly status: number,\n public readonly payload: ApiErrorPayload\n ) {\n super(message);\n this.name = \"RemitaApiError\";\n }\n}\n\nexport class HttpClient {\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n private readonly defaultHeaders: Record<string, string>;\n\n constructor(options: HttpClientOptions) {\n this.baseUrl = options.baseUrl.replace(/\\/+$/, \"\");\n this.fetchImpl =\n options.fetchImpl ?? (typeof fetch !== \"undefined\" ? fetch : (undefined as never));\n this.timeoutMs = options.timeoutMs ?? 30_000;\n this.defaultHeaders = options.defaultHeaders ?? {};\n\n if (!this.fetchImpl) {\n throw new Error(\"No fetch implementation available for HttpClient.\");\n }\n }\n\n async request<T>(\n method: string,\n path: string,\n opts: HttpRequestOptions = {}\n ): Promise<T> {\n // --- L6 fix: join WITHOUT dropping /api/v1 ---\n // path is expected to start with \"/\" and the baseUrl already ends with\n // \"/api/v1\", so plain concatenation is correct. Do NOT use `new URL(path, base)`.\n const url = opts.absoluteUrl\n ? opts.absoluteUrl\n : `${this.baseUrl}${path.startsWith(\"/\") ? \"\" : \"/\"}${path}`;\n\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n ...this.defaultHeaders,\n ...opts.headers,\n };\n\n let body: BodyInit | undefined;\n if (opts.body !== undefined) {\n if (opts.contentType === \"application/x-www-form-urlencoded\") {\n headers[\"Content-Type\"] = opts.contentType;\n body = new URLSearchParams(\n Object.entries(opts.body as Record<string, unknown>).map(([k, v]) => [\n k,\n String(v),\n ])\n ).toString();\n } else {\n headers[\"Content-Type\"] = opts.contentType ?? \"application/json\";\n body = JSON.stringify(opts.body);\n }\n }\n\n const controller = new AbortController();\n const timer =\n this.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.timeoutMs) : undefined;\n\n try {\n const response = await this.fetchImpl(url, {\n method,\n headers,\n body,\n signal: controller.signal,\n });\n\n const text = await response.text();\n let parsed: unknown = text;\n try {\n parsed = text ? JSON.parse(text) : undefined;\n } catch {\n // leave as raw string\n }\n\n if (!response.ok) {\n const payload = (parsed ?? {}) as ApiErrorPayload;\n const message =\n payload.responseMessage ??\n payload.message ??\n `Request failed with status ${response.status}`;\n throw new RemitaApiError(message, response.status, payload);\n }\n\n return parsed as T;\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n}","import { HttpClient } from \"./http-client\";\nimport { resolveEnvironment } from \"./environment\";\nimport type { ResolveEnvironmentOptions } from \"./environment\";\nimport type {\n PrePaymentRequest,\n PrePaymentResponse,\n FeeChargeRequest,\n FeeChargeResponse,\n InitiatePaymentRequest,\n InitiatePaymentResponse,\n AuthorizePayload,\n AuthorizeResponse,\n ValidatePaymentRequest,\n ValidatePaymentResponse,\n VerifyPaymentResponse,\n BankListResponse,\n UssdCodesResponse,\n WalletTypesResponse,\n} from \"./types\";\nimport { openStatusStream } from \"./stream\";\nimport type { StatusStreamHandle, StatusStreamOptions } from \"./stream\";\n\nexport interface RemitaCheckoutClientOptions extends ResolveEnvironmentOptions {\n publicKey: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n /** Extra headers merged after `publicKey` is set (so they can override). */\n defaultHeaders?: Record<string, string>;\n /** Called once if the environment couldn't be confidently detected from the key prefix (falls back to \"qa\"). */\n onEnvironmentDetectionFallback?: (guessed: \"qa\") => void;\n}\n\nexport class RemitaCheckoutClient {\n readonly publicKey: string;\n readonly environment: string;\n readonly baseUrl: string;\n private readonly http: HttpClient;\n\n constructor(options: RemitaCheckoutClientOptions) {\n this.publicKey = options.publicKey;\n\n const resolved = resolveEnvironment(options.publicKey, {\n environment: options.environment,\n baseUrls: options.baseUrls,\n });\n\n if (!resolved.detected) {\n options.onEnvironmentDetectionFallback?.(\"qa\");\n }\n\n this.environment = resolved.environment;\n this.baseUrl = resolved.baseUrl;\n\n this.http = new HttpClient({\n baseUrl: resolved.baseUrl,\n fetchImpl: options.fetchImpl,\n timeoutMs: options.timeoutMs,\n defaultHeaders: {\n // publicKey is required as a header on every checkout API call.\n publicKey: options.publicKey,\n ...options.defaultHeaders,\n },\n });\n }\n\n // --- Core payment lifecycle ---------------------------------------------\n\n prePayment(request: PrePaymentRequest): Promise<PrePaymentResponse> {\n return this.http.request(\"POST\", \"/payment/pre\", { body: request });\n }\n\n calculateFee(request: FeeChargeRequest): Promise<FeeChargeResponse> {\n return this.http.request(\"POST\", \"/payment/pre/feeCharge\", { body: request });\n }\n\n initiatePayment(request: InitiatePaymentRequest): Promise<InitiatePaymentResponse> {\n return this.http.request(\"POST\", \"/payment/initiate\", { body: request });\n }\n\n /**\n * Posts authorization params (OTP, PIN, 3DS form fields, etc) to the\n * processor-provided actionUrl from initiatePayment().\n *\n * NOTE: CyberSource/Cardinal 3DS endpoints expect\n * `application/x-www-form-urlencoded`. Pass `contentType` if the\n * actionUrl is a 3DS processor endpoint.\n */\n authorize(\n actionUrl: string,\n payload: AuthorizePayload,\n contentType?: string\n ): Promise<AuthorizeResponse> {\n void contentType;\n return this.http.request(\"POST\", \"\", {\n body: payload,\n absoluteUrl: actionUrl,\n });\n }\n\n /**\n * Validates second-factor authorization (e.g. OTP). Per the endpoint docs\n * this may go to a fixed /payment/validate endpoint OR to a dynamic\n * actionUrl — pass `actionUrl` if the preceding step returned one.\n */\n validatePayment(\n request: ValidatePaymentRequest,\n actionUrl?: string\n ): Promise<ValidatePaymentResponse> {\n return actionUrl\n ? this.http.request(\"POST\", \"\", { body: request, absoluteUrl: actionUrl })\n : this.http.request(\"POST\", \"/payment/validate\", { body: request });\n }\n\n verifyPayment(paymentRef: string): Promise<VerifyPaymentResponse> {\n return this.http.request(\"GET\", `/payment/verify/${encodeURIComponent(paymentRef)}`);\n }\n\n /** Opens an SSE stream for real-time status updates. Call the returned `close()` when done. */\n streamPaymentStatus(trxRef: string, options: StatusStreamOptions): StatusStreamHandle {\n return openStatusStream(this.baseUrl, trxRef, this.publicKey, options);\n }\n\n // --- Channel data / configuration ---------------------------------------\n\n getBankList(): Promise<BankListResponse> {\n return this.http.request(\"GET\", \"/payment/bank-list\");\n }\n\n getUssdCodes(): Promise<UssdCodesResponse> {\n return this.http.request(\"GET\", \"/misc/ussd-codes\");\n }\n\n getWalletTypes(): Promise<WalletTypesResponse> {\n return this.http.request(\"GET\", \"/misc/wallet-type\");\n }\n}"],"mappings":"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","names":[]}
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|