remita-checkout-client 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -3,6 +3,117 @@
3
3
  A typed client for the Remita Checkout REST API (`api-checkout*.systemspecsng.com`).
4
4
  Works in the browser and in Node 18+ (both have global `fetch`).
5
5
 
6
+ ## Two product surfaces
7
+
8
+ This package is built around two user types:
9
+
10
+ ### Merchant checkout
11
+
12
+ Use this when you are a business or storefront that only wants to accept payment
13
+ for a sale.
14
+
15
+ Typical needs:
16
+
17
+ - create a checkout session
18
+ - redirect or embed the payment flow
19
+ - verify payment completion
20
+ - update order status
21
+
22
+ Recommended client:
23
+
24
+ - `MerchantCheckoutClient`
25
+
26
+ ### PSP / platform
27
+
28
+ Use this when you are a payment facilitator, gateway, or platform operator that
29
+ needs the full Remita lifecycle.
30
+
31
+ Typical needs:
32
+
33
+ - pre-payment flows
34
+ - fee calculation
35
+ - payment initiation
36
+ - authorization and validation
37
+ - status verification
38
+ - channel lookup and platform configuration
39
+
40
+ Recommended client:
41
+
42
+ - `RemitaPlatformClient`
43
+
44
+ ### Compatibility
45
+
46
+ `RemitaCheckoutClient` remains available as a backwards-compatible alias of the
47
+ platform client for older integrations, but the preferred split is:
48
+
49
+ - `MerchantCheckoutClient` for checkout-only usage
50
+ - `RemitaPlatformClient` for PSP/platform usage
51
+
52
+ ---
53
+
54
+ ## Quick example: merchant checkout
55
+
56
+ ```ts
57
+ import { MerchantCheckoutClient } from "remita-checkout-client";
58
+
59
+ const checkout = new MerchantCheckoutClient({
60
+ publicKey: "MERCHANT_PUBLIC_KEY",
61
+ environment: "production",
62
+ });
63
+
64
+ const session = await checkout.createCheckoutSession({
65
+ amount: 25000,
66
+ paymentIdentifier: "TRANSACTION_REF_123",
67
+ narration: "Order 123",
68
+ paymentChannel: "CARD",
69
+ metaData: {},
70
+ merchantInformation: {},
71
+ });
72
+
73
+ const verified = await checkout.verifyPayment("PAYMENT_REF_123");
74
+ console.log(verified.status);
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Quick example: PSP / platform flow
80
+
81
+ ```ts
82
+ import { RemitaPlatformClient } from "remita-checkout-client";
83
+
84
+ const remita = new RemitaPlatformClient({
85
+ publicKey: "PSP_PUBLIC_KEY",
86
+ environment: "production",
87
+ });
88
+
89
+ const pre = await remita.prePayment({
90
+ firstName: "Test",
91
+ lastName: "User",
92
+ email: "test@example.com",
93
+ phoneNumber: "08000000000",
94
+ paymentIdentifier: "ORDER-123",
95
+ currency: "NGN",
96
+ amount: 25000,
97
+ });
98
+
99
+ const fee = await remita.calculateFee({
100
+ amount: 25000,
101
+ channel: "card",
102
+ });
103
+
104
+ const paymentIdentifier = pre.transactionId ?? pre.paymentReference;
105
+ if (!paymentIdentifier) throw new Error("Pre-payment did not return a transaction reference");
106
+
107
+ const initiated = await remita.initiatePayment({
108
+ amount: 25000,
109
+ paymentIdentifier,
110
+ paymentChannel: "CARD",
111
+ });
112
+
113
+ const verified = await remita.verifyPayment(initiated.paymentReference!);
114
+ console.log(verified.status);
115
+ ```
116
+
6
117
  ## ⚠️ Before you use this in anything real
7
118
 
8
119
  The request/response **field names** in `src/types.ts` were written from
@@ -46,9 +157,23 @@ const client = new RemitaCheckoutClient({
46
157
  console.warn(`Couldn't detect environment from key prefix, defaulting to ${guessed}`),
47
158
  });
48
159
 
49
- const pre = await client.prePayment({ publicKey: client.publicKey, rrr: "RRR12345" });
160
+ const pre = await client.prePayment({
161
+ firstName: "Test",
162
+ lastName: "User",
163
+ email: "test@example.com",
164
+ phoneNumber: "08000000000",
165
+ paymentIdentifier: "ORDER-123",
166
+ currency: "NGN",
167
+ amount: 5000,
168
+ });
50
169
  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" });
170
+ const paymentIdentifier = pre.transactionId ?? pre.paymentReference;
171
+ if (!paymentIdentifier) throw new Error("Pre-payment did not return a transaction reference");
172
+ const initiated = await client.initiatePayment({
173
+ amount: 5000,
174
+ paymentIdentifier: "TRANSACTION_REF_123",
175
+ paymentChannel: "CARD",
176
+ });
52
177
 
53
178
  if (initiated.actionUrl) {
54
179
  await client.authorize(initiated.actionUrl, { /* OTP / PIN / 3DS fields */ });
@@ -66,9 +191,23 @@ import { RemitaCheckoutClient, PaymentSession } from "remita-checkout-client";
66
191
  const client = new RemitaCheckoutClient({ publicKey: "YOUR_PUBLIC_KEY", environment: "qa" });
67
192
  const session = new PaymentSession(client);
68
193
 
69
- await session.pre({ publicKey: client.publicKey, rrr: "RRR12345" });
194
+ await session.pre({
195
+ firstName: "Test",
196
+ lastName: "User",
197
+ email: "test@example.com",
198
+ phoneNumber: "08000000000",
199
+ paymentIdentifier: "ORDER-123",
200
+ currency: "NGN",
201
+ amount: 5000,
202
+ });
70
203
  await session.fee({ publicKey: client.publicKey, amount: 5000, channel: "card" });
71
- await session.initiate({ publicKey: client.publicKey, amount: 5000, channel: "card" });
204
+ const paymentIdentifier = session.transactionId ?? session.paymentReference;
205
+ if (!paymentIdentifier) throw new Error("Pre-payment did not return a transaction reference");
206
+ await session.initiate({
207
+ amount: 5000,
208
+ paymentIdentifier,
209
+ paymentChannel: "CARD",
210
+ });
72
211
 
73
212
  // actionUrl from initiate() is remembered automatically
74
213
  await session.authorize({ otp: "123456" });
@@ -0,0 +1,268 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/environment.ts
9
+ var ENVIRONMENT_BASE_URLS = {
10
+ qa: "https://api-checkout-qa.systemspecsng.com/api/v1",
11
+ demo: "https://api-checkout-demo.systemspecsng.com/api/v1",
12
+ production: "https://api-checkout.systemspecsng.com/api/v1"
13
+ };
14
+ var PREFIX_RULES = [
15
+ { pattern: /^(qa[-_]|pk_qa_)/i, environment: "qa" },
16
+ { pattern: /^(demo[-_]|pk_demo_|pk_test_)/i, environment: "demo" }
17
+ ];
18
+ function resolveEnvironment(publicKey, options = {}) {
19
+ const baseUrls = { ...ENVIRONMENT_BASE_URLS, ...options.baseUrls };
20
+ if (options.environment) {
21
+ return {
22
+ environment: options.environment,
23
+ baseUrl: baseUrls[options.environment],
24
+ detected: true
25
+ };
26
+ }
27
+ for (const rule of PREFIX_RULES) {
28
+ if (rule.pattern.test(publicKey)) {
29
+ return {
30
+ environment: rule.environment,
31
+ baseUrl: baseUrls[rule.environment],
32
+ detected: true
33
+ };
34
+ }
35
+ }
36
+ return { environment: "qa", baseUrl: baseUrls.qa, detected: false };
37
+ }
38
+
39
+ // src/stream.ts
40
+ function openStatusStream(baseUrl, trxRef, publicKey, options) {
41
+ const fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
42
+ if (!fetchImpl) {
43
+ throw new Error("No fetch implementation available for streamPaymentStatus.");
44
+ }
45
+ const url = `${baseUrl.replace(/\/+$/, "")}/payment/status/stream/${encodeURIComponent(
46
+ trxRef
47
+ )}?publicKey=${encodeURIComponent(publicKey)}`;
48
+ const headers = { Accept: "text/event-stream" };
49
+ if (options.sendPublicKeyHeader) {
50
+ headers.publicKey = publicKey;
51
+ }
52
+ const controller = new AbortController();
53
+ (async () => {
54
+ try {
55
+ const response = await fetchImpl(url, {
56
+ headers,
57
+ signal: controller.signal
58
+ });
59
+ if (!response.ok || !response.body) {
60
+ throw new Error(`Status stream request failed with status ${response.status}`);
61
+ }
62
+ options.onOpen?.();
63
+ const reader = response.body.getReader();
64
+ const decoder = new TextDecoder();
65
+ let buffer = "";
66
+ while (true) {
67
+ const { done, value } = await reader.read();
68
+ if (done) break;
69
+ buffer += decoder.decode(value, { stream: true });
70
+ const events = buffer.split("\n\n");
71
+ buffer = events.pop() ?? "";
72
+ for (const rawEvent of events) {
73
+ const dataLines = rawEvent.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
74
+ if (dataLines.length === 0) continue;
75
+ const raw = dataLines.join("\n");
76
+ let data = raw;
77
+ try {
78
+ data = JSON.parse(raw);
79
+ } catch {
80
+ }
81
+ options.onMessage({ data, raw });
82
+ }
83
+ }
84
+ } catch (err) {
85
+ if (err instanceof Error && err.name === "AbortError") return;
86
+ options.onError?.(err);
87
+ }
88
+ })();
89
+ return {
90
+ close: () => controller.abort()
91
+ };
92
+ }
93
+
94
+ // src/http-client.ts
95
+ var RemitaApiError = class extends Error {
96
+ constructor(message, status, payload) {
97
+ super(message);
98
+ this.status = status;
99
+ this.payload = payload;
100
+ this.name = "RemitaApiError";
101
+ }
102
+ };
103
+ var HttpClient = class {
104
+ constructor(options) {
105
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
106
+ this.fetchImpl = options.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
107
+ this.timeoutMs = options.timeoutMs ?? 3e4;
108
+ this.defaultHeaders = options.defaultHeaders ?? {};
109
+ if (!this.fetchImpl) {
110
+ throw new Error("No fetch implementation available for HttpClient.");
111
+ }
112
+ }
113
+ async request(method, path, opts = {}) {
114
+ const url = opts.absoluteUrl ? opts.absoluteUrl : `${this.baseUrl}${path.startsWith("/") ? "" : "/"}${path}`;
115
+ const headers = {
116
+ Accept: "application/json",
117
+ ...opts.omitDefaultHeaders ? {} : this.defaultHeaders,
118
+ ...opts.headers
119
+ };
120
+ let body;
121
+ if (opts.body !== void 0) {
122
+ if (opts.contentType === "application/x-www-form-urlencoded") {
123
+ headers["Content-Type"] = opts.contentType;
124
+ body = new URLSearchParams(
125
+ Object.entries(opts.body).map(([k, v]) => [
126
+ k,
127
+ String(v)
128
+ ])
129
+ ).toString();
130
+ } else {
131
+ headers["Content-Type"] = opts.contentType ?? "application/json";
132
+ body = JSON.stringify(opts.body);
133
+ }
134
+ }
135
+ const controller = new AbortController();
136
+ const timer = this.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.timeoutMs) : void 0;
137
+ try {
138
+ const response = await this.fetchImpl(url, {
139
+ method,
140
+ headers,
141
+ body,
142
+ signal: controller.signal
143
+ });
144
+ const text = await response.text();
145
+ let parsed = text;
146
+ try {
147
+ parsed = text ? JSON.parse(text) : void 0;
148
+ } catch {
149
+ }
150
+ if (!response.ok) {
151
+ const payload = parsed ?? {};
152
+ const message = payload.responseMessage ?? payload.message ?? `Request failed with status ${response.status}`;
153
+ throw new RemitaApiError(message, response.status, payload);
154
+ }
155
+ return parsed;
156
+ } finally {
157
+ if (timer) clearTimeout(timer);
158
+ }
159
+ }
160
+ };
161
+
162
+ // src/client.ts
163
+ var CheckoutCoreClient = class {
164
+ constructor(options) {
165
+ this.publicKey = options.publicKey;
166
+ const resolved = resolveEnvironment(options.publicKey, {
167
+ environment: options.environment,
168
+ baseUrls: options.baseUrls
169
+ });
170
+ if (!resolved.detected) {
171
+ options.onEnvironmentDetectionFallback?.("qa");
172
+ }
173
+ this.environment = resolved.environment;
174
+ this.baseUrl = resolved.baseUrl;
175
+ this.http = new HttpClient({
176
+ baseUrl: resolved.baseUrl,
177
+ fetchImpl: options.fetchImpl,
178
+ timeoutMs: options.timeoutMs,
179
+ defaultHeaders: {
180
+ publicKey: options.publicKey,
181
+ ...options.defaultHeaders
182
+ }
183
+ });
184
+ }
185
+ verifyPayment(paymentRef) {
186
+ return this.http.request("GET", `/payment/verify/${encodeURIComponent(paymentRef)}`);
187
+ }
188
+ /** Opens an SSE stream for real-time status updates. Call the returned `close()` when done. */
189
+ streamPaymentStatus(trxRef, options) {
190
+ return openStatusStream(this.baseUrl, trxRef, this.publicKey, options);
191
+ }
192
+ };
193
+ var MerchantCheckoutClient = class extends CheckoutCoreClient {
194
+ /**
195
+ * Merchant checkout clients are intentionally narrow. They focus on the public
196
+ * payment flow rather than the full platform lifecycle used by PSPs.
197
+ */
198
+ createCheckoutSession(request) {
199
+ return this.http.request("POST", "/payment/initiate", { body: request });
200
+ }
201
+ createSession(request) {
202
+ return this.createCheckoutSession(request);
203
+ }
204
+ };
205
+ var RemitaPlatformClient = class extends CheckoutCoreClient {
206
+ // --- Core payment lifecycle ---------------------------------------------
207
+ prePayment(request) {
208
+ return this.http.request("POST", "/payment/pre", { body: request });
209
+ }
210
+ calculateFee(request) {
211
+ return this.http.request("POST", "/payment/pre/feeCharge", { body: request });
212
+ }
213
+ initiatePayment(request) {
214
+ return this.http.request("POST", "/payment/initiate", { body: request });
215
+ }
216
+ /**
217
+ * Posts authorization params (OTP, PIN, 3DS form fields, etc) to the
218
+ * processor-provided actionUrl from initiatePayment().
219
+ *
220
+ * Authorization URLs are external processor endpoints, so the Remita
221
+ * public-key header is not forwarded. Pass form content type for processors
222
+ * that require URL-encoded authorization fields.
223
+ */
224
+ authorize(actionUrl, payload, contentType) {
225
+ return this.http.request("POST", "", {
226
+ body: payload,
227
+ absoluteUrl: actionUrl,
228
+ omitDefaultHeaders: true,
229
+ contentType
230
+ });
231
+ }
232
+ /**
233
+ * Validates second-factor authorization (e.g. OTP). Per the endpoint docs
234
+ * this may go to a fixed /payment/validate endpoint OR to a dynamic
235
+ * actionUrl — pass `actionUrl` if the preceding step returned one.
236
+ */
237
+ validatePayment(request, actionUrl) {
238
+ return actionUrl ? this.http.request("POST", "", {
239
+ body: request,
240
+ absoluteUrl: actionUrl,
241
+ omitDefaultHeaders: true
242
+ }) : this.http.request("POST", "/payment/validate", { body: request });
243
+ }
244
+ // --- Channel data / configuration ---------------------------------------
245
+ getBankList() {
246
+ return this.http.request("GET", "/payment/bank-list");
247
+ }
248
+ getUssdCodes() {
249
+ return this.http.request("GET", "/misc/ussd-codes");
250
+ }
251
+ getWalletTypes() {
252
+ return this.http.request("GET", "/misc/wallet-type");
253
+ }
254
+ };
255
+ var RemitaCheckoutClient = class extends RemitaPlatformClient {
256
+ };
257
+
258
+ export {
259
+ __require,
260
+ ENVIRONMENT_BASE_URLS,
261
+ resolveEnvironment,
262
+ openStatusStream,
263
+ CheckoutCoreClient,
264
+ MerchantCheckoutClient,
265
+ RemitaPlatformClient,
266
+ RemitaCheckoutClient
267
+ };
268
+ //# sourceMappingURL=chunk-RLUJCMLB.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 omitDefaultHeaders?: boolean;\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 ...(opts.omitDefaultHeaders ? {} : 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 PaymentInitiationRequest,\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 CheckoutCoreClient {\n readonly publicKey: string;\n readonly environment: string;\n readonly baseUrl: string;\n protected 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: options.publicKey,\n ...options.defaultHeaders,\n },\n });\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\nexport class MerchantCheckoutClient extends CheckoutCoreClient {\n /**\n * Merchant checkout clients are intentionally narrow. They focus on the public\n * payment flow rather than the full platform lifecycle used by PSPs.\n */\n createCheckoutSession(request: PaymentInitiationRequest): Promise<Record<string, unknown>> {\n return this.http.request(\"POST\", \"/payment/initiate\", { body: request });\n }\n\n createSession(request: PaymentInitiationRequest): Promise<Record<string, unknown>> {\n return this.createCheckoutSession(request);\n }\n}\n\nexport class RemitaPlatformClient extends CheckoutCoreClient {\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 * Authorization URLs are external processor endpoints, so the Remita\n * public-key header is not forwarded. Pass form content type for processors\n * that require URL-encoded authorization fields.\n */\n authorize(\n actionUrl: string,\n payload: AuthorizePayload,\n contentType?: string\n ): Promise<AuthorizeResponse> {\n return this.http.request(\"POST\", \"\", {\n body: payload,\n absoluteUrl: actionUrl,\n omitDefaultHeaders: true,\n contentType,\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\", \"\", {\n body: request,\n absoluteUrl: actionUrl,\n omitDefaultHeaders: true,\n })\n : this.http.request(\"POST\", \"/payment/validate\", { body: request });\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}\n\nexport class RemitaCheckoutClient extends RemitaPlatformClient {}\n"],"mappings":";;;;;;;;AAEO,IAAM,wBAA2D;AAAA,EACtE,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,YAAY;AACd;AAUA,IAAM,eAA2E;AAAA,EAC/E,EAAE,SAAS,qBAAqB,aAAa,KAAK;AAAA,EAClD,EAAE,SAAS,kCAAkC,aAAa,OAAO;AACnE;AAgBO,SAAS,mBACd,WACA,UAAqC,CAAC,GACjB;AACrB,QAAM,WAAW,EAAE,GAAG,uBAAuB,GAAG,QAAQ,SAAS;AAEjE,MAAI,QAAQ,aAAa;AACvB,WAAO;AAAA,MACL,aAAa,QAAQ;AAAA,MACrB,SAAS,SAAS,QAAQ,WAAW;AAAA,MACrC,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,aAAW,QAAQ,cAAc;AAC/B,QAAI,KAAK,QAAQ,KAAK,SAAS,GAAG;AAChC,aAAO;AAAA,QACL,aAAa,KAAK;AAAA,QAClB,SAAS,SAAS,KAAK,WAAW;AAAA,QAClC,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,aAAa,MAAM,SAAS,SAAS,IAAI,UAAU,MAAM;AACpE;;;AC1CO,SAAS,iBACd,SACA,QACA,WACA,SACoB;AACpB,QAAM,YACJ,QAAQ,cAAc,OAAO,UAAU,cAAc,QAAQ;AAC/D,MAAI,CAAC,WAAW;AACd,UAAM,IAAI,MAAM,4DAA4D;AAAA,EAC9E;AAEA,QAAM,MAAM,GAAG,QAAQ,QAAQ,QAAQ,EAAE,CAAC,0BAA0B;AAAA,IAClE;AAAA,EACF,CAAC,cAAc,mBAAmB,SAAS,CAAC;AAE5C,QAAM,UAAkC,EAAE,QAAQ,oBAAoB;AACtE,MAAI,QAAQ,qBAAqB;AAC/B,YAAQ,YAAY;AAAA,EACtB;AAEA,QAAM,aAAa,IAAI,gBAAgB;AAEvC,GAAC,YAAY;AACX,QAAI;AACF,YAAM,WAAW,MAAM,UAAU,KAAK;AAAA,QACpC;AAAA,QACA,QAAQ,WAAW;AAAA,MACrB,CAAC;AAED,UAAI,CAAC,SAAS,MAAM,CAAC,SAAS,MAAM;AAClC,cAAM,IAAI,MAAM,4CAA4C,SAAS,MAAM,EAAE;AAAA,MAC/E;AAEA,cAAQ,SAAS;AAEjB,YAAM,SAAS,SAAS,KAAK,UAAU;AACvC,YAAM,UAAU,IAAI,YAAY;AAChC,UAAI,SAAS;AAGb,aAAO,MAAM;AACX,cAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,YAAI,KAAM;AAEV,kBAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAChD,cAAM,SAAS,OAAO,MAAM,MAAM;AAClC,iBAAS,OAAO,IAAI,KAAK;AAEzB,mBAAW,YAAY,QAAQ;AAC7B,gBAAM,YAAY,SACf,MAAM,IAAI,EACV,OAAO,CAAC,SAAS,KAAK,WAAW,OAAO,CAAC,EACzC,IAAI,CAAC,SAAS,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC;AAErC,cAAI,UAAU,WAAW,EAAG;AAE5B,gBAAM,MAAM,UAAU,KAAK,IAAI;AAC/B,cAAI,OAAgB;AACpB,cAAI;AACF,mBAAO,KAAK,MAAM,GAAG;AAAA,UACvB,QAAQ;AAAA,UAER;AAEA,kBAAQ,UAAU,EAAE,MAAM,IAAI,CAAC;AAAA,QACjC;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,eAAe,SAAS,IAAI,SAAS,aAAc;AACvD,cAAQ,UAAU,GAAG;AAAA,IACvB;AAAA,EACF,GAAG;AAEH,SAAO;AAAA,IACL,OAAO,MAAM,WAAW,MAAM;AAAA,EAChC;AACF;;;ACzEO,IAAM,iBAAN,cAA6B,MAAM;AAAA,EACxC,YACE,SACgB,QACA,SAChB;AACA,UAAM,OAAO;AAHG;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,aAAN,MAAiB;AAAA,EAMtB,YAAY,SAA4B;AACtC,SAAK,UAAU,QAAQ,QAAQ,QAAQ,QAAQ,EAAE;AACjD,SAAK,YACH,QAAQ,cAAc,OAAO,UAAU,cAAc,QAAS;AAChE,SAAK,YAAY,QAAQ,aAAa;AACtC,SAAK,iBAAiB,QAAQ,kBAAkB,CAAC;AAEjD,QAAI,CAAC,KAAK,WAAW;AACnB,YAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAAA,EACF;AAAA,EAEA,MAAM,QACJ,QACA,MACA,OAA2B,CAAC,GAChB;AAIZ,UAAM,MAAM,KAAK,cACb,KAAK,cACL,GAAG,KAAK,OAAO,GAAG,KAAK,WAAW,GAAG,IAAI,KAAK,GAAG,GAAG,IAAI;AAE5D,UAAM,UAAkC;AAAA,MACtC,QAAQ;AAAA,MACR,GAAI,KAAK,qBAAqB,CAAC,IAAI,KAAK;AAAA,MACxC,GAAG,KAAK;AAAA,IACV;AAEA,QAAI;AACJ,QAAI,KAAK,SAAS,QAAW;AAC3B,UAAI,KAAK,gBAAgB,qCAAqC;AAC5D,gBAAQ,cAAc,IAAI,KAAK;AAC/B,eAAO,IAAI;AAAA,UACT,OAAO,QAAQ,KAAK,IAA+B,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM;AAAA,YACnE;AAAA,YACA,OAAO,CAAC;AAAA,UACV,CAAC;AAAA,QACH,EAAE,SAAS;AAAA,MACb,OAAO;AACL,gBAAQ,cAAc,IAAI,KAAK,eAAe;AAC9C,eAAO,KAAK,UAAU,KAAK,IAAI;AAAA,MACjC;AAAA,IACF;AAEA,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QACJ,KAAK,YAAY,IAAI,WAAW,MAAM,WAAW,MAAM,GAAG,KAAK,SAAS,IAAI;AAE9E,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,UAAU,KAAK;AAAA,QACzC;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ,WAAW;AAAA,MACrB,CAAC;AAED,YAAM,OAAO,MAAM,SAAS,KAAK;AACjC,UAAI,SAAkB;AACtB,UAAI;AACF,iBAAS,OAAO,KAAK,MAAM,IAAI,IAAI;AAAA,MACrC,QAAQ;AAAA,MAER;AAEA,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,UAAW,UAAU,CAAC;AAC5B,cAAM,UACJ,QAAQ,mBACR,QAAQ,WACR,8BAA8B,SAAS,MAAM;AAC/C,cAAM,IAAI,eAAe,SAAS,SAAS,QAAQ,OAAO;AAAA,MAC5D;AAEA,aAAO;AAAA,IACT,UAAE;AACA,UAAI,MAAO,cAAa,KAAK;AAAA,IAC/B;AAAA,EACF;AACF;;;ACtFO,IAAM,qBAAN,MAAyB;AAAA,EAM9B,YAAY,SAAsC;AAChD,SAAK,YAAY,QAAQ;AAEzB,UAAM,WAAW,mBAAmB,QAAQ,WAAW;AAAA,MACrD,aAAa,QAAQ;AAAA,MACrB,UAAU,QAAQ;AAAA,IACpB,CAAC;AAED,QAAI,CAAC,SAAS,UAAU;AACtB,cAAQ,iCAAiC,IAAI;AAAA,IAC/C;AAEA,SAAK,cAAc,SAAS;AAC5B,SAAK,UAAU,SAAS;AAExB,SAAK,OAAO,IAAI,WAAW;AAAA,MACzB,SAAS,SAAS;AAAA,MAClB,WAAW,QAAQ;AAAA,MACnB,WAAW,QAAQ;AAAA,MACnB,gBAAgB;AAAA,QACd,WAAW,QAAQ;AAAA,QACnB,GAAG,QAAQ;AAAA,MACb;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,cAAc,YAAoD;AAChE,WAAO,KAAK,KAAK,QAAQ,OAAO,mBAAmB,mBAAmB,UAAU,CAAC,EAAE;AAAA,EACrF;AAAA;AAAA,EAGA,oBAAoB,QAAgB,SAAkD;AACpF,WAAO,iBAAiB,KAAK,SAAS,QAAQ,KAAK,WAAW,OAAO;AAAA,EACvE;AACF;AAEO,IAAM,yBAAN,cAAqC,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA,EAK7D,sBAAsB,SAAqE;AACzF,WAAO,KAAK,KAAK,QAAQ,QAAQ,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAAA,EACzE;AAAA,EAEA,cAAc,SAAqE;AACjF,WAAO,KAAK,sBAAsB,OAAO;AAAA,EAC3C;AACF;AAEO,IAAM,uBAAN,cAAmC,mBAAmB;AAAA;AAAA,EAG3D,WAAW,SAAyD;AAClE,WAAO,KAAK,KAAK,QAAQ,QAAQ,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAAA,EACpE;AAAA,EAEA,aAAa,SAAuD;AAClE,WAAO,KAAK,KAAK,QAAQ,QAAQ,0BAA0B,EAAE,MAAM,QAAQ,CAAC;AAAA,EAC9E;AAAA,EAEA,gBAAgB,SAAmE;AACjF,WAAO,KAAK,KAAK,QAAQ,QAAQ,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAAA,EACzE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,UACE,WACA,SACA,aAC4B;AAC5B,WAAO,KAAK,KAAK,QAAQ,QAAQ,IAAI;AAAA,MACnC,MAAM;AAAA,MACN,aAAa;AAAA,MACb,oBAAoB;AAAA,MACpB;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,gBACE,SACA,WACkC;AAClC,WAAO,YACH,KAAK,KAAK,QAAQ,QAAQ,IAAI;AAAA,MAC5B,MAAM;AAAA,MACN,aAAa;AAAA,MACb,oBAAoB;AAAA,IACtB,CAAC,IACD,KAAK,KAAK,QAAQ,QAAQ,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAAA,EACtE;AAAA;AAAA,EAIA,cAAyC;AACvC,WAAO,KAAK,KAAK,QAAQ,OAAO,oBAAoB;AAAA,EACtD;AAAA,EAEA,eAA2C;AACzC,WAAO,KAAK,KAAK,QAAQ,OAAO,kBAAkB;AAAA,EACpD;AAAA,EAEA,iBAA+C;AAC7C,WAAO,KAAK,KAAK,QAAQ,OAAO,mBAAmB;AAAA,EACrD;AACF;AAEO,IAAM,uBAAN,cAAmC,qBAAqB;AAAC;","names":[]}
package/dist/cli.d.mts CHANGED
@@ -1 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ type WritableLike = {
3
+ write: (text: string) => void;
4
+ };
5
+ type CliDeps = {
6
+ stdout?: WritableLike;
7
+ stderr?: WritableLike;
8
+ fetch?: typeof fetch;
9
+ };
10
+ declare function runCli(argv: string[], deps?: CliDeps): Promise<number>;
11
+
12
+ export { runCli };
package/dist/cli.d.ts CHANGED
@@ -1 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ type WritableLike = {
3
+ write: (text: string) => void;
4
+ };
5
+ type CliDeps = {
6
+ stdout?: WritableLike;
7
+ stderr?: WritableLike;
8
+ fetch?: typeof fetch;
9
+ };
10
+ declare function runCli(argv: string[], deps?: CliDeps): Promise<number>;
11
+
12
+ export { runCli };