@afriex/sdk 5.0.0 → 6.0.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/CHANGELOG.md +39 -0
- package/README.md +155 -148
- package/dist/cjs/client.d.ts +110 -0
- package/dist/cjs/client.js +231 -0
- package/dist/cjs/core/api-promise.d.ts +25 -0
- package/dist/cjs/core/api-promise.js +49 -0
- package/dist/cjs/core/errors.d.ts +78 -0
- package/dist/cjs/core/errors.js +143 -0
- package/dist/cjs/core/pagination.d.ts +47 -0
- package/dist/cjs/core/pagination.js +70 -0
- package/dist/cjs/core/resource.d.ts +18 -0
- package/dist/cjs/core/resource.js +36 -0
- package/dist/cjs/core/types.d.ts +43 -0
- package/dist/cjs/core/types.js +2 -0
- package/dist/cjs/generated/openapi.d.ts +5750 -0
- package/dist/cjs/generated/openapi.js +6 -0
- package/dist/cjs/index.d.ts +21 -0
- package/dist/cjs/index.js +34 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/resources/balance.d.ts +16 -0
- package/dist/cjs/resources/balance.js +19 -0
- package/dist/cjs/resources/checkout.d.ts +12 -0
- package/dist/cjs/resources/checkout.js +16 -0
- package/dist/cjs/resources/customers.d.ts +27 -0
- package/dist/cjs/resources/customers.js +56 -0
- package/dist/cjs/resources/media.d.ts +24 -0
- package/dist/cjs/resources/media.js +39 -0
- package/dist/cjs/resources/payment-batches.d.ts +41 -0
- package/dist/cjs/resources/payment-batches.js +76 -0
- package/dist/cjs/resources/payment-methods.d.ts +49 -0
- package/dist/cjs/resources/payment-methods.js +95 -0
- package/dist/cjs/resources/rates.d.ts +9 -0
- package/dist/cjs/resources/rates.js +13 -0
- package/dist/cjs/resources/sme-registration.d.ts +24 -0
- package/dist/cjs/resources/sme-registration.js +31 -0
- package/dist/cjs/resources/transactions.d.ts +39 -0
- package/dist/cjs/resources/transactions.js +67 -0
- package/dist/cjs/resources/webhooks.d.ts +43 -0
- package/dist/cjs/resources/webhooks.js +53 -0
- package/dist/cjs/version.d.ts +1 -0
- package/dist/cjs/version.js +5 -0
- package/dist/esm/client.d.ts +110 -0
- package/dist/esm/client.js +227 -0
- package/dist/esm/core/api-promise.d.ts +25 -0
- package/dist/esm/core/api-promise.js +45 -0
- package/dist/esm/core/errors.d.ts +78 -0
- package/dist/esm/core/errors.js +126 -0
- package/dist/esm/core/pagination.d.ts +47 -0
- package/dist/esm/core/pagination.js +65 -0
- package/dist/esm/core/resource.d.ts +18 -0
- package/dist/esm/core/resource.js +30 -0
- package/dist/esm/core/types.d.ts +43 -0
- package/dist/esm/core/types.js +1 -0
- package/dist/esm/generated/openapi.d.ts +5750 -0
- package/dist/esm/generated/openapi.js +5 -0
- package/dist/esm/index.d.ts +21 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/resources/balance.d.ts +16 -0
- package/dist/esm/resources/balance.js +15 -0
- package/dist/esm/resources/checkout.d.ts +12 -0
- package/dist/esm/resources/checkout.js +12 -0
- package/dist/esm/resources/customers.d.ts +27 -0
- package/dist/esm/resources/customers.js +52 -0
- package/dist/esm/resources/media.d.ts +24 -0
- package/dist/esm/resources/media.js +35 -0
- package/dist/esm/resources/payment-batches.d.ts +41 -0
- package/dist/esm/resources/payment-batches.js +72 -0
- package/dist/esm/resources/payment-methods.d.ts +49 -0
- package/dist/esm/resources/payment-methods.js +91 -0
- package/dist/esm/resources/rates.d.ts +9 -0
- package/dist/esm/resources/rates.js +9 -0
- package/dist/esm/resources/sme-registration.d.ts +24 -0
- package/dist/esm/resources/sme-registration.js +27 -0
- package/dist/esm/resources/transactions.d.ts +39 -0
- package/dist/esm/resources/transactions.js +63 -0
- package/dist/esm/resources/webhooks.d.ts +43 -0
- package/dist/esm/resources/webhooks.js +49 -0
- package/dist/esm/version.d.ts +1 -0
- package/dist/esm/version.js +2 -0
- package/package.json +52 -68
- package/dist/index.d.ts +0 -54
- package/dist/index.js +0 -54
- package/dist/version.d.ts +0 -8
- package/dist/version.js +0 -8
- package/skills/afriex-go-live/SKILL.md +0 -386
- package/skills/afriex-sdk/SKILL.md +0 -275
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: afriex-go-live
|
|
3
|
-
description: >
|
|
4
|
-
Pre-deploy security and correctness checklist for an Afriex Business API
|
|
5
|
-
integration built on @afriex/afriex. Verifies API key handling, Environment
|
|
6
|
-
selection, retry and timeout policy, transaction idempotency keys, webhook
|
|
7
|
-
signature verification against webhookPublicKey with raw bodies, at-least-once
|
|
8
|
-
delivery handling, sandbox-only endpoints, and error-code handling. Load
|
|
9
|
-
before shipping an Afriex integration to production, during a payments
|
|
10
|
-
security review, or when auditing an existing Afriex integration.
|
|
11
|
-
metadata:
|
|
12
|
-
type: security
|
|
13
|
-
library: '@afriex/sdk'
|
|
14
|
-
library_version: '4.1.0'
|
|
15
|
-
requires:
|
|
16
|
-
- 'afriex-sdk'
|
|
17
|
-
sources:
|
|
18
|
-
- 'Afri-exchange/afriex-sdk:packages/sdk/src/index.ts'
|
|
19
|
-
- 'Afri-exchange/afriex-sdk:packages/core/src/config/Config.ts'
|
|
20
|
-
- 'Afri-exchange/afriex-sdk:packages/webhooks/src/WebhookService.ts'
|
|
21
|
-
- 'Afri-exchange/afriex-sdk:packages/transactions/src/TransactionService.ts'
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
# Afriex SDK — Go-Live Checklist
|
|
25
|
-
|
|
26
|
-
Run through each section before pointing an integration at
|
|
27
|
-
`Environment.PRODUCTION`. This skill builds on `afriex-sdk`; read it first for
|
|
28
|
-
the configuration surface these checks refer to.
|
|
29
|
-
|
|
30
|
-
## Credential Checks
|
|
31
|
-
|
|
32
|
-
### Check: the API key comes from the environment, never source
|
|
33
|
-
|
|
34
|
-
Expected:
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
import { AfriexSDK, Environment } from "@afriex/sdk";
|
|
38
|
-
|
|
39
|
-
const apiKey = process.env.AFRIEX_API_KEY;
|
|
40
|
-
if (!apiKey) {
|
|
41
|
-
throw new Error("AFRIEX_API_KEY is not set");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const afriex = new AfriexSDK({ apiKey, environment: Environment.PRODUCTION });
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Fail condition: an `apiKey` string literal, a key in a committed `.env`, or a
|
|
48
|
-
key reaching client-side bundles.
|
|
49
|
-
Fix: move the key to a server-side secret store and rotate the exposed one.
|
|
50
|
-
|
|
51
|
-
### Check: production and sandbox keys are distinct variables
|
|
52
|
-
|
|
53
|
-
Expected:
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
import { AfriexSDK, Environment } from "@afriex/sdk";
|
|
57
|
-
|
|
58
|
-
const isProduction = process.env.NODE_ENV === "production";
|
|
59
|
-
|
|
60
|
-
export const afriex = new AfriexSDK({
|
|
61
|
-
apiKey: (isProduction ? process.env.AFRIEX_API_KEY : process.env.AFRIEX_SANDBOX_API_KEY)!,
|
|
62
|
-
environment: isProduction ? Environment.PRODUCTION : Environment.STAGING,
|
|
63
|
-
});
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Fail condition: one `AFRIEX_API_KEY` variable reused across environments, or
|
|
67
|
-
`environment` omitted entirely.
|
|
68
|
-
Fix: separate the variables — `Config` defaults `environment` to
|
|
69
|
-
`Environment.PRODUCTION`, so an omitted field silently targets live money.
|
|
70
|
-
|
|
71
|
-
## Transport Checks
|
|
72
|
-
|
|
73
|
-
### Check: retries are explicitly configured
|
|
74
|
-
|
|
75
|
-
Expected:
|
|
76
|
-
|
|
77
|
-
```ts
|
|
78
|
-
import { AfriexSDK, Environment } from "@afriex/sdk";
|
|
79
|
-
|
|
80
|
-
export const afriex = new AfriexSDK({
|
|
81
|
-
apiKey: process.env.AFRIEX_API_KEY!,
|
|
82
|
-
environment: Environment.PRODUCTION,
|
|
83
|
-
retryConfig: {
|
|
84
|
-
maxRetries: 3,
|
|
85
|
-
retryDelay: 1000,
|
|
86
|
-
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
Fail condition: no `retryConfig` — `DEFAULT_CONFIG` sets `maxRetries: 0`, so a
|
|
92
|
-
single 503 fails the request outright.
|
|
93
|
-
Fix: supply `retryConfig`.
|
|
94
|
-
|
|
95
|
-
### Check: writes are not retried blindly
|
|
96
|
-
|
|
97
|
-
Expected: `retryConfig.retryableMethods` is absent, or lists neither `POST`
|
|
98
|
-
nor `PATCH`.
|
|
99
|
-
|
|
100
|
-
With the default methods, the SDK retries reads and sends every `POST` and
|
|
101
|
-
`PATCH` once. A write that fails with a 5xx or a timeout may still have been
|
|
102
|
-
applied, so the application reads the state back (for a transaction,
|
|
103
|
-
`transactions.list({ reference })`) before it sends the write again, and
|
|
104
|
-
reuses the same `meta.idempotencyKey` when it does.
|
|
105
|
-
|
|
106
|
-
Fail condition: `retryableMethods` includes `POST` on a client that calls
|
|
107
|
-
`paymentBatches.withdraw` — a repeat starts a second run and pays every
|
|
108
|
-
recipient again.
|
|
109
|
-
Fix: remove `POST` from `retryableMethods` and retry writes in application
|
|
110
|
-
code, where the state can be checked first.
|
|
111
|
-
|
|
112
|
-
### Check: request logging does not leak request bodies
|
|
113
|
-
|
|
114
|
-
Expected:
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
import { AfriexSDK, Environment, LogLevel } from "@afriex/sdk";
|
|
118
|
-
|
|
119
|
-
export const afriex = new AfriexSDK({
|
|
120
|
-
apiKey: process.env.AFRIEX_API_KEY!,
|
|
121
|
-
environment: Environment.PRODUCTION,
|
|
122
|
-
enableLogging: true,
|
|
123
|
-
logLevel: LogLevel.ERROR,
|
|
124
|
-
});
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Fail condition: `logLevel: LogLevel.DEBUG` in production, which logs request
|
|
128
|
-
and response metadata for every call.
|
|
129
|
-
Fix: keep production at `LogLevel.ERROR` and scrub customer data from any
|
|
130
|
-
handler that serializes `ApiError.details`.
|
|
131
|
-
|
|
132
|
-
## Money-Movement Checks
|
|
133
|
-
|
|
134
|
-
### Check: every transaction carries a stable idempotency key
|
|
135
|
-
|
|
136
|
-
Expected:
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
import { AfriexSDK } from "@afriex/sdk";
|
|
140
|
-
import { randomUUID } from "node:crypto";
|
|
141
|
-
|
|
142
|
-
const afriex = new AfriexSDK({ apiKey: process.env.AFRIEX_API_KEY! });
|
|
143
|
-
|
|
144
|
-
export async function payout(orderId: string, idempotencyKey: string) {
|
|
145
|
-
return afriex.transactions.create({
|
|
146
|
-
customerId: "cus_123",
|
|
147
|
-
destinationId: "pm_456",
|
|
148
|
-
sourceAmount: "100.00",
|
|
149
|
-
sourceCurrency: "USD",
|
|
150
|
-
destinationAmount: "150000.00",
|
|
151
|
-
destinationCurrency: "NGN",
|
|
152
|
-
meta: { idempotencyKey, reference: orderId },
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export const keyForOrder = (orderId: string) => `${orderId}:${randomUUID()}`;
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Fail condition: `idempotencyKey: randomUUID()` generated inside a retry loop or
|
|
160
|
-
inside the request handler, so each attempt is a new key.
|
|
161
|
-
Fix: persist the key with the order before the first attempt and reuse it for
|
|
162
|
-
every retry.
|
|
163
|
-
|
|
164
|
-
### Check: settlement is driven by webhooks, not by create() resolving
|
|
165
|
-
|
|
166
|
-
Expected:
|
|
167
|
-
|
|
168
|
-
```ts
|
|
169
|
-
import type { TransactionWebhookPayload } from "@afriex/sdk";
|
|
170
|
-
|
|
171
|
-
const TERMINAL = new Set(["SUCCESS", "FAILED", "CANCELLED", "REJECTED", "REFUNDED"]);
|
|
172
|
-
|
|
173
|
-
export function isSettled(event: TransactionWebhookPayload): boolean {
|
|
174
|
-
return TERMINAL.has(event.data.status);
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Fail condition: order state marked paid immediately after
|
|
179
|
-
`transactions.create()` resolves.
|
|
180
|
-
Fix: treat `PENDING` and `PROCESSING` as in-flight and settle on
|
|
181
|
-
`TRANSACTION.UPDATED`.
|
|
182
|
-
|
|
183
|
-
### Check: no sandbox-only calls run in production
|
|
184
|
-
|
|
185
|
-
Expected:
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
import { AfriexSDK, Environment } from "@afriex/sdk";
|
|
189
|
-
|
|
190
|
-
const afriex = new AfriexSDK({
|
|
191
|
-
apiKey: process.env.AFRIEX_SANDBOX_API_KEY!,
|
|
192
|
-
environment: Environment.STAGING,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
export async function seedTestWallet(): Promise<void> {
|
|
196
|
-
if (process.env.NODE_ENV === "production") {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
await afriex.balance.topUpSandbox({ amount: 10000, currency: "USD" });
|
|
200
|
-
await afriex.webhooks.triggerTestWebhook({
|
|
201
|
-
event: "TRANSACTION.UPDATED",
|
|
202
|
-
entityId: "507f1f77bcf86cd799439011",
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Fail condition: `balance.topUpSandbox`, `webhooks.triggerTestWebhook`,
|
|
208
|
-
`transactions.simulate` or `paymentMethods.simulateTransfer` reachable on a
|
|
209
|
-
production code path — all four answer `403 FORBIDDEN` there.
|
|
210
|
-
Fix: guard them behind an environment check or keep them in test-only modules.
|
|
211
|
-
|
|
212
|
-
### Check: no sandbox test value reaches production
|
|
213
|
-
|
|
214
|
-
Expected:
|
|
215
|
-
|
|
216
|
-
```ts
|
|
217
|
-
const SANDBOX_OUTCOME_SUFFIXES = ["0001", "0002", "0003", "0004", "0005"];
|
|
218
|
-
|
|
219
|
-
export function assertRealAccount(accountNumber: string): void {
|
|
220
|
-
if (process.env.NODE_ENV !== "production") {
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (SANDBOX_OUTCOME_SUFFIXES.some((suffix) => accountNumber.endsWith(suffix))) {
|
|
224
|
-
console.warn("account number matches a sandbox test value", accountNumber);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
In the sandbox, an account number, phone number or card number ending in
|
|
230
|
-
`0001` to `0005` chooses the outcome of a transaction, the OTP is always
|
|
231
|
-
`123456`, and `SIMULATE_INSTANT` in `meta.reference` completes a transaction
|
|
232
|
-
early. None of this applies in production, where those are ordinary values.
|
|
233
|
-
|
|
234
|
-
Fail condition: a fixture account, the OTP `123456` or a `SIMULATE_INSTANT`
|
|
235
|
-
reference hard-coded on a path that runs in production.
|
|
236
|
-
Fix: keep test values in fixtures that only the test environment loads.
|
|
237
|
-
|
|
238
|
-
## Webhook Checks
|
|
239
|
-
|
|
240
|
-
### Check: the firewall admits Afriex's webhook addresses
|
|
241
|
-
|
|
242
|
-
Expected: inbound traffic to the webhook endpoint is allowed from
|
|
243
|
-
`34.197.33.100` in production and `34.234.189.210` in the sandbox.
|
|
244
|
-
|
|
245
|
-
Fail condition: a firewall or allowlist in front of the endpoint that does not
|
|
246
|
-
include the address for the environment — deliveries are dropped before they
|
|
247
|
-
reach the handler, and nothing is logged on the application side.
|
|
248
|
-
Fix: add the address to the allowlist. The address is not a substitute for
|
|
249
|
-
verifying the signature.
|
|
250
|
-
|
|
251
|
-
### Check: the endpoint verifies signatures against the raw body
|
|
252
|
-
|
|
253
|
-
Expected:
|
|
254
|
-
|
|
255
|
-
```ts
|
|
256
|
-
import express from "express";
|
|
257
|
-
import { AfriexSDK, WEBHOOK_SIGNATURE_HEADER } from "@afriex/sdk";
|
|
258
|
-
|
|
259
|
-
const webhookPublicKey = process.env.AFRIEX_WEBHOOK_PUBLIC_KEY;
|
|
260
|
-
if (!webhookPublicKey) {
|
|
261
|
-
throw new Error("AFRIEX_WEBHOOK_PUBLIC_KEY is not set");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const afriex = new AfriexSDK({ apiKey: process.env.AFRIEX_API_KEY!, webhookPublicKey });
|
|
265
|
-
const app = express();
|
|
266
|
-
|
|
267
|
-
app.post(
|
|
268
|
-
"/webhooks/afriex",
|
|
269
|
-
express.raw({ type: "application/json" }),
|
|
270
|
-
(req, res) => {
|
|
271
|
-
try {
|
|
272
|
-
const event = afriex.webhooks.verifyAndParse(
|
|
273
|
-
req.body.toString("utf8"),
|
|
274
|
-
req.header(WEBHOOK_SIGNATURE_HEADER) ?? ""
|
|
275
|
-
);
|
|
276
|
-
console.log(event.event);
|
|
277
|
-
res.sendStatus(200);
|
|
278
|
-
} catch {
|
|
279
|
-
res.sendStatus(400);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
);
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
Fail condition: `express.json()` applied to the webhook route, or
|
|
286
|
-
`JSON.stringify(req.body)` passed to `verify`.
|
|
287
|
-
Fix: mount `express.raw` on the webhook path and verify the exact bytes.
|
|
288
|
-
|
|
289
|
-
### Check: a missing public key fails closed
|
|
290
|
-
|
|
291
|
-
Expected:
|
|
292
|
-
|
|
293
|
-
```ts
|
|
294
|
-
import { AfriexSDK } from "@afriex/sdk";
|
|
295
|
-
|
|
296
|
-
const webhookPublicKey = process.env.AFRIEX_WEBHOOK_PUBLIC_KEY;
|
|
297
|
-
if (!webhookPublicKey) {
|
|
298
|
-
throw new Error("AFRIEX_WEBHOOK_PUBLIC_KEY is not set");
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export const afriex = new AfriexSDK({
|
|
302
|
-
apiKey: process.env.AFRIEX_API_KEY!,
|
|
303
|
-
webhookPublicKey,
|
|
304
|
-
});
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
Fail condition: the handler logs a warning and processes the payload when
|
|
308
|
-
`verify` returns `false`.
|
|
309
|
-
Fix: assert the key at boot and use `verifyAndParse`, which throws on a missing
|
|
310
|
-
key instead of returning `false`.
|
|
311
|
-
|
|
312
|
-
### Check: handlers are idempotent across redeliveries
|
|
313
|
-
|
|
314
|
-
Expected:
|
|
315
|
-
|
|
316
|
-
```ts
|
|
317
|
-
import type { TransactionWebhookPayload } from "@afriex/sdk";
|
|
318
|
-
|
|
319
|
-
export async function onTransaction(
|
|
320
|
-
event: TransactionWebhookPayload,
|
|
321
|
-
seen: { has(key: string): Promise<boolean>; add(key: string): Promise<void> }
|
|
322
|
-
): Promise<void> {
|
|
323
|
-
const key = `${event.data.transactionId}:${event.data.status}`;
|
|
324
|
-
if (await seen.has(key)) {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
await seen.add(key);
|
|
328
|
-
console.log("processing", key);
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
Fail condition: side effects run on every delivery with no deduplication key.
|
|
333
|
-
Fix: deduplicate on `transactionId` plus `status` in durable storage.
|
|
334
|
-
|
|
335
|
-
## Common Security Mistakes
|
|
336
|
-
|
|
337
|
-
### HIGH Returning ApiError.message to end users as an error code
|
|
338
|
-
|
|
339
|
-
Wrong:
|
|
340
|
-
|
|
341
|
-
```ts
|
|
342
|
-
import { ApiError } from "@afriex/sdk";
|
|
343
|
-
|
|
344
|
-
export function toClientError(error: unknown): { code: string } {
|
|
345
|
-
return { code: error instanceof ApiError ? error.message : "UNKNOWN" };
|
|
346
|
-
}
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
Correct:
|
|
350
|
-
|
|
351
|
-
```ts
|
|
352
|
-
import { ApiError, AfriexErrorCode } from "@afriex/sdk";
|
|
353
|
-
|
|
354
|
-
export function toClientError(error: unknown): { code: string; message: string } {
|
|
355
|
-
if (error instanceof ApiError) {
|
|
356
|
-
return {
|
|
357
|
-
code: error.errorCode ?? AfriexErrorCode.UNKNOWN_ERROR,
|
|
358
|
-
message: error.details?.friendlyMessage ?? "Payment could not be completed",
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
return { code: AfriexErrorCode.UNKNOWN_ERROR, message: "Payment could not be completed" };
|
|
362
|
-
}
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
`ApiError.message` falls back through `friendlyMessage`, `errorMessage`, and
|
|
366
|
-
`error`, so it can surface internal technical detail as a client-facing code
|
|
367
|
-
and it changes without a version bump.
|
|
368
|
-
|
|
369
|
-
Source: packages/core/src/errors/ApiError.ts
|
|
370
|
-
|
|
371
|
-
## Pre-Deploy Summary
|
|
372
|
-
|
|
373
|
-
- [ ] API key read from a secret store; no key in source or client bundles
|
|
374
|
-
- [ ] Separate sandbox and production keys; `environment` set explicitly
|
|
375
|
-
- [ ] `retryConfig` configured — retries are off by default
|
|
376
|
-
- [ ] `retryableMethods` does not include `POST` or `PATCH`
|
|
377
|
-
- [ ] Afriex's webhook IP address is allowed through the firewall
|
|
378
|
-
- [ ] `logLevel` is `LogLevel.ERROR` in production
|
|
379
|
-
- [ ] Every `transactions.create` carries a persisted, reused `meta.idempotencyKey`
|
|
380
|
-
- [ ] Order state settles on `TRANSACTION.UPDATED`, not on `create()` resolving
|
|
381
|
-
- [ ] `topUpSandbox`, `triggerTestWebhook`, `simulate` and `simulateTransfer` are unreachable in production
|
|
382
|
-
- [ ] No sandbox test account, OTP or `SIMULATE_INSTANT` reference on a production path
|
|
383
|
-
- [ ] Webhook route uses a raw body parser and `verifyAndParse`
|
|
384
|
-
- [ ] `AFRIEX_WEBHOOK_PUBLIC_KEY` asserted at boot
|
|
385
|
-
- [ ] Webhook handlers deduplicate on `transactionId` plus `status`
|
|
386
|
-
- [ ] Client-facing errors use `errorCode`, not `ApiError.message`
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: afriex-sdk
|
|
3
|
-
description: >
|
|
4
|
-
Entry point for the Afriex Business API TypeScript SDK (@afriex/sdk) — the
|
|
5
|
-
AfriexSDK / Afriex facade and its customers, transactions, paymentMethods,
|
|
6
|
-
paymentBatches, balance, rates, checkout, media, smeRegistration, and
|
|
7
|
-
webhooks services. Covers installation,
|
|
8
|
-
AfriexSDKConfig including webhookPublicKey, choosing sandbox vs production,
|
|
9
|
-
the end-to-end customer → payment method → transaction → webhook payout flow,
|
|
10
|
-
and which sub-skill covers each service. Load when starting an Afriex
|
|
11
|
-
integration, sending or collecting cross-border payments in TypeScript, or
|
|
12
|
-
deciding which Afriex service to call.
|
|
13
|
-
metadata:
|
|
14
|
-
type: core
|
|
15
|
-
library: '@afriex/sdk'
|
|
16
|
-
library_version: '4.1.0'
|
|
17
|
-
sources:
|
|
18
|
-
- 'Afri-exchange/afriex-sdk:packages/sdk/src/index.ts'
|
|
19
|
-
- 'Afri-exchange/afriex-sdk:packages/core/src/config/Config.ts'
|
|
20
|
-
- 'Afri-exchange/afriex-sdk:README.md'
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
# Afriex SDK
|
|
24
|
-
|
|
25
|
-
`@afriex/sdk` is the umbrella package for the Afriex Business API. `AfriexSDK`
|
|
26
|
-
extends `AfriexClient` from `@afriex/core` and constructs every service against
|
|
27
|
-
one shared `HttpClient`, so a single instance carries the API key, environment,
|
|
28
|
-
retry policy, and logging for the whole integration. Individual services also
|
|
29
|
-
ship as standalone `@afriex/*` packages when only part of the surface is
|
|
30
|
-
needed.
|
|
31
|
-
|
|
32
|
-
## Setup
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
import { AfriexSDK, Environment } from "@afriex/sdk";
|
|
36
|
-
|
|
37
|
-
export const afriex = new AfriexSDK({
|
|
38
|
-
apiKey: process.env.AFRIEX_API_KEY!,
|
|
39
|
-
environment:
|
|
40
|
-
process.env.NODE_ENV === "production"
|
|
41
|
-
? Environment.PRODUCTION
|
|
42
|
-
: Environment.STAGING,
|
|
43
|
-
webhookPublicKey: process.env.AFRIEX_WEBHOOK_PUBLIC_KEY,
|
|
44
|
-
retryConfig: {
|
|
45
|
-
maxRetries: 3,
|
|
46
|
-
retryDelay: 1000,
|
|
47
|
-
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Install with `npm install @afriex/sdk`. Create one instance at module scope and
|
|
53
|
-
import it where needed — each construction builds a fresh HTTP client.
|
|
54
|
-
|
|
55
|
-
## Services
|
|
56
|
-
|
|
57
|
-
| Need to... | Use | Skill |
|
|
58
|
-
| ------------------------------------------------- | ----------------------- | --------------------------------- |
|
|
59
|
-
| Register senders and recipients, submit KYC | `afriex.customers` | `afriex-customers` |
|
|
60
|
-
| Add bank, mobile money, or virtual account rails | `afriex.paymentMethods` | `afriex-payment-methods` |
|
|
61
|
-
| Send payouts, collect deposits, swap currencies | `afriex.transactions` | `afriex-transactions` |
|
|
62
|
-
| Pay many recipients in one call | `afriex.paymentBatches` | `afriex-payment-batches` |
|
|
63
|
-
| Read wallet balances, fund a sandbox wallet | `afriex.balance` | `afriex-balance` |
|
|
64
|
-
| Quote exchange rates | `afriex.rates` | `afriex-rates` |
|
|
65
|
-
| Host a payment page | `afriex.checkout` | `afriex-checkout` |
|
|
66
|
-
| Upload an invoice, a proof or a document | `afriex.media` | `afriex-media` |
|
|
67
|
-
| Register for dedicated KES virtual accounts | `afriex.smeRegistration` | `afriex-sme-registration` |
|
|
68
|
-
| Verify inbound events, replay test events | `afriex.webhooks` | `afriex-webhooks` |
|
|
69
|
-
| Configure transport, errors, retries | `AfriexClient`, errors | `afriex-core` |
|
|
70
|
-
| Ship to production safely | — | `afriex-go-live` |
|
|
71
|
-
|
|
72
|
-
## Core Patterns
|
|
73
|
-
|
|
74
|
-
### End-to-end payout
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
import { AfriexSDK, Environment } from "@afriex/sdk";
|
|
78
|
-
import { randomUUID } from "node:crypto";
|
|
79
|
-
|
|
80
|
-
const afriex = new AfriexSDK({
|
|
81
|
-
apiKey: process.env.AFRIEX_API_KEY!,
|
|
82
|
-
environment: Environment.STAGING,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const customer = await afriex.customers.create({
|
|
86
|
-
fullName: "Ada Lovelace",
|
|
87
|
-
email: "ada@example.com",
|
|
88
|
-
phone: "+2348012345678",
|
|
89
|
-
countryCode: "NG",
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const { data: institutions } = await afriex.paymentMethods.getInstitutions({
|
|
93
|
-
channel: "BANK_ACCOUNT",
|
|
94
|
-
countryCode: "NG",
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const destination = await afriex.paymentMethods.create({
|
|
98
|
-
channel: "BANK_ACCOUNT",
|
|
99
|
-
customerId: customer.customerId,
|
|
100
|
-
accountName: customer.name,
|
|
101
|
-
accountNumber: "0123456789",
|
|
102
|
-
countryCode: "NG",
|
|
103
|
-
institution: {
|
|
104
|
-
institutionCode: institutions[0].institutionCode,
|
|
105
|
-
institutionName: institutions[0].institutionName,
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const transaction = await afriex.transactions.create({
|
|
110
|
-
customerId: customer.customerId,
|
|
111
|
-
destinationId: destination.paymentMethodId,
|
|
112
|
-
sourceAmount: "100.00",
|
|
113
|
-
sourceCurrency: "USD",
|
|
114
|
-
destinationAmount: "150000.00",
|
|
115
|
-
destinationCurrency: "NGN",
|
|
116
|
-
meta: { idempotencyKey: randomUUID(), reference: "order_9981" },
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
console.log(transaction.transactionId, transaction.status);
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
The transaction resolves as `PENDING` or `PROCESSING`; settlement arrives as a
|
|
123
|
-
`TRANSACTION.UPDATED` webhook.
|
|
124
|
-
|
|
125
|
-
### Import only the services you use
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
import { AfriexClient, Environment } from "@afriex/core";
|
|
129
|
-
import { RateService } from "@afriex/rates";
|
|
130
|
-
|
|
131
|
-
const client = new AfriexClient({
|
|
132
|
-
apiKey: process.env.AFRIEX_API_KEY!,
|
|
133
|
-
environment: Environment.STAGING,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const rates = new RateService(client.getHttpClient());
|
|
137
|
-
console.log(await rates.getRate("USD", "NGN"));
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Every service takes the `HttpClient` returned by `client.getHttpClient()`.
|
|
141
|
-
|
|
142
|
-
### Handle errors once, at the edge
|
|
143
|
-
|
|
144
|
-
```ts
|
|
145
|
-
import { AfriexSDK, ApiError, RateLimitError, ValidationError } from "@afriex/sdk";
|
|
146
|
-
|
|
147
|
-
const afriex = new AfriexSDK({ apiKey: process.env.AFRIEX_API_KEY! });
|
|
148
|
-
|
|
149
|
-
export async function safeGetTransaction(id: string) {
|
|
150
|
-
try {
|
|
151
|
-
return await afriex.transactions.get(id);
|
|
152
|
-
} catch (error) {
|
|
153
|
-
if (error instanceof RateLimitError) {
|
|
154
|
-
throw new Error(`rate limited, retry after ${error.retryAfter ?? 1}s`);
|
|
155
|
-
}
|
|
156
|
-
if (error instanceof ValidationError) {
|
|
157
|
-
throw new Error(`bad request: ${JSON.stringify(error.fields)}`);
|
|
158
|
-
}
|
|
159
|
-
if (error instanceof ApiError) {
|
|
160
|
-
throw new Error(`afriex ${error.statusCode} ${error.errorCode}`);
|
|
161
|
-
}
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
`@afriex/sdk` re-exports everything from `@afriex/core`, so error classes and
|
|
168
|
-
`Environment` come from the same import.
|
|
169
|
-
|
|
170
|
-
## Common Mistakes
|
|
171
|
-
|
|
172
|
-
### HIGH Calling afriex.webhookVerifier without configuring a public key
|
|
173
|
-
|
|
174
|
-
Wrong:
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
import { AfriexSDK } from "@afriex/sdk";
|
|
178
|
-
|
|
179
|
-
const afriex = new AfriexSDK({ apiKey: process.env.AFRIEX_API_KEY! });
|
|
180
|
-
|
|
181
|
-
export function handle(rawBody: string, signature: string) {
|
|
182
|
-
return afriex.webhookVerifier!.verifyAndParse(rawBody, signature);
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
Correct:
|
|
187
|
-
|
|
188
|
-
```ts
|
|
189
|
-
import { AfriexSDK } from "@afriex/sdk";
|
|
190
|
-
|
|
191
|
-
const afriex = new AfriexSDK({
|
|
192
|
-
apiKey: process.env.AFRIEX_API_KEY!,
|
|
193
|
-
webhookPublicKey: process.env.AFRIEX_WEBHOOK_PUBLIC_KEY!,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
export function handle(rawBody: string, signature: string) {
|
|
197
|
-
return afriex.webhooks.verifyAndParse(rawBody, signature);
|
|
198
|
-
}
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
`webhookVerifier` is assigned only when `webhookPublicKey` is present, so the
|
|
202
|
-
non-null assertion throws `Cannot read properties of undefined` at the first
|
|
203
|
-
delivery; `afriex.webhooks` always exists and is the same object when the key is
|
|
204
|
-
configured.
|
|
205
|
-
|
|
206
|
-
Source: packages/sdk/src/index.ts (`if (config.webhookPublicKey) this.webhookVerifier = this.webhooks`)
|
|
207
|
-
|
|
208
|
-
### HIGH Constructing a new AfriexSDK per request
|
|
209
|
-
|
|
210
|
-
Wrong:
|
|
211
|
-
|
|
212
|
-
```ts
|
|
213
|
-
import { AfriexSDK, Environment } from "@afriex/sdk";
|
|
214
|
-
|
|
215
|
-
export async function getBalances() {
|
|
216
|
-
const afriex = new AfriexSDK({
|
|
217
|
-
apiKey: process.env.AFRIEX_API_KEY!,
|
|
218
|
-
environment: Environment.PRODUCTION,
|
|
219
|
-
});
|
|
220
|
-
return afriex.balance.getBalance();
|
|
221
|
-
}
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
Correct:
|
|
225
|
-
|
|
226
|
-
```ts
|
|
227
|
-
import { AfriexSDK, Environment } from "@afriex/sdk";
|
|
228
|
-
|
|
229
|
-
const afriex = new AfriexSDK({
|
|
230
|
-
apiKey: process.env.AFRIEX_API_KEY!,
|
|
231
|
-
environment: Environment.PRODUCTION,
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
export async function getBalances() {
|
|
235
|
-
return afriex.balance.getBalance();
|
|
236
|
-
}
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
Each `AfriexSDK` builds a fresh `ky` instance and seven service objects, so
|
|
240
|
-
per-request construction discards connection reuse and multiplies allocation
|
|
241
|
-
under load.
|
|
242
|
-
|
|
243
|
-
Source: packages/sdk/src/index.ts (constructor)
|
|
244
|
-
|
|
245
|
-
### MEDIUM Omitting environment and defaulting to production
|
|
246
|
-
|
|
247
|
-
Wrong:
|
|
248
|
-
|
|
249
|
-
```ts
|
|
250
|
-
import { AfriexSDK } from "@afriex/sdk";
|
|
251
|
-
|
|
252
|
-
const afriex = new AfriexSDK({ apiKey: process.env.AFRIEX_API_KEY! });
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
Correct:
|
|
256
|
-
|
|
257
|
-
```ts
|
|
258
|
-
import { AfriexSDK, Environment } from "@afriex/sdk";
|
|
259
|
-
|
|
260
|
-
const afriex = new AfriexSDK({
|
|
261
|
-
apiKey: process.env.AFRIEX_API_KEY!,
|
|
262
|
-
environment:
|
|
263
|
-
process.env.NODE_ENV === "production"
|
|
264
|
-
? Environment.PRODUCTION
|
|
265
|
-
: Environment.STAGING,
|
|
266
|
-
});
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
`Config` defaults `environment` to `Environment.PRODUCTION`, so an integration
|
|
270
|
-
test written without the field points at the live API and moves real money.
|
|
271
|
-
|
|
272
|
-
Source: packages/core/src/config/Config.ts (environment default)
|
|
273
|
-
|
|
274
|
-
See also: afriex-go-live/SKILL.md — the pre-deploy checklist for this
|
|
275
|
-
configuration.
|